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

# Conversation flows: route every agent turn through a versioned node graph

> Give an AI agent an in-conversation routing map — per-node instructions that replace the system prompt per turn, keyword and regex edge conditions, validation at save, deterministic replay, and experiment pin-in.

# Conversation flows

A conversation flow is an AI agent's **in-conversation routing map**. Attach one under an agent's `config.conversation_flow` and every turn picks an active **node** whose instructions replace the single static base system prompt that turn — instead of relying on one prompt for the whole conversation. This is the multi-state routing model most operators know from voice agents, available on any chat or voice agent in Orbit.

Classic [Flows](/flows/overview) route *contacts* through channel automations (send, delay, branch, run an agent). A conversation flow routes the *conversation itself* between named states once the agent has taken over.

## Concept

* Each node carries its own `instructions` — the per-state prompt that becomes the active prompt segment while that node is selected.
* Edges carry routing conditions evaluated against the customer's latest message (**first match wins**).
* When no condition matches, the turn either falls through a node's unconditional edge or stays on the current node — the conversation never resets mid-flow.
* A flat routing preamble is prepended ahead of your base persona/policy prompt: *"This agent is a multi-state conversation flow. Stay on the ACTIVE NODE's instructions; branch to one of the fallback nodes only when the customer's message matches its condition."* — the base prompt stays in place below it.

Limits per agent: **64 nodes, 16 edges per node, 30 000 characters of node instructions, 8 fallback ids**.

## Graph anatomy

The persisted shape, stored verbatim in `config.conversation_flow`:

```json theme={null}
{
  "version": 2,
  "entry": "triage",
  "nodes": [
    {
      "id": "triage",
      "label": "Triage",
      "instructions": "Classify the request as billing or technical before answering.",
      "edges": [
        { "to": "billing", "condition": "billing,invoice,payment", "label": "Billing intent" },
        { "to": "tech", "condition": "error,bug,not working", "label": "Tech intent" }
      ]
    },
    { "id": "billing", "instructions": "Handle billing: cite invoices, refunds, plan changes." },
    { "id": "tech", "instructions": "Handle technical issues: ask for the error text first." }
  ],
  "fallbacks": ["general"]
}
```

Field semantics:

