Overview
A configuration is one document that describes the automation resources you want to install into an account: actions, triggers, workflows, apps, and data schemas. Export produces the document from an existing account, and Import installs it into another one. It's a quick way to move workflows between environments or hand a ready-made setup to a teammate.
This guide covers each section so you can write a config by hand. The examples are all YAML. Import also accepts JSON with the same structure, so use whichever you prefer.
To import a file you already have, see Import Configuration. For the API endpoints, see the Configurations API.
Most failed imports come down to a handful of recurring errors: Select options written as bare strings, form field names that don't match their placeholders, conditions that read undeclared variables, or an invalid action type. If a config imports but doesn't behave, check Common mistakes first.
Document structure
Every top-level key is optional. Import processes whatever you include, so start with just the sections you need.
| Key | Type | Purpose |
|---|---|---|
action_types (alias actions) | array | HTTP/code actions that workflows can run |
triggers | array | Events that workflows bind to |
workflows | array | Workflow templates (nodes + edges) |
apps | array of strings | App identifiers to install |
user_data_schema | array | Per-user variables |
account_data_schema | array | Per-account variables |
account_data | hash | Account data values |
conflict_strategy | string | raise (default) or skip when a resource already exists |
A minimal skeleton:
1 2 3 4 5 6conflict_strategy: skip action_types: [] triggers: [] workflows: [] apps: []
Action types
Action types are the actions a workflow can run. The ones you author either call a URL or run custom code, and they go under action_types (actions works as an alias).
| Field | Required | Notes |
|---|---|---|
name | yes | Identity. A duplicate name triggers your conflict_strategy. |
type | yes | CustomApiRequest. See below. |
description | no | |
groups | no | Array of access-control tags. |
icon | no | Hash, e.g. { type: bolt, background_color: blue }. See Icons. |
url | no | The endpoint the action calls. |
http_method | no | get, post, and so on. |
headers | no | Hash of Header-Name: value. |
params | no | Hash of key: "{{ ... }}" (request body/query). See Placeholders. |
form | no | Array of Form fields, the inputs a user fills in. |
response_data_schema | no | Array of Data variables the response exposes. |
primary_category / secondary_category | no | Category keys for grouping this action in the catalog. |
Choosing a type
Set type to CustomApiRequest, an action that calls a URL. The flow-control steps (conditions, delays, loops, and so on) are built into Embed Workflow, so you don't define those yourself.
Categories
primary_category and secondary_category are optional and only affect how actions are grouped in the catalog. Each takes a category key.
You create the categories yourself in your account settings, where each one has a key, a name, an optional description, and an icon. On an action, set primary_category (and optionally secondary_category) to one of those keys to group it. A key that isn't in your list still imports, it just shows as the raw key instead of a named category.
Grouping is an account-level feature you can turn off, in which case categories aren't shown.
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26action_types: - name: Change Invoice Status type: CustomApiRequest primary_category: payment_processing url: https://example.com/actions/invoice-set-status http_method: post groups: [vip] headers: Content-Type: application/json params: invoice_uuid: "{{ trigger.invoice_uuid }}" status: "{{ form__status }}" form: - name: form__status type: Select label: Set invoice status to required: true data: options: - { label: Saved, value: saved } - { label: Sent, value: sent } - { label: Paid, value: paid } response_data_schema: - variable: invoice_status type: String data_path: status
Form fields
Form fields are the inputs a user fills in when configuring an action or workflow. They appear under action_types[].form and workflows[].form.
| Field | Required | Notes |
|---|---|---|
id | no | Auto-generated if omitted. |
name | yes | Reference key. The template that reads it must match exactly. |
type | no | Default TextField. See below. |
label | no | |
description | no | |
required | no | Default false. |
advanced | no | Default false. |
data | no | Type-specific configuration. |
Naming and referencing fields
Name a field with the flat underscore style, form__key, and read its value in params and templates with a matching placeholder, {{ form__key }}. The name and the placeholder have to be identical: form__status is read by {{ form__status }}. If they differ, the value never fills in.
Avoid the bracket style {{ form[status] }}. It looks reasonable but renders empty, because the text inside the brackets is treated as another variable rather than a literal key. Stick to form__status and {{ form__status }}.
For everything you can do inside {{ ... }}, including filters and default values, see Placeholders.
Field types
Set type to one of: TextField, TextArea, Number, Email, Phone, Secret, JSON, Boolean, Select, AsyncSelect, FromList, Connection, or Custom. Connection only does something on a workflow form, not on an action's own fields.
The field type isn't validated on import. A value that isn't in the list above still saves, but the builder shows a "This field type is not supported here" placeholder instead of a working input, so a typo like Dropdown instead of Select leaves the field unusable.
Configuring data
Select takes static options, given as an array of { label, value } objects rather than bare strings:
1 2 3 4data: options: - { label: Paid, value: paid } - { label: Sent, value: sent }
AsyncSelect fetches its options from a remote endpoint:
1 2 3 4 5 6data: target_url: https://api.example.com/users # required: the GET endpoint object_key: data # path to the array in the response; omit if the response is the array itself label_key: name # defaults to "name" value_key: id # defaults to "id" fetch_mode: prefetch # "ontype" (default) or "prefetch"
Only target_url is required. The one people forget is object_key, needed when the results are nested under a key in the response. allow_multiple, headers, params, filters, and pagination are also supported.
Other types usually take data: {}.
Triggers
Triggers are the events a workflow binds to. They go under triggers.
| Field | Required | Notes |
|---|---|---|
title | yes | Display name. |
event | yes | Identity key. Workflows bind to it via trigger.event. |
description | no | |
icon | no | Hash. See Icons. |
data_input_schema | no | Array of Data variables the event payload delivers. |
conflict_strategy | no | Per-resource override. |
1 2 3 4 5 6 7 8 9triggers: - title: Payment Requested event: payment_requested description: Fires when a payment is requested on an invoice. data_input_schema: - variable: invoice_total type: Number data_path: invoice_total required: true
A condition or template can only read a variable the trigger actually delivers. Declare every variable your workflows and conditions use in data_input_schema. If a condition reads a variable that isn't declared here (and isn't in the payload), it can't resolve, and a condition that can't resolve its field quietly blocks the workflow. See Common mistakes.
Data variables
Data variables describe the payload and response data that flows through a workflow. They're used by data_input_schema, response_data_schema, user_data_schema, and account_data_schema.
| Field | Required | Notes |
|---|---|---|
variable | yes | The key, e.g. invoice_total. |
type | yes | See the values below. Convention only, not validated. |
required | no | Default false. |
data_path | no | Where to read from the payload, e.g. user.email. |
display_label | no | Human-facing label. |
format | no | Free-form. |
secret | no | Default false. |
children | no | Array of Data variables, for Object. |
iterator / item_type / list_values_variable | no | For List. |
The type values are String, Date, Boolean, Integer, Float, Number, Email, Phone, Object (uses children:), and List (uses iterator: + item_type:). It isn't validated, so stick to these.
Data variable types are a separate list from Form field types. Data variables describe data; form fields describe UI inputs. Don't mix the two. TextField, for example, is a form field type, not a data variable type.
Nested and list shapes:
1 2 3 4 5 6 7 8 9data_input_schema: - variable: author type: Object children: - { variable: name, type: String, data_path: author.name } - variable: tags type: List iterator: tag item_type: String
Workflows
A workflow connects a trigger to a set of nodes joined by edges.
| Field | Notes |
|---|---|
name | Workflow name (also becomes its key). |
description | |
auto_clone_for_new_users | Boolean. Automatically copies the workflow for each new end-user. |
is_template | Boolean. Marks the workflow as a reusable template. |
trigger | { event, match_conditions, conditions }, the trigger filter (see Conditions). |
form | Array of Form fields for workflow-level inputs. |
edges | Array of "<from_id>-<to_id>" strings, e.g. "1-2". Node ids must not contain a hyphen, since edges split on it. |
nodes | Array of Nodes. |
Nodes
Every node has id and type (both required), plus optional name, action_type_id, and action_data.
action_type_id points at an action by placeholder:
- core:
{{ core.<action_name> }} - native:
{{ native.<action_name> }} - apps:
{{ apps.<connection>.<action_name> }}
<action_name> is the action's name lowercased with spaces turned into underscores, so Change Invoice Status becomes change_invoice_status.
action_data maps each form field name to its value.
Some node types carry extra fields:
Node type | Extra fields |
|---|---|
Condition, Path | conditions, match_conditions |
Delay | delay_n, delay_unit, plus schedule (minute_of_day, day_of_week, day_of_month, month) |
WaitUntil | minute_of_day, minute_of_day_end, days_of_week |
Loop | loop_variable, loop_iterator, loop_action_type_id |
Trigger | event_trigger, conditions, match_conditions, delay_n, delay_unit |
ApiRequest / Webhook | url, headers, params, http_method (the request lives on the node) |
CustomApiRequest | action_type_id + action_data (the request lives on the referenced action) |
Any node after a Condition | required_condition_result (boolean), which branch it sits on |
Conditions
Conditions filter execution. They show up in a workflow's trigger.conditions (the trigger filter) and in Condition and Path nodes. match_conditions sets the logic: "all" means AND, "any" means OR. If you omit it, it defaults to all.
1 2 3 4 5 6conditions: - id: a1b2c3 # any unique string; auto-generated if omitted type: gt # the OPERATOR field: invoice_total # variable to read from the payload value: "100" # literal to compare against match_conditions: all
Operators
The operator goes in the condition's type field:
| Intent | Operator |
|---|---|
| Greater than | gt |
| Less than | lt |
| Greater or equal | gte |
| Less or equal | lte |
| Equals | equal |
| Not equals | not_equal |
| Contains substring | contains |
| Excludes substring | excludes |
| Has any value | present |
| Is blank | empty |
Numeric operators (gt, lt, gte, lte) coerce both sides to numbers. equal compares numerically when it can, and falls back to case-insensitive text otherwise.
To compare a field against another variable instead of a literal value, use the _var and _field variants: equal_var, not_equal_var, contains_var, gt_var, lt_var, gte_var, lte_var, gt_field, lt_field.
The operator isn't validated. A symbol like > or a made-up name saves fine but never matches, so the condition silently blocks the workflow. Use one of the listed operators (gt, not >).
Apps and data schemas
apps is an array of app identifier strings. Import installs each one. If an app is already installed, the default raise stops with an error, while conflict_strategy: skip keeps the existing install and continues. Apps only use the top-level conflict_strategy.
1apps: [slack, lasso]
user_data_schema and account_data_schema are arrays of Data variables merged into the account. Import fails if a variable already exists, and conflict_strategy: skip does not change that. Like any failure, it rolls back the whole import.
account_data is a hash of values merged into the account. Imported values overwrite existing keys, but only at the top level (it isn't a deep merge).
Conflict strategy
When a resource in your config already exists in the target account, conflict_strategy decides what happens:
raise(the default) stops the import with an error.skipleaves the existing resource in place and keeps going.
Only raise and skip are accepted; any other value is an error. Set it once at the top level, or put it on an individual trigger, action, or workflow to override the global value. Apps are plain strings, so they always use the top-level value.
The whole import is atomic. If it fails partway, for any reason, everything rolls back and nothing is created, so you never end up with a half-imported config.
1conflict_strategy: skip
Common mistakes
These cause most import and runtime problems. Run through them before you import.
| Symptom | Cause | Fix |
|---|---|---|
| Node crashes / panel closes when added to a workflow | type set to an unsupported value | Use CustomApiRequest for an action that calls a URL |
| Select shows blank rows and the saved choice doesn't stick | data.options given as bare strings ([saved, sent]) | Use { label, value } objects |
| Form value never fills into the request | Bracket placeholder like {{ form[status] }}, or a name that doesn't match its placeholder | Use the flat style with an exact match: name form__status, read as {{ form__status }} |
| Trigger condition silently blocks the workflow | field doesn't resolve (variable not declared in data_input_schema, or misspelled), or an invalid operator like > | Declare the variable in the trigger's data_input_schema, use a listed operator (gt, not >), and reference the exact payload variable name |
| Groups missing after import | Historically dropped | groups now round-trips, so just set it on the source action |
| Import fails: "already exists" | Resource already present and conflict_strategy defaults to raise | Set conflict_strategy: skip (globally or per-resource) |
Full worked example
A complete config: one HTTP action, a trigger that declares its payload, and a workflow that fires the action when an invoice total goes over 100.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59conflict_strategy: skip action_types: - name: Change Invoice Status description: Sets an invoice to a new status. type: CustomApiRequest primary_category: payment_processing url: https://example.com/actions/invoice-set-status http_method: post headers: Content-Type: application/json params: invoice_uuid: "{{ trigger.invoice_uuid }}" status: "{{ form__status }}" form: - name: form__status type: Select label: Set invoice status to required: true data: options: - { label: Saved, value: saved } - { label: Sent, value: sent } - { label: Paid, value: paid } triggers: - title: Payment Requested event: payment_requested description: Fires when a payment is requested on an invoice. data_input_schema: - variable: invoice_uuid type: String data_path: invoice_uuid required: true - variable: invoice_total type: Number data_path: invoice_total required: true workflows: - name: Auto-send large invoices trigger: event: payment_requested match_conditions: all conditions: - id: a1b2c3 type: gt field: invoice_total value: "100" edges: [] nodes: - id: "1" name: Set Invoice Sent type: CustomApiRequest action_type_id: "{{ core.change_invoice_status }}" action_data: form__status: sent apps: []
Next steps
- Import Configuration: install a config through the admin portal
- Configurations API: import and export programmatically
- Icons: the glyphs and colors for the
iconfield - Placeholders: the
{{ ... }}syntax used throughout a config - Triggers, Actions, and Workflows: the concepts behind each section
