# Flow graphs (agent_config.flow_graph)

> How the Voice AI flow canvas saves a call graph, how the node-spec catalog drives the palette and the step form, which graph rules block a publish, and when a saved graph reaches a live call.

A voice agent can run in one of two ways. Either it runs as a
**single-model agent**, straight from its profile settings, or it runs a
**flow graph** — an explicit, step-by-step call graph that you draw on the
Flow canvas.

The canvas writes that graph to the `flow_graph` column (jsonb) of the
agent's `agent_config` row. The TeleQuick control plane's
`pipelineFromFlowGraph()` prefers a saved graph over the synthesized
ASR→LLM→TTS chain, so an agent with a saved graph runs exactly what is
drawn on the canvas rather than a pipeline inferred from its profile.

## The saved shape

`agent_config.flow_graph` holds a single object:

| Field         | Type                                        | Meaning                                                                 |
| ------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
| `entry_node`  | `string`                                    | The key of the step the call starts on.                                  |
| `nodes`       | `Record<string, Record<string, unknown>>`   | One entry per step. The key is the step id; the value is its config.     |
| `positions`   | `Record<string, { x, y }>` (optional)       | Canvas coordinates. Console-only.                                       |

`positions` exists for the editor. Everything else is forwarded to the
runtime. If a step has no saved position — a graph written by something
other than the canvas, or a step added after the last layout — the editor
lays the unpositioned steps out in a row so nothing is hidden off-canvas.

```json
{
  "entry_node": "greet",
  "positions": {
    "greet": { "x": 60, "y": 80 },
    "think": { "x": 280, "y": 80 }
  },
  "nodes": {
    "greet": { "node_type": "<name from the node-spec catalog>", "next_node": "think" },
    "think": { "node_type": "<name from the node-spec catalog>" }
  }
}
```

Two invariants matter:

- **`node_type` selects the spec.** Every node config carries a
  `node_type` string that must match the `name` of a spec in the
  node-spec catalog. The editor resolves the spec by that name to know
  what to render and what to validate.
- **Step ids are wire keys.** The runtime routes on the keys of `nodes`,
  so they must be unique and stable. When you add a step, the editor
  seeds the id from the spec's name and only appends a numeric suffix if
  that id is already taken. Renaming a step changes the key the runtime
  routes on.

## Where the palette comes from

The palette is not a fixed list in the console. It is the server's
**node-spec catalog**, fetched at screen load. Each spec declares its
`name`, `displayName`, `description`, a `category`, a list of
`properties`, and optional `graphConstraints`.

Three things follow from the spec, not from the console:

- **The palette groups.** Steps are grouped by the `category` the spec
  declares.
- **New-step defaults.** When you add a step, the editor writes
  `node_type` plus every property that declares a `default`. Properties
  without a default start unset.
- **The per-step form.** The inspector walks the spec's `properties` and
  applies the spec's display rules. There is no per-node-type form in the
  console.

The builder renders these category groups, in this order:

| Category       | Palette heading |
| -------------- | --------------- |
| `call_control` | Call control    |
| `media`        | Audio           |
| `reasoning`    | Thinking        |
| `bridge`       | External        |

A group with no specs in it is not rendered. The category list is kept
deliberately in step with the catalog's own vocabulary: a category the
server never sends would otherwise render as an empty heading. IVR menu
routing is not exposed in the builder — keypad capture lives under call
control.

Some properties reference other objects rather than holding a literal
value. The editor resolves those against two catalogs:

- **Tools** come from `admin.listAgentTools({ orgId, agentId })`, which
  returns the agent's `agent_tool` rows (`kind`, `name`, `description`,
  `silent`, `spec`) ordered by creation. The dropdown uses the tool name
  as the value and shows its `kind`.
- **Step targets** are the other steps currently on the canvas, computed
  live. Rename a step and every "next step" dropdown updates at once.

## One successor per step: how next_node behaves

A step's successor is a single field, `next_node`, holding one step id.
There is no branching fan-out in the saved shape.

The canvas enforces that in both directions:

- **Loading.** Edges are derived from the saved graph. For each node with
  a non-empty string `next_node`, the editor draws one edge from that node
  to the named target.
