Guides
Graph agents
Build a conversation as nodes and edges, validate it, and read the path a call took.
A graph agent replaces one long prompt with a graph. Each node has its own prompt or fixed message, and edges decide where the conversation goes next. Use it when a call must follow steps in a fixed order: verify identity, then state the amount, then record a promise.
In the dashboard: Build → Graph Agent.
The shape
A graph agent is an ordinary agent whose llm_agent has agent_type: "graph_agent". The graph is in llm_config.
{
"agent_type": "graph_agent",
"agent_flow_type": "streaming",
"llm_config": {
"provider": "openai", "model": "gpt-4.1-mini",
"agent_information": "You are Neha from Example Bank support.",
"current_node_id": "entry",
"variable_types": {"recipient_data.current_hour": "number"},
"nodes": [
{"id": "entry", "node_type": "router", "edges": [
{"to_node_id": "vip", "condition_type": "expression",
"expression": {"conditions": [{"variable": "recipient_data.customer_tier", "operator": "eq", "value": "vip"}]}},
{"to_node_id": "welcome", "condition_type": "unconditional"}
]},
{"id": "vip", "node_type": "static", "static_message": "Welcome back. Connecting you to your manager.", "edges": []},
{"id": "welcome", "prompt": "Greet the caller and ask how you can help.", "edges": [
{"to_node_id": "collect_order", "condition": "The caller asks about an order",
"parameters": {"order_id": "string"}}
]},
{"id": "collect_order", "prompt": "Tell the caller their order status.", "function_call": "fetch_order_status", "edges": []}
]
}
}Nodes
node_type | Behaviour |
|---|---|
llm (default) | The LLM speaks from agent_information plus the node's prompt |
static | Says static_message (a string or a map of language code to text); no LLM |
router | Never speaks. Resolves at once to another node |
A node may also set function_call (a tool name from api_tools, or end_call), its own rag_config, and collect_dtmf.
Edges
condition_type | When it is taken |
|---|---|
unset or llm | Intent edge: described by condition (and label) |
expression | A rule over variables: {"logic": "and"|"or", "conditions": [{"variable", "operator", "value"}]} |
unconditional | Always. The catch-all a router needs |
event | When the named event_name arrives |
Operators: eq neq gt gte lt lte contains in not_in exists not_exists. An edge's parameters ({"name": "string"|"number"|"integer"|"boolean"}, or an object with type, description, allowed_values) are collected when the edge is crossed.
How routing works
After each caller turn, on the current node:
- Expression and unconditional edges are tried in ascending
priority(default 0). The first that holds is taken. - Otherwise the intent edges (default priority 100) are considered.
- Otherwise the conversation stays on the node.
Routers resolve immediately: expression edges, then intent edges, then the unconditional catch-all. A node with no outgoing edges ends the call after speaking, unless it is the only node.
Safety transitions from every node
A caller can report danger at any moment, but an edge belongs to one node. List the transitions that must work everywhere in llm_config.global_edges:
"global_edges": [
{"to_node_id": "emergency", "label": "Safety",
"condition": "the caller reports danger: sparks, fire, a fallen wire or an electric shock"}
]Each entry is an intent edge without a source: to_node_id and condition are required, and label, function_name, function_description and parameters work as on any edge. On every caller turn, on every node that speaks, these transitions are offered to the routing model ahead of the node's own edges, also on nodes that route by rule or have no intent edge, and they win when the caller reports what the condition describes. A transition towards the node the conversation is already on is not offered, and once one has been taken the routing model only reads the conversation from that point on, so the same report does not send the caller back. A graph without global_edges routes exactly as before. In the editor they are under Agent setup, "Safety, from every node". Use them for danger, emergencies and suspected fraud; the workflow tests probe every node for them.
Step 2 differs by where the graph runs:
| Where | How intent edges are chosen |
|---|---|
mock engine, and test chat without LLM credentials | A deterministic keyword rule: the first edge whose target node id is said in the line, else the first whose condition or label shares a word of four or more letters with it |
| Test chat with LLM credentials | The agent's model picks the edge; on any failure the keyword rule takes over |
own engine | One short LLM request offers each edge as a function, plus "stay". A slow, failed or unclear answer means stay |
Keypad input
Add collect_dtmf to a speaking node to gather digits after it speaks:
{"id": "card_check", "prompt": "Ask for the last four digits of the card on the keypad.",
"collect_dtmf": {"variable": "card_last4", "max_digits": 4, "finish_on_key": "#",
"sensitive": true, "match": "{{lookup_customer_card_last4}}", "timeout": 20},
"edges": []}The digits go into node_extracted_data[variable]. With match, <variable>_verified is set to true or false and you can route on it with an expression edge. Limits: max_digits 1 to 32, timeout above 0 and at most 120 seconds, finish_on_key one of 0-9, *, # or empty.
Event edges can react to keys: event_name: "dtmf" (any key) or "dtmf:5" (one key).
Validate before saving
POST /graph-agent/validate returns every problem at once. Send the whole agent_config, or llm_config with an optional api_tools.
curl -X POST https://api.kshyamai.space/graph-agent/validate \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"llm_config": {"current_node_id": "welcome", "nodes": [
{"id": "welcome", "edges": [{"to_node_id": "ghost", "condition": "anything"}]}]}}'It answers 200 even when the graph is invalid:
{
"valid": false,
"errors": [{"severity": "error", "code": "missing_transition_target",
"message": "Node 'welcome': edge 0 points to unknown node 'ghost'",
"node_id": "welcome", "path": "nodes.0.edges.0"}],
"warnings": [{"severity": "warning", "code": "empty_prompt",
"message": "LLM node 'welcome' has no prompt", "node_id": "welcome", "path": "nodes.0.prompt"}]
}Error codes: no_nodes, invalid_node, missing_node_id, duplicate_node_id, no_start_node, invalid_agent_information, invalid_variable_type, knowledge_base_no_vectors, invalid_node_type, invalid_edges, unreachable_tool, invalid_collect_dtmf, router_collect_dtmf, router_speaks, router_no_catch_all, router_event_edge, router_cycle, missing_transition_target, invalid_condition_type, invalid_expression, invalid_operator, missing_event_name, invalid_dtmf_event, bad_transition_parameters, reserved_parameter_name, invalid_global_edges, invalid_global_edge.
Warning codes: unreachable_node, empty_prompt, static_without_message, empty_intent_condition, duplicate_priorities, no_global_safety.
Saving an agent runs the same checks and refuses the first error with 400 and Graph agent: <message>. Warnings never block a save. The endpoint itself answers 400 only when the body has neither key, or agent_config is not a graph agent: set llm_agent.agent_type to "graph_agent".
Test chat state
POST /agent-chat/{agent_id} returns a state object for graph agents. Send it back on the next turn so the chat knows where it is:
{"current_node_id": "welcome", "path": ["entry", "welcome"], "node_extracted_data": {},
"node_turns": 1, "total_turns": 1, "ended": false}Without state, the earlier user messages are routed again to find the node; no tool runs twice.
What a call reports
A graph call's execution, and its webhooks, carry two extra fields:
node_path: node ids in the order visited, routers included.node_extracted_data: the values edges and keypad entries collected, latest per key.
Each routing decision is also a line in the call's raw logs, component routing. See Post-call data.
Limitations
- On real calls, event edges other than keypad events never fire: nothing delivers outside events to the engine yet. The
mockengine can inject one with a script lineevent:<name>. - On the
ownengine the routing confidence is always reported as 1.0, and a turn on a node with intent edges waits for the routing answer before the reply starts. - The
mockengine does not replay a node after silence and does not use a node's own knowledge base.