---
title: "Agent nodes"
source: "https://docs.vertesiahq.com/processes/agent-nodes"
markdown: "https://docs.vertesiahq.com/llms/processes/agent-nodes.md"
---

# Agent nodes

An **agent node** delegates an open-ended sub-task to a child conversation workflow that can call tools. The parent process still owns control flow: the agent is a worker, not an orchestrator. This page covers the exact contract between the engine and the agent so you can author agent nodes that actually behave.

## The contract, in one sentence

> Given a prompt, a result schema, and a tool set, produce a single JSON object matching the schema. The engine does the rest.

The agent does **not** pick the next node via tool calls, does **not** write context via tool calls, and does **not** decide its own termination. It produces a value.

## What the engine provides to the agent

When the engine dispatches an agent node, it injects the following into the child conversation:

1. **A system/user prompt** (`sys:ProcessAgentNode` interaction by default) containing:
    - The process name and description.
    - The node id, the node's `prompt`, and the node's declared writes.
    - The current process context (serialized).
    - The available agent-triggered transitions (if any).
    - A human-readable description of the result schema.
2. **A derived `result_schema`** — a JSON Schema built from `node.writes` filtered through `process.context.schema.properties`. If the node has more than one agent-triggered transition, the schema also requires a `_next_node` field whose enum is the set of declared targets.
3. **The declared tools** from `node.tools`, plus any `learn_*` skills. Tools declared at node level are added to whatever the default tool set contains.

Agents do **not** receive process-control tools such as `set_context`, `transition_to`, `skip_node`, `continue_process`, `retry_node`, or `fail_process` — those belong to the higher-level supervised orchestrator and would be rejected by the result schema anyway.

## What the agent must return

A single JSON object that matches the result schema. Llumiverse drivers honour `result_schema` via native structured output when available, and the conversation workflow returns that structured block in its final output.

For a node with `writes: ["parties", "total_value"]` and a single agent transition `flag_clauses`, the schema is roughly:

```json
{
    "type": "object",
    "properties": {
        "parties": { "type": "string" },
        "total_value": { "type": "number" }
    },
    "required": ["parties", "total_value"],
    "additionalProperties": false
}
```

And a valid final response:

```json
{ "parties": "ACCOR SA and Vertesia SAS", "total_value": 97500 }
```

The engine auto-transitions to `flag_clauses` because the node has exactly one agent-triggered transition.

For a node with two agent-triggered transitions, the schema additionally requires `_next_node`:

```json
{
    "type": "object",
    "properties": {
        "classification": { "type": "string" },
        "_next_node": { "type": "string", "enum": ["human_review", "auto_publish"] }
    },
    "required": ["classification", "_next_node"],
    "additionalProperties": false
}
```

The `_next_node` value picks the transition.

## What happens on the parent side

1. The parent reads the child conversation's final output and looks for a structured block (`type: "json"` from the driver, or a JSON-parseable text block as a fallback).
2. It splits the result into `{ _next_node, ...writes }`.
3. It applies `writes` through the validator — rejecting anything outside `node.writes` and anything that violates the context schema.
4. It picks the transition:
    - If `_next_node` is present, that target must be a declared agent transition (or the engine errors).
    - Otherwise, if there's exactly one agent-triggered transition, it's auto-picked.
    - Otherwise, there's no target and the node fails.
5. It checkpoints context to Mongo and moves on.

## Failure behavior

The engine fails the node rather than auto-advancing when:

- A schema was declared but the child returned no parseable structured output matching it.
- The parsed `_next_node` is not in the declared enum.
- The returned writes violate `process.context.schema` or reference fields outside `node.writes`.

A failing node leaves the run in `failed` status with the node history entry marked `completed` up to the point of exit (so you can inspect what the child conversation actually did in the **Conversation** tab).

## Tools and skills

Declare tools the agent needs in `node.tools`:

```json
"extract_terms": {
    "type": "agent",
    "tools": ["fetch_document"],
    "writes": ["parties", "term_length"],
    ...
}
```

Skills (`learn_*`) are available by default so the agent can self-unlock capabilities. If you want a leaner tool set, explicit names without a `+`/`-` prefix replace the default; names prefixed with `+` are added to it, `-` removes.

## Artifact isolation

Each agent-node child conversation is launched with `launch_id = "-"`. That maps to a per-node artifact namespace under `agents//workstreams/<launch_id>/...` so sibling nodes never overwrite each other's `tools.json`, `conversation.json`, or generated artifacts.

## Authoring tips

- Always fill in `human_description` so the observability view can tell a human reader what the node does.
- Keep `node.writes` tight — the agent only sees and emits these fields, which both reduces hallucination surface and makes validation strict.
- Prefer single-transition nodes; push branching into dedicated `condition` nodes. Multi-transition agent nodes are harder to reason about and require the agent to decide routing.
- Reference context fields in `node.prompt` using `{{field}}` — the engine expands inputs against the context before dispatch. Missing fields throw at template time, so only reference fields guaranteed to exist.