Authoring a Configuration

How to hand-write an Embed Workflow configuration file for Config Import/Export, section by section, with YAML examples and the common mistakes to avoid.

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.

Read this first

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.

Jump to Common mistakes

Document structure

Every top-level key is optional. Import processes whatever you include, so start with just the sections you need.

KeyTypePurpose
action_types (alias actions)arrayHTTP/code actions that workflows can run
triggersarrayEvents that workflows bind to
workflowsarrayWorkflow templates (nodes + edges)
appsarray of stringsApp identifiers to install
user_data_schemaarrayPer-user variables
account_data_schemaarrayPer-account variables
account_datahashAccount data values
conflict_strategystringraise (default) or skip when a resource already exists

A minimal skeleton:

1
2
3
4
5
6
conflict_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).

FieldRequiredNotes
nameyesIdentity. A duplicate name triggers your conflict_strategy.
typeyesCustomApiRequest. See below.
descriptionno
groupsnoArray of access-control tags.
iconnoHash, e.g. { type: bolt, background_color: blue }. See Icons.
urlnoThe endpoint the action calls.
http_methodnoget, post, and so on.
headersnoHash of Header-Name: value.
paramsnoHash of key: "{{ ... }}" (request body/query). See Placeholders.
formnoArray of Form fields, the inputs a user fills in.
response_data_schemanoArray of Data variables the response exposes.
primary_category / secondary_categorynoCategory 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
26
action_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: "{{ 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.

FieldRequiredNotes
idnoAuto-generated if omitted.
nameyesReference key. The template that reads it must match exactly.
typenoDefault TextField. See below.
labelno
descriptionno
requirednoDefault false.
advancednoDefault false.
datanoType-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, Connection, or Custom. Connection only does something on a workflow form, not on an action's own fields.

An unknown field type renders a placeholder

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

A Select defines its options with allowed_values, either a fixed list under values or a reference to a data List under variable.

A fixed list:

1
2
3
4
5
data:
  allowed_values:
    values:
      - { value: paid, label: Paid }
      - { value: sent, label: Sent }

Each option is { value, label }. value is required; label defaults to the value. To pull the options from a List in your account or user data, reference it by name instead, and they resolve when the field renders:

1
2
3
data:
  allowed_values:
    variable: order_statuses

The older form, data.options as an array of { value, label }, still works and many connectors ship it. A field uses one or the other; if both are set, allowed_values wins.

allowed_values isn't validated on import

A malformed allowed_values doesn't raise an error. The field is dropped or renders with no options. If a Select shows "No options," check that values is an array of { value, label }, or that variable names a real data List.

AsyncSelect fetches its options from a remote endpoint:

1
2
3
4
5
6
data:
  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. See AsyncSelect for the full reference.

Other types usually take data: {}.

Triggers

Triggers are the events a workflow binds to. They go under triggers.

FieldRequiredNotes
titleyesDisplay name.
eventyesIdentity key. Workflows bind to it via trigger.event.
descriptionno
iconnoHash. See Icons.
data_input_schemanoArray of Data variables the event payload delivers.
conflict_strategynoPer-resource override.
1
2
3
4
5
6
7
8
9
triggers:
  - title: Payment Requested
    event: payment_requested
    description: Fires when a payment is requested on an invoice.
    data_input_schema:
      - variable: invoice_total
        type: Float
        data_path: invoice_total
        required: true
Declare every variable your workflow uses

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.

FieldRequiredNotes
variableyesThe key, e.g. invoice_total.
typeyesSee the values below.
requirednoDefault false.
data_pathnoWhere to read from the payload, e.g. user.email.
display_labelnoHuman-facing label.
formatnoFree-form.
secretnoDefault false.
childrennoArray of Data variables, for Object.
iterator / item_typenoFor List.
allowed_valuesnoLimit a String field to a set of options. See below.

The type values are String, Date, Boolean, Integer, Float, Object (uses children:), and List (uses iterator: + item_type:). Use Integer or Float for numbers.

On a trigger's data_input_schema, the type is validated on create, update, and import. An unsupported value like Number is rejected (a 422 from the API, and on import the whole import fails and rolls back), with an error like 'Number' is not a supported data type. On response_data_schema, user_data_schema, and account_data_schema, the type isn't validated yet, so an unsupported value saves silently. Stick to the list everywhere.

