zapier-platform-cli 15.17.0 → 15.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3953 +1,13 @@
1
- <!-- GENERATED! ONLY EDIT `README-source.md` -->
1
+ # Zapier Platform CLI
2
2
 
3
- <h1 align="center">
4
- <img alt="Zapier Logo" src="https://cdn.zappy.app/1cd66b15407db2d9a01fbe8d600772fe.svg" width="300px">
5
- <br>
6
- Platform CLI
7
- <br>
8
- <br>
9
- </h1>
3
+ This is the `zapier` CLI, which you can use to build integrations with Zapier.
10
4
 
11
- <p align="center">
12
- <a href="https://www.npmjs.com/package/zapier-platform-cli"><img src="https://img.shields.io/npm/v/zapier-platform-cli.svg" alt="npm version"></a>
13
- </p>
5
+ For documentation:
14
6
 
15
- Zapier is a platform for creating integrations and workflows. This CLI is your gateway to creating custom applications on the Zapier platform.
7
+ - [Platform Docs](https://docs.zapier.com/platform)
8
+ - [CLI Reference](https://github.com/zapier/zapier-platform/blob/main/packages/cli/docs/cli.md)
9
+ - [Schema Reference](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md)
16
10
 
17
- You may find some documents on the Zapier site duplicate or outdated. The most up-to-date contents are always available on GitHub:
11
+ ## Development
18
12
 
19
- - [Latest CLI Docs](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md)
20
- - [Latest CLI Reference](https://github.com/zapier/zapier-platform/blob/main/packages/cli/docs/cli.md)
21
- - [Latest Schema Docs](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md)
22
-
23
- Our code is updated frequently. To see a full list of changes, look no further than [the CHANGELOG](https://github.com/zapier/zapier-platform/blob/main/CHANGELOG.md).
24
-
25
- This doc describes the latest CLI version (**15.17.0**), as of this writing. If you're using an older version of the CLI, you may want to check out these historical releases:
26
-
27
- - CLI Docs: [14.x](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@14.1.2/packages/cli/README.md), [13.x](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@13.0.0/packages/cli/README.md)
28
- - CLI Reference: [14.x](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@14.1.2/packages/cli/docs/cli.md), [13.x](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@13.0.0/packages/cli/docs/cli.md)
29
- - Schema Docs: [14.x](https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@14.1.2/packages/schema/docs/build/schema.md), [13.x](https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@13.0.0/packages/schema/docs/build/schema.md)
30
-
31
- ## Table of Contents
32
-
33
- <!-- toc -->
34
-
35
- - [Getting Started](#getting-started)
36
- * [What is an App?](#what-is-an-app)
37
- * [How does Zapier Platform CLI Work?](#how-does-zapier-platform-cli-work)
38
- * [Zapier Platform CLI vs UI](#zapier-platform-cli-vs-ui)
39
- * [Requirements](#requirements)
40
- * [Quick Setup Guide](#quick-setup-guide)
41
- * [Tutorial](#tutorial)
42
- - [Creating a Local App](#creating-a-local-app)
43
- * [Local Project Structure](#local-project-structure)
44
- * [Local App Definition](#local-app-definition)
45
- - [Registering an App](#registering-an-app)
46
- - [Deploying an App Version](#deploying-an-app-version)
47
- * [Private App Version (default)](#private-app-version-default)
48
- * [Sharing an App Version](#sharing-an-app-version)
49
- * [Promoting an App Version](#promoting-an-app-version)
50
- - [Converting an Existing App](#converting-an-existing-app)
51
- - [Authentication](#authentication)
52
- * [Basic](#basic)
53
- * [Digest](#digest)
54
- * [Custom](#custom)
55
- * [Session](#session)
56
- * [OAuth1](#oauth1)
57
- * [OAuth2](#oauth2)
58
- * [OAuth2 with PKCE](#oauth2-with-pkce)
59
- * [Connection Label](#connection-label)
60
- * [Domain and subdomain validation](#domain-and-subdomain-validation)
61
- - [Resources](#resources)
62
- * [Resource Definition](#resource-definition)
63
- - [Triggers/Searches/Creates](#triggerssearchescreates)
64
- * [Return Types](#return-types)
65
- + [Returning Line Items (Array of Objects)](#returning-line-items-array-of-objects)
66
- * [Fallback Sample](#fallback-sample)
67
- - [Input Fields](#input-fields)
68
- * [Custom/Dynamic Fields](#customdynamic-fields)
69
- * [Dynamic Dropdowns](#dynamic-dropdowns)
70
- * [Search-Powered Fields](#search-powered-fields)
71
- * [Computed Fields](#computed-fields)
72
- * [Nested & Children (Line Item) Fields](#nested--children-line-item-fields)
73
- - [Output Fields](#output-fields)
74
- * [Nested & Children (Line Item) Fields](#nested--children-line-item-fields-1)
75
- - [Buffered Create Actions](#buffered-create-actions)
76
- - [Z Object](#z-object)
77
- * [`z.request([url], options)`](#zrequesturl-options)
78
- * [`z.console`](#zconsole)
79
- * [`z.dehydrate(func, inputData)`](#zdehydratefunc-inputdata)
80
- * [`z.dehydrateFile(func, inputData)`](#zdehydratefilefunc-inputdata)
81
- * [`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`](#zstashfilebufferstringstream-knownlength-filename-contenttype)
82
- * [`z.JSON`](#zjson)
83
- * [`z.hash()`](#zhash)
84
- * [`z.errors`](#zerrors)
85
- * [`z.cursor`](#zcursor)
86
- * [`z.generateCallbackUrl()`](#zgeneratecallbackurl)
87
- - [Bundle Object](#bundle-object)
88
- * [`bundle.authData`](#bundleauthdata)
89
- * [`bundle.inputData`](#bundleinputdata)
90
- * [`bundle.inputDataRaw`](#bundleinputdataraw)
91
- * [`bundle.meta`](#bundlemeta)
92
- * [`bundle.rawRequest`](#bundlerawrequest)
93
- * [`bundle.cleanedRequest`](#bundlecleanedrequest)
94
- * [`bundle.outputData`](#bundleoutputdata)
95
- * [`bundle.targetUrl`](#bundletargeturl)
96
- * [`bundle.subscribeData`](#bundlesubscribedata)
97
- - [BufferedBundle Object](#bufferedbundle-object)
98
- * [`bufferedBundle.authData`](#bufferedbundleauthdata)
99
- * [`bufferedBundle.groupedBy`](#bufferedbundlegroupedby)
100
- * [`bufferedBundle.buffer`](#bufferedbundlebuffer)
101
- + [`bufferedBundle.buffer[].inputData`](#bufferedbundlebufferinputdata)
102
- + [`bufferedBundle.buffer[].meta`](#bufferedbundlebuffermeta)
103
- - [Environment](#environment)
104
- * [Defining Environment Variables](#defining-environment-variables)
105
- * [Accessing Environment Variables](#accessing-environment-variables)
106
- - [Adding Throttle Configuration](#adding-throttle-configuration)
107
- - [Making HTTP Requests](#making-http-requests)
108
- * [Shorthand HTTP Requests](#shorthand-http-requests)
109
- * [Manual HTTP Requests](#manual-http-requests)
110
- + [POST and PUT Requests](#post-and-put-requests)
111
- * [Using HTTP middleware](#using-http-middleware)
112
- + [Error Response Handling](#error-response-handling)
113
- * [HTTP Request Options](#http-request-options)
114
- * [HTTP Response Object](#http-response-object)
115
- - [Dehydration](#dehydration)
116
- * [Merging Hydrated Data](#merging-hydrated-data)
117
- * [File Dehydration](#file-dehydration)
118
- - [Stashing Files](#stashing-files)
119
- - [Logging](#logging)
120
- * [Console Logging](#console-logging)
121
- * [Viewing Console Logs](#viewing-console-logs)
122
- * [Viewing Bundle Logs](#viewing-bundle-logs)
123
- * [HTTP Logging](#http-logging)
124
- * [Viewing HTTP Logs](#viewing-http-logs)
125
- - [Error Handling](#error-handling)
126
- * [General Errors](#general-errors)
127
- * [Halting Execution](#halting-execution)
128
- * [Stale Authentication Credentials](#stale-authentication-credentials)
129
- * [Handling Throttled Requests](#handling-throttled-requests)
130
- - [Testing](#testing)
131
- * [Using `zapier invoke` Command](#using-zapier-invoke-command)
132
- * [Writing Unit Tests](#writing-unit-tests)
133
- * [Using the `z` Object in Tests](#using-the-z-object-in-tests)
134
- * [Mocking Requests](#mocking-requests)
135
- * [Running Unit Tests](#running-unit-tests)
136
- * [Testing & Environment Variables](#testing--environment-variables)
137
- * [Testing in Your CI](#testing-in-your-ci)
138
- * [Debugging Tests](#debugging-tests)
139
- - [Using `npm` Modules](#using-npm-modules)
140
- - [Building Native Packages with Docker](#building-native-packages-with-docker)
141
- - [Using Transpilers](#using-transpilers)
142
- - [FAQs](#faqs)
143
- * [Why doesn't Zapier support newer versions of Node.js?](#why-doesnt-zapier-support-newer-versions-of-nodejs)
144
- * [How do I manually set the Node.js version to run my app with?](#how-do-i-manually-set-the-nodejs-version-to-run-my-app-with)
145
- * [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies)
146
- * [Does Zapier support XML (SOAP) APIs?](#does-zapier-support-xml-soap-apis)
147
- * [Is it possible to iterate over pages in a polling trigger?](#is-it-possible-to-iterate-over-pages-in-a-polling-trigger)
148
- * [How do search-powered fields relate to dynamic dropdowns and why are they both required together?](#how-do-search-powered-fields-relate-to-dynamic-dropdowns-and-why-are-they-both-required-together)
149
- * [What's the deal with pagination? When is it used and how does it work?](#whats-the-deal-with-pagination-when-is-it-used-and-how-does-it-work)
150
- * [How does deduplication work?](#how-does-deduplication-work)
151
- * [Why are my triggers complaining if I don't provide an explicit `id` field?](#why-are-my-triggers-complaining-if-i-dont-provide-an-explicit-id-field)
152
- * [Node X No Longer Supported](#node-x-no-longer-supported)
153
- * [What Analytics are Collected?](#what-analytics-are-collected)
154
- * [What's the Difference Between an "App" and an "Integration"?](#whats-the-difference-between-an-app-and-an-integration)
155
- - [Command Line Tab Completion](#command-line-tab-completion)
156
- - [The Zapier Platform Packages](#the-zapier-platform-packages)
157
- * [Updating These Packages](#updating-these-packages)
158
- - [Get Help!](#get-help)
159
- - [Developing on the CLI](#developing-on-the-cli)
160
-
161
- <!-- tocstop -->
162
-
163
- ## Getting Started
164
-
165
- > If you're new to Zapier Platform CLI, we strongly recommend you to walk through the [Tutorial](https://platform.zapier.com/quickstart/cli-tutorial) for a more thorough introduction.
166
-
167
- ### What is an App?
168
-
169
- > Note: this document uses "app" while modern Zapier nomenclature refers instead to "integrations". In both cases, the phrase refers to your code that connects your API with Zapier.
170
-
171
- A CLI App is an implementation of your app's API. You build a Node.js application
172
- that exports a single object ([JSON Schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#appschema)) and upload it to Zapier.
173
- Zapier introspects that definition to find out what your app is capable of and
174
- what options to present end users in the Zap Editor.
175
-
176
- For those not familiar with Zapier terminology, here is how concepts in the CLI map to the end user experience:
177
-
178
- * [Authentication](#authentication), (usually) which lets us know what credentials to ask users
179
- for. This is used during the "Connect Accounts" section of the Zap Editor.
180
- * [Triggers](#triggerssearchescreates), which read data *from* your API. These have their own section in the Zap Editor.
181
- * [Creates](#triggerssearchescreates), which send data *to* your API to create new records. These are listed under "Actions" in the Zap Editor.
182
- * [Searches](#triggerssearchescreates), which find specific records *in* your system. These are also listed under "Actions" in the Zap Editor.
183
- * [Resources](#resources), which define an object type in your API (say a contact) and the operations available to perform on it. These are automatically extracted into Triggers, Searches, and Creates.
184
-
185
- ### How does Zapier Platform CLI Work?
186
-
187
- Zapier takes the App you upload and sends it over to Amazon Web Service's Lambda. We then make calls to execute the operations your App defines as we execute Zaps. Your App takes the input data we provide (if any), makes the necessary HTTP calls, and returns the relevant data, which gets fed back into Zapier.
188
-
189
- ### Zapier Platform CLI vs UI
190
-
191
- The Zapier Platform includes two ways to build integrations: a CLI (to build integrations in your local development environment and deploy them from the command line), and a Visual Builder (to create integrations with a visual builder from your browser). Both use the same underlying platform, so pick the one that fits your team's needs best. The main difference is how you make changes to your code.
192
-
193
- Zapier Platform CLI is designed to be used by development teams who collaborate with version control and CI, and can be used to build more advanced integrations with custom coding for every part of your API calls and response parsing.
194
-
195
- [Zapier Platform UI](https://developer.zapier.com/) is designed to quickly spin up new integrations, and collaborate on them with teams that include non-developers. It's the quickest way to start using the Zapier platform—and you can manage your CLI apps' core details from its online UI as well. You can also [export](https://platform.zapier.com/docs/export) Zapier Platform UI integrations to CLI to start development in your browser, then finish out the core features in your local development environment.
196
-
197
- > Learn more in our [Zapier Platform UI vs CLI](https://platform.zapier.com/docs/vs) post.
198
-
199
- ### Requirements
200
-
201
- All Zapier CLI apps are run using Node.js `v18`.
202
-
203
- You can develop using any version of Node you'd like, but your eventual code must be compatible with `v18`. If you're using features not yet available in `v18`, you can transpile your code to a compatible format with [Babel](https://babeljs.io/) (or similar).
204
-
205
- To ensure stability for our users, we strongly encourage you run tests on `v18` sometime before your code reaches users. This can be done multiple ways.
206
-
207
- Firstly, by using a CI tool (like [Travis CI](https://travis-ci.org/) or [Circle CI](https://circleci.com/), which are free for open source projects). We provide a sample [.travis.yml](https://github.com/zapier/zapier-platform/blob/main/example-apps/trigger/.travis.yml) file in our template apps to get you started.
208
-
209
- Alternatively, you can change your local node version with tools such as [nvm](https://github.com/nvm-sh/nvm#installation-and-update). Then you can either swap to that version with `nvm use v18`, or do `nvm exec v18 zapier test` so you can run tests without having to switch versions while developing.
210
-
211
-
212
- ### Quick Setup Guide
213
-
214
- First up is installing the CLI and setting up your auth to create a working "Zapier Example" application. It will be private to you and visible in your live [Zap Editor](https://zapier.com/app/editor).
215
-
216
- ```bash
217
- # install the CLI globally
218
- npm install -g zapier-platform-cli
219
-
220
- # setup auth to Zapier's platform with a deploy key
221
- zapier login
222
- ```
223
- > Note: If you log into Zapier via the single sign-on (Google, Facebook, or Microsoft), you may not have a Zapier password. If that's the case, you'll need to generate a deploy key, go to [your Zapier developer account here](https://developer.zapier.com/partner-settings/deploy-keys/) and create/copy a key, then run ```zapier login``` command with the --sso flag.
224
-
225
- Your Zapier CLI should be installed and ready to go at this point. Next up, we'll create our first app!
226
-
227
- ```bash
228
- # create a directory with the minimum required files
229
- zapier init example-app
230
-
231
- > Note: When you run `zapier init`, you'll be presented with a list of templates to start with. Pick the one that matches a feature you'll need (such as "dynamic-dropdown" for an integration with [dynamic dropdown fields](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#dynamic-dropdowns)), or select "minimal" for an integration with only the essentials. [View more example apps here](https://github.com/zapier/zapier-platform/tree/main/example-apps).
232
-
233
- # move into the new directory
234
- cd example-app
235
-
236
- # install all the libraries needed for your app
237
- npm install
238
- ```
239
- Depending on the authentication method for your app, you'll also likely need to set your `CLIENT_ID` and `CLIENT_SECRET` as environment variables. These are the consumer key and secret in OAuth1 terminology.
240
-
241
- ```bash
242
- # setting the environment variables on Zapier.com
243
- $ zapier env:set 1.0.0 CLIENT_ID=1234
244
- $ zapier env:set 1.0.0 CLIENT_SECRET=abcd
245
- ```
246
-
247
- You should now have a working local app. You can run several local commands to try it out.
248
-
249
- ```bash
250
- # run the local tests
251
- # the same as npm test, but adds some extra things to the environment
252
- zapier test
253
- ```
254
-
255
- Next, you'll probably want to upload app to Zapier itself so you can start testing live.
256
-
257
- ```bash
258
- # push your app to Zapier
259
- zapier push
260
- ```
261
-
262
- > Go check out our [full CLI reference documentation](https://github.com/zapier/zapier-platform/blob/main/packages/cli/docs/cli.md#zapier-cli-reference) to see all the other commands!
263
-
264
-
265
- ### Tutorial
266
-
267
- For a full tutorial, head over to our [Tutorial](https://platform.zapier.com/cli_tutorials/getting-started) for a comprehensive walkthrough for creating your first app. If this isn't your first rodeo, read on!
268
-
269
- ## Creating a Local App
270
-
271
- > Tip: Check the [Quick Setup](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#quick-setup-guide) if this is your first time using the platform!
272
-
273
- Creating an App can be done entirely locally and they are fairly simple Node.js apps using the standard Node environment and should be completely testable. However, a local app stays local until you `zapier register`.
274
-
275
- ```bash
276
- # make your folder
277
- mkdir zapier-example
278
- cd zapier-example
279
-
280
- # create the needed files from a template
281
- zapier init . --template minimal
282
-
283
- # install all the libraries needed for your app
284
- npm install
285
- ```
286
-
287
- If you'd like to manage your **local App**, use these commands:
288
-
289
- * `zapier init myapp` - initialize/start a local app project
290
- * `zapier convert 1234 .` - initialize/start from an existing app
291
- * `zapier scaffold resource Contact` - auto-injects a new resource, trigger, etc.
292
- * `zapier test` - run the same tests as `npm test`
293
- * `zapier validate` - ensure your app is valid
294
- * `zapier describe` - print some helpful information about your app
295
-
296
- ### Local Project Structure
297
-
298
- In your app's folder, you should see this general recommended structure. The `index.js` is Zapier's entry point to your app. Zapier expects you to export an `App` definition there.
299
-
300
- ```
301
- $ tree .
302
- .
303
- ├── README.md
304
- ├── index.js
305
- ├── package.json
306
- ├── triggers
307
- │   └── contact-by-tag.js
308
- ├── resources
309
- │   └── Contact.js
310
- ├── test
311
- │   ├── basic.js
312
- │   ├── triggers.js
313
- │   └── resources.js
314
- ├── build
315
- │   └── build.zip
316
- └── node_modules
317
- ├── ...
318
- └── ...
319
- ```
320
-
321
- ### Local App Definition
322
-
323
- The core definition of your `App` will look something like this, and is what your `index.js` should provide as the _only_ export:
324
-
325
- ```js
326
- const App = {
327
- // both version strings are required
328
- version: require('./package.json').version,
329
- platformVersion: require('zapier-platform-core').version,
330
-
331
- // see "Authentication" section below
332
- authentication: {},
333
-
334
- // see "Dehydration" section below
335
- hydrators: {},
336
-
337
- // see "Making HTTP Requests" section below
338
- requestTemplate: {},
339
- beforeRequest: [],
340
- afterResponse: [],
341
-
342
- // See "Resources" section below
343
- resources: {},
344
-
345
- // See "Triggers/Searches/Creates" section below
346
- triggers: {},
347
- searches: {},
348
- creates: {},
349
- };
350
-
351
- module.exports = App;
352
-
353
- ```
354
-
355
- > Tip: You can use higher order functions to create any part of your App definition!
356
-
357
-
358
- ## Registering an App
359
-
360
- Registering your App with Zapier is a necessary first step which only enables basic administrative functions. It should happen before `zapier push` which is to used to actually expose an App Version in the Zapier interface and editor.
361
-
362
- ```bash
363
- # register your app
364
- zapier register "Zapier Example"
365
-
366
- # list your apps
367
- zapier integrations
368
- ```
369
-
370
- > Note: This doesn't put your app in the editor - see the docs on pushing an App Version to do that!
371
-
372
- If you'd like to manage your **App**, use these commands:
373
-
374
- * `zapier integrations` - list the apps in Zapier you can administer
375
- * `zapier register "App Title"` - creates a new app in Zapier
376
- * `zapier link` - lists and links a selected app in Zapier to your current folder
377
- * `zapier history` - print the history of your app
378
- * `zapier team:add user@example.com admin` - add an admin to help maintain/develop your app
379
- * `zapier users:add user@example.com 1.0.0` - invite a user try your app version 1.0.0
380
-
381
- ## Deploying an App Version
382
-
383
- An App Version is related to a specific App but is an "immutable" implementation of your app. This makes it easy to run multiple versions for multiple users concurrently. The App Version is pulled from the version within the `package.json`. To create a new App Version, update the version number in that file. By default, **every App Version is private** but you can `zapier promote` it to production for use by over 1 million Zapier users.
384
-
385
- ```bash
386
- # push your app version to Zapier
387
- zapier push
388
-
389
- # list your versions
390
- zapier versions
391
- ```
392
-
393
- If you'd like to manage your **Version**, use these commands:
394
-
395
- * `zapier versions` - list the versions for the current directory's app
396
- * `zapier push` - push the current version of current directory's app & version (read from `package.json`)
397
- * `zapier promote 1.0.0` - mark a version as the "production" version
398
- * `zapier migrate 1.0.0 1.0.1 [100%]` - move users between versions, regardless of deployment status
399
- * `zapier deprecate 1.0.0 2020-06-01` - mark a version as deprecated, but let users continue to use it (we'll email them)
400
- * `zapier env:set 1.0.0 KEY=VALUE` - set an environment variable to some value
401
- * `zapier delete:version 1.0.0` - delete a version entirely. This is mostly for clearing out old test apps you used personally. It will fail if there are any users. You probably want `deprecate` instead.
402
-
403
- > Note: To see the changes that were just pushed reflected in the browser, you have to manually refresh the browser each time you push.
404
-
405
-
406
- ### Private App Version (default)
407
-
408
- A simple `zapier push` will only create the App Version in your editor. No one else using Zapier can see it or use it.
409
-
410
-
411
- ### Sharing an App Version
412
-
413
- This is how you would share your app with friends, co-workers or clients. This is perfect for quality assurance, testing with active users or just sharing any app you like.
414
-
415
- ```bash
416
- # sends an email this user to let them view the app version 1.0.0 in the UI privately
417
- zapier users:add user@example.com 1.0.0
418
-
419
- # sends an email this user to let them admin the app (make changes just like you)
420
- zapier team:add user@example.com
421
- ```
422
-
423
- You can also invite anyone on the internet to your app by using the links from `zapier users:links`. The link should look something like `https://zapier.com/platform/public-invite/1/222dcd03aed943a8676dc80e2427a40d/`. You can put this in your help docs, post it to Twitter, add it to your email campaign, etc. You can choose an invite link specific to an app version or for the entire app (i.e. all app versions).
424
-
425
- ### Promoting an App Version
426
-
427
- Promotion is how you would share your app with every one of the 1 million+ Zapier users. If this is your first time promoting - you may have to wait for the Zapier team to review and approve your app.
428
-
429
- If this isn't the first time you've promoted your app - you might have users on older versions. You can `zapier migrate` to either move users over (which can be dangerous if you have breaking changes). Or, you can `zapier deprecate` to give users some time to move over themselves.
430
-
431
- ```bash
432
- # promote your app version to all Zapier users
433
- zapier promote 1.0.1
434
-
435
- # OPTIONAL - migrate your users between one app version to another
436
- zapier migrate 1.0.0 1.0.1
437
-
438
- # OR - mark the old version as deprecated
439
- zapier deprecate 1.0.0 2020-06-01
440
- ```
441
-
442
- ## Converting an Existing App
443
-
444
- If you have an existing Zapier [legacy Web Builder app](https://platform.zapier.com/conversion/maintaining), you can use it as a template to kickstart your local application.
445
-
446
- ```bash
447
- # Convert an existing Web Builder app to a CLI app in the my-app directory
448
- # App ID 1234 is from URL https://zapier.com/developer/builder/app/1234/development
449
- zapier convert 1234 my-app
450
- ```
451
-
452
- Your CLI app will be created and you can continue working on it.
453
-
454
- > Note: There is no way to convert a CLI app to a Web Builder app and we do not plan on implementing this.
455
-
456
- Introduced in v8.2.0, you are able to convert new integrations built in Zapier Platform UI to CLI.
457
-
458
- ```bash
459
- # the --version flag is what denotes this command is interacting with a Visual Builder app
460
- # zapier convert <APP_ID> --version <APP_VERSION> <PATH>
461
- zapier convert 1234 --version 1.0.1 my-app
462
- ```
463
-
464
- ## Authentication
465
-
466
- Most applications require some sort of authentication. The Zapier platform provides core behaviors for several common authentication methods that might be used with your application, as well as the ability to customize authentication further.
467
-
468
- When a user authenticates to your application through Zapier, a "connection" is created representing their authentication details. Data tied to a specific authentication connection is included in the [bundle object](#bundle-object) under `bundle.authData`.
469
-
470
- ### Basic
471
-
472
- Useful if your app requires two pieces of information to authenticate: `username` and `password`, which only the end user can provide. By default, Zapier will do the standard Basic authentication base64 header encoding for you (via an automatically registered middleware).
473
-
474
- > To create a new integration with basic authentication, run `zapier init [your app name] --template basic-auth`. You can also review an example of that code [here](https://github.com/zapier/zapier-platform/tree/main/example-apps/basic-auth).
475
-
476
- If your app uses Basic auth with an encoded API key rather than a username and password, like `Authorization: Basic APIKEYHERE:x`, consider the [Custom](#custom) authentication method instead.
477
-
478
- ```js
479
- const authentication = {
480
- type: 'basic',
481
- // "test" could also be a function
482
- test: {
483
- url: 'https://example.com/api/accounts/me.json',
484
- },
485
- connectionLabel: '{{username}}', // Can also be a function, check digest auth below for an example
486
- // you can provide additional fields, but we'll provide `username`/`password` automatically
487
- };
488
-
489
- const App = {
490
- // ...
491
- authentication,
492
- // ...
493
- };
494
-
495
- ```
496
-
497
- ### Digest
498
-
499
- *Added in v7.4.0.*
500
-
501
- The setup and user experience of Digest Auth is identical to Basic Auth. Users provide Zapier their username and password, and Zapier handles all the nonce and quality of protection details automatically.
502
-
503
- > To create a new integration with digest authentication, run `zapier init [your app name] --template digest-auth`. You can also review an example of that code [here](https://github.com/zapier/zapier-platform/tree/main/example-apps/digest-auth).
504
-
505
- > Limitation: Currently, MD5-sess and SHA are not implemented. Only the MD5 algorithm is supported. In addition, server nonces are not reused. That means for every `z.request` call, Zapier will send an additional request beforehand to get the server nonce.
506
-
507
- ```js
508
- const getConnectionLabel = (z, bundle) => {
509
- // bundle.inputData will contain what the "test" URL (or function) returns
510
- return bundle.inputData.username;
511
- };
512
-
513
- const authentication = {
514
- type: 'digest',
515
- // "test" could also be a function
516
- test: {
517
- url: 'https://example.com/api/accounts/me.json',
518
- },
519
- connectionLabel: getConnectionLabel,
520
-
521
- // you can provide additional fields, but we'll provide `username`/`password` automatically
522
- };
523
-
524
- const App = {
525
- // ...
526
- authentication,
527
- // ...
528
- };
529
-
530
- ```
531
-
532
- ### Custom
533
-
534
- Custom auth is most commonly used for apps that authenticate with API keys, although it also provides flexibility for any unusual authentication setup. You'll likely provide some custom `beforeRequest` middleware or a `requestTemplate` (see [Making HTTP Requests](#making-http-requests)) to pass in data returned from the authentication process, most commonly by adding/computing needed headers.
535
-
536
- > To create a new integration with custom authentication, run `zapier init [your app name] --template custom-auth`. You can also review an example of that code [here](https://github.com/zapier/zapier-platform/tree/main/example-apps/custom-auth).
537
-
538
- ```js
539
- const authentication = {
540
- type: 'custom',
541
- // "test" could also be a function
542
- test: {
543
- url: 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json',
544
- },
545
- fields: [
546
- {
547
- key: 'subdomain',
548
- type: 'string',
549
- required: true,
550
- helpText: 'Found in your browsers address bar after logging in.',
551
- },
552
- {
553
- key: 'api_key',
554
- type: 'string',
555
- required: true,
556
- helpText: 'Found on your settings page.',
557
- },
558
- ],
559
- };
560
-
561
- const addApiKeyToHeader = (request, z, bundle) => {
562
- request.headers['X-Subdomain'] = bundle.authData.subdomain;
563
- const basicHash = Buffer.from(`${bundle.authData.api_key}:x`).toString(
564
- 'base64'
565
- );
566
- request.headers.Authorization = `Basic ${basicHash}`;
567
- return request;
568
- };
569
-
570
- const App = {
571
- // ...
572
- authentication,
573
- beforeRequest: [addApiKeyToHeader],
574
- // ...
575
- };
576
-
577
- ```
578
-
579
- ### Session
580
-
581
- Session auth gives you the ability to exchange some user-provided data for some authentication data; for example, username and password for a session key. It can be used to implement almost any authentication method that uses that pattern - for example, alternative OAuth flows.
582
-
583
- > To create a new integration with session authentication, run `zapier init [your app name] --template session-auth`. You can also review an example of that code [here](https://github.com/zapier/zapier-platform/tree/main/example-apps/session-auth).
584
-
585
- ```js
586
- const getSessionKey = async (z, bundle) => {
587
- const response = await z.request({
588
- method: 'POST',
589
- url: 'https://example.com/api/accounts/login.json',
590
- body: {
591
- username: bundle.authData.username,
592
- password: bundle.authData.password,
593
- },
594
- });
595
-
596
- // response.throwForStatus() if you're using core v9 or older
597
-
598
- return {
599
- sessionKey: response.data.sessionKey,
600
- // or response.json.sessionKey if you're using core v9 and older
601
- };
602
- };
603
-
604
- const authentication = {
605
- type: 'session',
606
- // "test" could also be a function
607
- test: {
608
- url: 'https://example.com/api/accounts/me.json',
609
- },
610
- fields: [
611
- {
612
- key: 'username',
613
- type: 'string',
614
- required: true,
615
- helpText: 'Your login username.',
616
- },
617
- {
618
- key: 'password',
619
- type: 'string',
620
- required: true,
621
- helpText: 'Your login password.',
622
- },
623
- // For Session Auth we store `sessionKey` automatically in `bundle.authData`
624
- // for future use. If you need to save/use something that the user shouldn't
625
- // need to type/choose, add a "computed" field, like:
626
- // {key: 'something': type: 'string', required: false, computed: true}
627
- // And remember to return it in sessionConfig.perform
628
- ],
629
- sessionConfig: {
630
- perform: getSessionKey,
631
- },
632
- };
633
-
634
- const includeSessionKeyHeader = (request, z, bundle) => {
635
- if (bundle.authData.sessionKey) {
636
- request.headers = request.headers || {};
637
- request.headers['X-Session-Key'] = bundle.authData.sessionKey;
638
- }
639
- return request;
640
- };
641
-
642
- const App = {
643
- // ...
644
- authentication,
645
- beforeRequest: [includeSessionKeyHeader],
646
- // ...
647
- };
648
-
649
- ```
650
-
651
- For Session auth, the function that fetches the additional authentication data needed to make API calls (`authentication.sessionConfig.perform`) has the user-provided fields in `bundle.inputData`. Afterwards, `bundle.authData` contains the data returned by that function (usually the session key or token).
652
-
653
- ### OAuth1
654
-
655
- *Added in `v7.5.0`.*
656
-
657
- Zapier's OAuth1 implementation matches [Twitter](https://developer.twitter.com/en/docs/tutorials/authenticating-with-twitter-api-for-enterprise/authentication-method-overview#oauth1.0a) and [Trello](https://developer.atlassian.com/cloud/trello/guides/rest-api/authorization/#using-basic-oauth) implementations of the 3-legged OAuth flow.
658
-
659
- > To create a new integration with OAuth1, run `zapier init [your app name] --template oauth1-trello`. You can also check out [oauth1-trello](https://github.com/zapier/zapier-platform/tree/main/example-apps/oauth1-trello), [oauth1-tumblr](https://github.com/zapier/zapier-platform/tree/main/example-apps/oauth1-tumblr), and [oauth1-twitter](https://github.com/zapier/zapier-platform/tree/main/example-apps/oauth1-twitter) for working example apps with OAuth1.
660
-
661
- The flow works like this:
662
-
663
- 1. Zapier makes a call to your API requesting a "request token" (also known as "temporary credentials").
664
- 2. Zapier sends the user to the authorization URL, defined by your app, along with the request token.
665
- 3. Once authorized, your website sends the user to the `redirect_uri` Zapier provided. Use `zapier describe` command to find out what it is: ![](https://zappy.zapier.com/117ECB35-5CCA-4C98-B74A-35F1AD9A3337.png)
666
- 4. Zapier makes a backend call to your API to exchange the request token for an "access token" (also known as "long-lived credentials").
667
- 5. Zapier stores the `access_token` and uses it to make calls on behalf of the user.
668
-
669
- You are required to define:
670
-
671
- * `getRequestToken`: The API call to fetch the request token
672
- * `authorizeUrl`: The authorization URL
673
- * `getAccessToken`: The API call to fetch the access token
674
-
675
- You'll also likely need to set your `CLIENT_ID` and `CLIENT_SECRET` as environment variables. These are the consumer key and secret in OAuth1 terminology.
676
-
677
- ```bash
678
- # setting the environment variables on Zapier.com
679
- $ zapier env:set 1.0.0 CLIENT_ID=1234
680
- $ zapier env:set 1.0.0 CLIENT_SECRET=abcd
681
-
682
- # and when running tests locally, don't forget to define them in .env or in the command!
683
- $ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
684
- ```
685
-
686
- Your auth definition would look something like this:
687
-
688
- ```js
689
- const _ = require('lodash');
690
-
691
- const authentication = {
692
- type: 'oauth1',
693
- oauth1Config: {
694
- getRequestToken: {
695
- url: 'https://{{bundle.inputData.subdomain}}.example.com/request-token',
696
- method: 'POST',
697
- auth: {
698
- oauth_consumer_key: '{{process.env.CLIENT_ID}}',
699
- oauth_consumer_secret: '{{process.env.CLIENT_SECRET}}',
700
-
701
- // 'HMAC-SHA1' is used by default if not specified.
702
- // 'HMAC-SHA256', 'RSA-SHA1', 'PLAINTEXT' are also supported.
703
- oauth_signature_method: 'HMAC-SHA1',
704
- oauth_callback: '{{bundle.inputData.redirect_uri}}',
705
- },
706
- },
707
- authorizeUrl: {
708
- url: 'https://{{bundle.inputData.subdomain}}.example.com/authorize',
709
- params: {
710
- oauth_token: '{{bundle.inputData.oauth_token}}',
711
- },
712
- },
713
- getAccessToken: {
714
- url: 'https://{{bundle.inputData.subdomain}}.example.com/access-token',
715
- method: 'POST',
716
- auth: {
717
- oauth_consumer_key: '{{process.env.CLIENT_ID}}',
718
- oauth_consumer_secret: '{{process.env.CLIENT_SECRET}}',
719
- oauth_token: '{{bundle.inputData.oauth_token}}',
720
- oauth_token_secret: '{{bundle.inputData.oauth_token_secret}}',
721
- oauth_verifier: '{{bundle.inputData.oauth_verifier}}',
722
- },
723
- },
724
- },
725
- test: {
726
- url: 'https://{{bundle.authData.subdomain}}.example.com/me',
727
- },
728
- // If you need any fields upfront, put them here
729
- fields: [
730
- { key: 'subdomain', type: 'string', required: true, default: 'app' },
731
- // For OAuth1 we store `oauth_token` and `oauth_token_secret` automatically
732
- // in `bundle.authData` for future use. If you need to save/use something
733
- // that the user shouldn't need to type/choose, add a "computed" field, like:
734
- // {key: 'user_id': type: 'string', required: false, computed: true}
735
- // And remember to return it in oauth1Config.getAccessToken
736
- ],
737
- };
738
-
739
- // A middleware that is run before z.request() actually makes the request. Here we're
740
- // adding necessary OAuth1 parameters to `auth` property of the request object.
741
- const includeAccessToken = (req, z, bundle) => {
742
- if (
743
- bundle.authData &&
744
- bundle.authData.oauth_token &&
745
- bundle.authData.oauth_token_secret
746
- ) {
747
- // Just put your OAuth1 credentials in req.auth, Zapier will sign the request for
748
- // you.
749
- req.auth = req.auth || {};
750
- _.defaults(req.auth, {
751
- oauth_consumer_key: process.env.CLIENT_ID,
752
- oauth_consumer_secret: process.env.CLIENT_SECRET,
753
- oauth_token: bundle.authData.oauth_token,
754
- oauth_token_secret: bundle.authData.oauth_token_secret,
755
- });
756
- }
757
- return req;
758
- };
759
-
760
- const App = {
761
- // ...
762
- authentication,
763
- beforeRequest: [includeAccessToken],
764
- // ...
765
- };
766
-
767
- module.exports = App;
768
-
769
- ```
770
-
771
- For OAuth1, `authentication.oauth1Config.getRequestToken`, `authentication.oauth1Config.authorizeUrl`, and `authentication.oauth1Config.getAccessToken` have fields like `redirect_uri` and the temporary credentials in `bundle.inputData`. After `getAccessToken` runs, the resulting token value(s) will be stored in `bundle.authData` for the connection.
772
-
773
- Also, `authentication.oauth1Config.getAccessToken` has access to the additional return values in `rawRequest` and `cleanedRequest` should you need to extract other values (for example, from the query string).
774
-
775
- ### OAuth2
776
-
777
- Zapier's OAuth2 implementation is based on the `authorization_code` flow, similar to [GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps) and [Facebook](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow).
778
-
779
- > To create a new integration with OAuth2, run `zapier init [your app name] --template oauth2`. You can also check out [our working example app](https://github.com/zapier/zapier-platform/tree/main/example-apps/oauth2).
780
-
781
- If your app's OAuth2 flow uses a different grant type, such as `client_credentials`, try using [Session auth](#session) instead.
782
-
783
- The OAuth2 flow looks like this:
784
-
785
- 1. Zapier sends the user to the authorization URL defined by your app.
786
- 2. Once authorized, your website sends the user to the `redirect_uri` Zapier provided. Use the `zapier describe` command to find out what it is: ![](https://zappy.zapier.com/83E12494-0A03-4DB4-AA46-5A2AF6A9ECCC.png)
787
- 3. Zapier makes a backend call to your API to exchange the `code` for an `access_token`.
788
- 4. Zapier stores the `access_token` and uses it to make calls on behalf of the user.
789
- 5. (Optionally) Zapier can refresh the token if it expires.
790
-
791
- You are required to define:
792
-
793
- * `authorizeUrl`: The authorization URL
794
- * `getAccessToken`: The API call to fetch the access token
795
-
796
- If the access token has a limited life and you want to refresh the token when it expires, you'll also need to define the API call to perform that refresh (`refreshAccessToken`). You can choose to set `autoRefresh: true`, as in the example app, if you want Zapier to automatically make a call to refresh the token after receiving a 401. See [Stale Authentication Credentials](#stale-authentication-credentials) for more details on handling auth refresh.
797
-
798
- You'll also likely want to set your `CLIENT_ID` and `CLIENT_SECRET` as environment variables:
799
-
800
- ```bash
801
- # setting the environment variables on Zapier.com
802
- $ zapier env:set 1.0.0 CLIENT_ID=1234
803
- $ zapier env:set 1.0.0 CLIENT_SECRET=abcd
804
-
805
- # and when running tests locally, don't forget to define them in .env or in the command!
806
- $ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
807
- ```
808
-
809
- Your auth definition would look something like this:
810
-
811
- ```js
812
- const authentication = {
813
- type: 'oauth2',
814
- test: {
815
- url: 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json',
816
- },
817
- // you can provide additional fields for inclusion in authData
818
- oauth2Config: {
819
- // "authorizeUrl" could also be a function returning a string url
820
- authorizeUrl: {
821
- method: 'GET',
822
- url: 'https://{{bundle.inputData.subdomain}}.example.com/api/oauth2/authorize',
823
- params: {
824
- client_id: '{{process.env.CLIENT_ID}}',
825
- state: '{{bundle.inputData.state}}',
826
- redirect_uri: '{{bundle.inputData.redirect_uri}}',
827
- response_type: 'code',
828
- },
829
- },
830
- // Zapier expects a response providing {access_token: 'abcd'}
831
- // "getAccessToken" could also be a function returning an object
832
- getAccessToken: {
833
- method: 'POST',
834
- url: 'https://{{bundle.inputData.subdomain}}.example.com/api/v2/oauth2/token',
835
- body: {
836
- code: '{{bundle.inputData.code}}',
837
- client_id: '{{process.env.CLIENT_ID}}',
838
- client_secret: '{{process.env.CLIENT_SECRET}}',
839
- redirect_uri: '{{bundle.inputData.redirect_uri}}',
840
- grant_type: 'authorization_code',
841
- },
842
- headers: {
843
- 'Content-Type': 'application/x-www-form-urlencoded',
844
- },
845
- },
846
- scope: 'read,write',
847
- },
848
- // If you need any fields upfront, put them here
849
- fields: [
850
- { key: 'subdomain', type: 'string', required: true, default: 'app' },
851
- // For OAuth2 we store `access_token` and `refresh_token` automatically
852
- // in `bundle.authData` for future use. If you need to save/use something
853
- // that the user shouldn't need to type/choose, add a "computed" field, like:
854
- // {key: 'user_id': type: 'string', required: false, computed: true}
855
- // And remember to return it in oauth2Config.getAccessToken/refreshAccessToken
856
- ],
857
- };
858
-
859
- const addBearerHeader = (request, z, bundle) => {
860
- if (bundle.authData && bundle.authData.access_token) {
861
- request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
862
- }
863
- return request;
864
- };
865
-
866
- const App = {
867
- // ...
868
- authentication,
869
- beforeRequest: [addBearerHeader],
870
- // ...
871
- };
872
-
873
- module.exports = App;
874
-
875
- ```
876
-
877
- For OAuth2, `authentication.oauth2Config.authorizeUrl`, `authentication.oauth2Config.getAccessToken`, and `authentication.oauth2Config.refreshAccessToken` have fields like `redirect_uri` and `state` in `bundle.inputData`. After the code is exchanged for an access token and/or refresh token, those tokens are stored in `bundle.authData` for the connection.
878
-
879
- Also, `authentication.oauth2Config.getAccessToken` has access to the additional return values in `rawRequest` and `cleanedRequest` should you need to extract other values (for example, from the query string).
880
-
881
- If you define `fields` to collect additional details from the user, please note that `client_id` and `client_secret` are reserved keys and cannot be used as keys for input form fields.
882
-
883
- > Note: The OAuth2 `state` param is a [standard security feature](https://auth0.com/docs/secure/attack-protection/state-parameters) that helps ensure that authorization requests are only coming from your servers. Most OAuth clients have support for this and will send back the `state` query param that the user brings to your app. The Zapier Platform performs this check and this required field cannot be disabled. The state parameter is automatically generated by Zapier in the background, and can be accessed at `bundle.inputData.state`.
884
- Since Zapier uses the `state` to verify that GET requests to your redirect URL truly come from your app, it needs to be generated by Zapier so that it can be validated later (once the user confirms that they'd like to grant Zapier permission to access their account in your app).
885
-
886
-
887
- ### OAuth2 with PKCE
888
-
889
- *Added in v14.0.0.*
890
-
891
- Zapier's OAuth2 implementation also supports [PKCE](https://oauth.net/2/pkce/). This implementation is an extension of the OAuth2 `authorization_code` flow described above.
892
-
893
- To use PKCE in your OAuth2 flow, you'll need to set the following variables:
894
- 1. `enablePkce: true`
895
- 2. `getAccessToken.body` to include `code_verifier: "{{bundle.inputData.code_verifier}}"`
896
-
897
- The OAuth2 PKCE flow uses the same flow as OAuth2 but adds a few extra parameters:
898
-
899
- 1. Zapier computes a `code_verifier` and `code_challenge` internally and stores the `code_verifier` in the Zapier bundle.
900
- 2. Zapier sends the user to the authorization URL defined by your app. We automatically include the computed `code_challenge` and `code_challenge_method` in the authorization request.
901
- 3. Once authorized, your website sends the user to the `redirect_uri` Zapier provided.
902
- 4. Zapier makes a call to your API to exchange the code but you must include the computed `code_verifier` in the request for an `access_token`.
903
- 5. Zapier stores the `access_token` and uses it to make calls on behalf of the user.
904
-
905
- Your auth definition would look something like this:
906
-
907
- ```js
908
- const authentication = {
909
- type: 'oauth2',
910
- test: {
911
- url: 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json',
912
- },
913
- // you can provide additional fields for inclusion in authData
914
- oauth2Config: {
915
- // "authorizeUrl" could also be a function returning a string url
916
- authorizeUrl: {
917
- method: 'GET',
918
- url: 'https://{{bundle.inputData.subdomain}}.example.com/api/oauth2/authorize',
919
- params: {
920
- client_id: '{{process.env.CLIENT_ID}}',
921
- state: '{{bundle.inputData.state}}',
922
- redirect_uri: '{{bundle.inputData.redirect_uri}}',
923
- response_type: 'code',
924
- },
925
- },
926
- // Zapier expects a response providing {access_token: 'abcd'}
927
- // "getAccessToken" could also be a function returning an object
928
- getAccessToken: {
929
- method: 'POST',
930
- url: 'https://{{bundle.inputData.subdomain}}.example.com/api/v2/oauth2/token',
931
- body: {
932
- code: '{{bundle.inputData.code}}',
933
- client_id: '{{process.env.CLIENT_ID}}',
934
- client_secret: '{{process.env.CLIENT_SECRET}}',
935
- redirect_uri: '{{bundle.inputData.redirect_uri}}',
936
- grant_type: 'authorization_code',
937
- code_verifier: '{{bundle.inputData.code_verifier}}', // Added for PKCE
938
- },
939
- headers: {
940
- 'Content-Type': 'application/x-www-form-urlencoded',
941
- },
942
- },
943
- scope: 'read,write',
944
- enablePkce: true, // Added for PKCE
945
- },
946
- // If you need any fields upfront, put them here
947
- fields: [
948
- { key: 'subdomain', type: 'string', required: true, default: 'app' },
949
- // For OAuth2 we store `access_token` and `refresh_token` automatically
950
- // in `bundle.authData` for future use. If you need to save/use something
951
- // that the user shouldn't need to type/choose, add a "computed" field, like:
952
- // {key: 'user_id': type: 'string', required: false, computed: true}
953
- // And remember to return it in oauth2Config.getAccessToken/refreshAccessToken
954
- ],
955
- };
956
-
957
- const addBearerHeader = (request, z, bundle) => {
958
- if (bundle.authData && bundle.authData.access_token) {
959
- request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
960
- }
961
- return request;
962
- };
963
-
964
- const App = {
965
- // ...
966
- authentication,
967
- beforeRequest: [addBearerHeader],
968
- // ...
969
- };
970
-
971
- module.exports = App;
972
-
973
- ```
974
-
975
- The computed `code_verifier` uses this standard: [RFC 7636 Code Verifier](https://www.rfc-editor.org/rfc/rfc7636#section-4.1)
976
-
977
- The computed `code_challenge` uses this standard: [RFC 7636 Code Challenge](https://www.rfc-editor.org/rfc/rfc7636#section-4.2)
978
-
979
- ### Connection Label
980
-
981
- When a user connects to your app via Zapier and a connection is created to hold the related data in `bundle.authData`, the connection is automatically labeled with the app name. You also have the option of setting a connection label (`connectionLabel`), which can be extremely helpful to identify information like which user is connected or what instance of your app they are connected to. That way, users don't get confused if they have multiple connections to your app.
982
-
983
- When setting a connection label, you can use either a string with variable references (as shown in [Basic Auth](#basic)) or a function (as shown in [Digest Auth](#digest)).
984
-
985
- When using a string, you have access to the information in `bundle.authData` and the information returned from the test request in `bundle.inputData`, all at the top level. So in Basic auth, if `connectionLabel` is `{{username}}`, that refers to the username used for authentication.
986
-
987
- When using a function, this "hoisting" of data to the top level is skipped, and you must refer to data items by their fully qualified name, as shown in the line `return bundle.inputData.username;` in the Digest Auth snippet. `return username;` would not work in this context.
988
-
989
- **NOTE:** Do not use sensitive authentication data such as passwords or API keys in the connection label. It's visible in plain text on Zapier. The purpose of the label is to identify the connection for the user, so stick with data such as username or instance identifier that is meaningful but not sensitive.
990
-
991
- ### Domain and subdomain validation
992
-
993
- When adding a subdomain input field, commonly used in OAuth implementations, additional validation is strongly recommended to prevent a potential security vulnerability. If not taken into account, an attacker could utilize a maliciously constructed subdomain field (like `attacker-domain.com/`) in order to redirect OAuth connection requests to that attacker-controlled domain (because `attacker-domain.com/.your-domain.com` resolves to the attacker’s domain instead of the expected one).
994
-
995
- This vulnerability presents itself when:
996
-
997
- - The authentication method uses pre-configured tokens or secret values (for example, OAuth v2)
998
- - User is able to input a domain or subdomain when authenticating within Zapier
999
- - Integration stores sensitive authentication details (in environment variables, for example) which are used as part of the authentication process
1000
-
1001
- Taking the following steps prevents the potential for an attacker to access your integration’s sensitive authentication information, such as the OAuth client ID or secret.
1002
-
1003
- 1. If your integration allows for the user to provide a domain, validate the input against an allow-list of trusted domains.
1004
-
1005
- 2. If your integration allows for the user to provide a subdomain, add conditional validation for the subdomain string whenever you include the value in your OAuth HTTP requests. This change will prevent potential exploitation of the subdomain vulnerability.
1006
-
1007
- - If you’re using OAuth-based authentications, update the `getAccessToken` and optional `refreshAccessToken` configuration methods. If the integration uses [shorthand HTTP requests](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#shorthand-http-requests), switching to [manual HTTP requests](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#manual-http-requests) will allow you to perform this manual subdomain validation.
1008
-
1009
- Example code for handling subdomain validation:
1010
-
1011
- ```js
1012
- const refreshAccessToken = async (z, bundle) => {
1013
- // --- UPDATE: add your validation for the subdomain field before using it ---
1014
- if (!/^[a-z0-9-]+$/.test(bundle.authData.yourSubdomainField)) {
1015
- throw new Error(
1016
- "Subdomain can only contain letters, numbers and dashes (-)."
1017
- );
1018
- }
1019
-
1020
-
1021
- const response = await z.request({
1022
- url: `https://${bundle.authData.yourSubdomainField}.mydomain.com/oauth/token`,
1023
- method: "POST",
1024
- body: {
1025
- client_id: process.env.CLIENT_ID,
1026
- client_secret: process.env.CLIENT_SECRET,
1027
- grant_type: "refresh_token",
1028
- refresh_token: bundle.authData.refresh_token,
1029
- redirect_uri: bundle.inputData.redirect_uri,
1030
- },
1031
- });
1032
-
1033
-
1034
- return {
1035
- access_token: response.data.access_token,
1036
- refresh_token: response.data.refresh_token,
1037
- };
1038
- };
1039
- ```
1040
-
1041
- ## Resources
1042
-
1043
- A `resource` is a representation (as a JavaScript object) of one of the REST resources of your API. Say you have a `/recipes`
1044
- endpoint for working with recipes; you can define a recipe resource in your app that will tell Zapier how to do create,
1045
- read, and search operations on that resource.
1046
-
1047
- ```js
1048
- const Recipe = {
1049
- // `key` is the unique identifier the Zapier backend references
1050
- key: 'recipe',
1051
- // `noun` is the user-friendly name displayed in the Zapier UI
1052
- noun: 'Recipe',
1053
- // `list` and `create` are just a couple of the methods you can define
1054
- list: {
1055
- // ...
1056
- },
1057
- create: {
1058
- // ...
1059
- },
1060
- };
1061
-
1062
- ```
1063
-
1064
- The quickest way to create a resource is with the `zapier scaffold` command:
1065
-
1066
- ```bash
1067
- zapier scaffold resource "Recipe"
1068
- ```
1069
-
1070
- This will generate the resource file and add the necessary statements to the `index.js` file to import it.
1071
-
1072
-
1073
- ### Resource Definition
1074
-
1075
- A resource has a few basic properties. The first is the `key`, which allows Zapier to identify the resource on our backend.
1076
- The second is the `noun`, the user-friendly name of the resource that is presented to users throughout the Zapier UI.
1077
-
1078
- > Check out [this working example app](https://github.com/zapier/zapier-platform/tree/main/example-apps/resource) to see resources in action.
1079
-
1080
- After those, there is a set of optional properties that tell Zapier what methods can be performed on the resource.
1081
- The complete list of available methods can be found in the [Resource Schema Docs](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#resourceschema).
1082
- For now, let's focus on two:
1083
-
1084
- * `list` - Tells Zapier how to fetch a set of this resource. This becomes a Trigger in the Zapier Editor.
1085
- * `create` - Tells Zapier how to create a new instance of the resource. This becomes an Action in the Zapier Editor.
1086
-
1087
- Here is a complete example of what the list method might look like
1088
-
1089
- ```js
1090
- const Recipe = {
1091
- key: 'recipe',
1092
- // ...
1093
- list: {
1094
- display: {
1095
- label: 'New Recipe',
1096
- description: 'Triggers when a new recipe is added.',
1097
- },
1098
- operation: {
1099
- perform: {
1100
- url: 'https://example.com/recipes',
1101
- },
1102
- },
1103
- },
1104
- };
1105
-
1106
- ```
1107
-
1108
- The method is made up of two properties, a `display` and an `operation`. The `display` property ([schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#basicdisplayschema)) holds the info needed to present the method as an available Trigger in the Zapier Editor. The `operation` ([schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#resourceschema)) provides the implementation to make the API call.
1109
-
1110
- Adding a create method looks very similar.
1111
-
1112
- ```js
1113
- const Recipe = {
1114
- key: 'recipe',
1115
- // ...
1116
- list: {
1117
- // ...
1118
- },
1119
- create: {
1120
- display: {
1121
- label: 'Add Recipe',
1122
- description: 'Adds a new recipe to our cookbook.',
1123
- },
1124
- operation: {
1125
- perform: {
1126
- method: 'POST',
1127
- url: 'https://example.com/recipes',
1128
- body: {
1129
- name: 'Baked Falafel',
1130
- style: 'mediterranean',
1131
- },
1132
- },
1133
- },
1134
- },
1135
- };
1136
-
1137
- ```
1138
-
1139
- Every method you define on a `resource` Zapier converts to the appropriate Trigger, Create, or Search. Our examples
1140
- above would result in an app with a New Recipe Trigger and an Add Recipe Create.
1141
-
1142
- Note the keys for the Trigger, Create, Search, and Search or Create are automatically generated (in case you want to use them in a dynamic dropdown), like: `{resourceName}List`, `{resourceName}Create`, `{resourceName}Search`, and `{resourceName}SearchOrCreate`; in the examples above, `{resourceName}` would be `recipe`.
1143
-
1144
-
1145
- ## Triggers/Searches/Creates
1146
-
1147
- Triggers, Searches, and Creates are the way an app defines what it is able to do. Triggers read
1148
- data into Zapier (i.e. watch for new recipes). Searches locate individual records (find recipe by title). Creates create
1149
- new records in your system (add a recipe to the catalog).
1150
-
1151
- The definition for each of these follows the same structure. Here is an example of a trigger:
1152
-
1153
- ```js
1154
- const App = {
1155
- // ...
1156
- triggers: {
1157
- new_recipe: {
1158
- key: 'new_recipe', // uniquely identifies the trigger
1159
- noun: 'Recipe', // user-friendly word that is used to refer to the resource
1160
- // `display` controls the presentation in the Zapier Editor
1161
- display: {
1162
- label: 'New Recipe',
1163
- description: 'Triggers when a new recipe is added.',
1164
- },
1165
- // `operation` implements the API call used to fetch the data
1166
- operation: {
1167
- perform: {
1168
- url: 'https://example.com/recipes',
1169
- },
1170
- },
1171
- },
1172
- another_trigger: {
1173
- // Another trigger definition...
1174
- },
1175
- },
1176
- };
1177
-
1178
- ```
1179
-
1180
- You can find more details on the definition for each by looking at the [Trigger Schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#triggerschema),
1181
- [Search Schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#searchschema), and [Create Schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#createschema).
1182
-
1183
- > To create a new integration with a premade trigger, search, or create, run `zapier init [your app name]` and select from the list that appears. You can also check out our working example apps [here](https://github.com/zapier/zapier-platform/tree/main/example-apps).
1184
-
1185
- > To add a trigger, search, or create to an existing integration, run `zapier scaffold [trigger|search|create] [noun]` to create the necessary files to your project. For example, `zapier scaffold trigger post` will create a new trigger called "New Post".
1186
- ### Return Types
1187
-
1188
- Each of the 3 types of function should return a certain data type for use by the platform. There are automated checks to let you know when you're trying to pass the wrong type back. For reference, each expects:
1189
-
1190
- | Method | Return Type | Notes |
1191
- |---------|-------------|----------------------------------------------------------------------------------------------------------------------|
1192
- | Trigger | Array | 0 or more objects; passed to the [deduper](https://platform.zapier.com/docs/dedupe/) if polling |
1193
- | Search | Array | 0 or more objects. Only the first object will be returned, so if len > 1, put the best match first |
1194
- | Create | Object | Return values are evaluated by [`isPlainObject`](https://lodash.com/docs#isPlainObject) |
1195
-
1196
- When a trigger function returns an empty array, the Zap will not trigger. For REST Hook triggers, this can be used to filter data if the available subscription options are not specific enough for the Zap's needs.
1197
-
1198
- #### Returning Line Items (Array of Objects)
1199
-
1200
- In some cases, you may want to include multiple items in the data you return for Searches or Creates. To do that, return the set of items as an array of objects under a descriptive key. This may be as part of another object (like items in an invoice) or as multiple top-level items.
1201
-
1202
- For example, a Create Order action returning an order with multiple items might look like this:
1203
-
1204
- ```
1205
- order = {
1206
- name: 'Zap Zaplar',
1207
- total_cost: 25.96,
1208
- items: [
1209
- { name: 'Zapier T-Shirt', unit_price: 11.99, quantity: 3, line_amount: 35.97, category: 'shirts' },
1210
- { name: 'Orange Widget', unit_price: 7.99, quantity: 10, line_amount: 79.90, category: 'widgets' },
1211
- { name:'Stuff', unit_price: 2.99, quantity: 7, line_amount: 20.93, category: 'stuff' },
1212
- { name: 'Allbird Shoes', unit_price: 2.99, quantity: 7, line_amount: 20.93, category: 'shoes' },
1213
- ],
1214
- zip: 01002
1215
- }
1216
- ```
1217
-
1218
- While a Find Users search could return multiple items under an object key within an array, like this:
1219
-
1220
- ```
1221
- result = [{
1222
- users: [
1223
- { name: 'Zap Zaplar', age: 12, city: 'Columbia', region: 'Missouri' },
1224
- { name: 'Orange Crush', age: 28, city: 'West Ocean City', region: 'Maryland' },
1225
- { name: 'Lego Brick', age: 91, city: 'Billund', region: 'Denmark' },
1226
- ],
1227
- }];
1228
- ```
1229
-
1230
- A standard search would return just the inner array of users, and only the first user would be provided as a final result. Returning line items instead means that the "first result" return is the object containing all the user details within it.
1231
-
1232
- Using the standard approach is recommended, because not all Zapier integrations support line items directly, so users may need to take additional actions to reformat this data for use in their Zaps. More detail on that at [Use line items in Zaps](https://zapier.com/help/create/basics/use-line-items-in-zaps). However, there are use cases where returning multiple results is helpful enough to outweigh that additional effort.
1233
-
1234
- ### Fallback Sample
1235
- In cases where Zapier needs to show an example record to the user, but we are unable to get a live example from the API, Zapier will fallback to this hard-coded sample. This should reflect the data structure of the Trigger's perform method, and have dummy values that we can show to any user.
1236
-
1237
- ```js
1238
- ,sample: {
1239
- dummydata_field1: 'This will be compared against your perform method output'
1240
- style: 'mediterranean'
1241
- }
1242
- ```
1243
-
1244
- ## Input Fields
1245
-
1246
- On each trigger, search, or create in the `operation` directive, you can provide fields as an array of objects under `inputFields`. Input Fields are what your users see in Zapier when setting up your app's triggers and actions. For example, you might have a "Create Contact" action with fields like "First name", "Last name", "Email", etc. These fields will be able to accept input from the user, or from previous steps in a Zap. For example:
1247
-
1248
- ![gif of setting up an action field in Zap Editor](https://cdn.zappy.app/52721a3cb202446b7c298e303b710471.gif)
1249
-
1250
- You can find more details about setting action fields from a user perspective in [our help documentation](https://zapier.com/help/creating-zap/).
1251
-
1252
- Those fields have various options you can provide. Here is a brief example:
1253
-
1254
- ```js
1255
- const App = {
1256
- // ...
1257
- creates: {
1258
- create_recipe: {
1259
- // ...
1260
- operation: {
1261
- // an array of objects is the simplest way
1262
- inputFields: [
1263
- {
1264
- key: 'title',
1265
- required: true,
1266
- label: 'Title of Recipe',
1267
- helpText: 'Name your recipe!',
1268
- },
1269
- {
1270
- key: 'style',
1271
- required: true,
1272
- choices: { mexican: 'Mexican', italian: 'Italian' },
1273
- },
1274
- ],
1275
- perform: () => {},
1276
- },
1277
- },
1278
- },
1279
- };
1280
-
1281
- ```
1282
-
1283
- Notably, fields come in different types, which may look and act differently in the Zap editor. The default field display is a single-line input field.
1284
-
1285
- | Type | Behavior |
1286
- |------|----------|
1287
- | `string` | Accepts text input. |
1288
- | `text` | Displays large, `<textarea>`-style entry box, accepts text input. |
1289
- | `code` | Displays large, `<textarea>`-style box with a fixed-width font, accepts text input. |
1290
- | `integer` | Accepts integer number values. |
1291
- | `number` | Accepts any numeric value, including decimal numbers. |
1292
- | `boolean` | Displays dropdown menu offering true and false options. Passes along `true` or `false`. |
1293
- | `datetime` | Accepts both [precise and human-readable date-time values](https://help.zapier.com/hc/en-us/articles/8496259603341-Different-field-types-in-Zaps#date-time-fields-0-0). Passes along an ISO-formatted time string. |
1294
- | `file` | Accepts a file object or a string. If a URL is provided in the string, Zapier will automatically make a GET for that file. Otherwise, a text file will be generated. |
1295
- | `password` | Displays entered characters as hidden, accepts text input. Does not accept input from previous steps. |
1296
- | `copy` | Does not allow users to enter data. Shows the value of the Markdown-formatted Help Text for the field as a rich text note in the Zap editor. Good for important notices to users. |
1297
-
1298
- You can find more details on the different field schema options at [our Field Schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#fieldschema).
1299
-
1300
- ### Custom/Dynamic Fields
1301
-
1302
- In some cases, you may need to provide dynamically-generated fields - especially for custom ones. This is common functionality for CRMs, form software, databases, and other highly-customizable platforms. Instead of an explicit field definition, you can provide a function we'll evaluate to return a list of fields - merging the dynamic with the static fields.
1303
-
1304
- > You should see `bundle.inputData` partially filled in as users provide data - even in field retrieval. This allows you to build hierarchical relationships into fields (e.g. only show issues from the previously selected project).
1305
-
1306
- > A function that returns a list of dynamic fields cannot include additional functions in that list to call for dynamic fields.
1307
-
1308
- ```js
1309
- const recipeFields = async (z, bundle) => {
1310
- const response = await z.request('https://example.com/api/v2/fields.json');
1311
-
1312
- // Call response.throwForStatus() if you're using zapier-platform-core v9 or older
1313
-
1314
- // Should return an array like [{"key":"field_1"},{"key":"field_2"}]
1315
- return response.data; // response.json if you're using core v9 or older
1316
- };
1317
-
1318
- const App = {
1319
- // ...
1320
- creates: {
1321
- create_recipe: {
1322
- // ...
1323
- operation: {
1324
- // an array of objects is the simplest way
1325
- inputFields: [
1326
- {
1327
- key: 'title',
1328
- required: true,
1329
- label: 'Title of Recipe',
1330
- helpText: 'Name your recipe!',
1331
- },
1332
- {
1333
- key: 'style',
1334
- required: true,
1335
- choices: { mexican: 'Mexican', italian: 'Italian' },
1336
- },
1337
- recipeFields, // provide a function inline - we'll merge the results!
1338
- ],
1339
- perform: () => {},
1340
- },
1341
- },
1342
- },
1343
- };
1344
-
1345
- ```
1346
-
1347
- Additionally, if there is a field that affects the generation of dynamic fields, you can set the property `altersDynamicFields: true`. This informs the Zapier UI whenever the value of that field changes, the input fields need to be recomputed. For example, imagine the selection on a static dropdown called "Dessert Type" determining whether the function generating dynamic fields includes the field "With Sprinkles?" or not. If the value in one input field affects others, this is an important property to set.
1348
-
1349
- ```js
1350
- module.exports = {
1351
- key: 'dessert',
1352
- noun: 'Dessert',
1353
- display: {
1354
- label: 'Order Dessert',
1355
- description: 'Orders a dessert.',
1356
- },
1357
- operation: {
1358
- inputFields: [
1359
- {
1360
- key: 'type',
1361
- required: true,
1362
- choices: { 1: 'cake', 2: 'ice cream', 3: 'cookie' },
1363
- altersDynamicFields: true,
1364
- },
1365
- function (z, bundle) {
1366
- if (bundle.inputData.type === '2') {
1367
- return [{ key: 'with_sprinkles', type: 'boolean' }];
1368
- }
1369
- return [];
1370
- },
1371
- ],
1372
- perform: function (z, bundle) {
1373
- /* ... */
1374
- },
1375
- },
1376
- };
1377
-
1378
- ```
1379
-
1380
- > Only dropdowns support `altersDynamicFields`.
1381
-
1382
- When using dynamic fields, the fields will be retrieved in three different contexts:
1383
-
1384
- * Whenever the value of a field with `altersDynamicFields` is changed, as described above.
1385
- * Whenever the Zap Editor opens the "Set up" section for the trigger or action.
1386
- * Whenever the "Refresh fields" button at the bottom of the Editor's "Set up" section is clicked.
1387
-
1388
- Be sure to set up your code accordingly - for example, don't rely on any input fields already having a value, since they won't have one the first time the "Set up" section loads.
1389
-
1390
- ### Dynamic Dropdowns
1391
-
1392
- Sometimes, API endpoints require clients to specify a parent object in order to create or access the child resources. For instance, specifying a spreadsheet id in order to retrieve its worksheets. Since people don't speak in auto-incremented ID's, it is necessary that Zapier offer a simple way to select that parent using human readable handles.
1393
-
1394
- Our solution is to present users a dropdown that is populated by making a live API call to fetch a list of parent objects. We call these special dropdowns "dynamic dropdowns."
1395
-
1396
- To define one you include the `dynamic` property on the `inputFields` object. The value for the property is a dot-separated _string_ concatenation.
1397
-
1398
- ```js
1399
- //...
1400
- issue: {
1401
- key: 'issue',
1402
- //...
1403
- create: {
1404
- //...
1405
- operation: {
1406
- inputFields: [
1407
- {
1408
- key: 'project_id',
1409
- required: true,
1410
- label: 'This is a dynamic dropdown',
1411
- dynamic: 'project.id.name'
1412
- }, // will call the trigger with a key of project
1413
- {
1414
- key: 'title',
1415
- required: true,
1416
- label: 'Title',
1417
- helpText: 'What is the name of the issue?'
1418
- }
1419
- ]
1420
- }
1421
- }
1422
- }
1423
-
1424
- ```
1425
-
1426
- The dot-separated string concatenation follows this pattern:
1427
-
1428
- - The key of the trigger you want to use to power the dropdown. _required_
1429
- - The value to be made available in bundle.inputData. _required_
1430
- - The human friendly value to be shown on the left of the dropdown in bold. _optional_
1431
-
1432
- In the above code example the dynamic property makes reference to a trigger with a key of project. Assuming the project trigger returns an array of objects and each object contains an id and name key, i.e.
1433
-
1434
- ```js
1435
- [
1436
- { id: '1', name: 'First Option', dateCreated: '01/01/2000' },
1437
- { id: '2', name: 'Second Option', dateCreated: '01/01/2000' },
1438
- { id: '3', name: 'Third Option', dateCreated: '01/01/2000' },
1439
- { id: '4', name: 'Fourth Option', dateCreated: '01/01/2000' },
1440
- ];
1441
-
1442
- ```
1443
-
1444
- The dynamic dropdown would look something like this.
1445
- ![screenshot of dynamic dropdown in Zap Editor](https://cdn.zappy.app/6a90fcc532704f6c14b91586f5cd1d5b.png)
1446
-
1447
- In the first code example the dynamic dropdown is powered by a trigger. You can also use a resource to power a dynamic dropdown. To do this combine the resource key and the resource method using camel case.
1448
-
1449
- ```js
1450
- const App = {
1451
- // ...
1452
- resources: {
1453
- project: {
1454
- key: 'project',
1455
- // ...
1456
- list: {
1457
- // ...
1458
- operation: {
1459
- perform: () => {
1460
- return [{ id: 123, name: 'Project 1' }];
1461
- }, // called for project_id dropdown
1462
- },
1463
- },
1464
- },
1465
- issue: {
1466
- key: 'issue',
1467
- // ...
1468
- create: {
1469
- // ...
1470
- operation: {
1471
- inputFields: [
1472
- {
1473
- key: 'project_id',
1474
- required: true,
1475
- label: 'Project',
1476
- dynamic: 'projectList.id.name',
1477
- }, // calls project.list
1478
- {
1479
- key: 'title',
1480
- required: true,
1481
- label: 'Title',
1482
- helpText: 'What is the name of the issue?',
1483
- },
1484
- ],
1485
- },
1486
- },
1487
- },
1488
- },
1489
- };
1490
-
1491
- ```
1492
-
1493
- In some cases you will need to power a dynamic dropdown but do not want to make the Trigger available to the end user. Here it is best practice to create the trigger and set `hidden: true` on it's display object.
1494
-
1495
- ```js
1496
- const App = {
1497
- // ...
1498
- triggers: {
1499
- new_project: {
1500
- key: 'project',
1501
- noun: 'Project',
1502
- // `display` controls the presentation in the Zapier Editor
1503
- display: {
1504
- label: 'New Project',
1505
- description: 'Triggers when a new project is added.',
1506
- hidden: true,
1507
- },
1508
- operation: {
1509
- perform: projectListRequest,
1510
- },
1511
- },
1512
- another_trigger: {
1513
- // Another trigger definition...
1514
- },
1515
- },
1516
- };
1517
-
1518
- ```
1519
-
1520
- You can have multiple dynamic dropdowns in a single Trigger or Action. And a dynamic dropdown can depend on the value chosen in another dynamic dropdown when making it's API call. Such as a Spreadsheet and Worksheet dynamic dropdown in a trigger or action. This means you must make sure that the key of the first dynamic dropdown is the same as referenced in the trigger powering the second.
1521
-
1522
- Let's say you have a Worksheet trigger with a `perform` method similar to this.
1523
-
1524
- ```js
1525
- perform: async (z, bundle) => {
1526
- const response = await z.request('https://example.com/api/v2/projects.json', {
1527
- params: {
1528
- spreadsheet_id: bundle.inputData.spreadsheet_id,
1529
- },
1530
- });
1531
-
1532
- // response.throwForStatus() if you're using core v9 or older
1533
-
1534
- return response.data; // or response.json if you're using core v9 or older
1535
- };
1536
-
1537
- ```
1538
-
1539
- And your New Records trigger has a Spreadsheet and a Worksheet dynamic dropdown. The Spreadsheet dynamic dropdown must have a key of `spreadsheet_id`. When the user selects a spreadsheet via the dynamic dropdown the value chosen is made available in `bundle.inputData`. It will then be passed to the Worksheet trigger when the user clicks on the Worksheet dynamic dropdown.
1540
-
1541
- ```js
1542
- const App = {
1543
- // ...
1544
- triggers: {
1545
- // ...
1546
- issue: {
1547
- key: 'new_records',
1548
- // ...
1549
- operation: {
1550
- inputFields: [
1551
- {
1552
- key: 'spreadsheet_id',
1553
- required: true,
1554
- label: 'Spreadsheet',
1555
- dynamic: 'spreadsheet.id.name',
1556
- },
1557
- {
1558
- key: 'worksheet_id',
1559
- required: true,
1560
- label: 'Worksheet',
1561
- dynamic: 'worksheet.id.name',
1562
- },
1563
- ],
1564
- },
1565
- },
1566
- },
1567
- };
1568
-
1569
- ```
1570
-
1571
- The [Google Sheets](https://zapier.com/apps/google-sheets/integrations#triggers-and-actions) integration is an example of this pattern.
1572
-
1573
- If you want your trigger to perform specific scripting for a dynamic dropdown you will need to make use of `bundle.meta.isFillingDynamicDropdown`. This can be useful if need to make use of [pagination](#whats-the-deal-with-pagination-when-is-it-used-and-how-does-it-work) in the dynamic dropdown to load more options.
1574
-
1575
- ```js
1576
- const App = {
1577
- // ...
1578
- resources: {
1579
- project: {
1580
- key: 'project',
1581
- // ...
1582
- list: {
1583
- // ...
1584
- operation: {
1585
- canPaginate: true,
1586
- perform: () => {
1587
- if (bundle.meta.isFillingDynamicDropdown) {
1588
- // perform pagination request here
1589
- } else {
1590
- return [{ id: 123, name: 'Project 1' }];
1591
- }
1592
- },
1593
- },
1594
- },
1595
- },
1596
- issue: {
1597
- key: 'issue',
1598
- // ...
1599
- create: {
1600
- // ...
1601
- operation: {
1602
- inputFields: [
1603
- {
1604
- key: 'project_id',
1605
- required: true,
1606
- label: 'Project',
1607
- dynamic: 'projectList.id.name',
1608
- }, // calls project.list
1609
- {
1610
- key: 'title',
1611
- required: true,
1612
- label: 'Title',
1613
- helpText: 'What is the name of the issue?',
1614
- },
1615
- ],
1616
- },
1617
- },
1618
- },
1619
- },
1620
- };
1621
-
1622
- ```
1623
-
1624
- ### Search-Powered Fields
1625
-
1626
- For fields that take id of another object to create a relationship between the two (EG: a project id for a ticket), you can specify the `search` property on the field to indicate that Zapier needs to prompt the user to setup a Search step to populate the value for this field. Similar to dynamic dropdowns, the value for this property is a dot-separated concatenation of a search's key and the field to use for the value.
1627
-
1628
- ```js
1629
- const App = {
1630
- // ...
1631
- resources: {
1632
- project: {
1633
- key: 'project',
1634
- // ...
1635
- search: {
1636
- // ...
1637
- operation: {
1638
- perform: () => {
1639
- return [{ id: 123, name: 'Project 1' }];
1640
- }, // called for project_id
1641
- },
1642
- },
1643
- },
1644
- issue: {
1645
- key: 'issue',
1646
- // ...
1647
- create: {
1648
- // ...
1649
- operation: {
1650
- inputFields: [
1651
- {
1652
- key: 'project_id',
1653
- required: true,
1654
- label: 'Project',
1655
- dynamic: 'projectList.id.name',
1656
- search: 'projectSearch.id',
1657
- }, // calls project.search (requires a trigger in the "dynamic" property)
1658
- {
1659
- key: 'title',
1660
- required: true,
1661
- label: 'Title',
1662
- helpText: 'What is the name of the issue?',
1663
- },
1664
- ],
1665
- },
1666
- },
1667
- },
1668
- },
1669
- };
1670
-
1671
- ```
1672
-
1673
- **NOTE:** This has to be combined with the `dynamic` property to give the user a guided experience when setting up a Zap.
1674
-
1675
- If you don't define a trigger for the `dynamic` property, the search connector won't show.
1676
-
1677
- ### Computed Fields
1678
-
1679
- In OAuth and Session Auth, Zapier automatically stores every value from an integration’s auth API response i.e. that’s `getAccessToken` and `refreshAccessToken` for OAuth and `getSessionKey` for session auth.
1680
-
1681
- You can return additional fields in these responses, on top of the expected `access_token` or `refresh_token` for OAuth and `sessionKey` for Session auth. They will be saved in `bundle.authData`. You can reference these fields in any subsequent API call as needed.
1682
-
1683
- > Note: Only OAuth and Session Auth support computed fields.
1684
-
1685
- If you want Zapier to validate that these additional fields exist, you need to use Computed Fields. If you define computed fields in your integration, Zapier will check to make sure those fields exist when it runs the authentication test API call.
1686
-
1687
- Computed fields work like any other field, though with `computed: true` property, and `required: false` as user can not enter computed fields themselves. Reference computed fields in API calls as `{{bundle.authData.field}}`, replacing `field` with that field's name from your test API call response.
1688
-
1689
- You can see examples of computed fields in the [OAuth2](#oauth2) or [Session Auth](#session) example sections.
1690
-
1691
- ### Nested & Children (Line Item) Fields
1692
-
1693
- When your action needs to accept an array of items, you can include an input field with the `children` attribute. The `children` attribute accepts a list of [fields](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#fieldschema) that can be input for each item in this array.
1694
-
1695
- ```js
1696
- const App = {
1697
- // ...
1698
- operation: {
1699
- // ...
1700
- inputFields: [
1701
- {
1702
- key: 'lineItems',
1703
- children: [
1704
- {
1705
- key: 'lineItemId',
1706
- type: 'integer',
1707
- label: 'Line Item ID',
1708
- required: true,
1709
- },
1710
- {
1711
- key: 'name',
1712
- type: 'string',
1713
- label: 'Name',
1714
- required: true,
1715
- },
1716
- {
1717
- key: 'description',
1718
- type: 'string',
1719
- label: 'Description',
1720
- },
1721
- ],
1722
- },
1723
- ],
1724
- // ...
1725
- },
1726
- };
1727
-
1728
- ```
1729
-
1730
- ## Output Fields
1731
-
1732
- On each trigger, search, or create in the operation directive - you can provide an array of objects as fields under the `outputFields`. Output Fields are what users see when they select a field provided by your trigger, search or create to map it to another.
1733
-
1734
- Output Fields are optional, but can be used to:
1735
-
1736
- - Define friendly labels for the returned fields. By default, we will *humanize* for example `my_key` as *My Key*.
1737
- - Make sure that custom fields that may not be found in every live sample and - since they're custom to the connected account - cannot be defined in the static sample, can still be mapped.
1738
- - (Added in v15.6.0) Define what field(s) can be used to uniquely identify and [deduplicate](#how-does-deduplication-work) items returned by a polling trigger call.
1739
-
1740
- The [schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#fieldschema) for `outputFields` is shared with `inputFields` but only these properties are relevant:
1741
-
1742
- - `key` - includes the field when not present in the live sample. When no `label` property is provided, `key` will be *humanized* and displayed as the field name.
1743
- - `label` - defines the field name displayed to users.
1744
- - `type` - defines the type for static sample data. A [validation warning](https://platform.zapier.com/docs/integration-checks-reference#d024---static-sample-respects-output-field-definition) will be displayed if the static sample does not match the specified type.
1745
- - `required` - defines whether the field is required in static sample data. A [validation warning](https://platform.zapier.com/docs/integration-checks-reference#d024---static-sample-respects-output-field-definition) will be displayed if the value is true and the static sample does not contain the field.
1746
- - `primary` - defines whether the field is part of the primary key for polling trigger [deduplication](#how-does-deduplication-work).
1747
-
1748
- Custom/Dynamic Output Fields are defined in the same way as [Custom/Dynamic Input Fields](#customdynamic-fields).
1749
-
1750
- ### Nested & Children (Line Item) Fields
1751
-
1752
- To define an Output Field for a nested field use `{{parent}}__{{key}}`. For children (line item) fields use `{{parent}}[]{{key}}`.
1753
-
1754
- ```js
1755
- const recipeOutputFields = async (z, bundle) => {
1756
- const response = await z.request('https://example.com/api/v2/fields.json');
1757
-
1758
- // response.throwForStatus() if you're using core v9 or older
1759
-
1760
- // Should return an array like [{"key":"field_1","label":"Label for Custom Field"}]
1761
- return response.data; // or response.json if you're on core v9 or older
1762
- };
1763
-
1764
- const App = {
1765
- // ...
1766
- triggers: {
1767
- new_recipe: {
1768
- // ...
1769
- operation: {
1770
- perform: () => {},
1771
- sample: {
1772
- id: 1,
1773
- title: 'Pancake',
1774
- author: {
1775
- id: 1,
1776
- name: 'Amy',
1777
- },
1778
- ingredients: [
1779
- {
1780
- name: 'Egg',
1781
- amount: 1,
1782
- },
1783
- {
1784
- name: 'Milk',
1785
- amount: 60,
1786
- unit: 'g',
1787
- },
1788
- {
1789
- name: 'Flour',
1790
- amount: 30,
1791
- unit: 'g',
1792
- },
1793
- ],
1794
- },
1795
- // an array of objects is the simplest way
1796
- outputFields: [
1797
- {
1798
- key: 'id',
1799
- label: 'Recipe ID',
1800
- type: 'integer',
1801
- },
1802
- {
1803
- key: 'title',
1804
- label: 'Recipe Title',
1805
- type: 'string',
1806
- },
1807
- {
1808
- key: 'author__id',
1809
- label: 'Author User ID',
1810
- type: 'integer',
1811
- },
1812
- {
1813
- key: 'author__name',
1814
- label: 'Author Name',
1815
- type: 'string',
1816
- },
1817
- {
1818
- key: 'ingredients[]name',
1819
- label: 'Ingredient Name',
1820
- type: 'string',
1821
- },
1822
- {
1823
- key: 'ingredients[]amount',
1824
- label: 'Ingredient Amount',
1825
- type: 'number',
1826
- },
1827
- {
1828
- key: 'ingredients[]unit',
1829
- label: 'Ingredient Unit',
1830
- type: 'string',
1831
- },
1832
- recipeOutputFields, // provide a function inline - we'll merge the results!
1833
- ],
1834
- },
1835
- },
1836
- },
1837
- };
1838
-
1839
- ```
1840
-
1841
- ## Buffered Create Actions
1842
-
1843
- _Added in v15.15.0. This feature is currently **internal-only**._
1844
-
1845
- A Buffered Create allows you to create objects in bulk with a single or fewer API request(s). This is useful when you want to reduce the number of requests made to your server. When enabled, Zapier holds the data until the buffer reaches a size limit or a certain time has passed, then sends the buffered data using the `performBuffer` function you define.
1846
-
1847
- To implement a Buffered Create, you define a [`buffer`](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#bufferconfigschema) configuration object and a `performBuffer` function in the `operation` object. In the `buffer` config object, you specify how you want to group the buffered data using the `groupedBy` setting and the maximum number of items to buffer using the `limit` setting.
1848
-
1849
- The `performBuffer` function should replace the `perform` function. Note that `perform` cannot be defined along with `performBuffer`. Check out the [`create` action operation schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#basiccreateactionoperationschema) for details.
1850
-
1851
- Similar to the general `perform` function accepting two arguments, [`z`](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#z-object) and [`bundle`](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#bundle-object) objects, the `performBuffer` function accepts [`z`](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#z-object) and [`bufferedBundle`](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#bufferedbundle-object) objects. They share the same `z` object, but the `bufferedBundle` object is different from the `bundle` object. The `bufferedBundle` object has an idempotency ID set at `bufferedBundle.buffer[].meta.id` for each object in the buffer. `performBuffer` would have to return the idempotency IDs to tell Zapier which objects were successfully written. Find the details about the `bufferedBundle` object [here](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#bufferedbundle-object).
1852
-
1853
- Here is an example of a Buffered Create action:
1854
-
1855
- ```js
1856
- const performBuffer = async (z, bufferedBundle) => {
1857
- // Grab the line items, preserving the order
1858
- const rows = bufferedBundle.buffer.map(({ inputData }) => {
1859
- return { title: inputData.title, year: inputData.year };
1860
- });
1861
-
1862
- // Make the bulk-create API request
1863
- const response = await z.request({
1864
- method: 'POST',
1865
- url: 'https://api.example.com/add_rows',
1866
- body: {
1867
- spreadsheet: bufferedBundle.groupedBy.spreadsheet,
1868
- worksheet: bufferedBundle.groupedBy.worksheet,
1869
- rows,
1870
- },
1871
- });
1872
-
1873
- // Create a matching result using the idempotency ID for each buffered invocation run.
1874
- // The returned IDs will tell Zapier backend which items were successfully written.
1875
- const result = {};
1876
- bufferedBundle.buffer.forEach(({ inputData, meta }, index) => {
1877
- let error = '';
1878
- let outputData = {};
1879
-
1880
- // assuming request order matches response and
1881
- // response.data = {
1882
- // "rows": [
1883
- // {"id": "12910"},
1884
- // {"id": "92830"},
1885
- // {"error": "Not Created"},
1886
- // ...
1887
- // ]
1888
- // }
1889
- if (response.data.rows.length > index) {
1890
- // assuming an error is returned with an "error" key in the response data
1891
- if (response.data.rows[index].error) {
1892
- error = response.data.rows[index].error;
1893
- } else {
1894
- outputData = response.data.rows[index];
1895
- }
1896
- }
1897
-
1898
- // the performBuffer method must return a data just like this
1899
- // {
1900
- // "idempotency ID 1": {
1901
- // "outputData": {"id": "12910"},
1902
- // "error": ""
1903
- // },
1904
- // "idempotency ID 2": {
1905
- // "outputData": {"id": "92830"},
1906
- // "error": ""
1907
- // },
1908
- // "idempotency ID 3": {
1909
- // "outputData": {},
1910
- // "error": "Not Created"
1911
- // },
1912
- // ...
1913
- // }
1914
- result[meta.id] = { outputData, error };
1915
- });
1916
-
1917
- return result;
1918
- };
1919
-
1920
- module.exports = {
1921
- key: 'add_rows',
1922
- noun: 'Rows',
1923
- display: {
1924
- label: 'Add Rows',
1925
- description: 'Add rows to a worksheet.',
1926
- },
1927
- operation: {
1928
- buffer: {
1929
- groupedBy: ['spreadsheet', 'worksheet'],
1930
- limit: 3,
1931
- },
1932
- performBuffer,
1933
- inputFields: [
1934
- {
1935
- key: 'spreadsheet',
1936
- type: 'string',
1937
- required: true,
1938
- },
1939
- {
1940
- key: 'worksheet',
1941
- type: 'string',
1942
- required: true,
1943
- },
1944
- {
1945
- key: 'title',
1946
- type: 'string',
1947
- },
1948
- {
1949
- key: 'year',
1950
- type: 'string',
1951
- },
1952
- ],
1953
- outputFields: [{ key: 'id', type: 'string' }],
1954
- sample: { id: '12345' },
1955
- },
1956
- };
1957
-
1958
- ```
1959
-
1960
- ## Z Object
1961
-
1962
- We provide several methods off of the `z` object, which is provided as the first argument to all function calls in your app.
1963
-
1964
- > The `z` object is passed into your functions as the first argument - IE: `perform: (z) => {}`.
1965
-
1966
- ### `z.request([url], options)`
1967
-
1968
- `z.request([url], options)` is a promise based HTTP client with some Zapier-specific goodies. See [Making HTTP Requests](#making-http-requests). `z.request()` will [percent-encode](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding) non-ascii characters and these reserved characters: ``:$/?#[]@$&+,;=^@`\``. Use [`skipEncodingChars`](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#requestschema) to modify this behaviour.
1969
-
1970
- ### `z.console`
1971
-
1972
- `z.console.log(message)` is a logging console, similar to Node.js `console` but logs remotely, as well as to stdout in tests. See [Log Statements](#console-logging)
1973
-
1974
- ### `z.dehydrate(func, inputData)`
1975
-
1976
- `z.dehydrate(func, inputData)` is used to lazily evaluate a function, perfect to avoid API calls during polling or for reuse. See [Dehydration](#dehydration).
1977
-
1978
- ### `z.dehydrateFile(func, inputData)`
1979
-
1980
- `z.dehydrateFile` is used to lazily download a file, perfect to avoid API calls during polling or for reuse. See [File Dehydration](#file-dehydration).
1981
-
1982
- ### `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`
1983
-
1984
- `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` is a promise based file stasher that returns a URL file pointer. See [Stashing Files](#stashing-files).
1985
-
1986
- ### `z.JSON`
1987
-
1988
- `z.JSON` is similar to the JSON built-in like `z.JSON.parse('...')`, but catches errors and produces nicer tracebacks.
1989
-
1990
- ### `z.hash()`
1991
-
1992
- `z.hash()` is a crypto tool for doing things like `z.hash('sha256', 'my password')`
1993
-
1994
- ### `z.errors`
1995
-
1996
- `z.errors` is a collection error classes that you can throw in your code, like `throw new z.errors.HaltedError('...')`.
1997
-
1998
- The available errors are:
1999
-
2000
- * `Error` (_added in v9.3.0_) - Stops the current operation, allowing for (auto) replay. Read more on [General Errors](#general-errors)
2001
- * `HaltedError` - Stops current operation, but will never turn off Zap. Read more on [Halting Execution](#halting-execution)
2002
- * `ExpiredAuthError` - Stops the current operation and emails user to manually reconnect. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
2003
- * `RefreshAuthError` - (OAuth2 or Session Auth) Tells Zapier to refresh credentials and retry operation. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
2004
- * `ThrottledError` (_new in v11.2.0_) - Tells Zapier to retry the current operation after a delay specified in seconds. Read more on [Handling Throttled Requests](#handling-throttled-requests)
2005
-
2006
- For more details on error handling in general, see [here](#error-handling).
2007
-
2008
- ### `z.cursor`
2009
-
2010
- The `z.cursor` object exposes two methods:
2011
-
2012
- * `z.cursor.get(): Promise<string|null>`
2013
- * `z.cursor.set(string): Promise<null>`
2014
-
2015
- Any data you `set` will be available to that Zap for about an hour (or until it's overwritten). For more information, see: [paging](#paging).
2016
-
2017
- ### `z.generateCallbackUrl()`
2018
-
2019
- The `z.generateCallbackUrl()` will return a callback URL your app can `POST` to later for handling long running tasks (like transcription or encoding jobs). In the meantime, the Zap and Task will wait for your response and the user will see the Task marked as waiting.
2020
-
2021
- For example, in your `perform` you might do:
2022
-
2023
- ```js
2024
- const perform = async (z, bundle) => {
2025
- // something like this url:
2026
- // https://zapier.com/hooks/callback/123/abcdef01-2345-6789-abcd-ef0123456789/abcdef0123456789abcdef0123456789abcdef01/
2027
- // consider checking bundle.meta.isLoadingSample to determine if this is a test run or real run!
2028
- const callbackUrl = z.generateCallbackUrl();
2029
- await z.request({
2030
- url: 'https://example.com/api/slow-job',
2031
- method: 'POST',
2032
- body: {
2033
- // ... whatever your integration needs
2034
- url: callbackUrl,
2035
- },
2036
- });
2037
- return {"hello": "world"}; // available later in bundle.outputData
2038
- };
2039
- ```
2040
-
2041
- And in your own `/api/slow-job` view (or more likely, an async job) you'd make this request to Zapier when the long-running job completes to populate `bundle.cleanedRequest`:
2042
-
2043
- ```http
2044
- POST /hooks/callback/123/abcdef01-2345-6789-abcd-ef0123456789/abcdef0123456789abcdef0123456789abcdef01/ HTTP/1.1
2045
- Host: zapier.com
2046
- Content-Type: application/json
2047
-
2048
- {"foo":"bar"}
2049
- ```
2050
- > We recommend using `bundle.meta.isLoadingSample` to determine if the execution is happening in the foreground (IE: during Zap setup) as using `z.generateCallbackUrl()` can be inappropriate given the disconnect. Instead, wait for the long running request without generating a callback, or if you must, return stubbed data.
2051
-
2052
- By default the payload `POST`ed to the callback URL will augment the data returned from the initial `perform` to compose the final value.
2053
-
2054
- If you need to customize what the final value should be you can define a `performResume` method that receives three bundle properties:
2055
-
2056
- * `bundle.outputData` is `{"hello": "world"}`, the data returned from the initial `perform`
2057
- * `bundle.cleanedRequest` is `{"foo": "bar"}`, the payload from the callback URL
2058
- * `bundle.rawRequest` is the full request object corresponding to `bundle.cleanedRequest`
2059
-
2060
- ```js
2061
- const performResume = async (z, bundle) => {
2062
- // this will give a final value of: {"hello": "world", "foo": "bar"}
2063
- // which is the default behavior when a custom `performResume` is not
2064
- // defined.
2065
- return { ...bundle.outputData, ...bundle.cleanedRequest };
2066
- };
2067
- ```
2068
-
2069
- > The app will have a maximum of 30 days to `POST` to the callback URL. If a user deletes or modifies the Zap or Task in the meantime, we will not resume the task.
2070
-
2071
- > `performResume` will only run when the Zap runs live, and cannot be tested in the Zap Editor when configuring the Zap. It is possible to use `bundle.meta.isLoadingSample` to load a fixed sample to allow users to test a step that includes `performResume`.
2072
-
2073
-
2074
- ## Bundle Object
2075
-
2076
- This object holds the user's auth details and the data for the API requests.
2077
-
2078
- > The `bundle` object is passed into your functions as the second argument - IE: `perform: (z, bundle) => {}`.
2079
-
2080
- ### `bundle.authData`
2081
-
2082
- `bundle.authData` is user-provided authentication data, like `api_key` or `access_token`. [Read more on authentication.](#authentication)
2083
-
2084
- ### `bundle.inputData`
2085
-
2086
- `bundle.inputData` is user-provided data for this particular run of the trigger/search/create, as defined by the [`inputFields`](#input-fields). For example:
2087
-
2088
- ```js
2089
- {
2090
- createdBy: 'his name is Bobby Flay',
2091
- style: 'he cooks mediterranean',
2092
- scheduledAt: "2021-09-09T09:00:00-07:00"
2093
- }
2094
- ```
2095
-
2096
- ### `bundle.inputDataRaw`
2097
-
2098
- `bundle.inputDataRaw` is like `bundle.inputData`, but before processing such as interpreting friendly datetimes and rendering `{{curlies}}`:
2099
-
2100
- ```js
2101
- {
2102
- createdBy: 'his name is {{123__chef_name}}',
2103
- style: 'he cooks {{456__style}}',
2104
- scheduledAt: "today"
2105
- }
2106
- ```
2107
-
2108
- > "curlies" represent data mapped in from previous steps. They take the form `{{NODE_ID__key_name}}`.
2109
-
2110
- You'll usually want to use `bundle.inputData` instead.
2111
-
2112
- ### `bundle.meta`
2113
-
2114
- `bundle.meta` contains extra information useful for doing advanced behaviors depending on what the user is doing. It has the following options:
2115
-
2116
- | key | default | description |
2117
- | --- | --- | --- |
2118
- | `isLoadingSample` | `false` | If true, this run was initiated manually via the Zap Editor |
2119
- | `isFillingDynamicDropdown` | `false` | If true, this poll is being used to populate a dynamic dropdown. You only need to return the fields you specified (such as `id` and `name`), though returning everything is fine too |
2120
- | `isPopulatingDedupe` | `false` | If true, the results of this poll will be used to initialize the deduplication list rather than trigger a zap. You should grab as many items as possible. See also: [deduplication](#dedup) |
2121
- | `limit` | `-1` | The number of items you should fetch. `-1` indicates there's no limit. Build this into your calls insofar as you are able |
2122
- | `page` | `0` | Used in [paging](#paging) to uniquely identify which page of results should be returned |
2123
- | `isTestingAuth` | `false` | (legacy property) If true, the poll was triggered by a user testing their account (via [clicking "test"](https://cdn.zapier.com/storage/photos/5c94c304ce11b02c073a973466a7b846.png) or during setup). We use this data to populate the auth label, but it's mostly used to verify we made a successful authenticated request |
2124
- | `withSearch` | `undefined` | When a create is called as part of a search-or-create step, `withSearch` will be the key of the search. |
2125
-
2126
- > Before v8.0.0, the information in `bundle.meta` was different. See [the old docs](https://github.com/zapier/zapier-platform-cli/blob/a058e6d538a75d215d2e0c52b9f49a97218640c4/README.md#bundlemeta) for the previous values and [the wiki](https://github.com/zapier/zapier-platform/wiki/bundle.meta-changes) for a mapping of old values to new.
2127
-
2128
- Here's an example of a polling trigger that is also used to power a dynamic dropdown:
2129
-
2130
- ```js
2131
- const perform = async (z, bundle) => {
2132
- const params = { per_page: 100 }; // poll for the most recent 100 teams
2133
-
2134
- if (bundle.meta.isFillingDynamicDropdown) {
2135
- // dynamic dropdowns support pagination
2136
- params.per_page = 30;
2137
- params.offset = params.per_page * bundle.meta.page;
2138
- }
2139
-
2140
- const response = await z.request({
2141
- url: `${API_BASE_URL}/teams`,
2142
- params,
2143
- });
2144
-
2145
- return response.json;
2146
- };
2147
- // ...
2148
- ```
2149
-
2150
-
2151
-
2152
- ### `bundle.rawRequest`
2153
-
2154
- > `bundle.rawRequest` is only available in the `perform` for webhooks, `getAccessToken` for OAuth authentication methods, and `performResume` in a callback action.
2155
-
2156
- `bundle.rawRequest` holds raw information about the HTTP request that triggered the `perform` method or that represents the user's browser request that triggered the `getAccessToken` call:
2157
-
2158
- ```
2159
- {
2160
- method: 'POST',
2161
- querystring: 'foo=bar&baz=qux',
2162
- headers: {
2163
- 'Content-Type': 'application/json'
2164
- },
2165
- content: '{"hello": "world"}'
2166
- }
2167
- ```
2168
-
2169
- In `bundle.rawRequest`, headers other than `Content-Length` and `Content-Type` will be prefixed with `Http-`, and all headers will be named in Camel-Case. For example, the header `X-Time-GMT` would become `Http-X-Time-Gmt`.
2170
-
2171
- ### `bundle.cleanedRequest`
2172
-
2173
- > `bundle.cleanedRequest` is only available in the `perform` for webhooks, `getAccessToken` for OAuth authentication methods, and `performResume` in a callback action.
2174
-
2175
- `bundle.cleanedRequest` will return a formatted and parsed version of the request. Some or all of the following will be available:
2176
-
2177
- ```
2178
- {
2179
- method: 'POST',
2180
- querystring: {
2181
- foo: 'bar',
2182
- baz: 'qux'
2183
- },
2184
- headers: {
2185
- 'Content-Type': 'application/json'
2186
- },
2187
- content: {
2188
- hello: 'world'
2189
- }
2190
- }
2191
- ```
2192
-
2193
- ### `bundle.outputData`
2194
-
2195
- > `bundle.outputData` is only available in the `performResume` in a callback action.
2196
-
2197
- `bundle.outputData` will return a whatever data you originally returned in the `perform`, allowing you to mix that with `bundle.rawRequest` or `bundle.cleanedRequest`.
2198
-
2199
-
2200
- ### `bundle.targetUrl`
2201
-
2202
- > `bundle.targetUrl` is only available in the `performSubscribe` and `performUnsubscribe` methods for webhooks.
2203
-
2204
- This the URL to which you should send hook data. It'll look something like `https://hooks.zapier.com/1234/abcd`. We provide it so you can make a POST request to your server. Your server should store this URL and use is as a destination when there's new data to report.
2205
-
2206
- For example:
2207
-
2208
- ```js
2209
- const subscribeHook = async (z, bundle) => {
2210
-
2211
- const options = {
2212
- url: 'https://57b20fb546b57d1100a3c405.mockapi.io/api/hooks',
2213
- method: 'POST',
2214
- body: {
2215
- url: bundle.targetUrl, // bundle.targetUrl has the Hook URL this app should call
2216
- },
2217
- };
2218
-
2219
- const response = await z.request(options);
2220
- return response.data; // or response.json if you're using core v9 or older
2221
- };
2222
-
2223
- module.exports = {
2224
- // ...
2225
- performSubscribe: subscribeHook,
2226
- // ...
2227
- };
2228
- ```
2229
-
2230
- Read more in the [REST hook example](https://github.com/zapier/zapier-platform/blob/main/example-apps/rest-hooks/triggers/recipe.js).
2231
-
2232
- ### `bundle.subscribeData`
2233
-
2234
- > `bundle.subscribeData` is available in the `perform` and `performUnsubscribe` method for webhooks.
2235
-
2236
- This is an object that contains the data you returned from the `performSubscribe` function. It should contain whatever information you need send a `DELETE` request to your server to stop sending webhooks to Zapier.
2237
-
2238
- Read more in the [REST hook example](https://github.com/zapier/zapier-platform/blob/main/example-apps/rest-hooks/triggers/recipe.js).
2239
-
2240
- ## BufferedBundle Object
2241
-
2242
- *Added in v15.15.0.*
2243
-
2244
- This object holds a user's auth details (`bufferedBundle.authData`) and the buffered data (`bufferedBundle.buffer`) for the API requests. It is used only with a `create` action's `performBuffer` function.
2245
-
2246
- > The `bufferedBundle` object is passed into the `performBuffer` function as the second argument - IE: `performBuffer: async (z, bufferedBundle) => {}`.
2247
-
2248
- ### `bufferedBundle.authData`
2249
-
2250
- It is a user-provided authentication data, like `api_key` or `access_token`. [Read more on authentication.](#authentication)
2251
-
2252
- ### `bufferedBundle.groupedBy`
2253
-
2254
- It is a user-provided data for a set of selected [`inputFields`](#input-fields) to group the multiple runs of a `create` action by.
2255
-
2256
- ### `bufferedBundle.buffer`
2257
-
2258
- It is an array of objects of user-provided data and some meta data to allow multiple runs of a `create` action be processed in a single API request.
2259
-
2260
- #### `bufferedBundle.buffer[].inputData`
2261
-
2262
- It is a user-provided data for a particular run of a `create` action in the buffer, as defined by the [`inputFields`](#input-fields).
2263
-
2264
- #### `bufferedBundle.buffer[].meta`
2265
-
2266
- It contains an idempotency `id` provided to the `create` action to identify each run's data in the buffered data.
2267
-
2268
- ## Environment
2269
-
2270
- Apps can define environment variables that are available when the app's code executes. They work just like environment
2271
- variables defined on the command line. They are useful when you have data like an OAuth client ID and secret that you
2272
- don't want to commit to source control. Environment variables can also be used as a quick way to toggle between
2273
- a staging and production environment during app development.
2274
-
2275
- It is important to note that **variables are defined on a per-version basis!** When you push a new version, the
2276
- existing variables from the previous version are copied, so you don't have to manually add them. However, edits
2277
- made to one version's environment will not affect the other versions.
2278
-
2279
- ### Defining Environment Variables
2280
-
2281
- To define an environment variable, use the `env` command:
2282
-
2283
- ```bash
2284
- # Will set the environment variable on Zapier.com
2285
- zapier env:set 1.0.0 MY_SECRET_VALUE=1234
2286
- ```
2287
-
2288
- You will likely also want to set the value locally for testing.
2289
-
2290
- ```bash
2291
- export MY_SECRET_VALUE=1234
2292
- ```
2293
-
2294
- Alternatively, we provide some extra tooling to work with an `.env` (or `.environment`, see below note) that looks like this:
2295
-
2296
- ```
2297
- MY_SECRET_VALUE=1234
2298
- ```
2299
-
2300
- > `.env` is the new recommended name for the environment file since v5.1.0. The old name `.environment` is deprecated but will continue to work for backward compatibility.
2301
-
2302
- And then in your `test/basic.js` file:
2303
-
2304
- ```js
2305
- const zapier = require('zapier-platform-core');
2306
-
2307
- should('some tests', () => {
2308
- zapier.tools.env.inject(); // testing only!
2309
- console.log(process.env.MY_SECRET_VALUE);
2310
- // should print '1234'
2311
- });
2312
- ```
2313
-
2314
- > This is a popular way to provide `process.env.ACCESS_TOKEN || bundle.authData.access_token` for convenient testing.
2315
-
2316
- > **NOTE** Variables defined via `zapier env:set` will _always_ be uppercased. For example, you would access the variable defined by `zapier env:set 1.0.0 foo_bar=1234` with `process.env.FOO_BAR`.
2317
-
2318
-
2319
- ### Accessing Environment Variables
2320
-
2321
- To view existing environment variables, use the `env` command.
2322
-
2323
- ```bash
2324
- # Will print a table listing the variables for this version
2325
- zapier env:get 1.0.0
2326
- ```
2327
-
2328
- Within your app, you can access the environment via the standard `process.env` - any values set via local `export` or `zapier env:set` will be there.
2329
-
2330
- For example, you can access the `process.env` in your perform functions and in templates:
2331
-
2332
- ```js
2333
- const listExample = async (z, bundle) => {
2334
- const httpOptions = {
2335
- headers: {
2336
- 'my-header': process.env.MY_SECRET_VALUE,
2337
- },
2338
- };
2339
- const response = await z.request(
2340
- 'https://example.com/api/v2/recipes.json',
2341
- httpOptions
2342
- );
2343
-
2344
- // response.throwForStatus() if you're using core v9 or older
2345
-
2346
- return response.data; // or response.json if you're using core v9 or older
2347
- };
2348
-
2349
- const App = {
2350
- // ...
2351
- triggers: {
2352
- example: {
2353
- noun: '{{process.env.MY_NOUN}}',
2354
- operation: {
2355
- // ...
2356
- perform: listExample,
2357
- },
2358
- },
2359
- },
2360
- };
2361
-
2362
- ```
2363
-
2364
- > Note! Be sure to lazily access your environment variables - see [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies).
2365
-
2366
-
2367
- ## Adding Throttle Configuration
2368
-
2369
- *Added in v15.4.0.*
2370
-
2371
- When a throttle configuration is set for an action, Zapier uses it to apply throttling when the limit for the timeframe window is exceeded. It can be set at the root level and/or on an action. When set at the root level, it is the default throttle configuration used on each action of the integration. And when set in an action's operation object, the root-level default is overwritten for that action only. Note that the throttle limit is not shared across actions unless for those with the same key, window, limit, and scope when "action" is not in the scope.
2372
-
2373
- To throttle an action, you need to set a `throttle` object with the following variables:
2374
- 1. `window [integer]`: The timeframe, in seconds, within which the system tracks the number of invocations for an action. The number of invocations begins at zero at the start of each window.
2375
- 2. `limit [integer]`: The maximum number of invocations for an action, allowed within the timeframe window.
2376
- 3. `key [string]` (_added in v15.6.0_): The key to throttle with in combination with the scope. User data provided for the input fields can be used in the key with the use of the curly braces referencing. For example, to access the user data provided for the input field "test_field", use `{{bundle.inputData.test_field}}`. Note that a required input field should be referenced to get user data always.
2377
- 4. `retry [boolean]` (_added in v15.8.0_): The effect of throttling on the tasks of the action. `true` means throttled tasks are automatically retried after some delay, while `false` means tasks are held without retry. It defaults to `true`. NOTE that it has no effect on polling triggers and should not be set.
2378
- 5. `filter [string]` (_added in v15.8.0_): EXPERIMENTAL: Account-based attribute to override the throttle by. You can set to one of the following: "free", "trial", "paid". Therefore, the throttle scope would be automatically set to "account" and ONLY the accounts based on the specified filter will have their requests throttled based on the throttle overrides while the rest are throttled based on the original configuration.
2379
- 6. `scope [array]`: The granularity to throttle by. You can set the scope to one or more of the following options;
2380
- - 'user' - Throttles based on user ids.
2381
- - 'auth' - Throttles based on auth ids.
2382
- - 'account' - Throttles based on account ids for all users under a single account.
2383
- - 'action' - Throttles the action it is set on separately from other actions.
2384
- 7. `overrides [array[object]]` (_added in v15.6.0_): EXPERIMENTAL: Overrides the original throttle configuration based on a Zapier account attribute;
2385
- - `window [integer]`: Same description as above.
2386
- - `limit [integer]`: Same description as above.
2387
- - `filter [string]`: Account-based attribute to override the throttle by. You can set to one of the following: "free", "trial", "paid". Therefore, the throttle scope would be automatically set to "account" and ONLY the accounts based on the specified filter will have their requests throttled based on the throttle overrides while the rest are throttled based on the original configuration.
2388
- - `retry [boolean]` (_added in v15.6.1_): The effect of throttling on the tasks of the action. `true` means throttled tasks are automatically retried after some delay, while `false` means tasks are held without retry. It defaults to `true`. NOTE that it has no effect on polling triggers and should not be set.
2389
-
2390
- Both `window` and `limit` are required and others are optional. By default, throttling is scoped to the action and account.
2391
-
2392
- Here is a typical usage of the throttle configuration:
2393
-
2394
- ```js
2395
- const App = {
2396
- version: require('./package.json').version,
2397
- platformVersion: require('zapier-platform-core').version,
2398
-
2399
- // default throttle used for each action
2400
- throttle: {
2401
- window: 600,
2402
- limit: 50,
2403
- scope: ['account'],
2404
- },
2405
-
2406
- creates: {
2407
- upload_video: {
2408
- noun: 'Video',
2409
- display: {
2410
- label: 'Upload Video',
2411
- description: 'Upload a video.',
2412
- },
2413
- operation: {
2414
- perform: () => {},
2415
- inputFields: [{ key: 'name', required: true, type: 'string' }],
2416
- // overwrites the default, for this action
2417
- throttle: {
2418
- window: 600,
2419
- limit: 5,
2420
- key: 'test-key-{{bundle.inputData.name}}',
2421
- retry: false,
2422
- scope: ['account'],
2423
- overrides: [
2424
- {
2425
- window: 600,
2426
- limit: 10,
2427
- filter: 'free',
2428
- retry: false,
2429
- },
2430
- {
2431
- window: 600,
2432
- limit: 100,
2433
- filter: 'trial',
2434
- retry: false,
2435
- },
2436
- {
2437
- window: 0,
2438
- limit: 0,
2439
- filter: 'paid',
2440
- retry: true,
2441
- },
2442
- ],
2443
- },
2444
- },
2445
- },
2446
- },
2447
- };
2448
-
2449
- module.exports = App;
2450
-
2451
- ```
2452
-
2453
-
2454
- ## Making HTTP Requests
2455
-
2456
- There are two ways to make HTTP requests:
2457
-
2458
- 1. [**Shorthand HTTP Requests**](#shorthand-http-requests) - Easy to use, but limits what you can control. Best for simple requests.
2459
- 2. [**Manual HTTP Requests**](#manual-http-requests) - Gives you full control over the request and response.
2460
-
2461
- Use these helper constructs to reduce boilerplate:
2462
-
2463
- 1. `requestTemplate` - an object literal of [HTTP request options](#http-request-options) that will be merged with every request.
2464
- 2. `beforeRequest` - [middleware](#using-http-middleware) that mutates every request before it is sent.
2465
- 3. `afterResponse` - [middleware](#using-http-middleware) that mutates every response before it is completed.
2466
-
2467
- > Note: you can install any HTTP client you like - but this is greatly discouraged as you lose [automatic HTTP logging](#http-logging) and middleware.
2468
-
2469
- ### Shorthand HTTP Requests
2470
-
2471
- For simple HTTP requests that do not require special pre- or post-processing, you can specify the [HTTP request options](#http-request-options) as an object literal in your app definition.
2472
-
2473
- This features:
2474
-
2475
- 1. Lazy `{{curly}}` replacement.
2476
- 2. JSON and form body de-serialization.
2477
- 3. Automatic non-2xx error raising.
2478
-
2479
- ```js
2480
- const triggerShorthandRequest = {
2481
- url: 'https://{{bundle.authData.subdomain}}.example.com/v2/api/recipes.json',
2482
- method: 'GET',
2483
- params: {
2484
- sort_by: 'id',
2485
- sort_order: 'DESC',
2486
- },
2487
- };
2488
-
2489
- const App = {
2490
- // ...
2491
- triggers: {
2492
- example: {
2493
- // ...
2494
- operation: {
2495
- // ...
2496
- perform: triggerShorthandRequest,
2497
- },
2498
- },
2499
- },
2500
- };
2501
-
2502
- ```
2503
-
2504
- In the URL above, `{{bundle.authData.subdomain}}` is automatically replaced with the live value from the bundle. If the call returns a non 2xx return code, an error is automatically raised. The response body is automatically parsed as JSON or form-encoded and returned.
2505
-
2506
- An error will be raised if the response cannot be parsed as JSON or form-encoded. To use shorthand requests with other response types, add [middleware](#using-http-middleware) that sets `response.data` to the parsed response.
2507
-
2508
- ### Manual HTTP Requests
2509
-
2510
- Use this when you need full control over the request/response. For example:
2511
-
2512
- 1. To do processing (usually involving [`bundle.inputData`](#bundleinputdata)) before a request is made
2513
- 2. To do processing of an API's response before you return data to Zapier
2514
- 3. To process an unusual response type, such as XML
2515
-
2516
- To make a manual request, pass your [request options](#http-request-options) to `z.request()` then use the resulting [response object](#http-response-object) to return the data you want:
2517
-
2518
- ```js
2519
- const listRecipes = async (z, bundle) => {
2520
- // Custom processing of bundle.inputData would go here...
2521
-
2522
- const httpRequestOptions = {
2523
- url: 'https://{{bundle.authData.subdomain}}.example.com/v2/api/recipes.json',
2524
- method: 'GET',
2525
- params: {
2526
- cuisine: bundle.inputData.cuisine,
2527
- },
2528
- };
2529
- const response = await z.request(httpRequestOptions);
2530
- const recipes = response.data;
2531
-
2532
- // Custom processing of recipes would go here...
2533
-
2534
- return recipes;
2535
- };
2536
-
2537
- const App = {
2538
- // ...
2539
- triggers: {
2540
- example: {
2541
- // ...
2542
- operation: {
2543
- // ...
2544
- perform: listRecipes,
2545
- },
2546
- },
2547
- },
2548
- };
2549
-
2550
- ```
2551
-
2552
- Manual requests perform lazy `{{curly}}` replacement. In the URL above, `{{bundle.authData.subdomain}}` is automatically replaced with the live value from the bundle.
2553
-
2554
- #### POST and PUT Requests
2555
-
2556
- To POST or PUT data to your API you can do this:
2557
-
2558
- ```js
2559
- const App = {
2560
- // ...
2561
- triggers: {
2562
- example: {
2563
- // ...
2564
- operation: {
2565
- // ...
2566
- perform: async (z, bundle) => {
2567
- const recipe = {
2568
- name: 'Baked Falafel',
2569
- style: 'mediterranean',
2570
- directions: 'Get some dough....',
2571
- };
2572
-
2573
- const options = {
2574
- method: 'POST',
2575
- url: 'https://example.com/api/v2/recipes.json',
2576
- body: JSON.stringify(recipe),
2577
- };
2578
- const response = await z.request(options);
2579
-
2580
- // Throw and try to extract message from standard error responses
2581
- if (response.status !== 201) {
2582
- throw new z.errors.Error(
2583
- `Unexpected status code ${response.status}`,
2584
- 'CreateRecipeError',
2585
- response.status
2586
- );
2587
- }
2588
-
2589
- return response.data; // or response.json if you're using core v9 or older
2590
- },
2591
- },
2592
- },
2593
- },
2594
- };
2595
-
2596
- ```
2597
-
2598
- > Note: you need to call `z.JSON.stringify()` before setting the `body`.
2599
-
2600
- ### Using HTTP middleware
2601
-
2602
- To process all HTTP requests in a certain way, use the `beforeRequest` and `afterResponse` middleware functions.
2603
-
2604
- Middleware functions go in your app definition:
2605
-
2606
- ```js
2607
- const addHeader = (request, z, bundle) => {
2608
- request.headers['my-header'] = 'from zapier';
2609
- return request;
2610
- };
2611
-
2612
- // This example only works on core v10+!
2613
- const parseXML = (response, z, bundle) => {
2614
- // Parse content that is not JSON
2615
- // eslint-disable-next-line no-undef
2616
- response.data = xml.parse(response.content);
2617
- return response;
2618
- };
2619
-
2620
- // This example only works on core v10+!
2621
- const handleWeirdErrors = (response, z) => {
2622
- // Prevent `throwForStatus` from throwing for a certain status.
2623
- if (response.status === 456) {
2624
- response.skipThrowForStatus = true;
2625
- } else if (response.status === 200 && response.data.success === false) {
2626
- throw new z.errors.Error(response.data.message, response.data.code);
2627
- }
2628
- return response;
2629
- };
2630
-
2631
- const App = {
2632
- // ...
2633
- beforeRequest: [addHeader],
2634
- afterResponse: [parseXML, handleWeirdErrors],
2635
- // ...
2636
- };
2637
-
2638
- ```
2639
-
2640
- A `beforeRequest` middleware function takes a request options object, and returns a (possibly mutated) request object. An `afterResponse` middleware function takes a response object, and returns a (possibly mutated) response object. Middleware functions are executed in the order specified in the app definition, and each subsequent middleware receives the request or response object returned by the previous middleware.
2641
-
2642
- Middleware functions can be asynchronous - just return a promise from the middleware function.
2643
-
2644
- The second argument for middleware is the `z` object, but it does *not* include `z.request()` as using that would easily create infinite loops.
2645
-
2646
- Here is the full request lifecycle when you call `z.request({...})`:
2647
-
2648
- 1. set defaults on the `request` object
2649
- 2. run your `beforeRequest` middleware functions in order
2650
- 3. add applicable auth headers (e.g. adding `Basic ...` for `basic` auth), if applicable
2651
- 4. add `request.params` to `request.url`
2652
- 5. execute the `request`, store the result in `response`
2653
- 6. try to auto-parse response body for non-raw requests, store result in `response.data`
2654
- 7. log the request to Zapier's logging server
2655
- 8. if the status code is `401`, you're using a refresh-able auth (such as `oauth2` or `session`) _and_ `autoRefresh` is `true` in your auth configuration, throw a `RefreshAuthError`. The server will attempt to refresh the authentication again and retry the whole step
2656
- 9. run your `afterResponse` middleware functions in order
2657
- 10. call `response.throwForStatus()` unless `response.skipThrowForStatus` is `true`
2658
-
2659
- The resulting response object is returned from `z.request()`.
2660
-
2661
- > Example App: check out https://github.com/zapier/zapier-platform/tree/main/example-apps/middleware for a working example app using HTTP middleware.
2662
-
2663
- #### Error Response Handling
2664
-
2665
- Since `v10.0.0`, `z.request()` calls `response.throwForStatus()` before it returns a response. You can disable automatic error throwing by setting `skipThrowForStatus` on the request object:
2666
-
2667
- ```js
2668
- // Disable automatic error throwing on the request object
2669
- const perform = async (z, bundle) => {
2670
- const response = await z.request({
2671
- url: '...',
2672
- skipThrowForStatus: true
2673
- });
2674
- // Now you handle error response on your own.
2675
- // The following is equivalent to response.throwForStatus(),
2676
- // but you have to remember to do it on every request
2677
- if (response.status >= 400) {
2678
- throw new z.errors.ResponseError(response);
2679
- }
2680
- };
2681
- ```
2682
-
2683
- You can also do it in `afterResponse` if the API uses a status code >= 400 that should not be treated as an error.
2684
-
2685
- ```js
2686
- // Don't throw an error when response status is 456
2687
- const disableAutoThrowOn456 = (response, z) => {
2688
- if (response.status === 456) {
2689
- response.skipThrowForStatus = true;
2690
- }
2691
- return response;
2692
- };
2693
- const App = {
2694
- // ...
2695
- afterResponse: [disableAutoThrowOn456],
2696
- // ...
2697
- };
2698
- ```
2699
-
2700
- For developers using v9.x and below, it's your responsibility to throw an exception for an error response. That means you should call `response.throwForStatus()` or throw an error yourself, likely following the `z.request` call.
2701
-
2702
- This behavior has changed periodically across major versions, which changes how/when you have to worry about handling errors. Here's a diagram to illustrate that:
2703
-
2704
- <!-- diagram source: https://excalidraw.com/#json=stm4O1SLW3ko4FCX9rvsI,6NgRCAK81Cc8M3MuXhNDNA -->
2705
-
2706
- ![](https://cdn.zappy.app/e835d9beca1b6489a065d51a381613f3.png)
2707
-
2708
- Ensure you're handling errors correctly for your platform version. The latest released version is **15.17.0**.
2709
-
2710
- ### HTTP Request Options
2711
-
2712
- [Shorthand requests](#shorthand-http-requests) and [manual requests](#manual-http-requests) support the following HTTP `options`:
2713
-
2714
- * `url`: HTTP url, you can provide it as a separate argument (`z.request(url, options)`) or as part of the `options` object (`z.request({url: url, ...})`).
2715
- * `method`: HTTP method, default is `GET`.
2716
- * `headers`: request headers object, format `{'header-key': 'header-value'}`.
2717
- * `params`: URL query params object, format `{'query-key': 'query-value'}`.
2718
- * `body`: request body, can be a string, buffer, readable stream or plain object. When it is an object/array and the `Content-Type` header is `application/x-www-form-urlencoded` the body will be transformed to query string parameters, otherwise we'll set the header to `application/json; charset=utf-8` and JSON encode the body. Default is `null`.
2719
- * `allowGetBody`: include `body` in `GET` requests. Set to `true` to enable. Default is `false`. Set only if required by the receiving API. See [section 4.3.1 in RFC 7231](https://www.rfc-editor.org/rfc/rfc7231#section-4.3.1).
2720
- * `json`: shortcut object/array/etc. you want to JSON encode into body. Default is `null`.
2721
- * `form`: shortcut object. you want to form encode into body. Default is `null`.
2722
- * `raw`: set this to stream the response instead of consuming it immediately. Default is `false`.
2723
- * `redirect`: set to `manual` to extract redirect headers, `error` to reject redirect, default is `follow`.
2724
- * `follow`: maximum redirect count, set to `0` to not follow redirects. default is `20`.
2725
- * `compress`: support gzip/deflate content encoding. Set to `false` to disable. Default is `true`.
2726
- * `agent`: Node.js `http.Agent` instance, allows custom proxy, certificate etc. Default is `null`.
2727
- * `timeout`: request / response timeout in ms. Set to `0` to disable (OS limit still applies), timeout reset on `redirect`. Default is `0` (disabled).
2728
- * `signal` (_added in v15.14.1_): enables cancelling requests via a timeout set by an `AbortController`. More details in `node-fetch` docs [here](https://www.npmjs.com/package/node-fetch#request-cancellation-with-abortsignal). Default is `null`.
2729
- * `size`: maximum response body size in bytes. Set to `0` to disable. Default is `0` (disabled).
2730
- * `skipThrowForStatus` (_added in v10.0.0_): don't call `response.throwForStatus()` before resolving the request with `response`. See [HTTP Response Object](#http-response-object).
2731
-
2732
- ```js
2733
- const response = await z.request({
2734
- url: 'https://example.com',
2735
- method: 'POST',
2736
- headers: {
2737
- 'Content-Type': 'application/json'
2738
- },
2739
- // only provide body, json or form...
2740
- body: {hello: 'world'}, // or '{"hello": "world"}' or 'hello=world'
2741
- json: {hello: 'world'},
2742
- form: {hello: 'world'},
2743
- // access node-fetch style response.body
2744
- raw: false,
2745
- redirect: 'follow',
2746
- follow: 20,
2747
- compress: true,
2748
- agent: null,
2749
- timeout: 0,
2750
- size: 0,
2751
- })
2752
- ```
2753
-
2754
- ### HTTP Response Object
2755
-
2756
- The response object returned by `z.request([url], options)` supports the following fields and methods:
2757
-
2758
- * `status`: The response status code, i.e. `200`, `404`, etc.
2759
- * `content`: The response content as a String. For Buffer, try `options.raw = true`.
2760
- * `data` (_added in v10.0.0_): The response content as an object if the content is JSON or `application/x-www-form-urlencoded` (`undefined` otherwise).
2761
- * `headers`: Response headers object. The header keys are all lower case.
2762
- * `getHeader(key)`: Retrieve response header, case insensitive: `response.getHeader('My-Header')`
2763
- * `skipThrowForStatus` (_added in v10.0.0_): don't call `throwForStatus()` before resolving the request with this response.
2764
- * `throwForStatus()`: Throws an error if `400 <= statusCode < 600`.
2765
- * `request`: The original request options object (see above).
2766
-
2767
- Additionally, if `request.raw` is `true`, the raw response has the following properties:
2768
-
2769
- * `json()`: Get the response content as an object, if `options.raw = true` and content is JSON (returns a promise). `undefined` in non-raw requests.
2770
- * `body`: A stream available only if you provide `options.raw = true`.
2771
-
2772
- ```js
2773
- const response = await z.request({
2774
- // options
2775
- });
2776
-
2777
- // A bunch of examples for demonstration
2778
- response.status;
2779
- response.headers['Content-Type'];
2780
- response.getHeader('content-type');
2781
- response.request; // original request options
2782
- response.throwForStatus();
2783
-
2784
- if (options.raw === false) { // (default)
2785
- // If you're core v10+
2786
- response.data; // same as...
2787
- z.JSON.parse(response.content); // or...
2788
- querystring.parse(response.content);
2789
-
2790
- // If you're core v9 or older...
2791
- response.json; // same as
2792
- z.JSON.parse(response.content);
2793
- } else {
2794
- const buf = await response.buffer();
2795
- buf.toString();
2796
-
2797
- const text = await response.text();
2798
-
2799
- const json = await response.json();
2800
-
2801
- response.body.pipe(otherStream);
2802
- }
2803
- ```
2804
-
2805
- ## Dehydration
2806
-
2807
- Dehydration, and its counterpart Hydration, is a tool that can lazily load data that might be otherwise expensive to retrieve aggressively.
2808
-
2809
- * **Dehydration** - think of this as "make a pointer", you control the creation of pointers with `z.dehydrate(func, inputData)` (or `z.dehydrateFile(func, inputData)` for files). This usually happens in a trigger step.
2810
- * **Hydration** - think of this as an automatic step that "consumes a pointer" and "returns some data", Zapier does this automatically behind the scenes. This usually happens in an action step.
2811
-
2812
- > This is very common when [Stashing Files](#stashing-files) - but that isn't their only use!
2813
-
2814
- The method `z.dehydrate(func, inputData)` has two required arguments:
2815
-
2816
- * `func` - the function to call to fetch the extra data. Can be any raw `function`, defined in the file doing the dehydration or imported from another part of your app. You must also register the function in the app's `hydrators` property. Note that since v10.1.0, the maximum payload size to pass to `z.dehydrate` / `z.dehydrateFile` is 6KB.
2817
- * `inputData` - this is an object that contains things like a `path` or `id` - whatever you need to load data on the other side
2818
- * A known limitation of hydration is a 5 minute cache if the hydration call is made with identical `inputData` within that timeframe. To workaround this cache for records triggering hydration in close succession, include a unique value in the `inputData`, for example a `timestamp` in addition to the record `id`.
2819
-
2820
- > **Why do I need to register my functions?** Because of how JavaScript works with its module system, we need an explicit handle on the function that can be accessed from the App definition without trying to "automagically" (and sometimes incorrectly) infer code locations.
2821
-
2822
- Here is an example that pulls in extra data for a movie:
2823
-
2824
- ```js
2825
- const getMovieDetails = async (z, bundle) => {
2826
- const url = `https://example.com/movies/${bundle.inputData.id}.json`;
2827
- const response = await z.request(url);
2828
-
2829
- // reponse.throwForStatus() if you're using core v9 or older
2830
-
2831
- return response.data; // or response.json if you're using core v9 or older
2832
- };
2833
-
2834
- const movieList = async (z, bundle) => {
2835
- const response = await z.request('https://example.com/movies.json');
2836
-
2837
- // response.throwForStatus() if you're using core v9 or older
2838
-
2839
- return response.data.map((movie) => {
2840
- // so maybe /movies.json is thin content but /movies/:id.json has more
2841
- // details we want...
2842
- movie.details = z.dehydrate(getMovieDetails, { id: movie.id });
2843
- return movie;
2844
- });
2845
- };
2846
-
2847
- const App = {
2848
- version: require('./package.json').version,
2849
- platformVersion: require('zapier-platform-core').version,
2850
-
2851
- // don't forget to register hydrators here!
2852
- // it can be imported from any module
2853
- hydrators: {
2854
- getMovieDetails,
2855
- },
2856
-
2857
- triggers: {
2858
- new_movie: {
2859
- noun: 'Movie',
2860
- display: {
2861
- label: 'New Movie',
2862
- description: 'Triggers when a new Movie is added.',
2863
- },
2864
- operation: {
2865
- perform: movieList,
2866
- },
2867
- },
2868
- },
2869
- };
2870
-
2871
- module.exports = App;
2872
-
2873
- ```
2874
-
2875
- And in future steps of the Zap - if Zapier encounters a pointer as returned by `z.dehydrate(func, inputData)` - Zapier will tie it back to your app and pull in the data lazily.
2876
-
2877
- > **Why can't I just load the data immediately?** Isn't it easier? In some cases it can be - but imagine an API that returns 100 records when polling - doing 100x `GET /id.json` aggressive inline HTTP calls when 99% of the time Zapier doesn't _need_ the data yet is wasteful.
2878
-
2879
- ### Merging Hydrated Data
2880
-
2881
- As you've seen, the usual call to dehydrate will assign the result to an object property:
2882
-
2883
- ```js
2884
- movie.details = z.dehydrate(getMovieDetails, { id: movie.id });
2885
- ```
2886
-
2887
- In this example, all of the movie details will be located in the `details` property (e.g. `details.releaseDate`) after hydration occurs. But what if you want these results available at the top-level (e.g. `releaseDate`)? Zapier supports a specific keyword for this scenario:
2888
-
2889
- ```js
2890
- movie.$HOIST$ = z.dehydrate(getMovieDetails, { id: movie.id });
2891
- ```
2892
-
2893
- Using `$HOIST$` as the key will signal to Zapier that the results should be merged into the object containing the `$HOIST$` key. You can also use this to merge your hydrated data into a property containing "partial" data that exists before dehydration occurs:
2894
-
2895
- ```js
2896
- movie.details = {
2897
- title: movie.title,
2898
- $HOIST$: z.dehydrate(getMovieDetails, { id: movie.id })
2899
- };
2900
- ```
2901
-
2902
- ### File Dehydration
2903
-
2904
- *Added in v7.3.0.*
2905
-
2906
- The method `z.dehydrateFile(func, inputData)` allows you to download a file lazily. It takes the same arguments as `z.dehydrate(func, inputData)` does, but is recommended when the data is a file.
2907
-
2908
- An example can be found in the [Stashing Files](#stashing-files) section.
2909
-
2910
- What makes `z.dehydrateFile` different from `z.dehydrate` has to do with efficiency and when Zapier chooses to hydrate data. Knowing which pointers give us back files helps us delay downloading files until it's absolutely necessary. Not only will it help avoid unnecessary file downloads, it can also prevent errors if the file has a limited availability. (Stashing files, described below, can also help with that situation.)
2911
-
2912
- A good example is when users are creating Zaps in the Zap Editor. If a pointer is made by `z.dehydrate`, the Zap Editor will hydrate the data immediately after pulling in samples. This allows users to map fields from the hydrated data into the subsequent steps of the Zap. If, however, the pointer is made by `z.dehydrateFile`, the Zap Editor will wait to hydrate the file, and will display a placeholder instead. There's nothing inside binary file data for users to map in the subsequent steps.
2913
-
2914
- > `z.dehydrateFile(func, inputData)` was added in v7.3.0. We used to recommend using `z.dehydrate(func, inputData)` for files, but we now recommend changing it to `z.dehydrateFile(func, inputData)` for a better user experience.
2915
-
2916
- ## Stashing Files
2917
-
2918
- It can be expensive to download and stream files or they can require complex handshakes to authorize downloads - so we provide a helpful stash routine that will take any `String`, `Buffer` or `Stream` and return a URL file pointer suitable for returning from triggers, searches, creates, etc.
2919
-
2920
- The interface `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` takes a single required argument - the extra three arguments will be automatically populated in most cases. Here's a full example:
2921
-
2922
- ```js
2923
- const content = 'Hello world!';
2924
- const url = await z.stashFile(content, content.length, 'hello.txt', 'text/plain');
2925
- z.console.log(url);
2926
- // https://zapier-dev-files.s3.amazonaws.com/cli-platform/f75e2819-05e2-41d0-b70e-9f8272f9eebf
2927
- ```
2928
-
2929
- Most likely you'd want to stream from another URL - note the usage of `z.request({raw: true})`:
2930
-
2931
- ```js
2932
- const fileRequest = z.request({url: 'https://example.com/file.pdf', raw: true});
2933
- const url = await z.stashFile(fileRequest); // knownLength and filename will be sniffed from the request. contentType will be binary/octet-stream
2934
- z.console.log(url);
2935
- // https://zapier-dev-files.s3.amazonaws.com/cli-platform/74bc623c-d94d-4cac-81f1-f71d7d517bc7
2936
- ```
2937
-
2938
- > Note: you should only be using `z.stashFile()` in a hydration method or a hook trigger's `perform` if you're sending over a short-lived URL to a file. Otherwise, it can be very expensive to stash dozens of files in a polling call - for example!
2939
-
2940
- See a full example with dehydration/hydration wired in correctly:
2941
-
2942
- ```js
2943
- const stashPDFfunction = (z, bundle) => {
2944
- // use standard auth to request the file
2945
- const filePromise = z.request({
2946
- url: bundle.inputData.downloadUrl,
2947
- raw: true,
2948
- });
2949
- // and swap it for a stashed URL
2950
- return z.stashFile(filePromise);
2951
- };
2952
-
2953
- const pdfList = async (z, bundle) => {
2954
- const response = await z.request('https://example.com/pdfs.json');
2955
-
2956
- // response.throwForStatus() if you're using core v9 or older
2957
-
2958
- // response.json.map if you're using core v9 or older
2959
- return response.data.map((pdf) => {
2960
- // Lazily convert a secret_download_url to a stashed url
2961
- // zapier won't do this until we need it
2962
- pdf.file = z.dehydrateFile(stashPDFfunction, {
2963
- downloadUrl: pdf.secret_download_url,
2964
- });
2965
- delete pdf.secret_download_url;
2966
- return pdf;
2967
- });
2968
- };
2969
-
2970
- const App = {
2971
- version: require('./package.json').version,
2972
- platformVersion: require('zapier-platform-core').version,
2973
-
2974
- hydrators: {
2975
- stashPDF: stashPDFfunction,
2976
- },
2977
-
2978
- triggers: {
2979
- new_pdf: {
2980
- noun: 'PDF',
2981
- display: {
2982
- label: 'New PDF',
2983
- description: 'Triggers when a new PDF is added.',
2984
- },
2985
- operation: {
2986
- perform: pdfList,
2987
- },
2988
- },
2989
- },
2990
- };
2991
-
2992
- module.exports = App;
2993
-
2994
- ```
2995
-
2996
- > To create a new integration for handling files, run `zapier init [your app name] --template files`. You can also check out our working example app [here](https://github.com/zapier/zapier-platform/tree/main/example-apps/files).
2997
-
2998
-
2999
- ## Logging
3000
-
3001
- To view the logs for your application, use the `zapier logs` command.
3002
-
3003
- There are three types of logs for a Zapier app:
3004
-
3005
- * `http`: logged automatically by Zapier on HTTP requests
3006
- * `bundle`: logged automatically on every method execution
3007
- * `console`: manual logs via `z.console.log()` statements ([see below for details](#console-logging))
3008
-
3009
- Note that by default, this command will only fetch logs associated to your user.
3010
-
3011
- For advanced logging options, including the option to fetch logs for other users or specific app versions, look at the help for the logs command:
3012
-
3013
- ```bash
3014
- zapier help logs
3015
- ```
3016
-
3017
- ### Console Logging
3018
-
3019
- To manually print a log statement in your code, use `z.console.log`:
3020
-
3021
- ```js
3022
- z.console.log('Here are the input fields', bundle.inputData);
3023
- ```
3024
-
3025
- The `z.console` object has all the same methods and works just like the Node.js [`Console`](https://nodejs.org/docs/latest-v14.x/api/console.html) class - the only difference is we'll log to our distributed datastore and you can view the logs via `zapier logs` (more below).
3026
-
3027
- ### Viewing Console Logs
3028
-
3029
- To see your `z.console.log` logs, do:
3030
-
3031
- ```bash
3032
- zapier logs --type=console
3033
- ```
3034
-
3035
- ### Viewing Bundle Logs
3036
-
3037
- To see the bundle logs, do:
3038
-
3039
- ```bash
3040
- zapier logs --type=bundle
3041
- ```
3042
-
3043
- ### HTTP Logging
3044
-
3045
- If you are using [shorthand HTTP requests](#shorthand-http-requests) or the `z.request()` method that we provide, HTTP logging is handled automatically for you. For example:
3046
-
3047
- ```js
3048
- z.request('https://57b20fb546b57d1100a3c405.mockapi.io/api/recipes')
3049
- .then((res) => {
3050
- // do whatever you like, this request is already getting logged! :-D
3051
- return res;
3052
- })
3053
- ```
3054
-
3055
- HTTP logging will often work with other methods of making requests as well, but if you're using another method and having trouble seeing logs, try using `z.request()`.
3056
-
3057
- ### Viewing HTTP Logs
3058
-
3059
- To see the HTTP logs, do:
3060
-
3061
- ```bash
3062
- zapier logs --type=http
3063
- ```
3064
- To see detailed HTTP logs, including data such as headers and request and response bodies, do:
3065
-
3066
- ```bash
3067
- zapier logs --type=http --detailed
3068
- ```
3069
-
3070
-
3071
- ## Error Handling
3072
-
3073
- APIs are not always available. Users do not always input data correctly to
3074
- formulate valid requests. Thus, it is a good idea to write apps defensively and
3075
- plan for 4xx and 5xx responses from APIs. Without proper handling, errors often
3076
- have incomprehensible messages for end users, or possibly go uncaught.
3077
-
3078
- Zapier provides a couple of tools to help with error handling. First is the
3079
- `afterResponse` middleware ([docs](#using-http-middleware)), which provides a hook for
3080
- processing all responses from HTTP calls. Second is `response.throwForStatus()`
3081
- ([docs](#http-response-object)), which throws an error if the response status indicates
3082
- an error (status >= 400). Since v10.0.0, we automatically call this method before returning the
3083
- response, unless you set `skipThrowForStatus` on the request or response object. The
3084
- last tool is the collection of errors in `z.errors` ([docs](#zerrors)), which control
3085
- the behavior of Zaps when various kinds of errors occur.
3086
-
3087
- ### General Errors
3088
-
3089
- Errors due to a misconfiguration in a user's Zap should be handled in your app
3090
- by throwing `z.errors.Error` with a user-friendly message and optional error and
3091
- status code. Typically, this will be prettifying 4xx responses or APIs that return
3092
- errors as 200s with a payload that describes the error.
3093
-
3094
- Example: `throw new z.errors.Error('Contact name is too long.', 'InvalidData', 400);`
3095
-
3096
- > `z.errors.Error` was added in v9.3.0. If you're on an older version of `zapier-platform-core`, throw a standard JavaScript `Error` instead, such as `throw new Error('A user-friendly message')`.
3097
-
3098
- A couple best practices to keep in mind:
3099
-
3100
- * Elaborate on terse messages. "not_authenticated" -> "Your API Key is invalid. Please reconnect your account."
3101
- * If the error calls out a specific field, surface that information to the user. "Provided data is invalid" -> "Contact name is invalid"
3102
- * If the error provides details about why a field is invalid, add that in too! "Contact name is invalid" -> "Contact name is too long"
3103
- * The second, optional argument should be a code that a computer could use to identify the type of error.
3104
- * The last, optional argument should be the HTTP status code, if any.
3105
-
3106
- The code and status can be used by us to provide relevant troubleshooting to the
3107
- user when we communicate the error.
3108
-
3109
- Note that if a Zap raises too many error messages it will be automatically
3110
- turned off, so only use these if the scenario is truly an error that needs to
3111
- be fixed.
3112
-
3113
- ### Halting Execution
3114
-
3115
- Any operation can be interrupted or "halted" (not success, not error, but
3116
- stopped for some specific reason) with a `HaltedError`. You might find yourself
3117
- using this error in cases where a required pre-condition is not met. For instance,
3118
- in a create to add an email address to a list where duplicates are not allowed,
3119
- you would want to throw a `HaltedError` if the Zap attempted to add a duplicate.
3120
- This would indicate failure, but it would be treated as a soft failure.
3121
-
3122
- Unlike throwing `z.errors.Error`, a Zap will never by turned off when this error is thrown
3123
- (even if it is raised more often than not).
3124
-
3125
- Example: `throw new z.errors.HaltedError('Your reason.');`
3126
-
3127
- ### Stale Authentication Credentials
3128
-
3129
- For apps that require manual refresh of authorization on a regular basis, Zapier
3130
- provides a mechanism to notify users of expired credentials. With the
3131
- `ExpiredAuthError`, the current operation is interrupted and a predefined email
3132
- is sent out asking the user to refresh the credentials. While the auth is
3133
- disconnected, Zap runs will not be executed, to prevent more calls with expired
3134
- credentials. (The runs will be
3135
- [Held](https://help.zapier.com/hc/en-us/articles/8496291148685-View-and-manage-your-Zap-history#held-0-3),
3136
- and the user will be able to replay them after reconnecting.)
3137
-
3138
- Example: `throw new z.errors.ExpiredAuthError('You must manually reconnect this auth.');`
3139
-
3140
- For apps that use OAuth2 with `autoRefresh: true` or Session Auth, `core` injects
3141
- a built-in `afterResponse` middleware that throws an error when the response status
3142
- is 401. The error will signal Zapier to refresh the credentials and then retry the
3143
- failed operation. You can also throw this error manually if your server doesn't use the 401 status or you want to trigger an auth refresh even if the credentials aren't stale.
3144
-
3145
- Example: `throw new z.errors.RefreshAuthError();`
3146
-
3147
- ### Handling Throttled Requests
3148
-
3149
- Since v11.2.0, there are two types of errors that can cause Zapier to throttle an operation and retry at a later time.
3150
- This is useful if the API you're interfacing with reports it is receiving too many requests, often indicated by
3151
- receiving a response status code of 429.
3152
-
3153
- If a response receives a status code of 429 and is not caught, Zapier will re-attempt the operation after a delay.
3154
- The delay can be customized by the server response containing a specific
3155
- [Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header in your error response or with a specified time delay in seconds using a `ThrottledError`:
3156
-
3157
- ```js
3158
- const yourAfterResponse = (resp) => {
3159
- if (resp.status === 429) {
3160
- throw new z.errors.ThrottledError('message here', 60); // Zapier will retry in 60 seconds
3161
- }
3162
- return resp;
3163
- };
3164
- ```
3165
- Instead of a user’s Zap erroring and halting, the request will be repeatedly retried at the specified time.
3166
-
3167
- For throttled requests that occur during processing of a webhook trigger's perform, before results are produced; there is a max retry delay of 300 seconds and a default delay of 60 seconds if none is specified. For webhook processing only, if a request during the retry attempt is also throttled, it will not be re-attempted again.
3168
-
3169
- ## Testing
3170
-
3171
- There are several ways to test your Zapier integration:
3172
-
3173
- * You can use the `zapier invoke` command to invoke a trigger, search, create, or an auth operation locally.
3174
- * You can write unit tests for your Zapier integration that run locally, outside of the Zapier editor.
3175
- * You can run these tests in a CI tool like [Travis](https://travis-ci.com/).
3176
-
3177
- ### Using `zapier invoke` Command
3178
-
3179
- *Added in v15.17.0.*
3180
-
3181
- The `zapier invoke <ACTION_TYPE> <ACTION_KEY>` CLI command emulates how the Zapier production environment would invoke your app. Since it runs code locally, it's a fast way to debug and test interactively without needing to deploy the code to Zapier.
3182
-
3183
- Its general execution flow invovles calling `operation.inputFields` of an action, resolving the input data to the expected types, and then calling the `operation.perform` method.
3184
-
3185
- `zapier invoke --help` should be self-explanatory, but here's a quick rundown:
3186
-
3187
- ```bash
3188
- # Intialize auth data in .env file
3189
- zapier invoke auth start
3190
-
3191
- # Test your auth data in .env
3192
- zapier invoke auth test
3193
- zapier invoke auth label
3194
-
3195
- # Invoke a polling trigger
3196
- zapier invoke trigger new_recipe
3197
-
3198
- # Invoke a create action
3199
- zapier invoke create add_recipe --inputData '{"name": "Pancakes"}'
3200
- zapier invoke create add_recipe --inputData @file.json
3201
- ```
3202
-
3203
- ### Writing Unit Tests
3204
-
3205
- From v10 of `zapier-platform-cli`, we recommend using the [Jest](https://jestjs.io/) testing framework. After running `zapier init` you should find an example test to start from in the `test` directory.
3206
-
3207
- > Note: On v9, the recommendation was [Mocha](https://mochajs.org/). You can still use Mocha if you prefer.
3208
-
3209
- ```js
3210
- /* globals describe, expect, test */
3211
-
3212
- const zapier = require('zapier-platform-core');
3213
-
3214
- // createAppTester() makes it easier to test your app. It takes your raw app
3215
- // definition, and returns a function that will test you app.
3216
- const App = require('../index');
3217
- const appTester = zapier.createAppTester(App);
3218
-
3219
- // Inject the vars from the .env file to process.env. Do this if you have a .env
3220
- // file.
3221
- zapier.tools.env.inject();
3222
-
3223
- describe('triggers', () => {
3224
- test('new recipe', async () => {
3225
- const bundle = {
3226
- inputData: {
3227
- style: 'mediterranean',
3228
- },
3229
- };
3230
-
3231
- const results = await appTester(
3232
- App.triggers.recipe.operation.perform,
3233
- bundle
3234
- );
3235
- expect(results.length).toBeGreaterThan(1);
3236
-
3237
- const firstRecipe = results[0];
3238
- expect(firstRecipe.id).toBe(1);
3239
- expect(firstRecipe.name).toBe('Baked Falafel');
3240
- });
3241
- });
3242
-
3243
- ```
3244
-
3245
- ### Using the `z` Object in Tests
3246
-
3247
- Introduced in `core@11.1.0`, `appTester` can now run arbitrary functions:
3248
-
3249
- ```js
3250
- /* globals describe, expect, test */
3251
-
3252
- const zapier = require('zapier-platform-core');
3253
-
3254
- const App = require('../index');
3255
- const appTester = zapier.createAppTester(App);
3256
-
3257
- describe('triggers', () => {
3258
- test('new recipe', async () => {
3259
- const adHocResult = await appTester(
3260
- // your in-line function takes the same [z, bundle] arguments as normal
3261
- async (z, bundle) => {
3262
- // requests are made using your integration's actual middleware
3263
- // make sure to pass the normal `bundle` arg to `appTester` if your requests need auth
3264
- const response = await z.request(
3265
- 'https://example.com/some/setup/method',
3266
- {
3267
- params: {
3268
- numItems: bundle.inputData.someValue,
3269
- },
3270
- }
3271
- );
3272
-
3273
- return {
3274
- // you can use all the functions on the `z` object
3275
- someHash: z.hash('md5', 'mySecret'),
3276
- data: response.data,
3277
- };
3278
- },
3279
- {
3280
- // you must provide auth data for authenticated requests
3281
- // (just like running a normal trigger)
3282
- authData: { token: 'some-api-key' },
3283
- // put arbitrary function params in `inputData`
3284
- inputData: {
3285
- someValue: 3,
3286
- },
3287
- }
3288
- );
3289
-
3290
- expect(adHocResult.someHash).toEqual('a5beb6624e092adf7be31176c3079e64');
3291
- expect(adHocResult.data).toEqual({ whatever: true });
3292
-
3293
- // ... rest of test
3294
- });
3295
- });
3296
-
3297
- ```
3298
-
3299
- ### Mocking Requests
3300
-
3301
- It's useful to test your code without actually hitting any external services. [Nock](https://github.com/node-nock/nock) is a Node.js utility that intercepts requests before they ever leave your computer. You can specify a response code, body, headers, and more. It works out of the box with `z.request` by setting up your `nock` before calling `appTester`.
3302
-
3303
- ```js
3304
- /* globals describe, expect, test */
3305
-
3306
- const zapier = require('zapier-platform-core');
3307
-
3308
- const App = require('../index');
3309
- const appTester = zapier.createAppTester(App);
3310
-
3311
- const nock = require('nock');
3312
-
3313
- describe('triggers', () => {
3314
- test('new recipe', async () => {
3315
- const bundle = {
3316
- inputData: {
3317
- style: 'mediterranean',
3318
- },
3319
- };
3320
-
3321
- // mocks the next request that matches this url and querystring
3322
- nock('https://example.com/api')
3323
- .get('/recipes')
3324
- .query(bundle.inputData)
3325
- .reply(200, [
3326
- { name: 'name 1', directions: 'directions 1', id: 1 },
3327
- { name: 'name 2', directions: 'directions 2', id: 2 },
3328
- ]);
3329
-
3330
- const results = await appTester(
3331
- App.triggers.recipe.operation.perform,
3332
- bundle
3333
- );
3334
-
3335
- expect(results.length).toBeGreaterThan(1);
3336
-
3337
- const firstRecipe = results[0];
3338
- expect(firstRecipe.id).toBe(1);
3339
- expect(firstRecipe.name).toBe('name 1');
3340
- });
3341
- });
3342
-
3343
- ```
3344
-
3345
- Here's more info about nock and its usage in the [README](https://github.com/node-nock/nock/blob/master/README.md).
3346
-
3347
- ### Running Unit Tests
3348
-
3349
- To run all your tests do:
3350
-
3351
- ```bash
3352
- zapier test
3353
- ```
3354
-
3355
- > You can also go direct with `npm test` or `node_modules/.bin/jest`.
3356
-
3357
- ### Testing & Environment Variables
3358
-
3359
- The best way to store sensitive values (like API keys, OAuth secrets, or passwords) is in an `.env` (or `.environment`, see below note) file ([learn more](https://github.com/motdotla/dotenv#faq)). Then, you can include the following before your tests run:
3360
-
3361
- ```js
3362
- const zapier = require('zapier-platform-core');
3363
- zapier.tools.env.inject(); // inject() can take a filename; defaults to ".env"
3364
-
3365
- // now process.env has all the values in your .env file
3366
- ```
3367
-
3368
- > `.env` is the new recommended name for the environment file since v5.1.0. The old name `.environment` is deprecated but will continue to work for backward compatibility.
3369
-
3370
- > Remember: **NEVER** add your secrets file to version control!
3371
-
3372
- Additionally, you can provide them dynamically at runtime:
3373
-
3374
- ```bash
3375
- CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
3376
- ```
3377
-
3378
- Or, `export` them explicitly and place them into the environment:
3379
-
3380
- ```bash
3381
- export CLIENT_ID=1234
3382
- export CLIENT_SECRET=abcd
3383
- zapier test
3384
- ```
3385
-
3386
- ### Testing in Your CI
3387
-
3388
- Whether you use Travis, Circle, Jenkins, or another service, we aim to make it painless to test in an automated environment.
3389
-
3390
- Behind the scenes `zapier test` does a standard `npm test`, which could be [Jest](https://jestjs.io/) or [Mocha](https://mochajs.org/), based on your project setup.
3391
-
3392
- This makes it straightforward to integrate into your testing interface. For example, if you want to test with [Travis CI](https://travis-ci.com/), the `.travis.yml` would look something like this:
3393
-
3394
- ```yaml
3395
- language: node_js
3396
- node_js:
3397
- - "v18"
3398
- before_script: npm install -g zapier-platform-cli
3399
- script: CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
3400
- ```
3401
-
3402
- You can substitute `zapier test` with `npm test`, or a direct call to `node_modules/.bin/jest`. We recommend putting environment variables directly into the configuration screens Jenkins, Travis, or other services provide.
3403
-
3404
- Alternatively to reading the deploy key from root (the default location), you may set the `ZAPIER_DEPLOY_KEY` environment variable to run privileged commands without the human input needed for `zapier login`. We suggest encrypting your deploy key in the manner your CI provides (such as [these instructions](https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml), for Travis).
3405
-
3406
- ### Debugging Tests
3407
-
3408
- Sometimes tests aren't enough, and you may want to step through your code and set breakpoints. The testing suite is a regular Node.js process, so debugging it doesn't take anything special. Because we recommend `jest` for testing, these instructions will outline steps for debugging w/ jest, but other test runners will work similarly. You can also refer to [Jest's own docs on the subject](https://jestjs.io/docs/en/troubleshooting#tests-are-failing-and-you-dont-know-why).
3409
-
3410
- To start, add the following line to the `scripts` section of your `package.json`:
3411
-
3412
- ```
3413
- "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand"
3414
- ```
3415
-
3416
- This will tell `node` to inspect the `jest` processes, which is exactly what we need.
3417
-
3418
- Next, add a `debugger;` statement somewhere in your code, probably in a `perform` method:
3419
-
3420
- ```js
3421
- // triggers on a new pizza with a certain tag
3422
- const perform = async (z, bundle) => {
3423
- const response = await z.request({
3424
- url: "https://jsonplaceholder.typicode.com/posts",
3425
- params: {
3426
- tag: bundle.inputData.tagName,
3427
- },
3428
- });
3429
- debugger;
3430
- // this should return an array of objects
3431
- return response.data;
3432
- };
3433
- ```
3434
-
3435
- This creates a _breakpoint_ while `inspect`ing, or a starting point for our manual inspection.
3436
-
3437
- Next, you'll need an inspection client. The most available one is probably the Google Chrome browser, but there are [lots of options](https://nodejs.org/en/docs/guides/debugging-getting-started/#inspector-clients). We'll use Chrome for this example. In your terminal (and in your integration's root directory), run `yarn test:debug` (or `npm run test:debug`). You should see the following:
3438
-
3439
- ```
3440
- % yarn test:debug
3441
- yarn run v1.22.10
3442
- $ node --inspect-brk node_modules/.bin/jest --runInBand
3443
- Debugger listening on ws://127.0.0.1:9229/5edaab3c-a1d3-45e4-b374-0536095c559b
3444
- For help, see: https://nodejs.org/en/docs/inspector
3445
- ```
3446
-
3447
- Now in Chrome, go to chrome://inspect. Make sure `Discover Network Targets` is checked and you should see a path to your `jest` file on your local machine:
3448
-
3449
- ![](https://cdn.zappy.app/e2836d2950e1f8a03e3621a22452c3cd.png)
3450
-
3451
- Click `inspect`. A new window will open. Next, click the little blue arrow in the top right to actually run the code:
3452
-
3453
- ![](https://cdn.zappy.app/a64e7963a7090e9730d9c8e7b3595a6a.png)
3454
-
3455
- After a few seconds, you'll see your code, the `debugger` statement, and info about the current environment on the right panel. You should see familiar data in the `Locals` section, such as the `response` variable, and the `z` object.
3456
-
3457
- ![](https://cdn.zappy.app/4bfdfe079a344ab7aced64ad7728bc6a.png)
3458
-
3459
- Debugging combined with thorough unit tests will hopefully equip you in keeping your Zapier integration in smooth working order.
3460
-
3461
- ## Using `npm` Modules
3462
-
3463
- Use `npm` modules just like you would use them in any other node app, for example:
3464
-
3465
- ```bash
3466
- npm install --save jwt
3467
- ```
3468
-
3469
- And then `package.json` will be updated, and you can use them like anything else:
3470
-
3471
- ```js
3472
- const jwt = require('jwt');
3473
- ```
3474
-
3475
- During the `zapier build` or `zapier push` step - we'll copy all your code to a temporary folder and do a fresh re-install of modules.
3476
-
3477
- > Note: If your package isn't being pushed correctly (IE: you get "Error: Cannot find module 'whatever'" in production), try adding the `--disable-dependency-detection` flag to `zapier push`.
3478
-
3479
- > Note 2: You can also try adding a `includeInBuild` array property (with paths to include, which will be evaluated to RegExp, with a case insensitive flag) to your `.zapierapprc` file, to make it look like:
3480
-
3481
- ```json
3482
- {
3483
- "id": 1,
3484
- "key": "App1",
3485
- "includeInBuild": [
3486
- "test.txt",
3487
- "testing.json"
3488
- ]
3489
- }
3490
-
3491
- ```
3492
-
3493
- > Warning: Do not use compiled libraries unless you run your build on the AWS AMI `ami-4fffc834`, or follow the Docker instructions below.
3494
-
3495
- ## Building Native Packages with Docker
3496
-
3497
- Unfortunately if you are developing on a macOS or Windows box you won't be able to build native libraries locally. If you try and push locally build native modules, you'll get runtime errors during usage. However, you can use Docker and Docker Compose to do this in a pinch. Make sure you have all the necessary Docker programs installed and follow along.
3498
-
3499
- First, create your `Dockerfile`:
3500
-
3501
- ```Dockerfile
3502
- FROM amazonlinux:2017.03.1.20170812
3503
-
3504
- RUN yum install zip findutils wget gcc44 gcc-c++ libgcc44 cmake -y
3505
-
3506
- RUN wget https://nodejs.org/dist/v8.10.0/node-v8.10.0.tar.gz && \
3507
- tar -zxvf node-v8.10.0.tar.gz && \
3508
- cd node-v8.10.0 && \
3509
- ./configure && \
3510
- make && \
3511
- make install && \
3512
- cd .. && \
3513
- rm -rf node-v8.10.0 node-v8.10.0.tar.gz
3514
-
3515
- RUN npm i -g zapier-platform-cli
3516
-
3517
- WORKDIR /app
3518
- ```
3519
-
3520
- And finally, create your `docker-compose.yml` file:
3521
-
3522
- ```yml
3523
- version: '3.4'
3524
-
3525
- services:
3526
- pusher:
3527
- build: .
3528
- volumes:
3529
- - .:/app
3530
- - node_modules:/app/node_modules:delegated
3531
- - ~/.zapierrc:/root/.zapierrc
3532
- command: 'bash -c "npm i && zapier push"'
3533
- environment:
3534
- ZAPIER_DEPLOY_KEY: ${ZAPIER_DEPLOY_KEY}
3535
-
3536
- volumes:
3537
- node_modules:
3538
- ```
3539
-
3540
- > Note: Watch out for your `package-lock.json` file, if it exists for local install it might incorrectly pin a native version.
3541
-
3542
- Now you should be able to run `docker-compose run pusher` and see the build and push successfully complete!
3543
-
3544
-
3545
- ## Using Transpilers
3546
-
3547
- If you would like to use a transpiler like `babel`, you can add a script named `_zapier-build` to your `package.json`, which will be run during `zapier build`,
3548
- `zapier push`, and `zapier upload`. See the following example:
3549
-
3550
- ```json
3551
- {
3552
- "scripts": {
3553
- "zapier-dev": "babel src --out-dir lib --watch",
3554
- "_zapier-build": "babel src --out-dir lib"
3555
- }
3556
- }
3557
- ```
3558
-
3559
- Then, you can have your fancy ES7 code in `src/*` and a root `index.js` like this:
3560
-
3561
- ```js
3562
- module.exports = require('./lib');
3563
- ```
3564
-
3565
- And work with commands like this:
3566
-
3567
- ```bash
3568
- # watch and recompile
3569
- npm run zapier-dev
3570
-
3571
- # tests should work fine
3572
- zapier test
3573
-
3574
- # every build ensures a fresh build
3575
- zapier push
3576
- ```
3577
-
3578
- There are a lot of details left out - check out the full example app [here](https://github.com/zapier/zapier-platform/tree/main/example-apps/babel).
3579
-
3580
- > We recommend using `zapier init .` to create an app - you’ll be presented with a list of currently available example templates to start with.
3581
-
3582
- ## FAQs
3583
-
3584
- ### Why doesn't Zapier support newer versions of Node.js?
3585
-
3586
- We run your code on AWS Lambda, which only supports a few [versions](https://docs.aws.amazon.com/lambda/latest/dg/programming-model.html) of Node. Sometimes that doesn't include the latest version. Additionally, with thousands of apps running on the Zapier platform, we have to be sure upgrading to the latest Node version will not have a negative impact.
3587
-
3588
- ### How do I manually set the Node.js version to run my app with?
3589
-
3590
- Update your `zapier-platform-core` dependency in `package.json`. Each major version ties to a specific version of Node.js. You can find the mapping [here](https://github.com/zapier/zapier-platform/blob/main/packages/cli/src/version-store.js). We only support the version(s) supported by [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/programming-model.html).
3591
-
3592
- **IMPORTANT CAVEAT**: AWS periodically deprecates Node versions as they reach EOL. They announce this[on their blog](https://aws.amazon.com/blogs/developer/node-js-6-is-approaching-end-of-life-upgrade-your-aws-lambda-functions-to-the-node-js-10-lts/). Similar info and dates are available on [github](https://github.com/nodejs/Release). Well before this date, we'll have a version of `core` that targets the newer Node version.
3593
-
3594
- If you don't upgrade before the cutoff date, there's a chance that AWS will throw an error when attempting to run your app's code. If that's the case, we'll instead run it under the oldest Node version still supported. All that is to say, **we may run your code on a newer version of Node.js than you intend** if you don't update your app's dependencies periodically.
3595
-
3596
- ### When to use placeholders or curlies?
3597
-
3598
- You will see both [template literal placeholders](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Expression_interpolation) `${var}` and (double) "curlies" `{{var}}` used in examples.
3599
-
3600
- In general, use `${var}` within functions and use `{{var}}` anywhere else.
3601
-
3602
- Placeholders get evaluated as soon as the line of code is evaluated. This means that if you use `${process.env.VAR}` in a trigger configuration, `zapier push` will substitute it with your local environment's value for `VAR` when it builds your app and the value set via `zapier env:set` will not be used.
3603
-
3604
- > If you're not familiar with [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), know that `const val = "a" + b + "c"` is essentially the same as <code>const val = &#96;a${b}c&#96;</code>.
3605
-
3606
- ### Does Zapier support XML (SOAP) APIs?
3607
-
3608
- Not natively, but it can! Users have reported that the following `npm` modules are compatible with the CLI Platform:
3609
-
3610
- * [pixl-xml](https://github.com/jhuckaby/pixl-xml)
3611
- * [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)
3612
- * [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser)
3613
-
3614
- Since core v10, it's possible for [shorthand requests](#shorthand-http-requests) to parse XML. Use an `afterResponse` [middleware](#using-http-middleware) that sets `response.data` to the parsed XML:
3615
-
3616
- ```js
3617
- const xml = require('pixl-xml');
3618
-
3619
- const App = {
3620
- // ...
3621
- afterResponse: [
3622
- (response, z, bundle) => {
3623
- // Only works on core v10+!
3624
- response.throwForStatus();
3625
- response.data = xml.parse(response.content);
3626
- return response;
3627
- },
3628
- ],
3629
- // ...
3630
- };
3631
-
3632
- ```
3633
-
3634
- ### Is it possible to iterate over pages in a polling trigger?
3635
-
3636
- Yes, though there are caveats. Your entire function only gets 30 seconds to run. HTTP requests are costly, so paging through a list may time out (which you should avoid at all costs).
3637
-
3638
- ```js
3639
- // some async call
3640
- const makeCall = (z, start, limit) => {
3641
- return z.request({
3642
- url: 'https://jsonplaceholder.typicode.com/posts',
3643
- params: {
3644
- _start: start,
3645
- _limit: limit,
3646
- },
3647
- });
3648
- };
3649
-
3650
- // triggers on paging with a certain tag
3651
- const performPaging = async (z, bundle) => {
3652
- // array of promises
3653
- const promises = [];
3654
-
3655
- // 5 requests with page size = 3
3656
- let start = 0;
3657
- const limit = 3;
3658
- for (let i = 0; i < 5; i++) {
3659
- promises.push(makeCall(z, start, limit));
3660
- start += limit;
3661
- }
3662
-
3663
- // send requests concurrently
3664
- const responses = await Promise.all(promises);
3665
- return responses.map((res) => res.data);
3666
- };
3667
-
3668
- module.exports = {
3669
- key: 'paging',
3670
- noun: 'Paging',
3671
-
3672
- display: {
3673
- label: 'Get Paging',
3674
- description: 'Triggers on a new paging.',
3675
- },
3676
-
3677
- operation: {
3678
- inputFields: [],
3679
- perform: performPaging,
3680
- },
3681
- };
3682
-
3683
- ```
3684
-
3685
- If you need to do more requests conditionally based on the results of an HTTP call (such as the "next URL" param or similar value), using `async/await` (as shown in the example below) is a good way to go. If you go this route, only page as far as you need to. Keep an eye on the polling [guidelines](https://zapier.com/developer/documentation/v2/deduplication/), namely the part about only iterating until you hit items that have probably been seen in a previous poll.
3686
-
3687
- ```js
3688
- // a hypothetical API where payloads are big so we want to heavily limit how much comes back
3689
- // we want to only return items created in the last hour
3690
-
3691
- const asyncExample = async (z, bundle) => {
3692
- const limit = 3;
3693
- let start = 0;
3694
- const twoHourMilliseconds = 60 * 60 * 2 * 1000;
3695
- const hoursAgo = new Date() - twoHourMilliseconds;
3696
-
3697
- let response = await z.request({
3698
- url: 'https://jsonplaceholder.typicode.com/posts',
3699
- params: {
3700
- _start: start,
3701
- _limit: limit,
3702
- },
3703
- });
3704
-
3705
- let results = response.data; // response.json if you're using core v9 or older
3706
-
3707
- // keep paging until the last item was created over two hours ago
3708
- // then we know we almost certainly haven't missed anything and can let
3709
- // deduper handle the rest
3710
-
3711
- while (new Date(results[results.length - 1].createdAt) > hoursAgo) {
3712
- start += limit; // next page
3713
-
3714
- response = await z.request({
3715
- url: 'https://jsonplaceholder.typicode.com/posts',
3716
- params: {
3717
- _start: start,
3718
- _limit: limit,
3719
- },
3720
- });
3721
-
3722
- results = results.concat(response.data);
3723
- }
3724
-
3725
- return results;
3726
- };
3727
-
3728
- ```
3729
-
3730
- ### How do search-powered fields relate to dynamic dropdowns and why are they both required together?
3731
-
3732
- To understand search-powered fields, we have to have a good understanding of dynamic dropdowns.
3733
-
3734
- When users are selecting specific resources (for instance, a Google Sheet), it's important they're able to select the exact sheet they want. Instead of referencing the sheet by name (which may change), we match via `id` instead. Rather than directing the user copy and paste an id for every item they might encounter, there is the notion of a **dynamic dropdown**. A dropdown is a trigger that returns a list of resources. It can pull double duty and use its results to power another trigger, search, or action in the same app. It provides a list of ids with labels that show the item's name:
3735
-
3736
- ![](https://cdn.zappy.app/2d7eeda63ff34b70f1d1788de0117181.png)
3737
-
3738
- The field's value reaches your app as an id. You define this connection with the `dynamic` property, which is a string: `trigger_key.id_key.label_key`. This approach works great if the user setting up the Zap always wants the Zap to use the same spreadsheet. They specify the id during setup and the Zap runs happily.
3739
-
3740
- **Search fields** take this connection a step further. Rather than set the spreadsheet id at setup, the user could precede the action with a search field to make the id dynamic. For instance, let's say you have a different spreadsheet for every day of the week. You could build the following zap:
3741
-
3742
- 1. Some Trigger
3743
- 2. Calculate what day of the week it is today (Code)
3744
- 3. Find the spreadsheet that matches the day from Step 2
3745
- 4. Update the spreadsheet (with the id from step 3) with some data
3746
-
3747
- If the connection between steps 3 and 4 is a common one, you can indicate that in your field by specifying `search` as a `search_key.id_key`. When paired **with a dynamic dropdown**, this will add a button to the editor that will add the search step to the user's Zap and map the id field correctly.
3748
-
3749
- ![](https://cdn.zappy.app/081e63141ff05c131dadb8ebbea727b0.png)
3750
-
3751
- This is paired most often with "update" actions, where a required parameter will be a resource id.
3752
-
3753
- <a id="paging"></a>
3754
- ### What's the deal with pagination? When is it used and how does it work?
3755
-
3756
- Paging is **only used when a trigger is part of a dynamic dropdown**. Depending on how many items exist and how many are returned in the first poll, it's possible that the resource the user is looking for isn't in the initial poll. If they hit the "see more" button, we'll increment the value of `bundle.meta.page` and poll again.
3757
-
3758
- Paging is a lot like a regular trigger except the range of items returned is dynamic. The most common example of this is when you can pass a `offset` parameter:
3759
-
3760
- ```js
3761
- const perform = async (z, bundle) => {
3762
- const response = await z.request({
3763
- url: 'https://example.com/api/list.json',
3764
- params: {
3765
- limit: 100,
3766
- offset: 100 * bundle.meta.page
3767
- }
3768
- });
3769
- return response.data; // or response.json you're using core v9 or older
3770
- };
3771
- ```
3772
-
3773
- If your API uses cursor-based paging instead of an offset, you can use `z.cursor.get` and `z.cursor.set`:
3774
-
3775
- ```js
3776
- const perform = async (z, bundle) => {
3777
- let cursor;
3778
-
3779
- // if fetching a page other than the first (first page is 0),
3780
- // get the cursor stored after fetching the previous page.
3781
- if (bundle.meta.page > 0) {
3782
- cursor = await z.cursor.get();
3783
-
3784
- // if the previous page was the last one and cursor is empty/null,
3785
- // return an empty array.
3786
- if (!cursor) {
3787
- return [];
3788
- }
3789
- }
3790
-
3791
- const response = await z.request(
3792
- 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
3793
- {
3794
- // cursor typically is a param to pass along to the next request,
3795
- // or the full URL for the next page of items.
3796
- params: { cursor },
3797
- }
3798
- );
3799
-
3800
- // after fetching a page, set the returned cursor for the next page,
3801
- // or an empty string if the cursor is null
3802
- await z.cursor.set(response.nextPage ?? '');
3803
-
3804
- return response.items;
3805
- };
3806
-
3807
- ```
3808
-
3809
- Cursors are stored per-zap and last about an hour. Per the above, make sure to only include the cursor if `bundle.meta.page > 0`, so you don't accidentally reuse a cursor from a previous poll.
3810
-
3811
- Lastly, you need to set `canPaginate` to `true` in your polling definition (per the [schema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#basicpollingoperationschema)) for the `z.cursor` methods to work as expected.
3812
-
3813
- <a id="dedup"></a>
3814
- ### How does deduplication work?
3815
-
3816
- Each time a polling Zap runs, Zapier extracts a unique "primary key" for each item in the response. Zapier needs to decide which of the items should trigger the Zap. To do this, we compare the primary keys to all those we've seen before, trigger on new objects, and update the list of seen primary keys. When a Zap is turned on, we initialize the list of seen primary keys with a single poll. When it's turned off, we clear that list. For this reason, it's important that calls to a polling endpoint always return the newest items.
3817
-
3818
- For example, the initial poll returns objects 4, 5, and 6 (where a higher primary key is newer). If a later poll increases the limit and returns objects 1-6, then 1, 2, and 3 will be (incorrectly) treated like new objects.
3819
-
3820
- By default, the primary key is the item's `id` field. Since v15.6.0, you can customize the primary key by setting `primary` to true in `outputFields`.
3821
-
3822
- There's a more in-depth explanation [here](https://platform.zapier.com/build/deduplication).
3823
-
3824
- ### Why are my triggers complaining if I don't provide an explicit `id` field?
3825
-
3826
- For deduplication to work, we need to be able to identify and use a unique field. In older, legacy Zapier Web Builder apps, we guessed if `id` wasn't present. In order to ensure we don't guess wrong, we now require that the developers send us an `id` field. If your objects have a differently-named unique field, feel free to adapt this snippet and ensure this test passes:
3827
-
3828
- ```js
3829
- // ...
3830
- let items = response.data.items; // or response.json.items if you're using core v9 or older
3831
- return items.map((item) => {
3832
- item.id = item.contactId;
3833
- return item;
3834
- });
3835
- ```
3836
-
3837
- Since v15.6.0, instead of using the default `id` field, you can also define one or more `outputFields` as `primary`. For example:
3838
-
3839
- ```js
3840
- {
3841
- triggers: {
3842
- recipe: {
3843
- operation: {
3844
- outputField: [
3845
- { key: 'userId', primary: true },
3846
- { key: 'slug', primary: true },
3847
- { key: 'name' }
3848
- ]
3849
- }
3850
- }
3851
- }
3852
- }
3853
- ```
3854
-
3855
- will tell Zapier to use `(userId, slug)` as the unique primary key to deduplicate items when running a polling trigger.
3856
-
3857
- **Limitation:** The `primary` option currently doesn't support mixing top-level fields with nested fields that use double underscores in their keys. For example, if you set `primary: true` on both `id` and `user__id`, the `primary` setting on the `user__id` field will be ignored; only `id` will be used for deduplication. However, if all the `primary` fields are all nested, such as `user__id` + `user__name`, it will work as expected.
3858
-
3859
- ### Node X No Longer Supported
3860
-
3861
- If you're seeing errors like the following:
3862
-
3863
- ```
3864
- InvalidParameterValueException An error occurred (InvalidParameterValueException) when calling the CreateFunction operation: The runtime parameter of nodejs6.10 is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (nodejsX.Y) while creating or updating functions.
3865
- ```
3866
-
3867
- ... then you need to update your `zapier-platform-core` dependency to a non-deprecated version that uses a newer version of Node.js. Complete the following instructions as soon as possible:
3868
-
3869
- 1. Edit `package.json` to depend on a later major version of `zapier-platform-core`. There's a list of all breaking changes (marked with a :exclamation:) in the [changelog](https://github.com/zapier/zapier-platform/blob/main/CHANGELOG.md).
3870
- 2. Increment the `version` property in `package.json`
3871
- 3. Ensure you're using version `v18` (or greater) of node locally (`node -v`). Use [nvm](https://github.com/nvm-sh/nvm) to use a different one if need be.
3872
- 4. Run `rm -rf node_modules && npm i` to get a fresh copy of everything
3873
- 5. Run `zapier test` to ensure your tests still pass
3874
- 6. Run `zapier push`
3875
- 7. Run `zapier promote YOUR_NEW_VERSION` (from step 2)
3876
- 8. Migrate your users from the previous version (`zapier migrate OLD_VERSION YOUR_NEW_VERSION`)
3877
-
3878
- <a id="analytics"></a>
3879
- ### What Analytics are Collected?
3880
-
3881
- Starting with v8.4.0, Zapier collects information about each invocation of the CLI tool.
3882
-
3883
- This data is collected purely to improve the CLI experience and will **never** be used for advertising or any non-product purpose. There are 3 collection modes that are set on a per-computer basis.
3884
-
3885
- **Anonymous**
3886
-
3887
- When you run a command with analytics in `anonymous` mode, the following data is sent to Zapier:
3888
-
3889
- * which command you ran
3890
- * if that command is a known command
3891
- * how many arguments you supplied (but not the contents of the arguments)
3892
- * which flags you used (but not their contents)
3893
- * the version of CLI that you're using
3894
-
3895
- **Enabled** (the default)
3896
-
3897
- When analytics are fully `enabled`, the above is sent, plus:
3898
-
3899
- * your operating system (the result of calling [`process.platform`](https://nodejs.org/api/process.html#process_process_platform))
3900
- * your Zapier user id
3901
-
3902
- **Disabled**
3903
-
3904
- Lastly, analytics can be `disabled` entirely, either by running `zapier analytics --mode disabled` or setting the `DISABLE_ZAPIER_ANALYTICS` environment variable to `1`.
3905
-
3906
- We take great care not to collect any information about your filesystem or anything otherwise secret. You can see exactly what's being collecting at runtime by prefixing any command with `DEBUG=zapier:analytics`.
3907
-
3908
- ### What's the Difference Between an "App" and an "Integration"?
3909
-
3910
- We're in the process of doing some renaming across our Zapier marketing terms. Eventually we'll use "integration" everywhere. Until then, know that these terms are interchangeable and describe the code that you write that connects your API to Zapier.
3911
-
3912
- ## Command Line Tab Completion
3913
-
3914
- Introduced in v9.1.0, the `zapier autocomplete` command shows instructions for generating command line autocomplete.
3915
-
3916
- Follow those instructions to enable completion for `zapier` commands and flags!
3917
-
3918
- ## The Zapier Platform Packages
3919
-
3920
- The Zapier Platform consists of 3 npm packages that are released simultaneously.
3921
-
3922
- - [`zapier-platform-cli`](https://github.com/zapier/zapier-platform/tree/main/packages/cli) is the code that powers the `zapier` command. You use it most commonly with the `test`, `scaffold`, and `push` commands. It's installed with `npm install -g zapier-platform-cli` and does not correspond to a particular app.
3923
- - [`zapier-platform-core`](https://github.com/zapier/zapier-platform/tree/main/packages/core) is what allows your app to interact with Zapier. It holds the `z` object and app tester code. Your app depends on a specific version of `zapier-platform-core` in the `package.json` file. It's installed via `npm install` along with the rest of your app's dependencies.
3924
- - [`zapier-platform-schema`](https://github.com/zapier/zapier-platform/tree/main/packages/schema) enforces app structure behind the scenes. It's a dependency of `core`, so it will be installed automatically.
3925
-
3926
- To learn more about the structure of the code (especially if you're interested in contributing), check out the `ARCHITECTURE.md` file(s).
3927
-
3928
- ### Updating These Packages
3929
-
3930
- The Zapier platform and its tools are under active development. While you don't need to install every release, we release new versions because they are better than the last. We do our best to adhere to [Semantic Versioning](https://semver.org/) wherein we won't break your code unless there's a `major` release. Otherwise, we're just fixing bugs (`patch`) and adding features (`minor`).
3931
-
3932
- Broadly speaking, all releases will continue to work indefinitely. While you never *have* to upgrade your app's `zapier-platform-core` dependency, we recommend keeping an eye on the [changelog](https://github.com/zapier/zapier-platform/blob/main/CHANGELOG.md) to see what new features and bug fixes are available.
3933
-
3934
- For more info about which Node versions are supported, see [the faq](#how-do-i-manually-set-the-nodejs-version-to-run-my-app-with).
3935
-
3936
- <!-- TODO: if we decouple releases, change this -->
3937
- The most recently released version of `cli` and `core` is **15.17.0**. You can see the versions you're working with by running `zapier -v`.
3938
-
3939
- To update `cli`, run `npm install -g zapier-platform-cli`.
3940
-
3941
- To update the version of `core` your app depends on, set the `zapier-platform-core` dependency in your `package.json` to a version listed [here](https://www.npmjs.com/package/zapier-platform-core?activeTab=versions) and reinstall your dependencies (either `yarn` or `npm install`).
3942
-
3943
- For maximum compatibility, keep the versions of `cli` and `core` in sync.
3944
-
3945
- ## Get Help!
3946
-
3947
- You can get help by either emailing `partners@zapier.com` or by [joining our developer community here](https://community.zapier.com/developer-discussion-13).
3948
-
3949
- ---
3950
-
3951
- ## Developing on the CLI
3952
-
3953
- See [CONTRIBUTING.md](https://github.com/zapier/zapier-platform/blob/main/CONTRIBUTING.md).
13
+ See [CONTRIBUTING.md](https://github.com/zapier/zapier-platform/blob/main/CONTRIBUTING.md) and [ARCHITECTURE.md](https://github.com/zapier/zapier-platform/blob/main/packages/cli/ARCHITECTURE.md) of this package in particular.