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
| Approach | Best for |
|---|---|
| Data preparation | Sum, split, join, filter, compare, parse JSON, or compute a single value |
| Template | Multiline authored text (markdown, JSON, email) with optional {{ }} slots |
| Condition | Branch to a different next step when a rule is true or false |
| Agent / tool | Search, 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/falsefrom inputs, then branch with a condition step.
Step 1 — Add a data preparation step
- Open your workflow in the editor.
- Click Add Step, or choose From Template and open the Data Preparation category.
- Set Step kind to Data Preparation.
- 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.
- Output name — how later steps reference this value (for example
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:
| Source | Example |
|---|---|
| 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):
- Under Inputs, add an input (for example
numbers, type array, value source From expression). - Set the expression to
{{ steps.collect.outputs.scores }}. - 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:
| Goal | Formula 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:
let cells = split(row, "|")— split the row on|into an array of cell strings.;— then evaluate the object on the right (exprletsyntax).{ ... }— build a JSON object with named fields.date: trim(cells[1])— first field: trim cell index 1 for the date text.amount: trim(cells[2])— second field: amount stays a string (e.g."$324").customer: trim(cells[3])— third field: customer name.paid: trim(cells[4]) == "paid"— fourth field: compare the status cell to"paid"so the output is boolean (true/false), not the wordpaid.
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 case | Output type | Notes |
|---|---|---|
| Welcome or reminder email | string / text | Wire email_body to Send email notification body |
| Onboarding or meeting notes | string / text | Wire document_content to New document initial_content |
| CRM / webhook payload | object | Realistic JSON shape for agent tools or future HTTP steps |
| Approval summary | string / text | Wire approval_message to Human Approval Gate |
Add a template step from From Template → Template, 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
- Workflow authoring — formulas, chaining, and step kinds
- Loop steps —
context.loop_itemand related expressions inside loops - Sub-workflows