- **Connecting.** Dragging a new edge out of a step first removes any
  existing edge from that step, then adds the new one and writes
  `next_node` on the source. A second outgoing edge would silently
  overwrite the first on save, so it replaces instead.
- **Editing in the inspector.** Changing `next_node` in the step form
  re-derives that step's edge. Clearing it removes the edge.

A step whose spec marks it terminal takes no successor at all (see
below).

## Graph constraints

A spec may declare a `graphConstraints` object. These are the graph-shape
rules, and they live on the server so the editor and the runtime cannot
drift apart.

| Constraint     | Effect in the editor                                                                                     |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| `maxOutgoing: 0` | The step is **terminal**. It renders without a source handle, cannot be connected onward, and is not required to have a `next_node`. |
| `maxIncoming: 0` | The step renders without a target handle, so nothing can be connected into it.                         |
| `maxInstances`   | Caps how many steps of that `node_type` may appear in one graph. Exceeding it blocks the publish.      |

Constraints that a spec does not declare are not enforced by the editor.

## What blocks a publish

**Save & publish** is disabled while a save is in flight, while there is
nothing to save, and while the graph has any of the problems below. Each
problem is listed above the canvas with the offending step id:

- **No start step.** `entry_node` is unset, or it names a step that is
  not on the canvas.
- **An unknown step type.** A node's `node_type` does not match any spec
  in the catalog this build loaded. The node renders with its type or id
  rather than a blank box, marked invalid, and the inspector says the
  build does not know that type.
- **A step property fails its spec contract.** The editor runs the spec's
  full validation — required, min/max, URL shape, max length — over the
  properties the display rules actually show for that node.
- **A non-terminal step with no successor.** Reported as "the call would
  stop there".
- **An instance limit exceeded.** More steps of one type than that spec's
  `maxInstances` allows.

If the catalog itself cannot be loaded, the screen shows a load error
instead of an editor — there is no fallback palette. If the loaded
catalog version does not match what this build expects, a warning banner
appears above the editor and editing continues.

## Save, then hydrate

Publishing is two writes, and the screen performs both in order:

1. `adminAgents.saveFlowGraph({ orgId, agentId, flowGraph })` persists the
   `SavedFlowGraph` object to `agent_config.flow_graph`.
2. `admin.hydrateAgent({ orgId, agentId })` hydrates and publishes the
   agent's runtime config.

Step 2 is not optional. Until the config is hydrated, the engine still
answers calls with the **previous** graph. A row write alone changes
nothing that a caller hears.

`hydrateAgent` returns:

| Field               | Meaning                                                                 |
| ------------------- | ----------------------------------------------------------------------- |
| `ok`                | The hydrate call completed.                                             |
| `ready`             | Whether the agent can actually run.                                     |
| `missing_providers` | Providers with no usable credentials for this org.                      |
| `hydrated_at_ms`    | The hydration timestamp, returned so the UI can prove a roundtrip happened. |

If `ready` is `false`, the canvas reports that the graph saved but the
agent cannot run yet, naming the providers from `missing_providers`. The
graph is on disk and hydrated; the steps that need those credentials
cannot execute.

Every hydrate is recorded in the audit trail as action `hydrate` on
resource type `agent_config`, carrying `hydrated_at_ms` and `ready`. That
is what lets an operator later correlate "I hit Save, and the agent config
was hydrated shortly after".

## Agents with no flow graph

An agent whose `flow_graph` is null or has no `nodes` opens on an empty
canvas with an explanatory overlay rather than a blank grid. That is a
valid, working state: the agent runs as a single-model agent, straight
from its profile settings, on the synthesized pipeline.

Adding a step from the palette starts a multi-step flow. The first step
you add becomes the start step if none is set yet. Nothing changes for
live calls until you save and publish — and once a graph is published,
`pipelineFromFlowGraph()` prefers it, so the profile-derived pipeline is
no longer what runs.

Removing every step and publishing an empty graph is not something the
editor will publish: a graph with steps requires a valid start step, and
the save button stays disabled while problems remain.

## Related

- [Modalities overview](/modalities/overview)
- [Telemetry](/platform/telemetry) — `agent_id` on CDRs, set when a call routed through an agent graph
