Context Resolution
Every workflow run maintains a context object. It starts with the input payload and grows as each step adds its output. The @path syntax lets any step reference data produced by earlier steps.
How Context Accumulates
Each step can read anything from context that was set before it. Steps cannot read from steps that haven't executed yet.
@path Reference Table
| Path | Source | Description |
|---|---|---|
@input.* |
Workflow execution payload | The JSON body sent to POST /workflows/:id/execute |
@input.field.nested |
Nested input | Dot notation traverses nested objects |
@outputKey.* |
Step output | References a configured result key or a step's documented default output |
@matched |
Matcher | Array of matched record pairs |
@unmatchedLeft |
Matcher | Left-side records with no match (or custom name via outputUnmatchedLeft) |
@unmatchedRight |
Matcher | Right-side records with no match (or custom name via outputUnmatchedRight) |
@item |
Foreach loop | Current item being processed |
@item.field |
Foreach loop | Field on the current loop item |
@__run_id |
System | Current run identifier |
@__step |
System | Current step index (0-based) |
@now |
System | Current ISO timestamp |
@__approved |
PbotApproval | Boolean that is present when the reviewer approved |
@__comment |
PbotApproval | Comment from the reviewer's decision |
doc:doc_xxx |
Document Storage | Resolves supported uploaded content from the latest version |
doc:doc_xxx@N |
Document Storage | Resolves a supported document version for reproducible execution |
Naming Step Outputs
Matcher outputs use their configured output names (outputMatched, outputUnmatchedLeft, outputUnmatchedRight). ReAct loops use result_key. Registered LLM and DB workflow actions use properties.outputKey or their documented default.
{
"type": "loop",
"properties": {
"mode": "react",
"objective": "Investigate exceptions",
"tools": [{ "type": "action", "name": "lookup_record" }],
"result_key": "investigation"
}
}
The agent's final answer is stored at @investigation in context, accessible by all subsequent steps.
For registered LLM and DB workflow actions, properties.outputKey controls the context key:
{
"type": "get_customer_record",
"properties": { "outputKey": "customer_data" }
}
Result available at @customer_data for subsequent steps.
Nested Path Resolution
Dot notation traverses nested objects to any depth:
{
"type": "send_notification",
"properties": {
"email": "@customer_data.contact.email",
"name": "@customer_data.contact.first_name",
"company": "@customer_data.organization.name"
}
}
Array Indexing
Access specific array elements by index:
{
"first_match_id": "@matched.0.invoice_id",
"second_match_amount": "@matched.1.amount"
}
Access array length:
{
"condition": {
"greaterThan": [{ "length": "@unmatched_invoices" }, 0]
}
}
Template Interpolation: {{ }}
Double braces perform string interpolation: they convert a value to a string and embed it in surrounding text. Use for building human-readable messages, email subjects, Slack posts, and log entries.
{
"type": "gmail_send",
"properties": {
"subject": "Invoice {{input.invoice_id}}: {{input.vendor_name}}",
"body": "Dear {{input.vendor_name}},\n\nYour invoice for ${{input.amount}} has been processed.\n\nMatched records: {{matched.length}}\nExceptions: {{unmatched_invoices.length}}"
}
}
@path vs {{ }}: These serve different purposes. @path returns the raw value (object, array, number, boolean) and is used for data references in properties. {{template}} converts to string and embeds in text. Use @path when passing data. Use {{ }} when constructing messages.
{
"data_reference": "@input.invoices",
"message": "Processing {{input.invoices.length}} invoices"
}
Worked Example: 4-Step Workflow
{
"name": "full_context_example",
"definition": {
"actions": [
{
"type": "matcher",
"properties": {
"left": "@input.invoices",
"right": "@input.payments",
"matchOn": ["invoice_id"],
"tolerance": 50,
"outputMatched": "reconciled",
"outputUnmatchedLeft": "exceptions"
}
},
{
"type": "loop",
"properties": {
"mode": "foreach",
"items_path": "@exceptions",
"item_variable_name": "exception",
"actions_to_execute": [
{
"type": "custom-table",
"properties": {
"table": "vendor_exception_queue",
"operation": "upsert",
"key_fields": ["run_id", "invoice_id"],
"keys": ["run_id", "invoice_id", "vendor_id", "status"],
"values": ["@__run_id", "@exception.invoice_id", "@exception.vendor_id", "ready"]
}
}
],
"collect_results": true,
"result_key": "exception_writes"
}
},
{
"type": "PbotApproval",
"filter": {
"condition": { "greaterThan": ["@exceptions.length", 5] }
},
"properties": {
"comment": "{{exceptions.length}} exceptions found. Review before proceeding.",
"request_payload": {
"exception_count": "@exceptions.length",
"exceptions": "@exceptions",
"reconciled_count": "@reconciled.length"
}
}
},
{
"type": "custom-table",
"properties": {
"table": "recon_log",
"operation": "write",
"keys": ["run_id"],
"values": ["@__run_id"],
"fields": {
"matched": "@reconciled.length",
"exceptions": "@exceptions.length",
"approved": "@__approved",
"timestamp": "@now"
}
}
}
]
}
}
Context at each step:
| After Step | Keys Added | Available |
|---|---|---|
| Input | @input.invoices, @input.payments |
input |
| Step 1 (Matcher) | @reconciled, @exceptions |
input + matcher output |
| Step 2 (Loop) | @vendor_lookups |
input + matcher + loop results |
| Step 3 (Approval) | @__approved, @__comment |
input + matcher + loop + approval |
| Step 4 (Table) | : (writes to storage) | everything |
ā Next: Conditional Logic