---
title: "Node types"
source: "https://docs.vertesiahq.com/processes/node-types"
markdown: "https://docs.vertesiahq.com/llms/processes/node-types.md"
---

# Node types

Each node in a process definition has a `type`. The engine's behavior for executing the node, applying writes, and picking the next transition depends on the type.

Every node also accepts two documentation fields:

- **`description`** — developer-facing notes for authors of the definition.
- **`human_description`** — a one- or two-sentence plain-language explanation shown to users watching the run. Fill this in — undescribed nodes read badly in observability.

## `tool`

A deterministic step. Typically used to apply a fixed `context_update` (e.g. setting `legal_decision: "auto_approved"` when bypassing human review) or to invoke a pre-registered tool.

```json
"auto_approve": {
    "type": "tool",
    "config": {
        "context_update": { "legal_decision": "auto_approved" }
    },
    "writes": ["legal_decision"],
    "transitions": [ { "to": "store_output" } ]
}
```

- **Writes**: either the literal `config.context_update`, or derived from `node.writes` if set. Any non-empty context update requires `node.writes`.
- **Transitions**: `auto`-triggered by default; guards supported.

## `interaction`

Invokes a Vertesia interaction by name. Use for deterministic single-shot LLM calls (summarization, extraction, classification) where the interaction already has a prompt and is registered in the project.

```json
"classify_invoice": {
    "type": "interaction",
    "interaction": "ClassifyInvoice",
    "input": { "doc_id": "{{invoice_doc_id}}" },
    "writes": ["gl_code", "amount"],
    "transitions": [ { "to": "approve" } ]
}
```

The engine forwards a derived **`result_schema`** built from `node.writes` ∩ `process.context.schema.properties` to the interaction execution, so the returned JSON lines up with the node's writes contract. The interaction's own schema is overridden for the duration of the call.

- **Writes**: declared in `node.writes`; populated from the interaction's structured JSON output.
- **Transitions**: `auto`-triggered by default.

## `agent`

A worker agent that may use tools. The engine builds a `result_schema` from `node.writes` (plus a `_next_node` enum if the node declares more than one agent-triggered transition) and asks the child conversation to return exactly that JSON. The agent can use any tool declared in `node.tools`; `learn_*` skills remain available.

```json
"extract_terms": {
    "type": "agent",
    "prompt": "Extract key terms from the contract at {{contract_doc_id}}.",
    "tools": ["fetch_document"],
    "writes": ["parties", "term_length", "governing_law", "total_value", "auto_renewal"],
    "transitions": [ { "to": "flag_clauses", "trigger": "agent" } ],
    "human_description": "Reads the contract and extracts key terms into process context."
}
```

See [Agent nodes](/processes/agent-nodes) for the full contract. Key rules:

- Agent nodes do **not** get process-control tools such as `set_context`, `transition_to`, `skip_node`, `continue_process`, `retry_node`, or `fail_process`. Those are top-level orchestrator tools; worker agents produce structured output instead.
- If a node declares writes but the child conversation returns no parseable schema-matching output, the engine fails the node rather than silently advancing.

## `process`

Starts another process workflow and waits for it to complete. Use this for reusable subprocesses such as invoice review, document intake, or vendor onboarding. The child run gets its own process history, artifacts, gates, and human tasks.

```json
"review_invoice": {
    "type": "process",
    "process": "65f000000000000000000000",
    "input": {
        "invoice": "{{invoice}}"
    },
    "returns": {
        "from": "context.decision"
    },
    "writes": ["invoice_decision"],
    "transitions": [ { "to": "route_invoice" } ]
}
```

Inline definitions are also supported:

```json
"review_invoice": {
    "type": "process",
    "process_definition": {
        "format_version": 1,
        "process": "invoice_review",
        "initial": "extract",
        "context": {
            "schema": { "type": "object", "additionalProperties": true },
            "initial": {}
        },
        "nodes": {
            "extract": { "type": "agent", "writes": ["decision"], "transitions": [ { "to": "done" } ] },
            "done": { "type": "final" }
        }
    },
    "input": { "invoice": "{{invoice}}" },
    "writes": ["invoice_review"]
}
```

- **`process`** — stored process definition id, built-in `sys:*` process, or installed app process such as `app:contracts:contract_review`.
- **`process_version`** — optional published version to pin when `process` references a stored process history. Omit to use the latest head.
- **`process_definition`** — inline `ProcessDefinitionBody`; useful for local reusable building blocks. Pair with `process: "tmp:"` only when you need a temporary id for the inline definition.
- **`run_type`** — `programmatic` by default; `supervised` is allowed for child processes that need an orchestrator mode.
- **`input`** — merged over the child process definition's initial context.
- **`returns.from`** — path to read from the completed child state, commonly `context.some_field`.
- **`returns.context`** — list of child context fields to return when a single `from` path is not enough.
- **Writes**: declared in `node.writes`; populated from the selected child output.

## `human_task`

Pauses the process until a person submits an answer via the Task Inbox. The `task` definition describes what the reviewer sees.

