Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
zapier-platform-cli
Advanced tools
Zapier is a platform for creating integrations and workflows. This CLI is your gateway to creating custom applications on the Zapier platform.
These docs are available here and the CLI docs are available here.
npm
ModulesA CLI App is an implementation of your app's API. You build a Node.js application that exports a single object (JSON Schema) and upload it to Zapier. Zapier introspects that definition to find out what your app is capable of and what options to present end users in the Zap Editor.
For those not familiar with Zapier terminology, here is how concepts in the CLI map to the end user experience:
Zapier takes the App you upload and sends it over to Amazon Web Service's Lambda. We then make calls to execute the operations your App defines as we execute Zaps. Your App takes the input data we provide (if any), makes the necessary HTTP calls, and returns the relevant data, which gets fed back into Zapier.
From a user perspective, both the CLI and the existing web builder platform offer the same experience. The biggest difference is how they're developed. The CLI takes a much more code-first approach, allowing you to develop your Zapier app just like you would any other programming project. The web builder, on the other hand, is much better for folks who want to make an app with minimal coding involved. Both will continue to coexist, so pick whichever fits your needs best!
All Zapier CLI apps are run using Node.js v4.3.2
.
You can develop using any version of Node you'd like, but your code has to run on Node v4.3.2
. You can accomplish this by developing on your preferred version and then transpiling with Babel (or similar).
To ensure stability for our users, we also require that you run your tests on v4.3.2
as well. If you don't have it available, we recommend using either nvm or n to install v4.3.2
and run the tests locally. On Windows you can use nvm-windows or nodist.
For NVM on Mac (via homebrew):
brew install nvm
nvm install v4.3.2
You can then either swap to that version with nvm use v4.3.2
, or do nvm exec v4.3.2 zapier test
so you can run tests without having to switch versions while developing.
Be sure to check the Requirements before you start! Also, we recommend the Tutorial for a more thorough introduction.
First up is installing the CLI and setting up your auth to create a working "Zapier Example" application. It will be private to you and visible in your live Zap editor.
# install the CLI globally
npm install -g zapier-platform-cli
# setup auth to Zapier's platform with a deploy key
zapier login
Your Zapier CLI should be installed and ready to go at this point. Next up, we'll create our first app!
# create a directory with the minimum required files
zapier init example-app
# move into the new directory
cd example-app
# install all the libraries needed for your app
npm install
Note: there are plenty of templates & example apps to choose from! View all Example Apps here.
You should now have a working local app. You can run several local commands to try it out.
# run the local tests
# the same as npm test, but adds some extra things to the environment
zapier test
Next, you'll probably want to upload app to Zapier itself so you can start testing live.
# push your app to Zapier
zapier push
Go check out our full CLI reference documentation to see all the other commands!
For a full tutorial, head over to our wiki for a comprehensive walkthrough for creating your first app. If this isn't your first rodeo, read on!
Tip: check the Quick Setup if this is your first time using the platform!
Creating an App can be done entirely locally and they are fairly simple Node.js apps using the standard Node environment and should be completely testable. However, a local app stays local until you zapier register
.
# make your folder
mkdir zapier-example
cd zapier-example
# create the needed files from a template
zapier init . --template=trigger
# install all the libraries needed for your app
npm install
If you'd like to manage your local App, use these commands:
zapier init . --template=resource
- initialize/start a local app project (see templates here)zapier convert 1234 .
- initialize/start from an existing app (alpha)zapier scaffold resource Contact
- auto-injects a new resource, trigger, etc.zapier test
- run the same tests as npm test
zapier validate
- ensure your app is validzapier describe
- print some helpful information about your appIn 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.
$ tree .
.
├── README.md
├── index.js
├── package.json
├── triggers
│ └── contact-by-tag.js
├── resources
│ └── Contact.js
├── test
│ ├── basic.js
│ ├── triggers.js
│ └── resources.js
├── build
│ └── build.zip
└── node_modules
├── ...
└── ...
The core definition of your App
will look something like this, and is what your index.js
should provide as the only export:
const App = {
// both version strings are required
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
// see "Authentication" section below
authentication: {
},
// see "Dehydration" section below
hydrators: {
},
// see "Making HTTP Requests" section below
requestTemplate: {
},
beforeRequest: [
],
afterResponse: [
],
// See "Resources" section below
resources: {
},
// See "Triggers/Searches/Creates" section below
triggers: {
},
searches: {
},
creates: {
}
};
module.export = App;
Tip: you can use higher order functions to create any part of your App definition!
Registering your App with Zapier is a necessary first step which only enables basic administrative functions. It should happen before zapier push
which is to used to actually expose an App Version in the Zapier interface and editor.
# register your app
zapier register "Zapier Example"
# list your apps
zapier apps
Note: this doesn't put your app in the editor - see the docs on pushing an App Version to do that!
If you'd like to manage your App, use these commands:
zapier apps
- list the apps in Zapier you can administerzapier register "Name"
- creates a new app in Zapierzapier link
- lists and links a selected app in Zapier to your current folderzapier history
- print the history of your appzapier collaborate [user@example.com]
- add admins to your app who can pushzapier invite [user@example.com]
- add users to try your app before promotionAn 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. By default, every App Version is private but you can zapier promote
it to production for use by over 1 million Zapier users.
# push your app version to Zapier
zapier push
# list your versions
zapier versions
If you'd like to manage your Version, use these commands:
zapier versions
- list the versions for the current directory's appzapier push
- push the current version the of current directory's app & version (read from package.json
)zapier promote [1.0.0]
- mark a version as the "production" versionzapier migrate [1.0.0] [1.0.1] [100%]
- move users between versions, regardless of deployment statuszapier deprecate [1.0.0] [YYYY-MM-DD]
- mark a version as deprecated, but let users continue to use it (we'll email them)zapier env 1.0.0 [KEY] [value]
- set an environment variable to some valueA simple zapier push
will only create the App Version in your editor. No one else using Zapier can see it or use it.
This is how you would share your app with friends, co-workers or clients. This is perfect for quality assurance, testing with active users or just sharing any app you like.
# sends an email this user to let them view the app in the UI privately
zapier invite user@example.com
# sends an email this user to let them admin the app (make changes just like you)
zapier collaborate user@example.com
You can also invite anyone on the internet to your app by observing the URL at the bottom of zapier invite
, it 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.
Promotion is how you would share your app with every one of the 1 million+ Zapier users. If this is your first time promoting - you may have to wait for the Zapier team to review and approve your app.
If this isn't the first time you've promoted your app - you might have users on older versions. You can zapier migrate
to either move users over (which can be dangerous if you have breaking changes). Or, you can zapier deprecate
to give users some time to move over themselves.
# promote your app version to all Zapier users
zapier promote 1.0.1
# OPTIONAL - migrate your users between one app version to another
zapier migrate 1.0.0 1.0.1
# OR - mark the old version as deprecated
zapier deprecate 1.0.0 2017-01-01
Warning! This is in a very alpha state - the immediate goal is to provide some basic structure to match an existing application. It will not even get close to a working copy of your existing app (yet).
If you have an existing application (nominally named "V2") on https://zapier.com/developer/builder/ you can use it as a template to kickstart your local application.
# the 1234 is from the URL in https://zapier.com/developer/builder/
zapier convert 1234 .
You app will be created and you can continue working on it.
Note - there is no way to convert a CLI app to a V2 app and we do not plan on implementing this.
Most 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.
Hint: You can access the data tied to your authentication via the
bundle.authData
property in any method called in your app.
Useful 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).
Example App: check out https://github.com/zapier/zapier-platform-example-app-basic-auth for a working example app for basic auth.
Note: if you do the common API Key pattern like
Authorization: Basic APIKEYHERE:x
you should look at the "Custom" authentication method instead.
const authentication = {
type: 'basic',
// "test" could also be a function
test: {
url: 'https://example.com/api/accounts/me.json'
}
// you can provide additional fields, but we'll provide `username`/`password` automatically
};
const App = {
// ...
authentication: authentication,
// ...
};
This 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.
Example App: check out https://github.com/zapier/zapier-platform-example-app-custom-auth for a working example app for custom auth.
const authentication = {
type: 'custom',
// "test" could also be a function
test: {
url: 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json'
},
fields: [
{key: 'subdomain', type: 'string', required: true, helpText: 'Found in your browsers address bar after logging in.'},
{key: 'api_key', type: 'string', required: true, helpText: 'Found on your settings page.'}
]
};
const addApiKeyToHeader = (request, z, bundle) => {
request.headers['X-Subdomain'] = bundle.authData.subdomain;
const basicHash = Buffer(`${bundle.authData.api_key}:x`).toString('base64');
request.headers.Authorization = `Basic ${basicHash}`;
return request;
};
const App = {
// ...
authentication: authentication,
beforeRequest: [
addApiKeyToHeader,
],
// ...
};
Very similar to the "Basic" authentication method above, but uses digest authentication instead of Basic authentication.
const authentication = {
type: 'digest',
// "test" could also be a function
test: {
url: 'https://example.com/api/accounts/me.json'
}
// you can provide additional fields, but Zapier will provide `username`/`password` automatically
};
const App = {
// ...
authentication: authentication,
// ...
};
Probably 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).
Example App: check out https://github.com/zapier/zapier-platform-example-app-session-auth for a working example app for session auth.
const getSessionKey = (z, bundle) => {
const promise = z.request({
method: 'POST',
url: 'https://example.com/api/accounts/login.json',
body: {
username: bundle.authData.username,
password: bundle.authData.password,
}
});
return promise.then((response) => {
if (response.status === 401) {
throw new Error('The username/password you supplied is invalid');
}
return {
sessionKey: JSON.parse(response.content).sessionKey
};
});
};
const authentication = {
type: 'custom',
// "test" could also be a function
test: {
url: 'https://example.com/api/accounts/me.json'
},
fields: [
{key: 'username', type: 'string', required: true, helpText: 'Your login username.'},
{key: 'password', type: 'string', required: true, helpText: 'Your login password.'}
],
sessionConfig: {
perform: getSessionKey
}
};
const includeSessionKeyHeader = (request, z, bundle) => {
if (bundle.authData.sessionKey) {
request.headers = request.headers || {};
request.headers['X-Session-Key'] = bundle.authData.sessionKey;
}
return request;
};
const sessionRefreshIf401 = (response, z, bundle) => {
if (bundle.authData.sessionKey) {
if (response.status === 401) {
throw new z.errors.RefreshAuthError(); // ask for a refresh & retry
}
}
return response;
};
const App = {
// ...
authentication: authentication,
beforeRequest: [
includeSessionKeyHeader
],
afterResponse: [
sessionRefreshIf401
],
// ...
};
Zapier's OAuth2 implementation is based on the authorization_code
flow, similar to GitHub and Facebook.
Example App: check out https://github.com/zapier/zapier-platform-example-app-oauth2 for a working example app for oauth2.
It looks like this:
redirect_uri
Zapier provided (zapier describe
to find out what it is)code
for an access_token
access_token
and makes calls on behalf of the userYou 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:
# setting the environment variables on Zapier.com
$ zapier env 1.0.0 CLIENT_ID 1234
$ zapier env 1.0.0 CLIENT_SECRET abcd
# and when running tests locally, don't forget to define them!
$ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
Your auth definition would look something like this:
const authentication = {
type: 'oauth2',
test: {
url: 'https://example.com/api/accounts/me.json'
},
// you can provide additional fields for inclusion in authData
oauth2Config: {
// "authorizeUrl" could also be a function returning a string url
authorizeUrl: {
method: 'GET',
url: 'https://example.com/api/oauth2/authorize',
params: {
client_id: '{{process.env.CLIENT_ID}}',
state: '{{bundle.inputData.state}}',
redirect_uri: '{{bundle.inputData.redirect_uri}}',
response_type: 'code'
}
},
// Zapier expects a response providing {access_token: 'abcd'}
// "getAccessToken" could also be a function returning an object
getAccessToken: {
method: 'POST',
url: 'https://example.com/api/v2/oauth2/token',
body: {
code: '{{bundle.inputData.code}}',
client_id: '{{process.env.CLIENT_ID}}',
client_secret: '{{process.env.CLIENT_SECRET}}',
redirect_uri: '{{bundle.inputData.redirect_uri}}',
grant_type: 'authorization_code'
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
}
};
const addBearerHeader = (request, z, bundle) => {
request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
return request;
};
const App = {
// ...
authentication: authentication,
beforeRequest: [
addBearerHeader,
]
// ...
};
module.exports = App;
A resource
is a representation (as a JavaScript object) of one of the REST resources of your API. Say you have a /recipes
endpoint for working with recipes; you can define a recipe resource in your app that will tell Zapier how to do create,
read, and search operations on that resource.
const Recipe = {
// `key` is the unique identifier the Zapier backend references
key: 'recipe',
// `noun` is the user-friendly name displayed in the Zapier UI
noun: 'Recipe',
// `list` and `create` are just a couple of the methods you can define
list: {
//...
},
create: {
//...
}
};
The quickest way to create a resource is with the zapier scaffold
command:
zapier scaffold resource "Recipe"
This will generate the resource file and add the necessary statements to the index.js
file to import it.
A resource has a few basic properties. The first is the key
, which allows Zapier to identify the resource on our backend.
The second is the noun
, the user-friendly name of the resource that is presented to users throughout the Zapier UI.
Example App: check out https://github.com/zapier/zapier-platform-example-app-resource for a working example app using resources.
After those, there is a set of optional properties that tell Zapier what methods can be performed on the resource. The complete list of available methods can be found in the Resource Schema Docs. For now, let's focus on two:
list
- Tells Zapier how to fetch a set of this resource. This becomes a Trigger in the Zapier Editor.create
- Tells Zapier how to create a new instance of the resource. This becomes an Action in the Zapier Editor.Here is a complete example of what the list method might look like
const listRecipesRequest = {
url: 'http://example.com/recipes'
};
const Recipe = {
//...
list: {
display: {
label: 'New Recipe',
description: 'Triggers when a new recipe is added.'
},
operation: {
perform: listRecipesRequest
}
}
};
The method is made up of two properties, a display
and an operation
. The display
property (schema) holds the info needed to present the method as an available Trigger in the Zapier Editor. The operation
(schema) provides the implementation to make the API call.
Adding a create method looks very similar.
const createRecipeRequest = {
url: 'http://example.com/recipes',
method: 'POST',
body: {
name: 'Baked Falafel',
style: 'mediterranean'
}
};
const Recipe = {
//...
list: {
//...
},
create: {
display: {
label: 'Add Recipe',
description: 'Adds a new recipe to our cookbook.'
},
operation: {
perform: createRecipeRequest
}
}
};
Every method you define on a resource
Zapier converts to the appropriate Trigger, Create, or Search. Our examples
above would result in an app with a New Recipe Trigger and an Add Recipe Create.
Triggers, Searches, and Creates are the way an app defines what it is able to do. Triggers read data into Zapier (i.e. watch for new recipes). Searches locate individual records (find recipe by title). Creates create new records in your system (add a recipe to the catalog).
The definition for each of these follows the same structure. Here is an example of a trigger:
const recipeListRequest = {
url: 'http://example.com/recipes',
};
const App = {
//...
triggers: {
new_recipe: {
key: 'new_recipe', // uniquely identifies the trigger
noun: 'Recipe', // user-friendly word that is used to refer to the resource
// `display` controls the presentation in the Zapier Editor
display: {
label: 'New Recipe',
helpText: 'Triggers when a new recipe is added.'
},
// `operation` implements the API call used to fetch the data
operation: {
perform: recipeListRequest
}
},
another_trigger: {
// Another trigger definition...
}
}
};
You can find more details on the definition for each by looking at the Trigger Schema, Search Schema, and Create Schema.
Example App: check out https://github.com/zapier/zapier-platform-example-app-trigger for a working example app using triggers.
Example App: check out https://github.com/zapier/zapier-platform-example-app-rest-hooks for a working example app using REST hook triggers.
Example App: check out https://github.com/zapier/zapier-platform-example-app-search for a working example app using searches.
Example App: check out https://github.com/zapier/zapier-platform-example-app-create for a working example app using creates.
On each trigger, search, or create in the operation
directive - you can provide an array of objects as fields under the inputFields
. 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.
You can find more details on each and every field option at Field Schema.
Those fields have various options you can provide, here is a succinct example:
const App = {
//...
creates: {
create_recipe: {
//...
operation: {
// an array of objects is the simplest way
inputFields: [
{key: 'title', required: true, label: 'Title of Recipe', helpText: 'Name your recipe!'},
{key: 'style', required: true, choices: {mexican: 'Mexican', italian: 'Italian'}}
],
perform: () => {}
}
}
}
};
In 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.
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 (EG: only show issues from the previously selected project).
const recipeFields = (z, bundle) => {
const response = z.request('http://example.com/api/v2/fields.json');
// json is is [{"key":"field_1"},{"key":"field_2"}]
return response.then(res => res.json);
};
const App = {
//...
creates: {
create_recipe: {
//...
operation: {
// an array of objects is the simplest way
inputFields: [
{key: 'title', required: true, label: 'Title of Recipe', helpText: 'Name your recipe!'},
{key: 'style', required: true, choices: {mexican: 'Mexican', italian: 'Italian'}},
recipeFields // provide a function inline - we'll merge the results!
],
perform: () => {}
}
}
}
};
Additionally, 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 whenver 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."
module.exports = {
key: 'dessert',
noun: 'Dessert',
display: {
label: 'Order Dessert',
description: 'Orders a dessert.'
},
operation: {
inputFields: [
{key: 'type', required: true, choices: {1: 'cake', 2: 'ice cream', 3: 'cookie'}, altersDynamicFields: true},
function(z, bundle) {
if (bundle.inputData.type === '2') {
return [{key: 'with_sprinkles', type: 'boolean'}];
}
return [];
}
],
perform: function (z, bundle) {/* ... */}
}
};
Sometimes, API endpoints require clients to specify a parent object in order to create or access the child resources. Imagine having to specify a company id in order to get a list of employees for that company. 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.
Our solution is to present users a dropdown that is populated by making a live API call to fetch a list of parent objects. We call these special dropdowns "dynamic dropdowns."
To define one, you can provide the dynamic
property on your field to specify the trigger that should be used to populate the options for the dropdown. The value for the property is a dot-separated concatenation of a trigger's key, the field to use for the value, and the field to use for the label.
const App = {
//...
resources: {
project: {
key: 'project',
//...
list: {
//...
operation: {
perform: () => { return [{id: 123, name: 'Project 1'}]; } // called for project_id dropdown
}
}
},
issue: {
key: 'issue',
//...
create: {
//...
operation: {
inputFields: [
{key: 'project_id', required: true, label: 'Project', dynamic: 'projectList.id.name'}, // calls project.list
{key: 'title', required: true, label: 'Title', helpText: 'What is the name of the issue?'},
],
}
}
}
}
};
In the UI, users will see something like this:
Dynamic dropdowns are one of the few fields that automatically invalidate Zapier's field cache, so it is not necessary to set
altersDynamicFields
to true for these fields.
For fields that take id of another object to create a relationship between the two (EG: a project id for a ticket), you can specify the search
property on the field to indicate that Zapier needs to prompt the user to setup a Search step to populate the value for this field. Similar to dynamic dropdowns, the value for this property is a dot-separated concatenation of a search's key and the field to use for the value.
const App = {
//...
resources: {
project: {
key: 'project',
//...
search: {
//...
operation: {
perform: () => { return [{id: 123, name: 'Project 1'}]; } // called for project_id
}
}
},
issue: {
key: 'issue',
//...
create: {
//...
operation: {
inputFields: [
{key: 'project_id', required: true, label: 'Project', search: 'projectSearch.id'}, // calls project.search
{key: 'title', required: true, label: 'Title', helpText: 'What is the name of the issue?'},
],
}
}
}
}
};
This can be combined with the dynamic
property to give the user a guided experience when setting up a Zap.
We provide several methods off of the z
object, which is provided as the first argument to all function calls in your app.
The
z
object is passed into your functions as the first argument - IE:perform: (z) => {}
.
z.request([url], options)
z.request([url], options)
is a promise based HTTP client with some Zapier-specific goodies. See Making HTTP Requests.
z.console(message)
z.console(message)
is a logging console, similar to Node.js console
but logs remotely, as well as to stdout in tests. See Log Statements
z.dehydrate(func, inputData)
z.dehydrate(func, inputData)
is used to lazily evaluate a function, perfect to avoid API calls during polling or for reuse. See Dehydration.
z.stashFile(bufferStringStream, [knownLength], [filename])
z.stashFile(bufferStringStream, [knownLength], [filename])
is a promise based file stasher that returns a URL file pointer. See Stashing Files.
z.JSON
z.JSON
is similar to the JSON built-in like z.JSON.parse('...')
, but catches errors and produces nicer tracebacks.
z.hash()
z.hash()
is a crypto tool for doing things like z.hash('sha256', 'my password')
z.errors
z.errors
is a collection error classes that you can throw in your code, like throw new z.errors.HaltedError('...')
.
The available errors are:
For more details on error handling in general, see here.
This object holds the user's auth details and the data to for the API requests.
The
bundle
object is passed into your functions as the second argument - IE:perform: (z, bundle) => {}
.
bundle.authData
bundle.authData
is user-provided authentication data, like api_key
or access_token
. Read more on authentication.
bundle.inputData
bundle.inputData
is user-provided data for this particular run of the trigger/search/create, as defined by the inputFields. For example:
{
createdBy: 'Bobby Flay'
style: 'mediterranean'
}
bundle.inputDataRaw
bundle.inputDataRaw
is kind of like inputData
, but before rendering {{curlies}}
:
{
createdBy: '{{chef_name}}'
style: '{{style}}'
}
bundle.meta
bundle.meta
is extra information useful for doing advanced behaviors depending on what the user is doing. It looks something like this:
{
frontend: false,
prefill: false,
hydrate: true,
test_poll: false,
standard_poll: true,
first_poll: false,
limit: -1,
page: 0,
}
For example - if you want to do pagination - you could do:
const getList = (z, bundle) => {
const promise = z.request({
url: 'http://example.com/api/list.json',
params: {
limit: 100,
offset: 100 * bundle.meta.page
}
});
return promise.then((response) => response.json);
};
Apps can define environment variables that are available when the app's code executes. They work just like environment variables defined on the command line. They are useful when you have data like an OAuth client ID and secret that you don't want to commit to source control. Environment variables can also be used as a quick way to toggle between a a staging and production environment during app development.
It is important to note that variables are defined on a per-version basis! When you push a new version, the existing variables from the previous version are copied, so you don't have to manually add them. However, edits made to one version's environment will not affect the other versions.
To define an environment variable, use the env
command:
# Will set the environment variable on Zapier.com
zapier env 1.0.0 MY_SECRET_VALUE 1234
You will likely also want to set the value locally for testing.
export MY_SECRET_VALUE=1234
Alternatively, we provide some extra tooling to work with an .environment
that looks like this:
MY_SECRET_VALUE=1234
And then in your test/basic.js
file:
const zapier = require('zapier-platform-core');
should('some tests', () => {
zapier.tools.env.inject(); // testing only!
console.log(process.env.MY_SECRET_VALUE);
// should print '1234'
});
This is a popular way to provide
process.env.ACCESS_TOKEN || bundle.authData.access_token
for convenient testing.
To view existing environment variables, use the env
command.
# Will print a table listing the variables for this version
zapier env 1.0.0
Within your app, you can access the environment via the standard process.env
- any values set via local export
or zapier env
will be there.
For example, you can access the process.env
in your perform functions:
const listExample = (z, bundle) => {
const httpOptions = {
headers: {
'my-header': process.env.MY_SECRET_VALUE
}
};
const response = z.request('http://example.com/api/v2/recipes.json', httpOptions);
return response.then(res => res.json);
};
const App = {
// ...
triggers: {
example: {
// ...
operation: {
// ...
perform: listExample
}
}
}
};
Note! Be sure to lazily access your environment variables - we generally set the environment variables after your code is already loaded.
There are two primary ways to make HTTP requests in the Zapier platform:
z.request([url], options)
to make the requests and control the response.There are also a few helper constructs you can use to reduce boilerplate:
requestTemplate
which is an shorthand HTTP request that will be merged with every request.beforeRequest
middleware which is an array of functions to mutate a request before it is sent.afterResponse
middleware which is an array of functions to mutate a response before it is completed.Note: you can install any HTTP client you like - but this is greatly discouraged as you lose automatic HTTP logging and middleware.
For 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.
This features:
{{curly}}
replacement.const triggerShorthandRequest = {
method: 'GET',
url: 'http://{{bundle.authData.subdomain}}.example.com/v2/api/recipes.json',
params: {
sort_by: 'id',
sort_order: 'DESC'
}
};
const App = {
// ...
triggers: {
example: {
// ...
operation: {
// ...
perform: triggerShorthandRequest
}
}
}
};
In the url above, {{bundle.authData.subdomain}}
is automatically replaced with the live value from the bundle. If the call returns a non 2xx return code, an error is automatically raised. The response body is automatically parsed as JSON and returned.
An error will be raised if the response is not valid JSON, so do not use shorthand HTTP requests with non-JSON responses.
When 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.
To make a manual HTTP request, use the request
method of the z
object:
const listExample = (z, bundle) => {
const customHttpOptions = {
headers: {
'my-header': 'from zapier'
}
};
return z.request('http://example.com/api/v2/recipes.json', customHttpOptions)
.then(response => {
if (response.status >= 300) {
throw new Error(`Unexpected status code ${response.status}`);
}
const recipes = JSON.parse(response.content);
// do any custom processing of recipes here...
return recipes;
});
};
const App = {
// ...
triggers: {
example: {
// ...
operation: {
// ...
perform: listExample
}
}
}
};
To POST or PUT data to your API you can do this:
const App = {
// ...
triggers: {
example: {
// ...
operation: {
// ...
perform: (z, bundle) => {
const recipe = {
name: 'Baked Falafel',
style: 'mediterranean',
directions: 'Get some dough....'
};
const options = {
method: 'POST',
body: JSON.stringify(recipe)
};
return z.request('http://example.com/api/v2/recipes.json', options)
.then(response => {
if (response.status !== 201) {
throw new Error(`Unexpected status code ${response.status}`);
}
});
}
}
}
}
};
Note: you need to call
z.JSON.stringify()
before setting thebody
.
If you need to process all HTTP requests in a certain way, you may be able to use one of utility HTTP middleware functions.
Example App: check out https://github.com/zapier/zapier-platform-example-app-middleware for a working example app using HTTP middleware.
Try putting them in your app definition:
const addHeader = (request, /*z*/) => {
request.headers['my-header'] = 'from zapier';
return request;
};
const mustBe200 = (response, /*z*/) => {
if (response.status !== 200) {
throw new Error(`Unexpected status code ${response.status}`);
}
return response;
};
const autoParseJson = (response, z) => {
response.json = z.JSON.parse(response.content);
return response;
};
const App = {
// ...
beforeRequest: [
addHeader,
],
afterRequest: [
mustBe200,
autoParseJson,
]
// ...
};
A beforeRequest
middleware function takes a request options object, and returns a (possibly mutated) request object. An afterResponse
middleware function takes a response object, and returns a (possibly mutated) response object. Middleware functions are executed in the order specified in the app definition, and each subsequent middleware receives the request or response object returned by the previous middleware.
Middleware functions can be asynchronous - just return a promise from the middleware function.
Shorthand requests and manual z.request([url], options)
calls support the following HTTP options
:
url
: HTTP url, you can provide it both z.request(url, options)
or z.request({url: url, ...})
.method
: HTTP method, default is GET
.headers
: request headers object, format {'header-key': 'header-value'}
.params
: URL query params object, format {'query-key': 'query-value'}
.body
: request body, can be a string, buffer, or readable stream. Default is null
.json
: shortcut object/array/etc. you want to JSON encode into body. Default is null
.form
: shortcut object. you want to form encode into body. Default is null
.raw
: set this to stream the response instead of consuming it immediately. Default is false
.redirect
: set to manual
to extract redirect headers, error
to reject redirect, default is follow
.follow
: maximum redirect count, set to 0
to not follow redirects. default is 20
.compress
: support gzip/deflate content encoding. Set to false
to disable. Default is true
.agent
: Node.js http.Agent
instance, allows custom proxy, certificate etc. Default is null
.timeout
: request / response timeout in ms. Set to 0
to disable (OS limit still applies), timeout reset on redirect
. Default is 0
(disabled).size
: maximum response body size in bytes. Set to 0`` to disable. Default is
0` (disabled).z.request({
url: 'http://example.com',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// only provide body, json or form...
body: '{"hello":"world"}', // or 'hello=world'
json: {hello: 'world'},
form: {hello: 'world'},
// access node-fetch style response.body
raw: false,
redirect: 'follow',
follow: 20,
compress: true,
agent: null,
timeout: 0,
size: 0,
})
The response object returned by z.request([url], options)
supports the following fields and methods:
status
: The response status code, i.e. 200
, 404
, etc.content
: The response content as a String. For Buffer, try options.raw = true
.json
: The response content as an object (or undefined
). If options.raw = true
- is a promise.body
: A stream available only if you provide options.raw = true
.headers
: Response headers object. The header keys are all lower case.getHeader(key)
: Retrieve response header, case insensitive: response.getHeader('My-Header')
throwForStatus()
: Throw error if final response.status > 300
. Will throw z.error.RefreshAuthError
if 401.request
: The original request options object (see above).z.request({
// ..
}).then((response) => {
// a bunch of examples lines for cherry picking
response.status;
response.headers['Content-Type'];
response.getHeader('content-type');
response.request; // original request options
response.throwForStatus();
// if options.raw === false (default)...
JSON.parse(response.content);
response.json;
// if options.raw === true...
response.buffer().then(buf => buf.toString());
response.text().then(content => content);
response.json().then(json => json);
response.body.pipe(otherStream);
});
Dehydration, and it's counterpart Hydration, is a tool that can lazily load data that might be otherwise expensive to retrieve aggressively.
z.dehydrate(func, inputData)
This is very common when Stashing Files - but that isn't their only use!
The interface z.dehydrate(func, inputData)
has two required arguments:
func
- this should any raw function
that be found anywhere in your app definition (though usually in the root hydrators
mapping)inputData
- this is an object that contains things like a path
or id
- whatever you need to load data on the other sideWhy 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 "automatically" (and sometimes incorrectly) infer code locations.
This example that pulls in extra data for a movie:
const getExtraDataFunction = (z, bundle) => {
const url = `http://example.com/movies/${bundle.inputData.id}.json`;
return z.request(url)
.then(res => z.JSON.parse(res.content));
};
const movieList = (z, bundle) => {
return z.request('http://example.com/movies.json')
.then(res => z.JSON.parse(res.content))
.then(results => {
return results.map(result => {
// so maybe /movies.json is thin content but
// /movies/:id.json has more details we want...
result.moreData = z.dehydrate(getExtraDataFunction, {
id: result.id
});
return result;
});
});
};
const App = {
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
// don't forget to register hydrators here!
// it can be imported from any module
hydrators: {
getExtraData: getExtraDataFunction
},
triggers: {
new_movie: {
noun: 'Movie',
display: {
label: 'New Movie',
helpText: 'Triggers when a new Movie is added.'
},
operation: {
perform: movieList
}
}
}
};
module.exports = App;
And in future steps of the Zap - if Zapier encounters a pointer as returned by z.dehydrate(func, inputData)
- Zapier will tie it back to your app and pull in the data lazily.
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.
It can be expensive to download and stream files or they can require complex handshakes to authorize downloads - so we provide a helpful stash routine that will take any String
, Buffer
or Stream
and return a URL file pointer suitable for returning from triggers, searches, creates, etc.
The interface z.stashFile(bufferStringStream, [knownLength], [filename])
takes a single required argument - the extra two arguments will be automatically populated in most cases. For example - a full example is this:
const content = 'Hello world!';
z.stashFile(content, content.length, 'hello.txt')
.then(url => z.console.log(url));
// https://zapier-dev-files.s3.amazonaws.com/cli-platform/f75e2819-05e2-41d0-b70e-9f8272f9eebf
Most likely you'd want to stream from another URL - note the usage of z.request({raw: true})
:
const fileRequest = z.request({url: 'http://example.com/file.pdf', raw: true});
z.stashFile(fileRequest) // knownLength and filename will be sniffed from the request
.then(url => z.console.log(url));
// https://zapier-dev-files.s3.amazonaws.com/cli-platform/74bc623c-d94d-4cac-81f1-f71d7d517bc7
Note: you should only be using
z.stashFile()
in a hydration method - otherwise it can be very expensive to stash dozens of files in a polling call - for example!
See a full example with dehydration/hydration wired in correctly:
const stashPDFfunction = (z, bundle) => {
// use standard auth to request the file
const filePromise = z.request({
url: bundle.inputData.downloadUrl,
raw: true
});
// and swap it for a stashed URL
return z.stashFile(filePromise);
};
const pdfList = (z, bundle) => {
return z.request('http://example.com/pdfs.json')
.then(res => z.JSON.parse(res.content))
.then(results => {
return results.map(result => {
// lazily convert a secret_download_url to a stashed url
// zapier won't do this until we need it
result.file = z.dehydrate(stashPDFfunction, {
downloadUrl: result.secret_download_url
});
delete result.secret_download_url;
return result;
});
});
};
const App = {
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
hydrators: {
stashPDF: stashPDFfunction
},
triggers: {
new_pdf: {
noun: 'PDF',
display: {
label: 'New PDF',
helpText: 'Triggers when a new PDF is added.'
},
operation: {
perform: pdfList
}
}
}
};
module.exports = App;
There 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
method (see below for details). 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).
To view the logs for your application, use the zapier logs
command. There are two types of logs, http
(logged automatically by Zapier on HTTP requests) and console
(manual logs via z.console.log()
statements).
For advanced logging options including only displaying the logs for a certain user or app version, look at the help for the logs command:
zapier help logs
To manually print a log statement in your code, use z.console
:
z.console.log('Here are the input fields', bundle.inputData);
The z.console
object has all the same methods and works just like the Node.js Console
class - the only difference is we'll log to our distributed datastore and you can view them via zapier logs
(more below).
To see your z.console
logs, do:
zapier logs --type=console
If you are using the z.request()
shortcut that we provide - HTTP logging is handled automatically for you. For example:
z.request('http://57b20fb546b57d1100a3c405.mockapi.io/api/recipes')
.then((res) => {
// do whatever you like, this request is already getting logged! :-D
return res;
})
To see the HTTP logs, do:
zapier logs --type=http
To see detailed http logs including headers, request and response bodies, etc, do:
zapier logs --type=http --detailed
APIs are not always available. Users do not always input data correctly to formulate valid requests. Thus, it is a good idea to write apps defensively and plan for 4xx and 5xx responses from APIs. Without proper handling, errors often have incomprehensible messages for end users, or possibly go uncaught.
Zapier provides a couple tools to help with error handling. First is the afterResponse
middleware (docs), which provides a hook for processing
all responses from HTTP calls. The other tool is the collection of errors in
z.errors
(docs), which control the behavior of Zaps when
various kinds of errors occurr.
Errors due to a misconfiguration in a user's Zap should be handled in your app
by throwing a standard JavaScript Error
with a user-friendly message.
Typically, this will be prettifying 4xx responses or API's that return errors as
200s with a payload that describes the error.
Example: throw new Error('Your error message.');
A couple best practices to keep in mind:
Note that if a Zap raises too many error messages it will be automatically turned off, so only use these if the scenario is truly an error that needs to be fixed.
Any operation can be interrupted or "halted" (not success, not error, but
stopped for some specific reason) with a HaltedError
. You might find yourself
using this error in cases where a required pre-condition is not met. For instance,
in a create to add an email address to a list where duplicates are not allowed,
you would want to throw a HaltedError
if the Zap attempted to add a duplicate.
This would indicate failure, but it would be treated as a soft failure.
Unlike throwing Error
, a Zap will never by turned off when this error is thrown
(even if it is raised more often than not).
Example: throw new HaltedError('Your reason.');
For apps that require manual refresh of authorization on a regular basis, Zapier
provides a mechanism to notify users of expired credentials. With the
ExpiredAuthError
, the current operation is interrupted, the Zap is turned off
(to prevent more calls with expired credentials), and a predefined email is sent
out informing the user to refresh the credentials.
Example: throw new ExpiredAuthError('Your message.');
For apps that use OAuth2 + refresh or Session Auth, you can use the
RefreshAuthError
. This will signal Zapier to refresh the credentials and then
repeat the failed operation.
Example: throw new RefreshAuthError();
You can write unit tests for your Zapier app that run locally, outside of the zapier editor. You can run these tests in a CI tool like Travis.
We recommend using the Mocha testing framework. After running
zapier init
you should find an example test to start from in the test
directory.
// we use should assertions
const should = require('should');
const zapier = require('zapier-platform-core');
// createAppTester() makes it easier to test your app. It takes your
// raw app definition, and returns a function that will test you app.
const App = require('../index');
const appTester = zapier.createAppTester(App);
describe('triggers', () => {
describe('new recipe trigger', () => {
it('should load recipes', (done) => {
// This is what Zapier will send to your app as input.
// It contains trigger options the user choice in their zap.
const bundle = {
inputData: {
style: 'mediterranean'
}
};
// Pass appTester the path to the trigger you want to call,
// and the input bundle. appTester returns a promise for results.
appTester(App.App.triggers.recipe.operation.perform, bundle)
.then(results => {
// Make assertions
results.length.should.eql(10);
const firstRecipe = results[0];
firstRecipe.name.should.eql('Baked Falafel');
done();
})
.catch(done);
});
});
});
To run all your tests do:
zapier test
You can also go direct with
npm test
ornode_modules/mocha/bin/mocha
.
These work much like normal environment variables - for example:
CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
Or, export
them explicitly and place them into the environment:
export CLIENT_ID=1234
export CLIENT_SECRET=abcd
zapier test
When running a unit test via zapier test
, z.console
statements and detailed HTTP logs print to stdout
:
zapier test
Sometimes you don't want that much logging, for example in a CI test. To suppress the detailed HTTP logs do:
zapier test --quiet
To also suppress the HTTP summary logs do:
zapier test --very-quiet
Behind the scenes zapier test
is doing a pretty standard npm test
with mocha as the backend.
This makes it pretty straightforward to integrate into your testing interface. If you'd like to test with Travis CI for example - the .travis.yml
would look something like this:
language: node_js
node_js:
- "4.3.2"
before_script: npm install -g zapier-platform-cli
script: CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
But you can substitute zapier test
with npm test
, or a direct call to node_modules/mocha/bin/mocha
. Also, we generally recommend putting the environment variables into whatever configuration screen Jenkins or Travis provides!
npm
ModulesUse npm
modules just like you would use them in any other node app, for example:
npm install --save jwt
And then package.json
will be updated, and you can use them like anything else:
const jwt = require('jwt');
During the zapier build
or zapier push
step - we'll copy all your code to /tmp
folder and do a fresh re-install of modules.
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 tozapier push
.
Warning: do not use compiled libraries unless you run your build on the AWS AMI
ami-6869aa05
.
We do not yet support transpilers out of the box, but if you would like to use babel
or similar, we recommend creating a custom wrapper on zapier push
like this in your package.json
:
{
"scripts": {
"zapier-dev": "babel src --out-dir lib --watch",
"zapier-push": "babel src --out-dir lib && zapier push"
}
}
And then you can have your fancy ES7 code in src/*
and a root index.js
like this:
module.exports = require('./lib');
And work with commands like this:
# watch and recompile
npm run zapier-dev
# tests should work fine
zapier test
# every build ensures a fresh build
npm run zapier-push
There are a lot of details left out - check out the full example app for a working setup.
Example App: check out https://github.com/zapier/zapier-platform-example-app-babel for a working example app using Babel.
See the wiki for a full list of working examples (and installation instructions).
We have provided two tab completion scripts to make it easier to use the Zapier Platform CLI, for zsh and bash.
To use the zsh completion script, first setup support for completion, if you haven't already done so. This example assumes your completion scripts are in ~/.zsh/completion
. Adjust accordingly if you put them somewhere else:
# add custom completion scripts
fpath=(~/.zsh/completion $fpath)
# compsys initialization
autoload -U compinit
compinit
Next download our completion script to your completions directory:
cd ~/.zsh/completion
curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/zsh/_zapier -O
Finally, restart your shell and start hitting TAB with the zapier
command!
To use the bash completion script, first download the completion script. The example assumes your completion scripts are in ~/.bash_completion.d
directory. Adjust accordingly if you put them somewhere else.
cd ~/.bash_completion.d
curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/bash/_zapier -O
Next source the script from your ~/.bash_profile
:
source ~/.bash_completion.d/_zapier
Finally, restart your shell and start hitting TAB with the zapier
command!
export ZAPIER_BASE_ENDPOINT='http://localhost:8000'
if you're building against a local dev environmentnpm install
for getting startednpm run build
for updating ./lib
from ./src
npm test
for running tests (also runs npm run build
)npm run docs
for updating docsnpm run gen-completions
for updating the auto complete scriptsnpm version [patch|minor|major]
will pull, test, update docs, increment version in package.json, push tags, and publish to npmnpm run validate-templates
for validating the example appsnpm run set-template-versions VERSION
for updating the platform-core version in the example app repos to VERSION
You can get help by either emailing partners@zapier.com or by joining our Slack channel https://zapier-platform-slack.herokuapp.com.
FAQs
The CLI for managing integrations in Zapier Developer Platform.
The npm package zapier-platform-cli receives a total of 5,374 weekly downloads. As such, zapier-platform-cli popularity was classified as popular.
We found that zapier-platform-cli demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.