| Field                  | Meaning                                                                                                                                         |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`              | Operator-authored monotonic revision. Absent ⇒ treated as `1`. An [A/B experiment snapshot](#versioning--experiments) pins a specific revision. |
| `entry`                | The node every conversation starts on. Defaults to the first node when omitted.                                                                 |
| `nodes[].id`           | Unique node id — `[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}`. Duplicate ids are rejected at save.                                                          |
| `nodes[].label`        | Optional display name shown on the studio canvas. Defaults to the id.                                                                           |
| `nodes[].instructions` | The prompt segment active while the turn runs on this node (1–30 000 chars, required).                                                          |
| `edges[].to`           | Target node id. Edges to unknown ids are rejected at save.                                                                                      |
| `edges[].condition`    | Optional routing condition; omit it for an unconditional (fallback-branch) edge. See [Conditions](#conditions).                                 |
| `edges[].label`        | Optional human label ("Billing intent") shown alongside the edge in the studio.                                                                 |
| `fallbacks`            | Up to 8 node ids offered to the model as alternates when no edge condition matches.                                                             |

### Conditions

A condition is matched **case-insensitively** against the customer's latest message, in one of three shapes:

| Shape                    | Example                       | Matches when                                                       |                                            |                            |
| ------------------------ | ----------------------------- | ------------------------------------------------------------------ | ------------------------------------------ | -------------------------- |
| Single keyword           | `"refund"`                    | The message contains `refund` (any case)                           |                                            |                            |
| Comma-separated keywords | `"billing, invoice, payment"` | Any keyword appears (commas **and** pipes both delimit — \`billing | invoice                                    | payment\` is the same set) |
| Regex                    | \`/refund                     | chargeback/i\`                                                     | The pattern tests true against the message |                            |

An edge with a missing or empty condition is **unconditional** — it is the node's fallback branch, taken only when no condition edge fires. A `/pattern/flags` string that fails to compile is treated as a keyword list instead, so a bad regex cannot stall routing.

The evaluation order per turn: walk the active node's edges in declaration order; the **first condition that matches wins**. If nothing matches, take the unconditional edge if one exists; otherwise stay on the active node.

### Fallbacks

`fallbacks` names nodes the model may branch to when the active node itself has no answer. Fallback entries that name the entry node are deduplicated away. The composed system prompt lists fallbacks explicitly as `[FALLBACKS — branch ONLY when one of these matches]` with their instructions plus routing conditions, so the model can swap states mid-conversation without inventing a node that is not on the graph.

## Validation contract

Save validation rejects the dangerous classes **up front**, with one issue per path so the API error names exactly which edge or fallback is wrong:

* Duplicate node ids — `nodes` — `Duplicate conversation-flow node id…`.
* `entry` that names a node not on the graph — `entry`.
* An edge whose `to` names an unknown node — `nodes[i].edges[j].to`.
* A fallback id that names an unknown node — `fallbacks`.

Two limits apply at save too: no more than 64 nodes and 16 edges per node; each instruction is capped at 30 000 chars and each condition at 500 chars.

**Malformed saved rows degrade cleanly.** Conversation-flow config is a JSONB config-bag value; a row that fails the schema (for example an empty `nodes` array, a number where a string belongs, or a malformed edge) is *not* fatal at run time. The executor re-validates the blob when it builds the turn, treats any failure as **undefined**, and the agent runs exactly as it did before flows existed — on its base system prompt alone. A bad save can never block a customer-facing turn; at worst, the routing layer disappears until the config is corrected.

## Versioning & experiments

The flow graph is versioned through **agent versions** and **A/B experiments**:

1. Bump `version` in `config.conversation_flow` (2, 3, …) for a meaningful revision.
2. Save an **agent version snapshot** (`POST /agents/:id/versions`) — the snapshot captures the whole agent config, including the flow blob.
3. Run that candidate as an experiment variant; the variant's snapshot carries `conversation_flow` verbatim, so the experiment **pins a specific flow revision** and an A/B winner promotes the candidate's flow along with its prompt.

Everything on this surface is **tenant-owned** — the graph, the snapshots, and the experiment pins live in your workspace; nothing crosses tenant boundaries.

## Deterministic replay

Every turn **replays** the flow from the `entry` node over the conversation's user messages (chronological order, current turn appended). Resolution is deliberately stateless — no per-pod map, no shared cache — so:

* Any replica recomputes the **identical active node** for a given history.
* Both turn paths — `run` and `streamTurn` — produce the same routing.
* Voice and chat agents **share branch semantics**: the same graph routes a phone call and a chat session the same way.

A turn that no edge matches **stays on the active node** instead of hopping back to `entry`, so a conversation does not reset mid-flow.

### Testing a route before you send

The same logic powers a **simulate** step: pick an active node, supply a hypothetical customer message, and resolve which node *would* activate — without mutating any conversation state. Prove a branch set before traffic ever sees it.

## Dashboard surface

Two editors in the dashboard are built around conversation flows:

* **[Agent Studio](/agents/conversations) (`/agents/studio`)** — the visual canvas where you draw nodes, drag edges, and edit per-node instructions with their conditions. The same graph is what save-validation and the runtime executor consume.
* **[Conversations](/agents/conversations) (`/agents/conversations`)** — per-conversation transcripts. When an agent has a flow attached, the transcript shows the same deterministic routing the executor used, so a mis-routed branch is inspectable per turn against the customer's message.

Conversation flows are edited and versioned alongside the rest of the agent config; see [Agent versions](/agents/agent-versions) for how snapshots and experiments pin a revision.

## Worked examples

### Billing / tech triage

The overview-page sketch expanded into a complete graph — an unconditional edge plus one fallback keeps the agent on the rails when the customer mentions something outside both intents:

```json theme={null}
{
  "version": 2,
  "entry": "triage",
  "nodes": [
    {
      "id": "triage",
      "label": "Triage",
      "instructions": "Classify the request as billing or technical. Answer only once classified.",
      "edges": [
        { "to": "billing", "condition": "billing, invoice, payment, charge, refund", "label": "Billing intent" },
        { "to": "tech", "condition": "/error|bug|not working|crash|5\\d\\d/i", "label": "Tech intent" }
      ]
    },
    {
      "id": "billing",
      "instructions": "Handle billing: cite invoice numbers, quote the plan table, explain proration."
    },
    {
      "id": "tech",
      "instructions": "Handle technical issues: ask for the exact error text, reproduce from the customer's words."
    },
    {
      "id": "general",
      "instructions": "Handle anything outside billing and technical scope politely; bounce the customer back to triage.",
      "edges": [{ "to": "triage" }]
    }
  ],
  "fallbacks": ["general"]
}
```

Turn flow: *"My invoice looks wrong"* — keyword `invoice` fires on the first edge, the turn runs on `billing`. *"The app keeps crashing"* — the tech regex fires, the turn runs on `tech`. *"What are your hours?"* — no condition matches, the triage node has no unconditional edge, so the conversation **stays on `triage`**; the model can still use the `general` fallback.

### Escalation with fallbacks

An escalation graph where unresolved or unhappy customers move to an escalation node with an explicit human-handoff fallback:

```json theme={null}
{
  "version": 1,
  "entry": "support",
  "nodes": [
    {
      "id": "support",
      "label": "Support",
      "instructions": "Solve the issue in this conversation. Escalate only when the customer explicitly asks or the instructions say to.",
      "edges": [
        { "to": "escalation", "condition": "human, agent, representative, supervisor", "label": "Explicit human request" },
        { "to": "escalation", "condition": "/cancel|lawsuit|chargeback|angry|furious/i", "label": "High-stakes signal" }
      ]
    },
    {
      "id": "escalation",
      "label": "Escalation",
      "instructions": "Summarize the issue, what was tried, and hand off with the pre-set escalation message. Do not re-diagnose."
    },
    {
      "id": "closing",
      "label": "Closing",
      "instructions": "Confirm resolution and close politely."
    }
  ],
  "fallbacks": ["escalation"]
}
```

Turn flow: *"I want a supervisor"* — an explicit human request fires, the turn activates `escalation`. *"I'll file a chargeback"* — the high-stakes regex fires. Anything else stays on `support`; the unconditional fallback pool still offers `escalation` when the support node itself cannot answer.

## See also

* [Flows overview](/flows/overview) — channel-facing flows that trigger an agent, versus the in-conversation routing this page covers.
* [Creating agents](/agents/creating-agents) — the base agent config the flow attaches to.
* [Agent versions](/agents/agent-versions) — snapshots that pin a flow revision.
* [Continuous production evals](/agents/continuous-production-evals) — graded replays against a pinned version.
