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: "{{ 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.

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, FromList, 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

Select takes static options, given as an array of { label, value } objects rather than bare strings:

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

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.

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: Number
        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. Convention only, not validated.
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_type / list_values_variablenoFor 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.

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.

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.

Operators are convention only

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.

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 }}
Trigger condition silently blocks the workflowfield 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 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: "{{ 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