UNPKG

134 kBMarkdownView Raw
1<!-- GENERATED! ONLY EDIT `README-source.md` -->
2
3<h1 align="center">
4 <a href="https://zapier.com"><img src="https://raw.githubusercontent.com/zapier/zapier-platform/master/packages/cli/goodies/zapier-logomark.png" alt="Zapier" width="200"></a>
5 <br>
6 Zapier Platform CLI
7 <br>
8 <br>
9</h1>
10
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>
14
15Zapier is a platform for creating integrations and workflows. This CLI is your gateway to creating custom applications on the Zapier platform.
16
17You may find docs duplicate or outdated across the Zapier site. The most up-to-date contents are always available on GitHub:
18
19- [Latest CLI Docs](https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md)
20- [Latest CLI Reference](https://github.com/zapier/zapier-platform/blob/master/packages/cli/docs/cli.md)
21- [Latest Schema Docs](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md)
22
23Our code is updated frequently. To see a full list of changes, look no further than [the CHANGELOG](https://github.com/zapier/zapier-platform/blob/master/CHANGELOG.md).
24
25This doc describes the latest CLI version (**11.0.1**), 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: [10.2.0](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@10.2.0/packages/cli/README.md), [9.4.2](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@9.4.2/packages/cli/README.md), [8.4.2](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@8.4.2/packages/cli/README.md)
28- CLI Reference: [10.2.0](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@10.2.0/packages/cli/docs/cli.md), [9.4.2](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@9.4.2/packages/cli/docs/cli.md), [8.4.2](https://github.com/zapier/zapier-platform/blob/zapier-platform-cli@8.4.2/packages/cli/docs/cli.md)
29- Schema Docs: [10.2.0](https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@10.2.0/packages/schema/docs/build/schema.md), [9.4.2](https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@9.4.2/packages/schema/docs/build/schema.md), [8.4.2](https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@8.4.2/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- [Resources](#resources)
59 * [Resource Definition](#resource-definition)
60- [Triggers/Searches/Creates](#triggerssearchescreates)
61 * [Return Types](#return-types)
62 * [Fallback Sample](#fallback-sample)
63- [Input Fields](#input-fields)
64 * [Custom/Dynamic Fields](#customdynamic-fields)
65 * [Dynamic Dropdowns](#dynamic-dropdowns)
66 * [Search-Powered Fields](#search-powered-fields)
67 * [Computed Fields](#computed-fields)
68 * [Nested & Children (Line Item) Fields](#nested--children-line-item-fields)
69- [Output Fields](#output-fields)
70 * [Nested & Children (Line Item) Fields](#nested--children-line-item-fields-1)
71- [Z Object](#z-object)
72 * [`z.request([url], options)`](#zrequesturl-options)
73 * [`z.console`](#zconsole)
74 * [`z.dehydrate(func, inputData)`](#zdehydratefunc-inputdata)
75 * [`z.dehydrateFile(func, inputData)`](#zdehydratefilefunc-inputdata)
76 * [`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`](#zstashfilebufferstringstream-knownlength-filename-contenttype)
77 * [`z.JSON`](#zjson)
78 * [`z.hash()`](#zhash)
79 * [`z.errors`](#zerrors)
80 * [`z.cursor`](#zcursor)
81 * [`z.generateCallbackUrl()`](#zgeneratecallbackurl)
82- [Bundle Object](#bundle-object)
83 * [`bundle.authData`](#bundleauthdata)
84 * [`bundle.inputData`](#bundleinputdata)
85 * [`bundle.inputDataRaw`](#bundleinputdataraw)
86 * [`bundle.meta`](#bundlemeta)
87 * [`bundle.rawRequest`](#bundlerawrequest)
88 * [`bundle.cleanedRequest`](#bundlecleanedrequest)
89 * [`bundle.outputData`](#bundleoutputdata)
90 * [`bundle.targetUrl`](#bundletargeturl)
91 * [`bundle.subscribeData`](#bundlesubscribedata)
92- [Environment](#environment)
93 * [Defining Environment Variables](#defining-environment-variables)
94 * [Accessing Environment Variables](#accessing-environment-variables)
95- [Making HTTP Requests](#making-http-requests)
96 * [Shorthand HTTP Requests](#shorthand-http-requests)
97 * [Manual HTTP Requests](#manual-http-requests)
98 + [POST and PUT Requests](#post-and-put-requests)
99 * [Using HTTP middleware](#using-http-middleware)
100 + [Error Response Handling](#error-response-handling)
101 * [HTTP Request Options](#http-request-options)
102 * [HTTP Response Object](#http-response-object)
103- [Dehydration](#dehydration)
104 * [Merging Hydrated Data](#merging-hydrated-data)
105 * [File Dehydration](#file-dehydration)
106- [Stashing Files](#stashing-files)
107- [Logging](#logging)
108 * [Console Logging](#console-logging)
109 * [Viewing Console Logs](#viewing-console-logs)
110 * [Viewing Bundle Logs](#viewing-bundle-logs)
111 * [HTTP Logging](#http-logging)
112 * [Viewing HTTP Logs](#viewing-http-logs)
113- [Error Handling](#error-handling)
114 * [General Errors](#general-errors)
115 * [Halting Execution](#halting-execution)
116 * [Stale Authentication Credentials](#stale-authentication-credentials)
117 + [v10 Breaking Change: Auth Refresh](#v10-breaking-change-auth-refresh)
118- [Testing](#testing)
119 * [Writing Unit Tests](#writing-unit-tests)
120 * [Mocking Requests](#mocking-requests)
121 * [Running Unit Tests](#running-unit-tests)
122 * [Testing & Environment Variables](#testing--environment-variables)
123 * [Testing in Your CI](#testing-in-your-ci)
124 * [Debugging Tests](#debugging-tests)
125- [Using `npm` Modules](#using-npm-modules)
126- [Building Native Packages with Docker](#building-native-packages-with-docker)
127- [Using Transpilers](#using-transpilers)
128- [FAQs](#faqs)
129 * [Why doesn't Zapier support newer versions of Node.js?](#why-doesnt-zapier-support-newer-versions-of-nodejs)
130 * [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)
131 * [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies)
132 * [Does Zapier support XML (SOAP) APIs?](#does-zapier-support-xml-soap-apis)
133 * [Is it possible to iterate over pages in a polling trigger?](#is-it-possible-to-iterate-over-pages-in-a-polling-trigger)
134 * [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)
135 * [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)
136 * [How does deduplication work?](#how-does-deduplication-work)
137 * [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)
138 * [Node X No Longer Supported](#node-x-no-longer-supported)
139 * [What Analytics are Collected?](#what-analytics-are-collected)
140 * [What's the Difference Between an "App" and an "Integration"?](#whats-the-difference-between-an-app-and-an-integration)
141- [Command Line Tab Completion](#command-line-tab-completion)
142- [The Zapier Platform Packages](#the-zapier-platform-packages)
143 * [Updating These Packages](#updating-these-packages)
144- [Get Help!](#get-help)
145- [Developing on the CLI](#developing-on-the-cli)
146
147<!-- tocstop -->
148
149## Getting Started
150
151> If you're new to Zapier Platform CLI, we strongly recommend you to walk through the [Tutorial](https://zapier.com/developer/start) for a more thorough introduction.
152
153### What is an App?
154
155> 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.
156
157A CLI App is an implementation of your app's API. You build a Node.js application
158that exports a single object ([JSON Schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#appschema)) and upload it to Zapier.
159Zapier introspects that definition to find out what your app is capable of and
160what options to present end users in the Zap Editor.
161
162For those not familiar with Zapier terminology, here is how concepts in the CLI map to the end user experience:
163
164 * [Authentication](#authentication), (usually) which lets us know what credentials to ask users
165 for. This is used during the "Connect Accounts" section of the Zap Editor.
166 * [Triggers](#triggerssearchescreates), which read data *from* your API. These have their own section in the Zap Editor.
167 * [Creates](#triggerssearchescreates), which send data *to* your API to create new records. These are listed under "Actions" in the Zap Editor.
168 * [Searches](#triggerssearchescreates), which find specific records *in* your system. These are also listed under "Actions" in the Zap Editor.
169 * [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.
170
171### How does Zapier Platform CLI Work?
172
173Zapier 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.
174
175### Zapier Platform CLI vs UI
176
177The 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.
178
179Zapier 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.
180
181[Zapier Platform UI](https://zapier.com/app/developer/) 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.
182
183> Learn more in our [Zapier Platform UI vs CLI](https://platform.zapier.com/docs/vs) post.
184
185### Requirements
186
187All Zapier CLI apps are run using Node.js `v14`.
188
189You can develop using any version of Node you'd like, but your eventual code must be compatible with `v14`. If you're using features not yet available in `v14`, you can transpile your code to a compatible format with [Babel](https://babeljs.io/) (or similar).
190
191To ensure stability for our users, we strongly encourage you run tests on `v14` sometime before your code reaches users. This can be done multiple ways.
192
193Firstly, 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/master/example-apps/minimal/.travis.yml) file in our template apps to get you started.
194
195Alternatively, 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 v14`, or do `nvm exec v14 zapier test` so you can run tests without having to switch versions while developing.
196
197
198### Quick Setup Guide
199
200First 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).
201
202```bash
203# install the CLI globally
204npm install -g zapier-platform-cli
205
206# setup auth to Zapier's platform with a deploy key
207zapier login
208```
209
210Your Zapier CLI should be installed and ready to go at this point. Next up, we'll create our first app!
211
212```bash
213# create a directory with the minimum required files
214zapier init example-app
215
216# move into the new directory
217cd example-app
218
219# install all the libraries needed for your app
220npm install
221```
222
223> Note: there are plenty of templates & example apps to choose from! [View all Example Apps here.](#example-apps).
224
225You should now have a working local app. You can run several local commands to try it out.
226
227```bash
228# run the local tests
229# the same as npm test, but adds some extra things to the environment
230zapier test
231```
232
233Next, you'll probably want to upload app to Zapier itself so you can start testing live.
234
235```bash
236# push your app to Zapier
237zapier push
238```
239
240> Go check out our [full CLI reference documentation](https://zapier.github.io/zapier-platform/cli) to see all the other commands!
241
242
243### Tutorial
244
245For a full tutorial, head over to our [Tutorial](https://zapier.com/developer/start) for a comprehensive walkthrough for creating your first app. If this isn't your first rodeo, read on!
246
247## Creating a Local App
248
249> Tip: Check the [Quick Setup](#quick-setup-guide) if this is your first time using the platform!
250
251Creating 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`.
252
253```bash
254# make your folder
255mkdir zapier-example
256cd zapier-example
257
258# create the needed files from a template
259zapier init . --template minimal
260
261# install all the libraries needed for your app
262npm install
263```
264
265If you'd like to manage your **local App**, use these commands:
266
267* `zapier init myapp` - initialize/start a local app project
268* `zapier convert 1234 .` - initialize/start from an existing app
269* `zapier scaffold resource Contact` - auto-injects a new resource, trigger, etc.
270* `zapier test` - run the same tests as `npm test`
271* `zapier validate` - ensure your app is valid
272* `zapier describe` - print some helpful information about your app
273
274### Local Project Structure
275
276In 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.
277
278```
279$ tree .
280.
281├── README.md
282├── index.js
283├── package.json
284├── triggers
285│   └── contact-by-tag.js
286├── resources
287│   └── Contact.js
288├── test
289│   ├── basic.js
290│   ├── triggers.js
291│   └── resources.js
292├── build
293│   └── build.zip
294└── node_modules
295 ├── ...
296 └── ...
297```
298
299### Local App Definition
300
301The core definition of your `App` will look something like this, and is what your `index.js` should provide as the _only_ export:
302
303```js
304const App = {
305 // both version strings are required
306 version: require('./package.json').version,
307 platformVersion: require('zapier-platform-core').version,
308
309 // see "Authentication" section below
310 authentication: {},
311
312 // see "Dehydration" section below
313 hydrators: {},
314
315 // see "Making HTTP Requests" section below
316 requestTemplate: {},
317 beforeRequest: [],
318 afterResponse: [],
319
320 // See "Resources" section below
321 resources: {},
322
323 // See "Triggers/Searches/Creates" section below
324 triggers: {},
325 searches: {},
326 creates: {},
327};
328
329module.exports = App;
330
331```
332
333> Tip: You can use higher order functions to create any part of your App definition!
334
335
336## Registering an App
337
338Registering 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.
339
340```bash
341# register your app
342zapier register "Zapier Example"
343
344# list your apps
345zapier integrations
346```
347
348> Note: This doesn't put your app in the editor - see the docs on pushing an App Version to do that!
349
350If you'd like to manage your **App**, use these commands:
351
352* `zapier integrations` - list the apps in Zapier you can administer
353* `zapier register "App Title"` - creates a new app in Zapier
354* `zapier link` - lists and links a selected app in Zapier to your current folder
355* `zapier history` - print the history of your app
356* `zapier team:add user@example.com admin` - add an admin to help maintain/develop your app
357* `zapier users:add user@example.com 1.0.0` - invite a user try your app version 1.0.0
358
359## Deploying an App Version
360
361An 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.
362
363```bash
364# push your app version to Zapier
365zapier push
366
367# list your versions
368zapier versions
369```
370
371If you'd like to manage your **Version**, use these commands:
372
373* `zapier versions` - list the versions for the current directory's app
374* `zapier push` - push the current version of current directory's app & version (read from `package.json`)
375* `zapier promote 1.0.0` - mark a version as the "production" version
376* `zapier migrate 1.0.0 1.0.1 [100%]` - move users between versions, regardless of deployment status
377* `zapier deprecate 1.0.0 2020-06-01` - mark a version as deprecated, but let users continue to use it (we'll email them)
378* `zapier env:set 1.0.0 KEY=VALUE` - set an environment variable to some value
379* `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.
380
381> Note: To see the changes that were just pushed reflected in the browser, you have to manually refresh the browser each time you push.
382
383
384### Private App Version (default)
385
386A simple `zapier push` will only create the App Version in your editor. No one else using Zapier can see it or use it.
387
388
389### Sharing an App Version
390
391This 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.
392
393```bash
394# sends an email this user to let them view the app version 1.0.0 in the UI privately
395zapier users:add user@example.com 1.0.0
396
397# sends an email this user to let them admin the app (make changes just like you)
398zapier team:add user@example.com
399```
400
401You 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).
402
403### Promoting an App Version
404
405Promotion 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.
406
407If 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.
408
409```bash
410# promote your app version to all Zapier users
411zapier promote 1.0.1
412
413# OPTIONAL - migrate your users between one app version to another
414zapier migrate 1.0.0 1.0.1
415
416# OR - mark the old version as deprecated
417zapier deprecate 1.0.0 2020-06-01
418```
419
420## Converting an Existing App
421
422If you have an existing Zapier [legacy Web Builder app](https://zapier.com/developer/builder/), you can use it as a template to kickstart your local application.
423
424```bash
425# Convert an existing Web Builder app to a CLI app in the my-app directory
426# App ID 1234 is from URL https://zapier.com/developer/builder/app/1234/development
427zapier convert 1234 my-app
428```
429
430Your CLI app will be created and you can continue working on it.
431
432> Note: There is no way to convert a CLI app to a Web Builder app and we do not plan on implementing this.
433
434Introduced in v8.2.0, you are able to convert new integrations built in Zapier Platform UI to CLI.
435
436```bash
437# the --version flag is what denotes this command is interacting with a Visual Builder app
438# zapier convert <APP_ID> --version <APP_VERSION> <PATH>
439zapier convert 1234 --version 1.0.1 my-app
440```
441
442## Authentication
443
444Most applications require some sort of authentication - and Zapier provides a handful of methods for helping your users authenticate with your application. Zapier will provide some of the core behaviors, but you'll likely need to handle the rest.
445
446> Hint: You can access the data tied to your authentication via the `bundle.authData` property in any method called in your app. Exceptions exist in OAuth and Session auth. Please see them below.
447
448### Basic
449
450Useful if your app requires two pieces of information to authentication: `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).
451
452> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/basic-auth for a working example app for basic auth.
453
454> Note: If you do the common API Key pattern like `Authorization: Basic APIKEYHERE:x` you should look at the "Custom" authentication method instead.
455
456```js
457const authentication = {
458 type: 'basic',
459 // "test" could also be a function
460 test: {
461 url: 'https://example.com/api/accounts/me.json',
462 },
463 connectionLabel: '{{username}}', // Can also be a function, check digest auth below for an example
464 // you can provide additional fields, but we'll provide `username`/`password` automatically
465};
466
467const App = {
468 // ...
469 authentication: authentication,
470 // ...
471};
472
473```
474
475### Digest
476
477*New in v7.4.0.*
478
479The setup and user experience of Digest Auth is identical to Basic Auth. Users will provide Zapier their username and password and Zapier will handle all the nonce and quality of protection details automatically.
480
481> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/digest-auth for a working example app for digest auth.
482
483> 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 sends an additional request beforehand to get the server nonce.
484
485```js
486const getConnectionLabel = (z, bundle) => {
487 // bundle.inputData will contain what the "test" URL (or function) returns
488 return bundle.inputData.username;
489};
490
491const authentication = {
492 type: 'digest',
493 // "test" could also be a function
494 test: {
495 url: 'https://example.com/api/accounts/me.json',
496 },
497 connectionLabel: getConnectionLabel,
498
499 // you can provide additional fields, but we'll provide `username`/`password` automatically
500};
501
502const App = {
503 // ...
504 authentication: authentication,
505 // ...
506};
507
508```
509
510### Custom
511
512This is what most "API Key" driven apps should default to using. You'll likely provide some custom `beforeRequest` middleware or a `requestTemplate` to complete the authentication by adding/computing needed headers.
513
514> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/custom-auth for a working example app for custom auth.
515
516```js
517const authentication = {
518 type: 'custom',
519 // "test" could also be a function
520 test: {
521 url:
522 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json',
523 },
524 fields: [
525 {
526 key: 'subdomain',
527 type: 'string',
528 required: true,
529 helpText: 'Found in your browsers address bar after logging in.',
530 },
531 {
532 key: 'api_key',
533 type: 'string',
534 required: true,
535 helpText: 'Found on your settings page.',
536 },
537 ],
538};
539
540const addApiKeyToHeader = (request, z, bundle) => {
541 request.headers['X-Subdomain'] = bundle.authData.subdomain;
542 const basicHash = Buffer.from(`${bundle.authData.api_key}:x`).toString(
543 'base64'
544 );
545 request.headers.Authorization = `Basic ${basicHash}`;
546 return request;
547};
548
549const App = {
550 // ...
551 authentication: authentication,
552 beforeRequest: [addApiKeyToHeader],
553 // ...
554};
555
556```
557
558### Session
559
560Probably the most "powerful" mechanism for authentication - it gives you the ability to exchange some user provided data for some authentication data (IE: username & password for a session key).
561
562> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/session-auth for a working example app for session auth.
563
564```js
565const getSessionKey = async (z, bundle) => {
566 const response = await z.request({
567 method: 'POST',
568 url: 'https://example.com/api/accounts/login.json',
569 body: {
570 username: bundle.authData.username,
571 password: bundle.authData.password,
572 },
573 });
574
575 // response.throwForStatus() if you're using core v9 or older
576
577 return {
578 sessionKey: response.data.sessionKey,
579 // or response.json.sessionKey if you're using core v9 and older
580 };
581};
582
583const authentication = {
584 type: 'session',
585 // "test" could also be a function
586 test: {
587 url: 'https://example.com/api/accounts/me.json',
588 },
589 fields: [
590 {
591 key: 'username',
592 type: 'string',
593 required: true,
594 helpText: 'Your login username.',
595 },
596 {
597 key: 'password',
598 type: 'string',
599 required: true,
600 helpText: 'Your login password.',
601 },
602 // For Session Auth we store `sessionKey` automatically in `bundle.authData`
603 // for future use. If you need to save/use something that the user shouldn't
604 // need to type/choose, add a "computed" field, like:
605 // {key: 'something': type: 'string', required: false, computed: true}
606 // And remember to return it in sessionConfig.perform
607 ],
608 sessionConfig: {
609 perform: getSessionKey,
610 },
611};
612
613const includeSessionKeyHeader = (request, z, bundle) => {
614 if (bundle.authData.sessionKey) {
615 request.headers = request.headers || {};
616 request.headers['X-Session-Key'] = bundle.authData.sessionKey;
617 }
618 return request;
619};
620
621const App = {
622 // ...
623 authentication: authentication,
624 beforeRequest: [includeSessionKeyHeader],
625 // ...
626};
627
628```
629
630> Note: For Session auth, `authentication.sessionConfig.perform` will have the provided fields in `bundle.inputData` instead of `bundle.authData` because `bundle.authData` will only have "previously existing" values, which will be empty the first time the Zap runs.
631
632### OAuth1
633
634*New in `v7.5.0`.*
635
636Zapier's OAuth1 implementation matches [Twitter's](https://developer.twitter.com/en/docs/basics/authentication/overview) and [Trello's](https://developers.trello.com/page/authorization) implementation of the 3-legged OAuth flow.
637
638> Example Apps: Check out [oauth1-trello](https://github.com/zapier/zapier-platform/tree/master/example-apps/oauth1-trello), [oauth1-tumblr](https://github.com/zapier/zapier-platform/tree/master/example-apps/oauth1-tumblr), and [oauth1-twitter](https://github.com/zapier/zapier-platform/tree/master/example-apps/oauth1-twitter) for working example apps with OAuth1.
639
640The flow works like this:
641
642 1. Zapier makes a call to your API requesting a "request token" (also known as "temporary credentials")
643 2. Zapier sends the user to the authorization URL, defined by your app, along with the request token
644 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)
645 4. Zapier makes a call on our backend to your API to exchange the request token for an "access token" (also known as "long-lived credentials")
646 5. Zapier remembers the access token and makes calls on behalf of the user
647
648You are required to define:
649
650 * `getRequestToken`: The API call to fetch the request token
651 * `authorizeUrl`: The authorization URL
652 * `getAccessToken`: The API call to fetch the access token
653
654You'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.
655
656```bash
657# setting the environment variables on Zapier.com
658$ zapier env:set 1.0.0 CLIENT_ID=1234
659$ zapier env:set 1.0.0 CLIENT_SECRET=abcd
660
661# and when running tests locally, don't forget to define them in .env or in the command!
662$ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
663```
664
665Your auth definition would look something like this:
666
667```js
668const _ = require('lodash');
669
670const authentication = {
671 type: 'oauth1',
672 oauth1Config: {
673 getRequestToken: {
674 url: 'https://{{bundle.inputData.subdomain}}.example.com/request-token',
675 method: 'POST',
676 auth: {
677 oauth_consumer_key: '{{process.env.CLIENT_ID}}',
678 oauth_consumer_secret: '{{process.env.CLIENT_SECRET}}',
679
680 // 'HMAC-SHA1' is used by default if not specified.
681 // 'HMAC-SHA256', 'RSA-SHA1', 'PLAINTEXT' are also supported.
682 oauth_signature_method: 'HMAC-SHA1',
683 oauth_callback: '{{bundle.inputData.redirect_uri}}',
684 },
685 },
686 authorizeUrl: {
687 url: 'https://{{bundle.inputData.subdomain}}.example.com/authorize',
688 params: {
689 oauth_token: '{{bundle.inputData.oauth_token}}',
690 },
691 },
692 getAccessToken: {
693 url: 'https://{{bundle.inputData.subdomain}}.example.com/access-token',
694 method: 'POST',
695 auth: {
696 oauth_consumer_key: '{{process.env.CLIENT_ID}}',
697 oauth_consumer_secret: '{{process.env.CLIENT_SECRET}}',
698 oauth_token: '{{bundle.inputData.oauth_token}}',
699 oauth_token_secret: '{{bundle.inputData.oauth_token_secret}}',
700 oauth_verifier: '{{bundle.inputData.oauth_verifier}}',
701 },
702 },
703 },
704 test: {
705 url: 'https://{{bundle.authData.subdomain}}.example.com/me',
706 },
707 // If you need any fields upfront, put them here
708 fields: [
709 { key: 'subdomain', type: 'string', required: true, default: 'app' },
710 // For OAuth1 we store `oauth_token` and `oauth_token_secret` automatically
711 // in `bundle.authData` for future use. If you need to save/use something
712 // that the user shouldn't need to type/choose, add a "computed" field, like:
713 // {key: 'user_id': type: 'string', required: false, computed: true}
714 // And remember to return it in oauth1Config.getAccessToken
715 ],
716};
717
718// A middleware that is run before z.request() actually makes the request. Here we're
719// adding necessary OAuth1 parameters to `auth` property of the request object.
720const includeAccessToken = (req, z, bundle) => {
721 if (
722 bundle.authData &&
723 bundle.authData.oauth_token &&
724 bundle.authData.oauth_token_secret
725 ) {
726 // Just put your OAuth1 credentials in req.auth, Zapier will sign the request for
727 // you.
728 req.auth = req.auth || {};
729 _.defaults(req.auth, {
730 oauth_consumer_key: process.env.CLIENT_ID,
731 oauth_consumer_secret: process.env.CLIENT_SECRET,
732 oauth_token: bundle.authData.oauth_token,
733 oauth_token_secret: bundle.authData.oauth_token_secret,
734 });
735 }
736 return req;
737};
738
739const App = {
740 // ...
741 authentication: authentication,
742 beforeRequest: [includeAccessToken],
743 // ...
744};
745
746module.exports = App;
747
748```
749
750> Note: For OAuth1, `authentication.oauth1Config.getRequestToken`, `authentication.oauth1Config.authorizeUrl`, and `authentication.oauth1Config.getAccessToken` will have the provided fields in `bundle.inputData` instead of `bundle.authData` because `bundle.authData` will only have "previously existing" values, which will be empty when the user hasn't connected their account on your service to Zapier. Also note that `authentication.oauth1Config.getAccessToken` has access to the users return values in `rawRequest` and `cleanedRequest` should you need to extract other values (for example from the query string).
751
752### OAuth2
753
754Zapier's OAuth2 implementation is based on the `authorization_code` flow, similar to [GitHub](https://developer.github.com/v3/oauth/) and [Facebook](https://developers.facebook.com/docs/authentication/server-side/).
755
756> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/oauth2 for a working example app for OAuth2.
757
758It looks like this:
759
760 1. Zapier sends the user to the authorization URL defined by your app
761 2. 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/83E12494-0A03-4DB4-AA46-5A2AF6A9ECCC.png)
762 3. Zapier makes a call on our backend to your API to exchange the `code` for an `access_token`
763 4. Zapier remembers the `access_token` and makes calls on behalf of the user
764 5. (Optionally) Zapier can refresh the token if it expires
765
766You are required to define the authorization URL and the API call to fetch the access token. You'll also likely want to set your `CLIENT_ID` and `CLIENT_SECRET` as environment variables:
767
768```bash
769# setting the environment variables on Zapier.com
770$ zapier env:set 1.0.0 CLIENT_ID=1234
771$ zapier env:set 1.0.0 CLIENT_SECRET=abcd
772
773# and when running tests locally, don't forget to define them in .env or in the command!
774$ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
775```
776
777Your auth definition would look something like this:
778
779```js
780const authentication = {
781 type: 'oauth2',
782 test: {
783 url:
784 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json',
785 },
786 // you can provide additional fields for inclusion in authData
787 oauth2Config: {
788 // "authorizeUrl" could also be a function returning a string url
789 authorizeUrl: {
790 method: 'GET',
791 url:
792 'https://{{bundle.inputData.subdomain}}.example.com/api/oauth2/authorize',
793 params: {
794 client_id: '{{process.env.CLIENT_ID}}',
795 state: '{{bundle.inputData.state}}',
796 redirect_uri: '{{bundle.inputData.redirect_uri}}',
797 response_type: 'code',
798 },
799 },
800 // Zapier expects a response providing {access_token: 'abcd'}
801 // "getAccessToken" could also be a function returning an object
802 getAccessToken: {
803 method: 'POST',
804 url:
805 'https://{{bundle.inputData.subdomain}}.example.com/api/v2/oauth2/token',
806 body: {
807 code: '{{bundle.inputData.code}}',
808 client_id: '{{process.env.CLIENT_ID}}',
809 client_secret: '{{process.env.CLIENT_SECRET}}',
810 redirect_uri: '{{bundle.inputData.redirect_uri}}',
811 grant_type: 'authorization_code',
812 },
813 headers: {
814 'Content-Type': 'application/x-www-form-urlencoded',
815 },
816 },
817 scope: 'read,write',
818 },
819 // If you need any fields upfront, put them here
820 fields: [
821 { key: 'subdomain', type: 'string', required: true, default: 'app' },
822 // For OAuth2 we store `access_token` and `refresh_token` automatically
823 // in `bundle.authData` for future use. If you need to save/use something
824 // that the user shouldn't need to type/choose, add a "computed" field, like:
825 // {key: 'user_id': type: 'string', required: false, computed: true}
826 // And remember to return it in oauth2Config.getAccessToken/refreshAccessToken
827 ],
828};
829
830const addBearerHeader = (request, z, bundle) => {
831 if (bundle.authData && bundle.authData.access_token) {
832 request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
833 }
834 return request;
835};
836
837const App = {
838 // ...
839 authentication: authentication,
840 beforeRequest: [addBearerHeader],
841 // ...
842};
843
844module.exports = App;
845
846```
847
848> Note: For OAuth2, `authentication.oauth2Config.authorizeUrl`, `authentication.oauth2Config.getAccessToken`, and `authentication.oauth2Config.refreshAccessToken` will have the provided fields in `bundle.inputData` instead of `bundle.authData` because `bundle.authData` will only have "previously existing" values, which will be empty when the user hasn't connected their account on your service to Zapier. Also note that `authentication.oauth2Config.getAccessToken` has access to the users return values in `rawRequest` and `cleanedRequest` should you need to extract other values (for example from the query string).
849
850
851## Resources
852
853A `resource` is a representation (as a JavaScript object) of one of the REST resources of your API. Say you have a `/recipes`
854endpoint for working with recipes; you can define a recipe resource in your app that will tell Zapier how to do create,
855read, and search operations on that resource.
856
857```js
858const Recipe = {
859 // `key` is the unique identifier the Zapier backend references
860 key: 'recipe',
861 // `noun` is the user-friendly name displayed in the Zapier UI
862 noun: 'Recipe',
863 // `list` and `create` are just a couple of the methods you can define
864 list: {
865 // ...
866 },
867 create: {
868 // ...
869 },
870};
871
872```
873
874The quickest way to create a resource is with the `zapier scaffold` command:
875
876```bash
877zapier scaffold resource "Recipe"
878```
879
880This will generate the resource file and add the necessary statements to the `index.js` file to import it.
881
882
883### Resource Definition
884
885A resource has a few basic properties. The first is the `key`, which allows Zapier to identify the resource on our backend.
886The second is the `noun`, the user-friendly name of the resource that is presented to users throughout the Zapier UI.
887
888> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/resource for a working example app using resources.
889
890After those, there is a set of optional properties that tell Zapier what methods can be performed on the resource.
891The complete list of available methods can be found in the [Resource Schema Docs](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#resourceschema).
892For now, let's focus on two:
893
894 * `list` - Tells Zapier how to fetch a set of this resource. This becomes a Trigger in the Zapier Editor.
895 * `create` - Tells Zapier how to create a new instance of the resource. This becomes an Action in the Zapier Editor.
896
897Here is a complete example of what the list method might look like
898
899```js
900const Recipe = {
901 key: 'recipe',
902 // ...
903 list: {
904 display: {
905 label: 'New Recipe',
906 description: 'Triggers when a new recipe is added.',
907 },
908 operation: {
909 perform: {
910 url: 'https://example.com/recipes',
911 },
912 },
913 },
914};
915
916```
917
918The method is made up of two properties, a `display` and an `operation`. The `display` property ([schema](https://github.com/zapier/zapier-platform/blob/master/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/master/packages/schema/docs/build/schema.md#resourceschema)) provides the implementation to make the API call.
919
920Adding a create method looks very similar.
921
922```js
923const Recipe = {
924 key: 'recipe',
925 // ...
926 list: {
927 // ...
928 },
929 create: {
930 display: {
931 label: 'Add Recipe',
932 description: 'Adds a new recipe to our cookbook.',
933 },
934 operation: {
935 perform: {
936 method: 'POST',
937 url: 'https://example.com/recipes',
938 body: {
939 name: 'Baked Falafel',
940 style: 'mediterranean',
941 },
942 },
943 },
944 },
945};
946
947```
948
949Every method you define on a `resource` Zapier converts to the appropriate Trigger, Create, or Search. Our examples
950above would result in an app with a New Recipe Trigger and an Add Recipe Create.
951
952Note 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`.
953
954
955## Triggers/Searches/Creates
956
957Triggers, Searches, and Creates are the way an app defines what it is able to do. Triggers read
958data into Zapier (i.e. watch for new recipes). Searches locate individual records (find recipe by title). Creates create
959new records in your system (add a recipe to the catalog).
960
961The definition for each of these follows the same structure. Here is an example of a trigger:
962
963```js
964const App = {
965 // ...
966 triggers: {
967 new_recipe: {
968 key: 'new_recipe', // uniquely identifies the trigger
969 noun: 'Recipe', // user-friendly word that is used to refer to the resource
970 // `display` controls the presentation in the Zapier Editor
971 display: {
972 label: 'New Recipe',
973 description: 'Triggers when a new recipe is added.',
974 },
975 // `operation` implements the API call used to fetch the data
976 operation: {
977 perform: {
978 url: 'https://example.com/recipes',
979 },
980 },
981 },
982 another_trigger: {
983 // Another trigger definition...
984 },
985 },
986};
987
988```
989
990You can find more details on the definition for each by looking at the [Trigger Schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#triggerschema),
991[Search Schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#searchschema), and [Create Schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#createschema).
992
993> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/trigger for a working example app using triggers.
994
995> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/rest-hooks for a working example app using REST hook triggers.
996
997> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/search for a working example app using searches.
998
999> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/create for a working example app using creates.
1000
1001### Return Types
1002
1003Each of the 3 types of function expects a certain type of object. As of core v1.0.11, there are automated checks to let you know when you're trying to pass the wrong type back. There's more info in each relevant `post_X` section of the [v2 docs](https://zapier.com/developer/documentation/v2/scripting/#available-methods). For reference, each expects:
1004
1005| Method | Return Type | Notes |
1006|---------|-------------|----------------------------------------------------------------------------------------------------------------------|
1007| Trigger | Array | 0 or more objects that will be passed to the [deduper](https://zapier.com/developer/documentation/v2/deduplication/) |
1008| Search | Array | 0 or more objects. If len > 0, put the best match first |
1009| Action | Object | Return values are evaluated by [`isPlainObject`](https://lodash.com/docs#isPlainObject) |
1010
1011### Fallback Sample
1012In 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.
1013
1014```js
1015,sample: {
1016 dummydata_field1: 'This will be compared against your perform method output'
1017 style: 'mediterranean'
1018}
1019```
1020
1021## Input Fields
1022
1023On each trigger, search, or create in the `operation` directive - you can provide an array of objects as fields under the `inputFields`. Input Fields are what your users would see in the main Zapier user interface. 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 previous steps in a Zap, for example:
1024
1025![gif of setting up an action field in Zap Editor](https://cdn.zapier.com/storage/photos/6bd938f7cad7e34c75ba1c1d3be75ac5.gif)
1026
1027You can find more details about setting action fields from a user perspective in [our help documentation](https://zapier.com/help/creating-zap/#set-up-action-template).
1028
1029Those fields have various options you can provide, here is a succinct example:
1030
1031```js
1032const App = {
1033 // ...
1034 creates: {
1035 create_recipe: {
1036 // ...
1037 operation: {
1038 // an array of objects is the simplest way
1039 inputFields: [
1040 {
1041 key: 'title',
1042 required: true,
1043 label: 'Title of Recipe',
1044 helpText: 'Name your recipe!',
1045 },
1046 {
1047 key: 'style',
1048 required: true,
1049 choices: { mexican: 'Mexican', italian: 'Italian' },
1050 },
1051 ],
1052 perform: () => {},
1053 },
1054 },
1055 },
1056};
1057
1058```
1059
1060You can find more details on the different field schema options at [our Field Schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#fieldschema).
1061
1062### Custom/Dynamic Fields
1063
1064In some cases, it might be necessary to provide fields that are dynamically generated - especially for custom fields. This is a common pattern for CRMs, form software, databases and more. Basically - you can provide a function instead of a field and we'll evaluate that function - merging the dynamic fields with the static fields.
1065
1066> 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).
1067
1068> A function that returns a list of dynamic fields cannot include additional functions in that list to call for dynamic fields.
1069
1070```js
1071const recipeFields = async (z, bundle) => {
1072 const response = await z.request('https://example.com/api/v2/fields.json');
1073
1074 // Call reponse.throwForStatus() if you're using core v9 or older
1075
1076 // Should return an array like [{"key":"field_1"},{"key":"field_2"}]
1077 return response.data; // response.json if you're using core v9 or older
1078};
1079
1080const App = {
1081 // ...
1082 creates: {
1083 create_recipe: {
1084 // ...
1085 operation: {
1086 // an array of objects is the simplest way
1087 inputFields: [
1088 {
1089 key: 'title',
1090 required: true,
1091 label: 'Title of Recipe',
1092 helpText: 'Name your recipe!',
1093 },
1094 {
1095 key: 'style',
1096 required: true,
1097 choices: { mexican: 'Mexican', italian: 'Italian' },
1098 },
1099 recipeFields, // provide a function inline - we'll merge the results!
1100 ],
1101 perform: () => {},
1102 },
1103 },
1104 },
1105};
1106
1107```
1108
1109Additionally, if there is a field that affects the generation of dynamic fields, you can set the `altersDynamicFields: true` property. This informs the Zapier UI that whenever the value of that field changes, fields need to be recomputed. An example could be a static dropdown of "dessert type" that will change whether the function that generates dynamic fields includes a field "with sprinkles." If your field affects others, this is an important property to set.
1110
1111```js
1112module.exports = {
1113 key: 'dessert',
1114 noun: 'Dessert',
1115 display: {
1116 label: 'Order Dessert',
1117 description: 'Orders a dessert.',
1118 },
1119 operation: {
1120 inputFields: [
1121 {
1122 key: 'type',
1123 required: true,
1124 choices: { 1: 'cake', 2: 'ice cream', 3: 'cookie' },
1125 altersDynamicFields: true,
1126 },
1127 function (z, bundle) {
1128 if (bundle.inputData.type === '2') {
1129 return [{ key: 'with_sprinkles', type: 'boolean' }];
1130 }
1131 return [];
1132 },
1133 ],
1134 perform: function (z, bundle) {
1135 /* ... */
1136 },
1137 },
1138};
1139
1140```
1141
1142> Only dropdowns support `altersDynamicFields`.
1143
1144When using dynamic fields, the fields will be retrieved in three different contexts:
1145
1146* Whenever the value of a field with `altersDynamicFields` is changed, as described above.
1147* Whenever Zap Editor opens the "Set up" section for the trigger or action.
1148* Whenever the Refresh Fields button is used on the trigger or action.
1149
1150Be 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.
1151
1152### Dynamic Dropdowns
1153
1154Sometimes, 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.
1155
1156Our 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."
1157
1158To define one you include the `dynamic` property on the `inputFields` object. The value for the property is a dot-separated _string_ concatenation.
1159
1160```js
1161//...
1162issue: {
1163 key: 'issue',
1164 //...
1165 create: {
1166 //...
1167 operation: {
1168 inputFields: [
1169 {
1170 key: 'project_id',
1171 required: true,
1172 label: 'This is a dynamic dropdown',
1173 dynamic: 'project.id.name'
1174 }, // will call the trigger with a key of project
1175 {
1176 key: 'title',
1177 required: true,
1178 label: 'Title',
1179 helpText: 'What is the name of the issue?'
1180 }
1181 ]
1182 }
1183 }
1184}
1185
1186```
1187
1188The dot-separated string concatenation follows this pattern:
1189
1190- The key of the trigger you want to use to power the dropdown. _required_
1191- The value to be made available in bundle.inputData. _required_
1192- The human friendly value to be shown on the left of the dropdown in bold. _optional_
1193
1194In 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.
1195
1196```js
1197[
1198 { id: '1', name: 'First Option', dateCreated: '01/01/2000' },
1199 { id: '2', name: 'Second Option', dateCreated: '01/01/2000' },
1200 { id: '3', name: 'Third Option', dateCreated: '01/01/2000' },
1201 { id: '4', name: 'Fourth Option', dateCreated: '01/01/2000' },
1202];
1203
1204```
1205
1206The dynamic dropdown would look something like this.
1207![screenshot of dynamic dropdown in Zap Editor](https://cdn.zapier.com/storage/photos/dd31fa761e0cf9d0abc9b50438f95210.png)
1208
1209In 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.
1210
1211```js
1212const App = {
1213 // ...
1214 resources: {
1215 project: {
1216 key: 'project',
1217 // ...
1218 list: {
1219 // ...
1220 operation: {
1221 perform: () => {
1222 return [{ id: 123, name: 'Project 1' }];
1223 }, // called for project_id dropdown
1224 },
1225 },
1226 },
1227 issue: {
1228 key: 'issue',
1229 // ...
1230 create: {
1231 // ...
1232 operation: {
1233 inputFields: [
1234 {
1235 key: 'project_id',
1236 required: true,
1237 label: 'Project',
1238 dynamic: 'projectList.id.name',
1239 }, // calls project.list
1240 {
1241 key: 'title',
1242 required: true,
1243 label: 'Title',
1244 helpText: 'What is the name of the issue?',
1245 },
1246 ],
1247 },
1248 },
1249 },
1250 },
1251};
1252
1253```
1254
1255In 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.
1256
1257```js
1258const App = {
1259 // ...
1260 triggers: {
1261 new_project: {
1262 key: 'project',
1263 noun: 'Project',
1264 // `display` controls the presentation in the Zapier Editor
1265 display: {
1266 label: 'New Project',
1267 description: 'Triggers when a new project is added.',
1268 hidden: true,
1269 },
1270 operation: {
1271 perform: projectListRequest,
1272 },
1273 },
1274 another_trigger: {
1275 // Another trigger definition...
1276 },
1277 },
1278};
1279
1280```
1281
1282You 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.
1283
1284Let's say you have a Worksheet trigger with a `perform` method similar to this.
1285
1286```js
1287perform: async (z, bundle) => {
1288 const response = await z.request('https://example.com/api/v2/projects.json', {
1289 params: {
1290 spreadsheet_id: bundle.inputData.spreadsheet_id,
1291 },
1292 });
1293
1294 // response.throwForStatus() if you're using core v9 or older
1295
1296 return response.data; // or response.json if you're using core v9 or older
1297};
1298
1299```
1300
1301And 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.
1302
1303```js
1304const App = {
1305 // ...
1306 triggers: {
1307 // ...
1308 issue: {
1309 key: 'new_records',
1310 // ...
1311 operation: {
1312 inputFields: [
1313 {
1314 key: 'spreadsheet_id',
1315 required: true,
1316 label: 'Spreadsheet',
1317 dynamic: 'spreadsheet.id.name',
1318 },
1319 {
1320 key: 'worksheet_id',
1321 required: true,
1322 label: 'Worksheet',
1323 dynamic: 'worksheet.id.name',
1324 },
1325 ],
1326 },
1327 },
1328 },
1329};
1330
1331```
1332
1333The [Google Sheets](https://zapier.com/apps/google-sheets/integrations#triggers-and-actions) integration is an example of this pattern.
1334
1335If 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.
1336
1337```js
1338const App = {
1339 // ...
1340 resources: {
1341 project: {
1342 key: 'project',
1343 // ...
1344 list: {
1345 // ...
1346 operation: {
1347 canPaginate: true,
1348 perform: () => {
1349 if (bundle.meta.isFillingDynamicDropdown) {
1350 // perform pagination request here
1351 } else {
1352 return [{ id: 123, name: 'Project 1' }];
1353 }
1354 },
1355 },
1356 },
1357 },
1358 issue: {
1359 key: 'issue',
1360 // ...
1361 create: {
1362 // ...
1363 operation: {
1364 inputFields: [
1365 {
1366 key: 'project_id',
1367 required: true,
1368 label: 'Project',
1369 dynamic: 'projectList.id.name',
1370 }, // calls project.list
1371 {
1372 key: 'title',
1373 required: true,
1374 label: 'Title',
1375 helpText: 'What is the name of the issue?',
1376 },
1377 ],
1378 },
1379 },
1380 },
1381 },
1382};
1383
1384```
1385
1386### Search-Powered Fields
1387
1388For 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.
1389
1390```js
1391const App = {
1392 // ...
1393 resources: {
1394 project: {
1395 key: 'project',
1396 // ...
1397 search: {
1398 // ...
1399 operation: {
1400 perform: () => {
1401 return [{ id: 123, name: 'Project 1' }];
1402 }, // called for project_id
1403 },
1404 },
1405 },
1406 issue: {
1407 key: 'issue',
1408 // ...
1409 create: {
1410 // ...
1411 operation: {
1412 inputFields: [
1413 {
1414 key: 'project_id',
1415 required: true,
1416 label: 'Project',
1417 dynamic: 'projectList.id.name',
1418 search: 'projectSearch.id',
1419 }, // calls project.search (requires a trigger in the "dynamic" property)
1420 {
1421 key: 'title',
1422 required: true,
1423 label: 'Title',
1424 helpText: 'What is the name of the issue?',
1425 },
1426 ],
1427 },
1428 },
1429 },
1430 },
1431};
1432
1433```
1434
1435**NOTE:** This has to be combined with the `dynamic` property to give the user a guided experience when setting up a Zap.
1436
1437If you don't define a trigger for the `dynamic` property, the search connector won't show.
1438
1439### Computed Fields
1440
1441In 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.
1442
1443You 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.
1444
1445> Note: Only OAuth and Session Auth support computed fields.
1446
1447If 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.
1448
1449Computed 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.
1450
1451You can see examples of computed fields in the [OAuth2](#oauth2) or [Session Auth](#session) example sections.
1452
1453### Nested & Children (Line Item) Fields
1454
1455When 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/master/packages/schema/docs/build/schema.md#fieldschema) that can be input for each item in this array.
1456
1457```js
1458const App = {
1459 // ...
1460 operation: {
1461 // ...
1462 inputFields: [
1463 {
1464 key: 'lineItems',
1465 children: [
1466 {
1467 key: 'lineItemId',
1468 type: 'integer',
1469 label: 'Line Item ID',
1470 required: true,
1471 },
1472 {
1473 key: 'name',
1474 type: 'string',
1475 label: 'Name',
1476 required: true,
1477 },
1478 {
1479 key: 'description',
1480 type: 'string',
1481 label: 'Description',
1482 },
1483 ],
1484 },
1485 ],
1486 // ...
1487 },
1488};
1489
1490```
1491
1492## Output Fields
1493
1494On 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 your users would see when they select a field provided by your trigger, search or create to map it to another.
1495
1496Output Fields are optional, but can be used to:
1497
1498- Define friendly labels for the returned fields. By default, we will *humanize* for example `my_key` as *My Key*.
1499- 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.
1500
1501The [schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#fieldschema) for `outputFields` is shared with `inputFields` but only the `key` and `required` properties are relevant.
1502
1503Custom/Dynamic Output Fields are defined in the same way as [Custom/Dynamic Input Fields](#customdynamic-fields).
1504
1505### Nested & Children (Line Item) Fields
1506
1507To define an Output Field for a nested field use `{{parent}}__{{key}}`. For children (line item) fields use `{{parent}}[]{{key}}`.
1508
1509```js
1510const recipeOutputFields = async (z, bundle) => {
1511 const response = await z.request('https://example.com/api/v2/fields.json');
1512
1513 // response.throwForStatus() if you're using core v9 or older
1514
1515 // Should return an array like [{"key":"field_1","label":"Label for Custom Field"}]
1516 return response.data; // or response.json if you're on core v9 or older
1517};
1518
1519const App = {
1520 // ...
1521 triggers: {
1522 new_recipe: {
1523 // ...
1524 operation: {
1525 perform: () => {},
1526 sample: {
1527 id: 1,
1528 title: 'Pancake',
1529 author: {
1530 id: 1,
1531 name: 'Amy',
1532 },
1533 ingredients: [
1534 {
1535 name: 'Egg',
1536 amount: 1,
1537 },
1538 {
1539 name: 'Milk',
1540 amount: 60,
1541 unit: 'g',
1542 },
1543 {
1544 name: 'Flour',
1545 amount: 30,
1546 unit: 'g',
1547 },
1548 ],
1549 },
1550 // an array of objects is the simplest way
1551 outputFields: [
1552 {
1553 key: 'id',
1554 label: 'Recipe ID',
1555 type: 'integer',
1556 },
1557 {
1558 key: 'title',
1559 label: 'Recipe Title',
1560 type: 'string',
1561 },
1562 {
1563 key: 'author__id',
1564 label: 'Author User ID',
1565 type: 'integer',
1566 },
1567 {
1568 key: 'author__name',
1569 label: 'Author Name',
1570 type: 'string',
1571 },
1572 {
1573 key: 'ingredients[]name',
1574 label: 'Ingredient Name',
1575 type: 'string',
1576 },
1577 {
1578 key: 'ingredients[]amount',
1579 label: 'Ingredient Amount',
1580 type: 'number',
1581 },
1582 {
1583 key: 'ingredients[]unit',
1584 label: 'Ingredient Unit',
1585 type: 'string',
1586 },
1587 recipeOutputFields, // provide a function inline - we'll merge the results!
1588 ],
1589 },
1590 },
1591 },
1592};
1593
1594```
1595
1596## Z Object
1597
1598We provide several methods off of the `z` object, which is provided as the first argument to all function calls in your app.
1599
1600> The `z` object is passed into your functions as the first argument - IE: `perform: (z) => {}`.
1601
1602### `z.request([url], options)`
1603
1604`z.request([url], options)` is a promise based HTTP client with some Zapier-specific goodies. See [Making HTTP Requests](#making-http-requests).
1605
1606### `z.console`
1607
1608`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)
1609
1610### `z.dehydrate(func, inputData)`
1611
1612`z.dehydrate(func, inputData)` is used to lazily evaluate a function, perfect to avoid API calls during polling or for reuse. See [Dehydration](#dehydration).
1613
1614### `z.dehydrateFile(func, inputData)`
1615
1616`z.dehydrateFile` is used to lazily download a file, perfect to avoid API calls during polling or for reuse. See [File Dehydration](#file-dehydration).
1617
1618### `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`
1619
1620`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` is a promise based file stasher that returns a URL file pointer. See [Stashing Files](#stashing-files).
1621
1622### `z.JSON`
1623
1624`z.JSON` is similar to the JSON built-in like `z.JSON.parse('...')`, but catches errors and produces nicer tracebacks.
1625
1626### `z.hash()`
1627
1628`z.hash()` is a crypto tool for doing things like `z.hash('sha256', 'my password')`
1629
1630### `z.errors`
1631
1632`z.errors` is a collection error classes that you can throw in your code, like `throw new z.errors.HaltedError('...')`.
1633
1634The available errors are:
1635
1636* `Error` (_new in v9.3.0_) - Stops the current operation, allowing for (auto) replay. Read more on [General Errors](#general-errors)
1637* `HaltedError` - Stops current operation, but will never turn off Zap. Read more on [Halting Execution](#halting-execution)
1638* `ExpiredAuthError` - Turns off Zap and emails user to manually reconnect. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
1639* `RefreshAuthError` - (OAuth2 or Session Auth) Tells Zapier to refresh credentials and retry operation. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
1640
1641For more details on error handling in general, see [here](#error-handling).
1642
1643### `z.cursor`
1644
1645The `z.cursor` object exposes two methods:
1646
1647* `z.cursor.get(): Promise<string|null>`
1648* `z.cursor.set(string): Promise<null>`
1649
1650Any data you `set` will be available to that Zap for about an hour (or until it's overwritten). For more information, see: [paging](#paging).
1651
1652### `z.generateCallbackUrl()`
1653
1654The `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.
1655
1656For example, in your `perform` you might do:
1657
1658```js
1659const perform = async (z, bundle) => {
1660 // something like this url:
1661 // https://zapier.com/hooks/callback/123/abcdef01-2345-6789-abcd-ef0123456789/abcdef0123456789abcdef0123456789abcdef01/
1662 const callbackUrl = z.generateCallbackUrl();
1663 await z.request({
1664 url: 'https://example.com/api/slow-job',
1665 method: 'POST',
1666 body: {
1667 // ... whatever your integration needs
1668 url: callbackUrl,
1669 },
1670 });
1671 return {"hello": "world"}; // available later in bundle.outputData
1672};
1673```
1674
1675And 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`:
1676
1677```http
1678POST /hooks/callback/123/abcdef01-2345-6789-abcd-ef0123456789/abcdef0123456789abcdef0123456789abcdef01/ HTTP/1.1
1679Host: zapier.com
1680Content-Type: application/json
1681
1682{"foo":"bar"}
1683```
1684
1685And finally, in a `performResume` to handle the final step which will receive three bundle properties:
1686
1687* `bundle.outputData` is `{"hello": "world"}`, the data returned from the initial `perform`
1688* `bundle.cleanedRequest` is `{"foo": "bar"}`, the payload from the callback URL
1689* `bundle.rawRequest` is the full request object corresponding to `bundle.cleanedRequest`
1690
1691```js
1692const performResume = async (z, bundle) => {
1693 // this will give a final value of: {"hello": "world", "foo": "bar"}
1694 return { ...bundle.outputData, ...bundle.cleanedRequest };
1695};
1696```
1697
1698> 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.
1699
1700
1701## Bundle Object
1702
1703This object holds the user's auth details and the data for the API requests.
1704
1705> The `bundle` object is passed into your functions as the second argument - IE: `perform: (z, bundle) => {}`.
1706
1707### `bundle.authData`
1708
1709`bundle.authData` is user-provided authentication data, like `api_key` or `access_token`. [Read more on authentication.](#authentication)
1710
1711### `bundle.inputData`
1712
1713`bundle.inputData` is user-provided data for this particular run of the trigger/search/create, as defined by the inputFields. For example:
1714
1715```js
1716{
1717 createdBy: 'his name is Bobby Flay'
1718 style: 'he cooks mediterranean'
1719}
1720```
1721
1722### `bundle.inputDataRaw`
1723
1724`bundle.inputDataRaw` is kind of like `inputData`, but before rendering `{{curlies}}`:
1725
1726```js
1727{
1728 createdBy: 'his name is {{123__chef_name}}'
1729 style: 'he cooks {{456__style}}'
1730}
1731```
1732
1733> "curlies" are data mapped in from previous steps. They take the form `{{NODE_ID__key_name}}`. You'll usually want to use `bundle.inputData` instead.
1734
1735### `bundle.meta`
1736
1737`bundle.meta` contains extra information useful for doing advanced behaviors depending on what the user is doing. It has the following options:
1738
1739| key | default | description |
1740| --- | --- | --- |
1741| `isLoadingSample` | `false` | If true, this run was initiated manually via the Zap Editor |
1742| `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 |
1743| `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) |
1744| `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 |
1745| `page` | `0` | Used in [paging](#paging) to uniquely identify which page of results should be returned |
1746| `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 |
1747
1748> 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.
1749
1750Here's an example of a polling trigger that is also used to power a dynamic dropdown:
1751
1752```js
1753const perform = async (z, bundle) => {
1754 const params = { per_page: 100 }; // poll for the most recent 100 teams
1755
1756 if (bundle.meta.isFillingDynamicDropdown) {
1757 // dynamic dropdowns support pagination
1758 params.per_page = 30;
1759 params.offset = params.per_page * bundle.meta.page;
1760 }
1761
1762 const response = await z.request({
1763 url: `${API_BASE_URL}/teams`,
1764 params,
1765 });
1766
1767 return response.json;
1768};
1769 // ...
1770```
1771
1772
1773
1774### `bundle.rawRequest`
1775
1776> `bundle.rawRequest` is only available in the `perform` for web hooks, `getAccessToken` for oauth authentication methods, and `performResume` in a callback action.
1777
1778`bundle.rawRequest` holds raw information about the HTTP request that triggered the `perform` method or that represents the users browser request that triggered the `getAccessToken` call:
1779
1780```
1781{
1782 method: 'POST',
1783 querystring: 'foo=bar&baz=qux',
1784 headers: {
1785 'Content-Type': 'application/json'
1786 },
1787 content: '{"hello": "world"}'
1788}
1789```
1790
1791
1792
1793### `bundle.cleanedRequest`
1794
1795> `bundle.cleanedRequest` is only available in the `perform` for webhooks, `getAccessToken` for oauth authentication methods, and `performResume` in a callback action.
1796
1797`bundle.cleanedRequest` will return a formatted and parsed version of the request. Some or all of the following will be available:
1798
1799```
1800{
1801 method: 'POST',
1802 querystring: {
1803 foo: 'bar',
1804 baz: 'qux'
1805 },
1806 headers: {
1807 'Content-Type': 'application/json'
1808 },
1809 content: {
1810 hello: 'world'
1811 }
1812}
1813```
1814
1815### `bundle.outputData`
1816
1817> `bundle.outputData` is only available in the `performResume` in a callback action.
1818
1819`bundle.outputData` will return a whatever data you originally returned in the `perform` allowing you to mix that with `bundle.rawRequest` or `bundle.cleanedRequest`.
1820
1821
1822### `bundle.targetUrl`
1823
1824> `bundle.targetUrl` is only available in the `performSubscribe` and `performUnsubscribe` methods for webhooks.
1825
1826This 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.
1827
1828For example:
1829
1830```js
1831const subscribeHook = async (z, bundle) => {
1832
1833 const options = {
1834 url: 'https://57b20fb546b57d1100a3c405.mockapi.io/api/hooks',
1835 method: 'POST',
1836 body: {
1837 url: bundle.targetUrl, // bundle.targetUrl has the Hook URL this app should call
1838 },
1839 };
1840
1841 const response = await z.request(options);
1842 return response.data; // or response.json if you're using core v9 or older
1843};
1844
1845module.exports = {
1846 // ...
1847 performSubscribe: subscribeHook,
1848 // ...
1849};
1850```
1851
1852Read more in the [REST hook example](https://github.com/zapier/zapier-platform/blob/master/example-apps/rest-hooks/triggers/recipe.js).
1853
1854### `bundle.subscribeData`
1855
1856> `bundle.subscribeData` is available in the `perform` and `performUnsubscribe` method for webhooks.
1857
1858This 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.
1859
1860Read more in the [REST hook example](https://github.com/zapier/zapier-platform/blob/master/example-apps/rest-hooks/triggers/recipe.js).
1861
1862## Environment
1863
1864Apps can define environment variables that are available when the app's code executes. They work just like environment
1865variables defined on the command line. They are useful when you have data like an OAuth client ID and secret that you
1866don't want to commit to source control. Environment variables can also be used as a quick way to toggle between
1867a staging and production environment during app development.
1868
1869It is important to note that **variables are defined on a per-version basis!** When you push a new version, the
1870existing variables from the previous version are copied, so you don't have to manually add them. However, edits
1871made to one version's environment will not affect the other versions.
1872
1873### Defining Environment Variables
1874
1875To define an environment variable, use the `env` command:
1876
1877```bash
1878# Will set the environment variable on Zapier.com
1879zapier env:set 1.0.0 MY_SECRET_VALUE=1234
1880```
1881
1882You will likely also want to set the value locally for testing.
1883
1884```bash
1885export MY_SECRET_VALUE=1234
1886```
1887
1888Alternatively, we provide some extra tooling to work with an `.env` (or `.environment`, see below note) that looks like this:
1889
1890```
1891MY_SECRET_VALUE=1234
1892```
1893
1894> `.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.
1895
1896And then in your `test/basic.js` file:
1897
1898```js
1899const zapier = require('zapier-platform-core');
1900
1901should('some tests', () => {
1902 zapier.tools.env.inject(); // testing only!
1903 console.log(process.env.MY_SECRET_VALUE);
1904 // should print '1234'
1905});
1906```
1907
1908> This is a popular way to provide `process.env.ACCESS_TOKEN || bundle.authData.access_token` for convenient testing.
1909
1910> **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`.
1911
1912
1913### Accessing Environment Variables
1914
1915To view existing environment variables, use the `env` command.
1916
1917```bash
1918# Will print a table listing the variables for this version
1919zapier env:get 1.0.0
1920```
1921
1922Within 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.
1923
1924For example, you can access the `process.env` in your perform functions and in templates:
1925
1926```js
1927const listExample = async (z, bundle) => {
1928 const httpOptions = {
1929 headers: {
1930 'my-header': process.env.MY_SECRET_VALUE,
1931 },
1932 };
1933 const response = await z.request(
1934 'https://example.com/api/v2/recipes.json',
1935 httpOptions
1936 );
1937
1938 // response.throwForStatus() if you're using core v9 or older
1939
1940 return response.data; // or response.json if you're using core v9 or older
1941};
1942
1943const App = {
1944 // ...
1945 triggers: {
1946 example: {
1947 noun: '{{process.env.MY_NOUN}}',
1948 operation: {
1949 // ...
1950 perform: listExample,
1951 },
1952 },
1953 },
1954};
1955
1956```
1957
1958> Note! Be sure to lazily access your environment variables - see [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies).
1959
1960
1961## Making HTTP Requests
1962
1963There are two primary ways to make HTTP requests in the Zapier platform:
1964
19651. **Shorthand HTTP Requests** - these are simple object literals that make it easy to define simple requests.
19662. **Manual HTTP Requests** - you use `z.request([url], options)` to make the requests and control the response. Use this when you need to change options for certain requests (for all requests, use middleware).
1967
1968There are also a few helper constructs you can use to reduce boilerplate:
1969
19701. `requestTemplate` which is an shorthand HTTP request that will be merged with every request.
19712. `beforeRequest` middleware which is an array of functions to mutate a request before it is sent.
19723. `afterResponse` middleware which is an array of functions to mutate a response before it is completed.
1973
1974> Note: you can install any HTTP client you like - but this is greatly discouraged as you lose [automatic HTTP logging](#http-logging) and middleware.
1975
1976### Shorthand HTTP Requests
1977
1978For simple HTTP requests that do not require special pre or post processing, you can specify the HTTP options as an object literal in your app definition.
1979
1980This features:
1981
19821. Lazy `{{curly}}` replacement.
19832. JSON and form body de-serialization.
19843. Automatic non-2xx error raising.
1985
1986```js
1987const triggerShorthandRequest = {
1988 method: 'GET',
1989 url: 'https://{{bundle.authData.subdomain}}.example.com/v2/api/recipes.json',
1990 params: {
1991 sort_by: 'id',
1992 sort_order: 'DESC',
1993 },
1994};
1995
1996const App = {
1997 // ...
1998 triggers: {
1999 example: {
2000 // ...
2001 operation: {
2002 // ...
2003 perform: triggerShorthandRequest,
2004 },
2005 },
2006 },
2007};
2008
2009```
2010
2011In 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 and returned.
2012
2013An error will be raised if the response is not valid JSON, so _do not use shorthand HTTP requests with non-JSON responses_.
2014
2015### Manual HTTP Requests
2016
2017When you need to do custom processing of the response, or need to process non-JSON responses, you can make manual HTTP requests. This approach does not perform any magic - no status code checking, no automatic JSON parsing. Use this method when you need more control. Manual requests do perform lazy `{{curly}}` replacement.
2018
2019To make a manual HTTP request, use the `request` method of the `z` object:
2020
2021```js
2022const listExample = async (z, bundle) => {
2023 const customHttpOptions = {
2024 url: 'https://example.com/api/v2/recipes.json',
2025 headers: {
2026 'my-header': 'from zapier',
2027 },
2028 };
2029 const response = await z.request(customHttpOptions);
2030
2031 const recipes = response.data; // or response.json if you're using core v9 or older
2032 // You can do any custom processing of recipes here...
2033 return recipes;
2034};
2035
2036const App = {
2037 // ...
2038 triggers: {
2039 example: {
2040 // ...
2041 operation: {
2042 // ...
2043 perform: listExample,
2044 },
2045 },
2046 },
2047};
2048
2049```
2050
2051#### POST and PUT Requests
2052
2053To POST or PUT data to your API you can do this:
2054
2055```js
2056const App = {
2057 // ...
2058 triggers: {
2059 example: {
2060 // ...
2061 operation: {
2062 // ...
2063 perform: async (z, bundle) => {
2064 const recipe = {
2065 name: 'Baked Falafel',
2066 style: 'mediterranean',
2067 directions: 'Get some dough....',
2068 };
2069
2070 const options = {
2071 method: 'POST',
2072 url: 'https://example.com/api/v2/recipes.json',
2073 body: JSON.stringify(recipe),
2074 };
2075 const response = await z.request(options);
2076
2077 // Throw and try to extract message from standard error responses
2078 if (response.status !== 201) {
2079 throw new z.errors.Error(
2080 `Unexpected status code ${response.status}`,
2081 'CreateRecipeError',
2082 response.status
2083 );
2084 }
2085
2086 return response.data; // or response.json if you're using core v9 or older
2087 },
2088 },
2089 },
2090 },
2091};
2092
2093```
2094
2095> Note: you need to call `z.JSON.stringify()` before setting the `body`.
2096
2097### Using HTTP middleware
2098
2099If you need to process all HTTP requests in a certain way, you may be able to use one of utility HTTP middleware functions.
2100
2101> Example App: check out https://github.com/zapier/zapier-platform/tree/master/example-apps/middleware for a working example app using HTTP middleware.
2102
2103Try putting them in your app definition:
2104
2105```js
2106const addHeader = (request, z, bundle) => {
2107 request.headers['my-header'] = 'from zapier';
2108 return request;
2109};
2110
2111// This example only works on core v10+!
2112const handleErrors = (response, z) => {
2113 // Prevent `throwForStatus` from throwing for a certain status.
2114 if (response.status === 456) {
2115 response.skipThrowForStatus = true;
2116 } else if (response.status === 200 && response.data.success === false) {
2117 throw new z.errors.Error(response.data.message, response.data.code);
2118 }
2119};
2120
2121// This example only works on core v10+!
2122const parseXML = (response, z, bundle) => {
2123 // Parse content that is not JSON
2124 // eslint-disable-next-line no-undef
2125 response.data = xml.parse(response.content);
2126 return response;
2127};
2128
2129const App = {
2130 // ...
2131 beforeRequest: [addHeader],
2132 afterResponse: [parseXML, handleErrors],
2133 // ...
2134};
2135
2136```
2137
2138A `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.
2139
2140Middleware functions can be asynchronous - just return a promise from the middleware function.
2141
2142The second argument for middleware is the `z` object, but it does *not* include `z.request()` as using that would easily create infinite loops.
2143
2144#### Error Response Handling
2145
2146Since v10, we call `response.throwForStatus()` before we return a response. You can prevent this by setting `skipThrowForStatus` on the request or response object. You can do this in `afterResponse` middleware if the API uses a status code >= 400 that should not be treated as an error.
2147
2148For 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.
2149
2150### HTTP Request Options
2151
2152Shorthand requests and manual `z.request([url], options)` calls support the following HTTP `options`:
2153
2154* `url`: HTTP url, you can provide it both `z.request(url, options)` or `z.request({url: url, ...})`.
2155* `method`: HTTP method, default is `GET`.
2156* `headers`: request headers object, format `{'header-key': 'header-value'}`.
2157* `params`: URL query params object, format `{'query-key': 'query-value'}`.
2158* `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`.
2159* `json`: shortcut object/array/etc. you want to JSON encode into body. Default is `null`.
2160* `form`: shortcut object. you want to form encode into body. Default is `null`.
2161* `raw`: set this to stream the response instead of consuming it immediately. Default is `false`.
2162* `redirect`: set to `manual` to extract redirect headers, `error` to reject redirect, default is `follow`.
2163* `follow`: maximum redirect count, set to `0` to not follow redirects. default is `20`.
2164* `compress`: support gzip/deflate content encoding. Set to `false` to disable. Default is `true`.
2165* `agent`: Node.js `http.Agent` instance, allows custom proxy, certificate etc. Default is `null`.
2166* `timeout`: request / response timeout in ms. Set to `0` to disable (OS limit still applies), timeout reset on `redirect`. Default is `0` (disabled).
2167* `size`: maximum response body size in bytes. Set to `0` to disable. Default is `0` (disabled).
2168* `skipThrowForStatus` (_new in v10.0.0_): don't call `response.throwForStatus()` before resolving the request with `response`. See [HTTP Response Object](#http-response-object).
2169
2170```js
2171const response = await z.request({
2172 url: 'https://example.com',
2173 method: 'POST',
2174 headers: {
2175 'Content-Type': 'application/json'
2176 },
2177 // only provide body, json or form...
2178 body: {hello: 'world'}, // or '{"hello": "world"}' or 'hello=world'
2179 json: {hello: 'world'},
2180 form: {hello: 'world'},
2181 // access node-fetch style response.body
2182 raw: false,
2183 redirect: 'follow',
2184 follow: 20,
2185 compress: true,
2186 agent: null,
2187 timeout: 0,
2188 size: 0,
2189})
2190```
2191
2192### HTTP Response Object
2193
2194The response object returned by `z.request([url], options)` supports the following fields and methods:
2195
2196* `status`: The response status code, i.e. `200`, `404`, etc.
2197* `content`: The response content as a String. For Buffer, try `options.raw = true`.
2198* `data` (_new in v10.0.0_): The response content as an object if the content is JSON or ` application/x-www-form-urlencoded` (`undefined` otherwise).
2199* `json`: The response content as an object if the content is JSON (`undefined` otherwise). Deprecated since v10.0.0: Use `data` instead.
2200* `json()`: Get the response content as an object, if `options.raw = true` and content is JSON (returns a promise).
2201* `body`: A stream available only if you provide `options.raw = true`.
2202* `headers`: Response headers object. The header keys are all lower case.
2203* `getHeader(key)`: Retrieve response header, case insensitive: `response.getHeader('My-Header')`
2204* `skipThrowForStatus` (_new in v10.0.0_): don't call `throwForStatus()` before resolving the request with this response.
2205* `throwForStatus()`: Throw error if 400 <= `status` < 600.
2206* `request`: The original request options object (see above).
2207
2208```js
2209const response = await z.request({
2210 // options
2211});
2212
2213// A bunch of examples lines for cherry picking
2214response.status;
2215response.headers['Content-Type'];
2216response.getHeader('content-type');
2217response.request; // original request options
2218response.throwForStatus();
2219
2220if (options.raw === false) { // (default)
2221 // If you're core v10+
2222 response.data; // same as...
2223 z.JSON.parse(response.content); // or...
2224 querystring.parse(response.content);
2225
2226 // If you're core v9 or older...
2227 response.json; // same as
2228 z.JSON.parse(response.content);
2229} else {
2230 const buf = await response.buffer();
2231 buf.toString();
2232
2233 const text = await response.text();
2234
2235 const json = await response.json();
2236
2237 response.body.pipe(otherStream);
2238}
2239```
2240
2241
2242## Dehydration
2243
2244Dehydration, and its counterpart Hydration, is a tool that can lazily load data that might be otherwise expensive to retrieve aggressively.
2245
2246* **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.
2247* **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.
2248
2249> This is very common when [Stashing Files](#stashing-files) - but that isn't their only use!
2250
2251The method `z.dehydrate(func, inputData)` has two required arguments:
2252
2253* `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
2254* `inputData` - this is an object that contains things like a `path` or `id` - whatever you need to load data on the other side
2255
2256> **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.
2257
2258Here is an example that pulls in extra data for a movie:
2259
2260```js
2261const getMovieDetails = async (z, bundle) => {
2262 const url = `https://example.com/movies/${bundle.inputData.id}.json`;
2263 const response = await z.request(url);
2264
2265 // reponse.throwForStatus() if you're using core v9 or older
2266
2267 return response.data; // or response.json if you're using core v9 or older
2268};
2269
2270const movieList = async (z, bundle) => {
2271 const response = await z.request('https://example.com/movies.json');
2272
2273 // response.throwForStatus() if you're using core v9 or older
2274
2275 return response.data.map((movie) => {
2276 // so maybe /movies.json is thin content but /movies/:id.json has more
2277 // details we want...
2278 movie.details = z.dehydrate(getMovieDetails, { id: movie.id });
2279 return movie;
2280 });
2281};
2282
2283const App = {
2284 version: require('./package.json').version,
2285 platformVersion: require('zapier-platform-core').version,
2286
2287 // don't forget to register hydrators here!
2288 // it can be imported from any module
2289 hydrators: {
2290 getMovieDetails: getMovieDetails,
2291 },
2292
2293 triggers: {
2294 new_movie: {
2295 noun: 'Movie',
2296 display: {
2297 label: 'New Movie',
2298 description: 'Triggers when a new Movie is added.',
2299 },
2300 operation: {
2301 perform: movieList,
2302 },
2303 },
2304 },
2305};
2306
2307module.exports = App;
2308
2309```
2310
2311And 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.
2312
2313> **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.
2314
2315### Merging Hydrated Data
2316
2317As you've seen, the usual call to dehydrate will assign the result to an object property:
2318
2319```js
2320movie.details = z.dehydrate(getMovieDetails, { id: movie.id });
2321```
2322
2323In 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:
2324
2325```js
2326movie.$HOIST$ = z.dehydrate(getMovieDetails, { id: movie.id });
2327```
2328
2329Using `$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:
2330
2331```js
2332movie.details = {
2333 title: movie.title,
2334 $HOIST$: z.dehydrate(getMovieDetails, { id: movie.id })
2335};
2336```
2337
2338### File Dehydration
2339
2340*New in v7.3.0.*
2341
2342The method `z.dehydrateFile(func, inputData)` allows you to download a file lazily. It takes the identical arguments as `z.dehydrate(func, inputData)` does.
2343
2344An example can be found in the [Stashing Files](#stashing-files) section.
2345
2346What 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 its absolutely necessary. A good example is users 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. There's nothing in binary file data for users to map in the subsequent steps.
2347
2348> `z.dehydrateFile(func, inputData)` is new in v7.3.0. We used to recommend to use `z.dehydrate(func, inputData)` for files, but it's not the case anymore. Please change it to `z.dehydrateFile(func, inputData)` for a better user experience.
2349
2350## Stashing Files
2351
2352It 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.
2353
2354The interface `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` takes a single required argument - the extra three arguments will be automatically populated in most cases. For example - a full example is this:
2355
2356```js
2357const content = 'Hello world!';
2358const url = await z.stashFile(content, content.length, 'hello.txt', 'text/plain');
2359z.console.log(url);
2360// https://zapier-dev-files.s3.amazonaws.com/cli-platform/f75e2819-05e2-41d0-b70e-9f8272f9eebf
2361```
2362
2363Most likely you'd want to stream from another URL - note the usage of `z.request({raw: true})`:
2364
2365```js
2366const fileRequest = z.request({url: 'https://example.com/file.pdf', raw: true});
2367const url = await z.stashFile(fileRequest); // knownLength and filename will be sniffed from the request. contentType will be binary/octet-stream
2368z.console.log(url);
2369// https://zapier-dev-files.s3.amazonaws.com/cli-platform/74bc623c-d94d-4cac-81f1-f71d7d517bc7
2370```
2371
2372> 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!
2373
2374See a full example with dehydration/hydration wired in correctly:
2375
2376```js
2377const stashPDFfunction = (z, bundle) => {
2378 // use standard auth to request the file
2379 const filePromise = z.request({
2380 url: bundle.inputData.downloadUrl,
2381 raw: true,
2382 });
2383 // and swap it for a stashed URL
2384 return z.stashFile(filePromise);
2385};
2386
2387const pdfList = async (z, bundle) => {
2388 const response = await z.request('https://example.com/pdfs.json');
2389
2390 // response.throwForStatus() if you're using core v9 or older
2391
2392 // response.json.map if you're using core v9 or older
2393 return response.data.map((pdf) => {
2394 // Lazily convert a secret_download_url to a stashed url
2395 // zapier won't do this until we need it
2396 pdf.file = z.dehydrateFile(stashPDFfunction, {
2397 downloadUrl: pdf.secret_download_url,
2398 });
2399 delete pdf.secret_download_url;
2400 return pdf;
2401 });
2402};
2403
2404const App = {
2405 version: require('./package.json').version,
2406 platformVersion: require('zapier-platform-core').version,
2407
2408 hydrators: {
2409 stashPDF: stashPDFfunction,
2410 },
2411
2412 triggers: {
2413 new_pdf: {
2414 noun: 'PDF',
2415 display: {
2416 label: 'New PDF',
2417 description: 'Triggers when a new PDF is added.',
2418 },
2419 operation: {
2420 perform: pdfList,
2421 },
2422 },
2423 },
2424};
2425
2426module.exports = App;
2427
2428```
2429
2430> Example App: check out https://github.com/zapier/zapier-platform/tree/master/example-apps/files for a working example app using files.
2431
2432
2433## Logging
2434
2435There are two types of logs for a Zapier app, console logs and HTTP logs. The console logs are created by your app through the use of the `z.console.log` method ([see below for details](#console-logging)). The HTTP logs are created automatically by Zapier whenever your app makes HTTP requests (as long as you use `z.request([url], options)` or shorthand request objects).
2436
2437To view the logs for your application, use the `zapier logs` command. There are three types of logs, `http` (logged automatically by Zapier on HTTP requests), `bundle` (logged automatically on every method execution), and `console` (manual logs via `z.console.log()` statements).
2438
2439For advanced logging options including only displaying the logs for a certain user or app version, look at the help for the logs command:
2440
2441```bash
2442zapier help logs
2443```
2444
2445### Console Logging
2446
2447To manually print a log statement in your code, use `z.console.log`:
2448
2449```js
2450z.console.log('Here are the input fields', bundle.inputData);
2451```
2452
2453The `z.console` object has all the same methods and works just like the Node.js [`Console`](https://nodejs.org/docs/latest-v6.x/api/console.html) class - the only difference is we'll log to our distributed datastore and you can view them via `zapier logs` (more below).
2454
2455### Viewing Console Logs
2456
2457To see your `z.console.log` logs, do:
2458
2459```bash
2460zapier logs --type=console
2461```
2462
2463### Viewing Bundle Logs
2464
2465To see the bundle logs, do:
2466
2467```bash
2468zapier logs --type=bundle
2469```
2470
2471### HTTP Logging
2472
2473If you are using the `z.request()` shortcut that we provide - HTTP logging is handled automatically for you. For example:
2474
2475```js
2476z.request('https://57b20fb546b57d1100a3c405.mockapi.io/api/recipes')
2477 .then((res) => {
2478 // do whatever you like, this request is already getting logged! :-D
2479 return res;
2480 })
2481```
2482
2483### Viewing HTTP Logs
2484
2485To see the HTTP logs, do:
2486
2487```bash
2488zapier logs --type=http
2489```
2490To see detailed http logs including headers, request and response bodies, etc, do:
2491
2492```bash
2493zapier logs --type=http --detailed
2494```
2495
2496
2497## Error Handling
2498
2499APIs are not always available. Users do not always input data correctly to
2500formulate valid requests. Thus, it is a good idea to write apps defensively and
2501plan for 4xx and 5xx responses from APIs. Without proper handling, errors often
2502have incomprehensible messages for end users, or possibly go uncaught.
2503
2504Zapier provides a couple of tools to help with error handling. First is the
2505`afterResponse` middleware ([docs](#using-http-middleware)), which provides a hook for
2506processing all responses from HTTP calls. Second is `response.throwForStatus()`
2507([docs](#http-response-object)), which throws an error if the response status indicates
2508an error (status >= 400). Since v10.0.0, we automatically call this method before returning the
2509response, unless you set `skipThrowForStatus` on the request or response object. The
2510last tool is the collection of errors in `z.errors` ([docs](#zerrors)), which control
2511the behavior of Zaps when various kinds of errors occur.
2512
2513### General Errors
2514
2515Errors due to a misconfiguration in a user's Zap should be handled in your app
2516by throwing `z.errors.Error` with a user-friendly message and optional error and
2517status code. Typically, this will be prettifying 4xx responses or APIs that return
2518errors as 200s with a payload that describes the error.
2519
2520Example: `throw new z.errors.Error('Contact name is too long.', 'InvalidData', 400);`
2521
2522> `z.errors.Error` is new 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')`.
2523
2524A couple best practices to keep in mind:
2525
2526 * Elaborate on terse messages. "not_authenticated" -> "Your API Key is invalid. Please reconnect your account."
2527 * If the error calls out a specific field, surface that information to the user. "Provided data is invalid" -> "Contact name is invalid"
2528 * If the error provides details about why a field is invalid, add that in too! "Contact name is invalid" -> "Contact name is too long"
2529 * The second, optional argument should be a code that a computer could use to identify the type of error.
2530 * The last, optional argument should be the HTTP status code, if any.
2531
2532The code and status can be used by us to provide relevant troubleshooting to the
2533user when we communicate the error.
2534
2535Note that if a Zap raises too many error messages it will be automatically
2536turned off, so only use these if the scenario is truly an error that needs to
2537be fixed.
2538
2539### Halting Execution
2540
2541Any operation can be interrupted or "halted" (not success, not error, but
2542stopped for some specific reason) with a `HaltedError`. You might find yourself
2543using this error in cases where a required pre-condition is not met. For instance,
2544in a create to add an email address to a list where duplicates are not allowed,
2545you would want to throw a `HaltedError` if the Zap attempted to add a duplicate.
2546This would indicate failure, but it would be treated as a soft failure.
2547
2548Unlike throwing `z.errors.Error`, a Zap will never by turned off when this error is thrown
2549(even if it is raised more often than not).
2550
2551Example: `throw new z.errors.HaltedError('Your reason.');`
2552
2553### Stale Authentication Credentials
2554
2555For apps that require manual refresh of authorization on a regular basis, Zapier
2556provides a mechanism to notify users of expired credentials. With the
2557`ExpiredAuthError`, the current operation is interrupted, the Zap is turned off
2558(to prevent more calls with expired credentials), and a predefined email is sent
2559out informing the user to refresh the credentials.
2560
2561Example: `throw new z.errors.ExpiredAuthError('Your message.');`
2562
2563For apps that use OAuth2 + refresh or Session Auth, the core injects a built-in
2564`afterResponse` middleware that throws an error when the response status is 401.
2565The error will signal Zapier to refresh the credentials and then repeat the
2566failed operation. For some cases, e.g, your server doesn't use the 401 status
2567for auth refresh, you may have to throw the `RefreshAuthError` on your own,
2568which will also signal Zapier to refresh the credentials.
2569
2570Example: `throw new z.errors.RefreshAuthError();`
2571
2572#### v10 Breaking Change: Auth Refresh
2573
2574A breaking change on v10+ is that the built-in `afterResponse` middleware the
2575handles auth refresh is changed to happen AFTER your app's `afterResponse`. On
2576v9 and older, it happens before your app's `afterResponse`. So it will break if
2577your `afterReponse` does something like:
2578
2579```js
2580// Auth refresh will stop working on v10 this way!
2581const yourAfterResponse = (resp) => {
2582 if (resp.status !== 200) {
2583 throw new Error('hi');
2584 }
2585 return resp;
2586};
2587```
2588
2589This is because on v10 the `throw new Error('hi')` line will take precedence
2590over the built-in middleware that does auth refresh. One way to fix is to let
2591the 401 response fall back to the built-in middleware that does the auth
2592refresh:
2593
2594```js
2595const yourAfterResponse = (resp) => {
2596 if (resp.status !== 200 && resp.status !== 401) {
2597 throw new Error('hi');
2598 }
2599 return resp;
2600};
2601```
2602
2603Another way to fix is to handle the 401 response yourself by throwing a
2604`RefreshAuthError`:
2605
2606```js
2607const yourAfterResponse = (resp) => {
2608 if (resp.status === 401) {
2609 throw new z.errors.RefreshAuthError();
2610 }
2611 if (resp.status !== 200) {
2612 throw new Error('hi');
2613 }
2614 return resp;
2615};
2616```
2617
2618## Testing
2619
2620You can write unit tests for your Zapier app that run locally, outside of the Zapier editor.
2621You can run these tests in a CI tool like [Travis](https://travis-ci.com/).
2622
2623### Writing Unit Tests
2624
2625Since v10, 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.
2626
2627> Note: On v9, the recommendation was [Mocha](https://mochajs.org/). You can still use it if you prefer Mocha.
2628
2629```js
2630/* globals describe, expect, test */
2631
2632const zapier = require('zapier-platform-core');
2633
2634// createAppTester() makes it easier to test your app. It takes your raw app
2635// definition, and returns a function that will test you app.
2636const App = require('../index');
2637const appTester = zapier.createAppTester(App);
2638
2639// Inject the vars from the .env file to process.env. Do this if you have a .env
2640// file.
2641zapier.tools.env.inject();
2642
2643describe('triggers', () => {
2644 test('load recipes', async () => {
2645 const bundle = {
2646 inputData: {
2647 style: 'mediterranean',
2648 },
2649 };
2650
2651 const results = await appTester(
2652 App.triggers.species.operation.perform,
2653 bundle
2654 );
2655 expect(results.length).toBeGreaterThan(1);
2656
2657 const firstRecipe = results[0];
2658 expect(firstRecipe.id).toBe(1);
2659 expect(firstRecipe.name).toBe('Baked Falafel');
2660 });
2661});
2662
2663```
2664
2665### Mocking Requests
2666
2667While testing, 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`.
2668
2669```js
2670/* globals describe, expect, test */
2671
2672const zapier = require('zapier-platform-core');
2673
2674const App = require('../index');
2675const appTester = zapier.createAppTester(App);
2676
2677const nock = require('nock');
2678
2679describe('triggers', () => {
2680 test('load recipes', async () => {
2681 const bundle = {
2682 inputData: {
2683 style: 'mediterranean',
2684 },
2685 };
2686
2687 // mocks the next request that matches this url and querystring
2688 nock('https://example.com/api')
2689 .get('/recipes')
2690 .query(bundle.inputData)
2691 .reply(200, [
2692 { name: 'name 1', directions: 'directions 1', id: 1 },
2693 { name: 'name 2', directions: 'directions 2', id: 2 },
2694 ]);
2695
2696 const results = await appTester(
2697 App.triggers.recipe.operation.perform,
2698 bundle
2699 );
2700
2701 expect(results.length).toBeGreaterThan(1);
2702
2703 const firstRecipe = results[0];
2704 expect(firstRecipe.id).toBe(1);
2705 expect(firstRecipe.name).toBe('name 1');
2706 });
2707});
2708
2709```
2710
2711There's more info about nock and its usage in its [readme](https://github.com/node-nock/nock/blob/master/README.md).
2712
2713### Running Unit Tests
2714
2715To run all your tests do:
2716
2717```bash
2718zapier test
2719```
2720
2721> You can also go direct with `npm test` or `node_modules/.bin/jest`.
2722
2723### Testing & Environment Variables
2724
2725The 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:
2726
2727```js
2728const zapier = require('zapier-platform-core');
2729zapier.tools.env.inject(); // inject() can take a filename; defaults to ".env"
2730
2731// now process.env has all the values in your .env file
2732```
2733
2734> `.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.
2735
2736> Remember: **NEVER** add your secrets file to version control!
2737
2738Additionally, you can provide them dynamically at runtime:
2739
2740```bash
2741CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
2742```
2743
2744Or, `export` them explicitly and place them into the environment:
2745
2746```bash
2747export CLIENT_ID=1234
2748export CLIENT_SECRET=abcd
2749zapier test
2750```
2751
2752### Testing in Your CI
2753
2754Whether you use Travis, Circle, Jenkins, or anything else, we aim to make it painless to test in an automated environment.
2755
2756Behind the scenes `zapier test` is doing a pretty standard `npm test`, which could be [Jest](https://jestjs.io/) or [Mocha](https://mochajs.org/), based on your project setup.
2757
2758This makes it pretty straightforward to integrate into your testing interface. If you'd like to test with [Travis CI](https://travis-ci.com/) for example - the `.travis.yml` would look something like this:
2759
2760```yaml
2761language: node_js
2762node_js:
2763 - "v14"
2764before_script: npm install -g zapier-platform-cli
2765script: CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
2766```
2767
2768You can substitute `zapier test` with `npm test`, or a direct call to `node_modules/.bin/jest`. Also, we generally recommend putting the environment variables into whatever configuration screen Jenkins or Travis provides!
2769
2770As an alternative 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 whatever manner you CI provides (such as [these instructions](https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml), for Travis).
2771
2772### Debugging Tests
2773
2774Sometimes 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).
2775
2776To start, add the following line to the `scripts` section of your `package.json`:
2777
2778```
2779"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand"
2780```
2781
2782This will tell `node` to inspect the `jest` processes, which is exactly what we need.
2783
2784Next, add a `debugger;` statement somewhere in your code, probably in a `perform` method:
2785
2786```js
2787// triggers on a new pizza with a certain tag
2788const perform = async (z, bundle) => {
2789 const response = await z.request({
2790 url: "https://jsonplaceholder.typicode.com/posts",
2791 params: {
2792 tag: bundle.inputData.tagName,
2793 },
2794 });
2795 debugger;
2796 // this should return an array of objects
2797 return response.data;
2798};
2799```
2800
2801This creates a _breakpoint_ while `inspect`ing, or a starting point for our manual inspection.
2802
2803Next, 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:
2804
2805```
2806% yarn test:debug
2807yarn run v1.22.10
2808$ node --inspect-brk node_modules/.bin/jest --runInBand
2809Debugger listening on ws://127.0.0.1:9229/5edaab3c-a1d3-45e4-b374-0536095c559b
2810For help, see: https://nodejs.org/en/docs/inspector
2811```
2812
2813Now 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:
2814
2815![](https://cdn.zappy.app/e2836d2950e1f8a03e3621a22452c3cd.png)
2816
2817Click `inspect`. A new window will open. Next, click the little blue arrow in the top right to actually run the code:
2818
2819![](https://cdn.zappy.app/a64e7963a7090e9730d9c8e7b3595a6a.png)
2820
2821After 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.
2822
2823![](https://cdn.zappy.app/4bfdfe079a344ab7aced64ad7728bc6a.png)
2824
2825Using debugging in combination with thorough unit tests, you will hopefully be able to keep your Zapier integration in smooth working order.
2826
2827## Using `npm` Modules
2828
2829Use `npm` modules just like you would use them in any other node app, for example:
2830
2831```bash
2832npm install --save jwt
2833```
2834
2835And then `package.json` will be updated, and you can use them like anything else:
2836
2837```js
2838const jwt = require('jwt');
2839```
2840
2841During 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.
2842
2843> 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`.
2844
2845> 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:
2846
2847```json
2848{
2849 "id": 1,
2850 "key": "App1",
2851 "includeInBuild": [
2852 "test.txt",
2853 "testing.json"
2854 ]
2855}
2856
2857```
2858
2859> Warning: Do not use compiled libraries unless you run your build on the AWS AMI `ami-4fffc834`, or follow the Docker instructions below.
2860
2861## Building Native Packages with Docker
2862
2863Unfortunately 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.
2864
2865First, create your `Dockerfile`:
2866
2867```Dockerfile
2868FROM amazonlinux:2017.03.1.20170812
2869
2870RUN yum install zip findutils wget gcc44 gcc-c++ libgcc44 cmake -y
2871
2872RUN wget https://nodejs.org/dist/v8.10.0/node-v8.10.0.tar.gz && \
2873 tar -zxvf node-v8.10.0.tar.gz && \
2874 cd node-v8.10.0 && \
2875 ./configure && \
2876 make && \
2877 make install && \
2878 cd .. && \
2879 rm -rf node-v8.10.0 node-v8.10.0.tar.gz
2880
2881RUN npm i -g zapier-platform-cli
2882
2883WORKDIR /app
2884```
2885
2886And finally, create your `docker-compose.yml` file:
2887
2888```yml
2889version: '3.4'
2890
2891services:
2892 pusher:
2893 build: .
2894 volumes:
2895 - .:/app
2896 - node_modules:/app/node_modules:delegated
2897 - ~/.zapierrc:/root/.zapierrc
2898 command: 'bash -c "npm i && zapier push"'
2899 environment:
2900 ZAPIER_DEPLOY_KEY: ${ZAPIER_DEPLOY_KEY}
2901
2902volumes:
2903 node_modules:
2904```
2905
2906> Note: Watch out for your `package-lock.json` file, if it exists for local install it might incorrectly pin a native version.
2907
2908Now you should be able to run `docker-compose run pusher` and see the build and push successfully complete!
2909
2910
2911## Using Transpilers
2912
2913If 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`,
2914`zapier push`, and `zapier upload`. See the following example:
2915
2916```json
2917{
2918 "scripts": {
2919 "zapier-dev": "babel src --out-dir lib --watch",
2920 "_zapier-build": "babel src --out-dir lib"
2921 }
2922}
2923```
2924
2925Then, you can have your fancy ES7 code in `src/*` and a root `index.js` like this:
2926
2927```js
2928module.exports = require('./lib');
2929```
2930
2931And work with commands like this:
2932
2933```bash
2934# watch and recompile
2935npm run zapier-dev
2936
2937# tests should work fine
2938zapier test
2939
2940# every build ensures a fresh build
2941zapier push
2942```
2943
2944There are a lot of details left out - check out the full example app for a working setup.
2945
2946> Example App: Check out https://github.com/zapier/zapier-platform/tree/master/example-apps/babel for a working example app using Babel.
2947
2948## FAQs
2949
2950### Why doesn't Zapier support newer versions of Node.js?
2951
2952We 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 (the latest of which is `v14`. As that updates, so too will we.
2953
2954### How do I manually set the Node.js version to run my app with?
2955
2956Update 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/master/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).
2957
2958**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.
2959
2960If 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.
2961
2962### When to use placeholders or curlies?
2963
2964You 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.
2965
2966In general, use `${var}` within functions and use `{{var}}` anywhere else.
2967
2968Placeholders 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.
2969
2970> 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>.
2971
2972### Does Zapier support XML (SOAP) APIs?
2973
2974Not natively, but it can! Users have reported that the following `npm` modules are compatible with the CLI Platform:
2975
2976* [pixl-xml](https://github.com/jhuckaby/pixl-xml)
2977* [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)
2978* [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser)
2979
2980Since 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:
2981
2982```js
2983const xml = require('pixl-xml');
2984
2985const App = {
2986 // ...
2987 afterResponse: [
2988 (response, z, bundle) => {
2989 // Only works on core v10+!
2990 response.throwForStatus();
2991 response.data = xml.parse(response.content);
2992 return response;
2993 },
2994 ],
2995 // ...
2996};
2997
2998```
2999
3000### Is it possible to iterate over pages in a polling trigger?
3001
3002Yes, 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).
3003
3004```js
3005// some async call
3006const makeCall = (z, start, limit) => {
3007 return z.request({
3008 url: 'https://jsonplaceholder.typicode.com/posts',
3009 params: {
3010 _start: start,
3011 _limit: limit,
3012 },
3013 });
3014};
3015
3016// triggers on paging with a certain tag
3017const performPaging = async (z, bundle) => {
3018 // array of promises
3019 const promises = [];
3020
3021 // 5 requests with page size = 3
3022 let start = 0;
3023 const limit = 3;
3024 for (let i = 0; i < 5; i++) {
3025 promises.push(makeCall(z, start, limit));
3026 start += limit;
3027 }
3028
3029 // send requests concurrently
3030 const responses = await Promise.all(promises);
3031 return responses.map((res) => res.data);
3032};
3033
3034module.exports = {
3035 key: 'paging',
3036 noun: 'Paging',
3037
3038 display: {
3039 label: 'Get Paging',
3040 description: 'Triggers on a new paging.',
3041 },
3042
3043 operation: {
3044 inputFields: [],
3045 perform: performPaging,
3046 },
3047};
3048
3049```
3050
3051If 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.
3052
3053```js
3054// a hypothetical API where payloads are big so we want to heavily limit how much comes back
3055// we want to only return items created in the last hour
3056
3057const asyncExample = async (z, bundle) => {
3058 const limit = 3;
3059 let start = 0;
3060 const twoHourMilliseconds = 60 * 60 * 2 * 1000;
3061 const hoursAgo = new Date() - twoHourMilliseconds;
3062
3063 let response = await z.request({
3064 url: 'https://jsonplaceholder.typicode.com/posts',
3065 params: {
3066 _start: start,
3067 _limit: limit,
3068 },
3069 });
3070
3071 let results = response.data; // response.json if you're using core v9 or older
3072
3073 // keep paging until the last item was created over two hours ago
3074 // then we know we almost certainly haven't missed anything and can let
3075 // deduper handle the rest
3076
3077 while (new Date(results[results.length - 1].createdAt) > hoursAgo) {
3078 start += limit; // next page
3079
3080 response = await z.request({
3081 url: 'https://jsonplaceholder.typicode.com/posts',
3082 params: {
3083 _start: start,
3084 _limit: limit,
3085 },
3086 });
3087
3088 results = results.concat(response.data);
3089 }
3090
3091 return results;
3092};
3093
3094```
3095
3096### How do search-powered fields relate to dynamic dropdowns and why are they both required together?
3097
3098To understand search-powered fields, we have to have a good understanding of dynamic dropdowns.
3099
3100When 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:
3101
3102![](https://cdn.zapier.com/storage/photos/fb56bdc2aab91504be0e51800bec4d64.png)
3103
3104The 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.
3105
3106**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:
3107
31081. Some Trigger
31092. Calculate what day of the week it is today (Code)
31103. Find the spreadsheet that matches the day from Step 2
31114. Update the spreadsheet (with the id from step 3) with some data
3112
3113If 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.
3114
3115![](https://cdn.zapier.com/storage/photos/d263fd3a56cf8108cb89195163e7c9aa.png)
3116
3117This is paired most often with "update" actions, where a required parameter will be a resource id.
3118
3119<a id="paging"></a>
3120### What's the deal with pagination? When is it used and how does it work?
3121
3122Paging 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.
3123
3124Paging 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:
3125
3126```js
3127const perform = async (z, bundle) => {
3128 const response = await z.request({
3129 url: 'https://example.com/api/list.json',
3130 params: {
3131 limit: 100,
3132 offset: 100 * bundle.meta.page
3133 }
3134 });
3135 return response.data; // or response.json you're using core v9 or older
3136};
3137```
3138
3139If your API uses cursor-based paging instead of an offset, you can use `z.cursor.get` and `z.cursor.set`:
3140
3141```js
3142// the perform method of our trigger
3143// ensure operation.canPaginate is true!
3144
3145const performWithoutAsync = (z, bundle) => {
3146 return Promise.resolve()
3147 .then(() => {
3148 if (bundle.meta.page === 0) {
3149 // first page, no need to fetch a cursor
3150 return Promise.resolve();
3151 } else {
3152 return z.cursor.get(); // Promise<string | null>
3153 }
3154 })
3155 .then((cursor) => {
3156 return z.request(
3157 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
3158 {
3159 params: { cursor: cursor }, // if cursor is null, it's ignored here
3160 }
3161 );
3162 })
3163 .then((response) => {
3164 // need to save the cursor and return a promise, but also need to pass the data along
3165 return Promise.all([response.items, z.cursor.set(response.nextPage)]);
3166 })
3167 .then(([items /* null */]) => {
3168 return items;
3169 });
3170};
3171
3172// ---------------------------------------------------
3173
3174const performWithAsync = async (z, bundle) => {
3175 let cursor;
3176 if (bundle.meta.page) {
3177 cursor = await z.cursor.get(); // string | null
3178 }
3179
3180 const response = await z.request(
3181 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
3182 {
3183 // if cursor is null, it's sent as an empty query
3184 // param and should be ignored by the server
3185 params: { cursor: cursor },
3186 }
3187 );
3188
3189 // we successfully got page 1, should store the cursor in case the user wants page 2
3190 await z.cursor.set(response.nextPage);
3191
3192 return response.items;
3193};
3194
3195```
3196
3197Cursors 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.
3198
3199Lastly, you need to set `canPaginate` to `true` in your polling definition (per the [schema](https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#basicpollingoperationschema)) for the `z.cursor` methods to work as expected.
3200
3201<a id="dedup"></a>
3202### How does deduplication work?
3203
3204Each time a polling Zap runs, Zapier needs to decide which of the items in the response should trigger the zap. To do this, we compare the `id`s to all those we've seen before, trigger on new objects, and update the list of seen `id`s. When a Zap is turned on, we initialize the list of seen `id`s 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.
3205
3206For example, the initial poll returns objects 4, 5, and 6 (where a higher `id` 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.
3207
3208There's a more in-depth explanation [here](https://platform.zapier.com/legacy/dedupe).
3209
3210### Why are my triggers complaining if I don't provide an explicit `id` field?
3211
3212For 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:
3213
3214```js
3215// ...
3216let items = response.data.items; // or response.json.items if you're using core v9 or older
3217return items.map((item) => {
3218 item.id = item.contactId;
3219 return item;
3220});
3221```
3222
3223### Node X No Longer Supported
3224
3225If you're seeing errors like the following:
3226
3227```
3228InvalidParameterValueException 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.
3229```
3230
3231... 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:
3232
32331. 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/master/CHANGELOG.md).
32342. Increment the `version` property in `package.json`
32353. Ensure you're using version `v14` (or greater) of node locally (`node -v`). Use [nvm](https://github.com/nvm-sh/nvm) to use a different one if need be.
32364. Run `rm -rf node_modules && npm i` to get a fresh copy of everything
32375. Run `zapier test` to ensure your tests still pass
32386. Run `zapier push`
32397. Run `zapier promote YOUR_NEW_VERSION` (from step 2)
32408. Migrate your users from the previous version (`zapier migrate OLD_VERSION YOUR_NEW_VERSION`)
3241
3242<a id="analytics"></a>
3243### What Analytics are Collected?
3244
3245Starting with v8.4.0, Zapier collects information about each invocation of the CLI tool.
3246
3247This 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.
3248
3249**Anonymous**
3250
3251When you run a command with analytics in `anonymous` mode, the following data is sent to Zapier:
3252
3253* which command you ran
3254* if that command is a known command
3255* how many arguments you supplied (but not the contents of the arguments)
3256* which flags you used (but not their contents)
3257* the version of CLI that you're using
3258
3259**Enabled** (the default)
3260
3261When analytics are fully `enabled`, the above is sent, plus:
3262
3263* your operating system (the result of calling [`process.platform`](https://nodejs.org/api/process.html#process_process_platform))
3264* your Zapier user id
3265
3266**Disabled**
3267
3268Lastly, analytics can be `disabled` entirely, either by running `zapier analytics --mode disabled` or setting the `DISABLE_ZAPIER_ANALYTICS` environment variable to `1`.
3269
3270We 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`.
3271
3272### What's the Difference Between an "App" and an "Integration"?
3273
3274We'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.
3275
3276## Command Line Tab Completion
3277
3278Introduced in v9.1.0, the `zapier autocomplete` command shows instructions for generating command line autocomplete.
3279
3280Follow those instructions to enable completion for `zapier` commands and flags!
3281
3282## The Zapier Platform Packages
3283
3284The Zapier Platform consists of 3 npm packages that are released simultaneously.
3285
3286- [`zapier-platform-cli`](https://github.com/zapier/zapier-platform/tree/master/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.
3287- [`zapier-platform-core`](https://github.com/zapier/zapier-platform/tree/master/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.
3288- [`zapier-platform-schema`](https://github.com/zapier/zapier-platform/tree/master/packages/schema) enforces app structure behind the scenes. It's a dependency of `core`, so it will be installed automatically.
3289
3290To learn more about the structure of the code (especially if you're interested in contributing), check out the `ARCHITECTURE.md` file(s).
3291
3292### Updating These Packages
3293
3294The 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`).
3295
3296Broadly 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/master/CHANGELOG.md) to see what new features and bug fixes are available.
3297
3298For more info about which Node versions are supported, see [the faq](#how-do-i-manually-set-the-nodejs-version-to-run-my-app-with).
3299
3300<!-- TODO: if we decouple releases, change this -->
3301The most recently released version of `cli` and `core` is **11.0.1**. You can see the versions you're working with by running `zapier -v`.
3302
3303To update `cli`, run `npm install -g zapier-platform-cli`.
3304
3305To 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`).
3306
3307For maximum compatibility, keep the versions of `cli` and `core` in sync.
3308
3309## Get Help!
3310
3311You can get help by either emailing `partners@zapier.com` or by [joining our developer community here](https://community.zapier.com/developer-discussion-13).
3312
3313---
3314
3315## Developing on the CLI
3316
3317For Zapier employees, see [this quip doc](https://zapier.quip.com/bns4AxqwaMIm/Working-on-the-CLI-Platform-CoreSchemaCLI) for info about creating releases.