```json
"legal_review": {
    "type": "human_task",
    "task": {
        "title": "Legal Review Required: {{parties}}",
        "description": "Please review and submit your decision.",
        "assignee": "group:legal",
        "fields": [
            { "name": "legal_decision", "type": "select", "required": true, "options": ["approve", "reject", "request_edits"] },
            { "name": "legal_notes", "type": "text", "required": false }
        ]
    },
    "writes": ["legal_decision", "legal_notes"],
    "transitions": [
        { "to": "store_output",   "guard": { "==": [{ "var": "legal_decision" }, "approve"] } },
        { "to": "rejected",        "guard": { "==": [{ "var": "legal_decision" }, "reject"] } },
        { "to": "flag_clauses",    "guard": { "==": [{ "var": "legal_decision" }, "request_edits"] } }
    ]
}
```

Details:

- `task.title` and `task.description` support `{{var}}` placeholders that are expanded against the process context at task creation time and stored expanded on the task. A new task (new context) will render fresh values.
- `assignee` must be either `group:` or a concrete user id. `role:` is not supported.
- When the task is submitted, each answer field is copied into context following `node.writes`; then the guarded transitions pick the next node.

## `condition`

Pure routing. Evaluates `branches` in order, picks the first whose `when` rule passes, or falls through to the branch marked `default: true`. Every condition node **must** declare either a matching `when` for all possible states or a `default` branch, otherwise runtime fails.

```json
"risk_route": {
    "type": "condition",
    "branches": [
        { "to": "legal_review",
          "when": {
              "or": [
                  { ">": [{ "var": "total_value" }, 50000] },
                  { "==": [{ "var": "has_critical_flag" }, true] }
              ]
          }
        },
        { "to": "auto_approve", "default": true }
    ]
}
```

Guards and branch rules use [JSON Logic](https://jsonlogic.com/) with a Vertesia-specific extension: `artifact_exists: { var: "some_path" }` resolves to `true` if an artifact with that path exists on the run.

## `foreach`

Runs a child node body once per item in a context array. The child can be a `tool`, `interaction`, `agent`, or `process` node.

```json
"process_each_line": {
    "type": "foreach",
    "foreach": "invoice_lines",
    "as": "line",
    "node": {
        "type": "interaction",
        "interaction": "ClassifyLine",
        "input": { "line": "{{line}}" },
        "writes": ["gl_code", "amount"]
    },
    "collect": "classified_lines",
    "failure_policy": "collect_errors",
    "writes": ["classified_lines"]
}
```

- **`foreach`** — context path to an array (max 1000 items).
- **`as`** — the variable name each item is exposed as inside the child node's context.
- **`item_id`** — optional template for a stable per-item id, e.g. `{{invoice.id}}`.
- **`max_concurrency`** — optional positive integer cap. If unset, all items are launched together.
- **`collect`** — context key to accumulate per-item results into.
- **`failure_policy`** — `fail_fast` (default) or `collect_errors` to keep the survivors and return error info for the failures.

For subprocess fanout, use `node.type: "process"`:

```json
"review_invoices": {
    "type": "foreach",
    "foreach": "invoices",
    "as": "invoice",
    "item_id": "{{invoice.id}}",
    "max_concurrency": 25,
    "node": {
        "type": "process",
        "process": "65f000000000000000000000",
        "input": { "invoice": "{{invoice}}" },
        "returns": { "from": "context.decision" }
    },
    "collect": {
        "into": "invoice_results",
        "include": ["status", "index", "item_id", "output", "error", "child_run_id"]
    },
    "failure_policy": "collect_errors",
    "writes": ["invoice_results"]
}
```

String `collect` keeps the compact form. Object `collect` returns an envelope for each item. The common fields are `status`, `index`, `item_id`, `item`, `output`, `context_update`, `error`, `child_run_id`, `child_workflow_id`, and `child_workflow_run_id`.

The nested `node` is task-like leaf work only: use `tool`, `interaction`, `agent`, or `process`, and keep routing on the parent `foreach` node.

## `branch`

Use `branch` for structured split/join semantics:

- a fixed named set of branches
- each branch usually modeled as a child subprocess or inline child process definition
- the engine launches all branches
- the parent waits for all of them, then continues

That is the clean mapping for BPMN structured parallel split/join. It is intentionally different from collection fanout:

- `foreach` means "run the same child body once per item"
- `branch` means "run these named branches and join"

Each branch child is also task-like leaf work only: use `tool`, `interaction`, `agent`, or `process`, and keep routing on the parent `branch` node.

```json
"review_tracks": {
    "type": "branch",
    "join": "all",
    "branches": [
        {
            "id": "legal",
            "title": "Legal Review",
            "node": {
                "type": "process",
                "process": "legal_review_subprocess"
            }
        },
        {
            "id": "finance",
            "title": "Finance Review",
            "node": {
                "type": "process",
                "process": "finance_review_subprocess"
            }
        }
    ],
    "collect": {
        "into": "review_results",
        "include": ["status", "branch_id", "branch_title", "output", "error"]
    },
    "writes": ["review_results"],
    "transitions": [{ "to": "consolidate" }]
}
```

## `final`

Terminal state. Entering a `final` node completes the process run.

```json
"done":      { "type": "final" },
"rejected":  { "type": "final" }
```

Use multiple final nodes when you want the run's final status to carry meaning (e.g. `done` vs `rejected`), which is visible in the run graph.