> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentruntime.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflow JSON cookbook

> Copy-paste workflow graph examples — MCP chains, Lua transforms, LLM steps, and for-each loops.

Workflow graphs are JSON documents describing a directed acyclic graph of steps. Author them in **Workflow Studio** or submit them via `POST /v1/workflows`.

For editing graphs in the Console, see [JSON editor](/workflows/json-editor) (Canvas | JSON mode, IntelliSense, **Apply to canvas**).

**JSON Schema:** [agentruntime.io/schemas/workflow-graph.schema.json](https://agentruntime.io/schemas/workflow-graph.schema.json)

## Graph structure

Every workflow graph requires:

| Field         | Description                                  |
| ------------- | -------------------------------------------- |
| `tenant_id`   | Workspace identifier                         |
| `workflow_id` | Workflow UUID                                |
| `params`      | Optional string key-value trigger parameters |
| `steps`       | Array of step objects (min 1)                |

Every step requires `id`, `type`, and `name`.

## Example 1 — MCP chain with parallel branches

Fetch data, fan out into parallel processing, merge, and validate:

```json theme={null}
{
  "tenant_id": "acme",
  "workflow_id": "550e8400-e29b-41d4-a716-446655440000",
  "params": { "user_id": "user-1" },
  "steps": [
    {
      "id": "step-a",
      "type": "mcp_call",
      "name": "Fetch base value",
      "tool_name": "add",
      "tool_args": { "a": 5, "b": 7 },
      "timeout_s": 10
    },
    {
      "id": "step-b1",
      "type": "mcp_call",
      "name": "Double",
      "tool_name": "multiply",
      "tool_args": { "a": "{{steps.step-a.result.result}}", "b": 2 },
      "depends_on": ["step-a"],
      "timeout_s": 10
    },
    {
      "id": "step-b2",
      "type": "mcp_call",
      "name": "Triple",
      "tool_args": { "a": "{{steps.step-b1.result.result}}", "b": 3 },
      "depends_on": ["step-b1"],
      "timeout_s": 10
    },
    {
      "id": "step-c",
      "type": "mcp_call",
      "name": "Format text",
      "tool_name": "text_processor",
      "tool_args": {
        "text": "Intermediate: {{steps.step-b1.result.result}}",
        "operation": "uppercase"
      },
      "depends_on": ["step-b1"],
      "timeout_s": 10
    },
    {
      "id": "step-d",
      "type": "mcp_call",
      "name": "Validate merged output",
      "tool_name": "data_validator",
      "tool_args": {
        "data": {
          "math_result": "{{steps.step-b2.result.result}}",
          "text_result": "{{steps.step-c.result.processed_text}}"
        }
      },
      "depends_on": ["step-b2", "step-c"],
      "timeout_s": 10
    }
  ]
}
```

`step-b2` and `step-c` run in **parallel** after `step-b1` completes because they share a dependency but not each other.

## Example 2 — MCP + Lua + LLM pipeline

Fetch product and customer data, calculate pricing in Lua, generate invoice text with an LLM:

```json theme={null}
{
  "tenant_id": "acme",
  "workflow_id": "550e8400-e29b-41d4-a716-446655440001",
  "params": { "sku": "WIDGET-01", "email": "buyer@example.com" },
  "steps": [
    {
      "id": "fetch-product",
      "type": "mcp_call",
      "name": "Fetch product",
      "tool_name": "get_product",
      "tool_args": { "sku": "{{input.sku}}" },
      "timeout_s": 10
    },
    {
      "id": "fetch-customer",
      "type": "mcp_call",
      "name": "Fetch customer",
      "tool_name": "get_customer",
      "tool_args": { "email": "{{input.email}}" },
      "timeout_s": 10
    },
    {
      "id": "calculate-pricing",
      "type": "lua_script",
      "name": "Calculate discount",
      "depends_on": ["fetch-product", "fetch-customer"],
      "timeout_s": 5,
      "script": "local product = steps.fetch_product.result\nlocal customer = steps.fetch_customer.result\nlocal discount = 0\nif customer.tier == 'gold' then discount = 0.15\nelseif customer.tier == 'silver' then discount = 0.10 end\nlocal total = product.price * (1 - discount)\nreturn { total = math.floor(total * 100) / 100, summary = string.format('%s: $%.2f', product.name, total) }"
    },
    {
      "id": "generate-invoice",
      "type": "llm_call",
      "name": "Generate invoice line",
      "model": "gpt-4o-mini",
      "prompt": "Write a one-line invoice for: {{steps.calculate-pricing.result.summary}}",
      "depends_on": ["calculate-pricing"]
    }
  ]
}
```

## Example 3 — for\_each over a list

Generate items with an LLM, then process each in parallel with Lua:

```json theme={null}
{
  "tenant_id": "acme",
  "workflow_id": "550e8400-e29b-41d4-a716-446655440002",
  "steps": [
    {
      "id": "generate-items",
      "type": "llm_call",
      "name": "Generate item list",
      "model": "gpt-4o-mini",
      "prompt": "Return JSON only: {\"items\": [\"alpha\", \"beta\", \"gamma\"]}",
      "timeout_s": 60
    },
    {
      "id": "process-each",
      "type": "for_each",
      "name": "Process each item",
      "depends_on": ["generate-items"],
      "for_each_items": "{{steps.generate-items.result.items}}",
      "for_each_max_parallel": 10,
      "for_each_body_steps": [
        {
          "id": "process",
          "type": "lua_script",
          "name": "Process one",
          "script": "return { index = index, label = tostring(item) }",
          "timeout_s": 5
        }
      ],
      "timeout_s": 120
    }
  ]
}
```

`for_each` output includes `result.count`, `result.max_parallel`, and `result.results[]` with per-item `index` and `result` or `error`.

## Example 3b — for\_each multi-step body

Run a pipeline per list item (agent → lua tag):

```json theme={null}
{
  "id": "triage-each",
  "type": "for_each",
  "name": "Triage tickets",
  "depends_on": ["load-tickets"],
  "for_each_items": "{{steps.load-tickets.result.tickets}}",
  "for_each_max_parallel": 5,
  "for_each_body_steps": [
    {
      "id": "triage",
      "type": "agent_call",
      "agent_id": "YOUR_AGENT_ID",
      "prompt": "Triage: {{foreach.item}}"
    },
    {
      "id": "tag",
      "type": "lua_script",
      "depends_on": ["triage"],
      "script": "return { ticket = tostring(item), ok = true }"
    }
  ]
}
```

`for_each_body_steps` is required (use a one-element array for a single inner step). Legacy `for_each_body` is rejected at validation.

## Example 3c — while (refine until good)

```json theme={null}
{
  "id": "refine",
  "type": "while",
  "name": "Refine until score passes",
  "while_mode": "until",
  "while_max_iterations": 5,
  "while_condition": "{{body.score.pass}}",
  "while_body_steps": [
    {
      "id": "draft",
      "type": "lua_script",
      "script": "local n = loop.iteration or 0\nreturn { text = string.rep('x', (n+1)*40) }"
    },
    {
      "id": "score",
      "type": "lua_script",
      "depends_on": ["draft"],
      "script": "local n = #tostring(body.draft.text or '')\nreturn { pass = n >= 90 }"
    }
  ]
}
```

While output includes `iteration_count`, `stopped_reason` (`condition_met` or `max_iterations`), `last` (final body map), and `iterations[]`.

## Example 4 — Human approval gate

Pause before sending an email:

```json theme={null}
{
  "id": "approve-send",
  "type": "human_task",
  "name": "Approve outbound email",
  "task_type": "approval",
  "task_payload": {
    "title": "Review email draft",
    "description": "Confirm before sending to {{input.recipient}}",
    "draft": "{{steps.draft-email.result.body}}"
  },
  "depends_on": ["draft-email"]
}
```

Complete the task from [Command Center](/workflows/runs-and-command-center) or the API. Downstream steps reference the completion result.

## Template variables

| Syntax                           | Resolves to                                                              |
| -------------------------------- | ------------------------------------------------------------------------ |
| `{{input.field}}`                | Workflow trigger payload                                                 |
| `{{steps.step-id.result.field}}` | Upstream step output (after the step completes)                          |
| `{{steps.step-id.input.field}}`  | Resolved parameters the step was called with (after template expansion)  |
| `{{foreach.item}}`               | Current item in `for_each` body                                          |
| `{{foreach.index}}`              | Zero-based index in `for_each` body                                      |
| `{{body.inner-id.field}}`        | Inner step output in `while` / `for_each_body_steps` (current iteration) |
| `{{while.iteration}}`            | Current while iteration (0-based)                                        |
| `{{while.prev.inner-id.field}}`  | Previous while iteration inner output                                    |

**Composite loops:** Inner body steps also expose `{{steps.<inner-id>.input.<field>}}` using the inner step `id` from `for_each_body_steps` or `while_body_steps`. Parent loop nodes expose their own resolved inputs, e.g. `{{steps.process-each.input.for_each_items}}` or `{{steps.refine.input.while_condition}}`.

**Legacy shapes:** Do not use `for_each_body`, slug step ids, or catalog-agent-only canvas nodes.

Hyphenated step IDs work in templates; in Lua use `steps.fetch_product` (underscore alias) or `steps["fetch-product"]`.

## Execution controls

Apply to any step type:

```json theme={null}
{
  "depends_on": ["upstream-step"],
  "condition": "{{steps.check.result.should_run}}",
  "enabled": true,
  "retry_count": 2,
  "timeout_s": 30
}
```

Set `"enabled": false` to skip a step with a synthetic completion (dependencies still resolve).

## Validate before publish

Always dry-run before publishing:

```
POST /v1/workflows/{id}/validate
```

Validation checks graph structure, dependency cycles, Lua syntax, and MCP bindings — without executing tools.

## Next steps

* [Step types](/workflows/step-types) — field reference per type
* [Workflow Studio](/workflows/studio) — visual authoring guide
* [Human tasks](/workflows/human-tasks) — approval patterns
