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: 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.
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: 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. 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: 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.