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 output by outputKey or default output name |
@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 ā whether the reviewer approved |
@__approval_data |
PbotApproval | Additional data from the reviewer's response |
Naming Step Outputs
By default, matcher outputs use their configured output names (outputMatched, outputUnmatchedLeft, outputUnmatchedRight). Other steps store results under their outputKey or under a default name.
{
"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 actions, outputKey controls the context key:
{
"type": "fetch_customer",
"properties": { "customer_id": "@input.id" },
"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": 0.02,
"outputMatched": "reconciled",
"outputUnmatchedLeft": "exceptions"
}
},
{
"type": "loop",
"properties": {
"mode": "foreach",
"items_path": "@exceptions",
"item_variable_name": "exception",
"actions_to_execute": [
{
"type": "lookup_vendor",
"properties": {
"vendor_id": "@exception.vendor_id"
}
}
],
"collect_results": true,
"result_key": "vendor_lookups"
}
},
{
"type": "PbotApproval",
"filter": {
"condition": { "greaterThan": ["@exceptions.length", 5] }
},
"properties": {
"comment": "{{exceptions.length}} exceptions found ā review before proceeding",
"request_payload": {
"exception_count": "@exceptions.length",
"vendor_details": "@vendor_lookups",
"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, @__approval_data |
input + matcher + loop + approval |
| Step 4 (Table) | ā (writes to storage) | everything |
ā Next: Conditional Logic