Documentation

Data preparation steps

A data preparation step runs one or more formulas on values from workflow inputs, step inputs, or earlier steps — then writes the results as named outputs you can chain into agents, tools, or conditions.

Use it when you need to change data between action steps without calling an agent or external tool — for example summing scores, joining labels, or building a true/false flag before a condition.

When to use data preparation

ApproachBest for
Data preparationSum, split, join, filter, compare, parse JSON, or compute a single value
TemplateMultiline authored text (markdown, JSON, email) with optional {{ }} slots
ConditionBranch to a different next step when a rule is true or false
Agent / toolSearch, write content, call APIs, or create documents

Good reasons to use data preparation:

  • Aggregate a list — total, count, or filtered subset before the next step.
  • Shape text — join array items into one string, or split CSV-like text into a list.
  • Prepare a flag — produce true/false from inputs, then branch with a condition step.

Step 1 — Add a data preparation step

  1. Open your workflow in the editor.
  2. Click Add Step, or choose From Template and open the Data Preparation category.
  3. Set Step kind to Data Preparation.
  4. Under Output mappings, add one row per result you need:
    • Output name — how later steps reference this value (for example total).
    • Type — string, number, boolean, array, or object.
    • Formula — a single {{ … }} expression that produces the value.

Templates such as Data Preparation - Sum array pre-fill a common mapping; adjust names and formulas to match your workflow.

Step 2 — Pass data into formulas (optional)

Formulas can read:

SourceExample
Workflow inputs{{ workflow_inputs.mode }}
Earlier step outputs{{ steps.search.outputs.results }}
Step inputs on this step{{ numbers }} (after you add an input named numbers)

To reuse a long expression or coerce a type first, add step inputs on the data preparation step (same pattern as agent chaining):

  1. Under Inputs, add an input (for example numbers, type array, value source From expression).
  2. Set the expression to {{ steps.collect.outputs.scores }}.
  3. In the mapping formula, use the short name: {{ sum(numbers) }}.

Formula reference

Formulas use the same {{ … }} syntax described in Workflow expressions (and the overview in Workflow authoring). Common helpers:

GoalFormula example
Sum numbers in an array{{ sum(numbers) }}
Count items{{ len(items) }}
Join strings{{ join(labels, ", ") }}
Split text{{ split(text, ",") }}
Compare values{{ workflow_inputs.mode == "deep" }}
Filter a list{{ filter(items, #.active == true) }}
Split markdown table rows{{ filter(split(text, "\n")[2:], len(trim(#)) > 0) }}
Today’s date (YYYY-MM-DD){{ now().Format("2006-01-02") }}
Parse date to YYYY-MM-DD{{ date(date_string, "2006-01-02").Format("2006-01-02") }}
Days between two dates{{ floor((date(end_date, "2006-01-02") - date(start_date, "2006-01-02")).Hours() / 24) }}
Days remaining until due date{{ floor((date(due_date, "2006-01-02") - date(now().UTC().Format("2006-01-02"))).Hours() / 24) }}
Is due date overdue{{ date(due_date, "2006-01-02") < date(now().UTC().Format("2006-01-02")) }}
Parse JSON text to object{{ parse_json(text) }} or {{ text }} with output type object
Read a field from an object{{ data.field_name }} or {{ parse_json(text).field_name }}
Embed array/object in template JSON{{ json(values) }} inside a Template step body
Build an object (map) inline{{ { name: customer_name, email: email } }}
Object/map → JSON text string{{ json(record) }} with output type string
Parse JSON array{{ parse_json(text) }} with output type array

Later steps reference results with:

{{ steps.<step_name>.outputs.<output_name> }}

JSON: parse and serialize

Use data preparation when JSON is a single computed value (parse a payload, extract a field, or build a small object). Use a template step when you need authored JSON with fixed keys and a few {{ }} slots (see Template steps).

Parse JSON text (webhook or API body)

This pattern parses a JSON string (for example an HTTP response body) into an object you can reference in later steps.

- name: parse_webhook
  kind: data_preparation
  config:
    mappings:
      - name: parsed
        expression: "{{ parse_json(body) }}"
  inputs:
    - name: body
      type: string
      format: text
      value_source: from_expression
      expression: "{{ steps.fetch.outputs.response_body }}"
  outputs:
    - name: parsed
      type: object
      format: json
  on_error: fail

To read a field after parsing:

{{ parse_json(body).status }}

If your output is declared as type object, you can also map {{ body }} and rely on output coercion to parse the JSON string.

Build an object and serialize to JSON text

If a later step needs JSON text (not a typed object), build an object and wrap it with json(...). Set the output type to string.

- name: build_contact_json
  kind: data_preparation
  config:
    mappings:
      - name: contact_json
        expression: '{{ json({ name: customer_name, email: email, source: source }) }}'
  inputs:
    - name: customer_name
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.customer_name }}"
    - name: email
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.email }}"
    - name: source
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.source }}"
  outputs:
    - name: contact_json
      type: string
      format: text
  on_error: fail

Example result:

{"name":"Ada","email":"[email protected]","source":"webhook"}

If the next step accepts a typed object input, you usually do not need json() — output an object directly instead.

Example workflow (sum then branch)

workflow_inputs:
  - name: scores
    type: array
    required: true

steps:
  - name: sum_scores
    kind: data_preparation
    config:
      mappings:
        - name: total
          expression: "{{ sum(scores) }}"
    inputs:
      - name: scores
        type: array
        format: json
        value_source: from_expression
        expression: "{{ workflow_inputs.scores }}"
    outputs:
      - name: total
        type: number
        format: json
    on_error: fail

  - name: check_total
    kind: condition
    config:
      choices:
        - condition: "{{ steps.sum_scores.outputs.total > 100 }}"
          next_step: high_path
    on_error: fail

  - name: high_path
    kind: tool
    # ...
    on_error: fail

Markdown tables → rows and contact objects

Templates Split markdown table rows, Parse markdown table row, and Parse markdown table rows cover a common pattern: turn a markdown table into row strings, then into JSON objects for loops or later steps.

Assume columns in this order: checkbox, full name, role, website, email. A data row looks like:

| [] | John Doe | Product Manager | https://example.com | [email protected] |

After split(row, "|") and trim, use indices 2–5 for name, role, website, and email (index 1 is the checkbox column).

Step 1 — split table into row strings (output type array):

{{ filter(split(text, "
")[2:], len(trim(#)) > 0) }}

Step 2 — one row to one object (output type object), e.g. inside a loop child where row is {{ context.loop_item }}:

{{ let cells = split(row, "|"); { fullName: trim(cells[2]), role: trim(cells[3]), website: trim(cells[4]), email: trim(cells[5]) } }}

Step 2 — all rows to contact objects (output type array), chaining after step 1:

{{ map(rows, { let cells = split(#, "|"); { fullName: trim(cells[2]), role: trim(cells[3]), website: trim(cells[4]), email: trim(cells[5]) } }) }}

Example result for the sample row above:

{
  "fullName": "John Doe",
  "role": "Product Manager",
  "website": "https://example.com",
  "email": "[email protected]"
}

Invoice row example (how the formula is built)

A different table layout needs different column indices and field names. Suppose each data row looks like:

| 2026-02-11 | $324 | John Doe | paid |

After split(row, "|") and trim, indices 1–4 are date, amount, customer, and status (index 0 is empty from the leading |).

One row → one invoice object (output type object):

{{ let cells = split(row, "|"); { date: trim(cells[1]), amount: trim(cells[2]), customer: trim(cells[3]), paid: trim(cells[4]) == "paid" } }}

Read the expression in order:

  1. let cells = split(row, "|") — split the row on | into an array of cell strings.
  2. ; — then evaluate the object on the right (expr let syntax).
  3. { ... } — build a JSON object with named fields.
  4. date: trim(cells[1]) — first field: trim cell index 1 for the date text.
  5. amount: trim(cells[2]) — second field: amount stays a string (e.g. "$324").
  6. customer: trim(cells[3]) — third field: customer name.
  7. paid: trim(cells[4]) == "paid" — fourth field: compare the status cell to "paid" so the output is boolean (true / false), not the word paid.

Example result:

{
  "date": "2026-02-11",
  "amount": "$324",
  "customer": "John Doe",
  "paid": true
}

For many rows, wrap the same object literal in map (as in the contact example), using # instead of row for each line.

Column positions are fixed in the formula — if the table layout changes, update the indices. For variable headers or cells that contain |, use an agent or tool step instead.

Template steps (markdown, JSON, plain text)

A template step uses the same {{ … }} interpolation as agent prompts, but without an LLM. Use it when the output is authored text with slots — not a single computed formula.

Use caseOutput typeNotes
Welcome or reminder emailstring / textWire email_body to Send email notification body
Onboarding or meeting notesstring / textWire document_content to New document initial_content
CRM / webhook payloadobjectRealistic JSON shape for agent tools or future HTTP steps
Approval summarystring / textWire approval_message to Human Approval Gate

Add a template step from From TemplateTemplate, or set Step kind to Template. Each output row has a Template body (multiline). Static text needs no {{ }}; add slots only where values come from inputs or earlier steps.

Chain template output into the next step with {{ steps.<template_step>.outputs.<output_name> }} — for example {{ steps.compose_welcome_email.outputs.email_body }} as the email body, or {{ steps.compose_meeting_recap.outputs.document_content }} as document content.

Welcome email (output type string):

- name: compose_welcome_email
  kind: template
  config:
    mappings:
      - name: email_body
        template: |
          Hi {{ customer_name }},

          Welcome to {{ product_name }}!

          Get started: {{ getting_started_url }}
  inputs:
    - name: customer_name
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.customer_name }}"
    - name: product_name
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.product_name }}"
    - name: getting_started_url
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.getting_started_url }}"
  outputs:
    - name: email_body
      type: string
      format: text
  on_error: fail

- name: send_welcome
  kind: tool
  config:
    tool_name: a1kh_email_send
    subject: "Welcome aboard"
    body: "{{ steps.compose_welcome_email.outputs.email_body }}"
  on_error: fail

CRM lead sync payload (output type object):

- name: build_crm_lead_payload
  kind: template
  config:
    mappings:
      - name: payload
        template: |
          {
            "external_id": "{{ lead_id }}",
            "name": "{{ name }}",
            "email": "{{ email }}",
            "company": "{{ company }}",
            "source": "{{ source }}",
            "status": "new",
            "score": {{ score }}
          }
  inputs:
    - name: lead_id
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.lead_id }}"
    - name: name
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.name }}"
    - name: email
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.email }}"
    - name: company
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.company }}"
    - name: source
      type: string
      value_source: from_expression
      expression: "{{ workflow_inputs.source }}"
    - name: score
      type: number
      value_source: from_expression
      expression: "{{ workflow_inputs.score }}"
  outputs:
    - name: payload
      type: object
      format: json
  on_error: fail

For JSON slots that hold arrays or objects, use {{ json(values) }} inside the template body — for example:

"b": {{ json(values) }}

Running and checking results

After a run, open the execution detail page and expand the data preparation step. Resolved inputs and computed outputs appear on the step record. Use those output names in formulas on steps below.

Limits and tips

  • Each data preparation mapping row must be one formula wrapped in {{ }}.
  • Template bodies can be plain text; use {{ }} only for dynamic slots.
  • Output names in mappings must match the step outputs list (the editor keeps them in sync).
  • All mappings in one step evaluate against the same inputs — you cannot use one mapping’s result inside another mapping in the same step yet. Add a second data preparation step if you need a chain.
  • To chain parse → transform → serialize, use multiple data preparation steps (mappings in one step cannot reference each other).
  • For branching, use a condition step after data preparation — conditions need a boolean formula, while data preparation can produce any type.

Related guides