Two different type lists

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.

Limiting a field to a set of options

A String field can carry allowed_values to limit it to a fixed set or a data List. On a data-schema field it sits at the field's top level, not under data:

1
2
3
4
5
6
- variable: status
  type: String
  allowed_values:
    values:
      - { value: open, label: Open }
      - { value: closed, label: Closed }

Or reference a List by name, so the options come from your account or user data:

1
2
3
4
- variable: status
  type: String
  allowed_values:
    variable: order_statuses

A condition on a field with allowed_values shows its options as a dropdown and narrows the operators to equal and not_equal. An optional source (inline, account_data, or user_data) may appear on schema fields; it's advisory, and resolution ignores it.

Use allowed_values on trigger data_input_schema and action response_data_schema fields. Account and user data schemas hold the List that a variable points at; they don't take allowed_values themselves. This replaces the deprecated list_values_variable; legacy configs are still accepted.

Nested and list shapes:

1
2
3
4
5
6
7
8
9
data_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.

FieldNotes
nameWorkflow name (also becomes its key).
description
auto_clone_for_new_usersBoolean. Automatically copies the workflow for each new end-user.
is_templateBoolean. Marks the workflow as a reusable template.
trigger{ event, match_conditions, conditions }, the trigger filter (see Conditions).
formArray of Form fields for workflow-level inputs.
edgesArray of "<from_id>-<to_id>" strings, e.g. "1-2". Node ids must not contain a hyphen, since edges split on it.
nodesArray 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 typeExtra fields
Condition, Pathconditions, match_conditions
Delaydelay_n, delay_unit, plus schedule (minute_of_day, day_of_week, day_of_month, month)
WaitUntilminute_of_day, minute_of_day_end, days_of_week
Looploop_variable, loop_iterator, loop_action_type_id
Triggerevent_trigger, conditions, match_conditions, delay_n, delay_unit
ApiRequest / Webhookurl, headers, params, http_method (the request lives on the node)
CustomApiRequestaction_type_id + action_data (the request lives on the referenced action)
Any node after a Conditionrequired_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
6
conditions:
  - 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:

IntentOperator
Greater thangt
Less thanlt
Greater or equalgte
Less or equallte
Equalsequal
Not equalsnot_equal
Contains substringcontains
Excludes substringexcludes
Has any valuepresent
Is blankempty

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.

When the field has allowed_values, the builder shows its options as a dropdown and limits the operators to equal and not_equal.

Operators are validated on import

On import, the operator is validated. An unlisted operator like > fails the whole import with Unknown condition operator, and since imports are atomic, everything rolls back. Use a listed operator (gt, not >). In the builder or direct API an unknown operator saves quietly and just never matches, but on import it's a hard error.

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.

1
apps: [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.
  • skip leaves 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.

1
conflict_strategy: skip

Common mistakes

These cause most import and runtime problems. Run through them before you import.

SymptomCauseFix
Node crashes / panel closes when added to a workflowtype set to an unsupported valueUse CustomApiRequest for an action that calls a URL
Select shows blank rows and the saved choice doesn't stickdata.options given as bare strings ([saved, sent])Use { label, value } objects
Form value never fills into the requestBracket placeholder like {{ form[status] }}, or a name that doesn't match its placeholderUse the flat style with an exact match: name form__status, read as {{ form__status }}
Condition silently blocks the workflow at runtimeIts field doesn't resolve: the variable isn't declared in data_input_schema, or it's misspelledDeclare the variable in the trigger's data_input_schema and reference its exact name
Import fails with Unknown condition operatorA condition uses an operator that isn't in the list, like >Use a listed operator (gt, not >)
Import fails with not a supported data typeA trigger data_input_schema field uses an unsupported type, like NumberUse a listed data type (Integer or Float for numbers)
Groups missing after importHistorically droppedgroups now round-trips, so just set it on the source action
Import fails: "already exists"Resource already present and conflict_strategy defaults to raiseSet 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
59
conflict_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: "{{ 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: Float
        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