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.

json
{
  "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_typeBehaviour
llm (default)The LLM speaks from agent_information plus the node's prompt
staticSays static_message (a string or a map of language code to text); no LLM
routerNever 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_typeWhen it is taken
unset or llmIntent edge: described by condition (and label)
expressionA rule over variables: {"logic": "and"|"or", "conditions": [{"variable", "operator", "value"}]}
unconditionalAlways. The catch-all a router needs
eventWhen 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:

  1. Expression and unconditional edges are tried in ascending priority (default 0). The first that holds is taken.
  2. Otherwise the intent edges (default priority 100) are considered.
  3. 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:

json
"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:

WhereHow intent edges are chosen
mock engine, and test chat without LLM credentialsA 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 credentialsThe agent's model picks the edge; on any failure the keyword rule takes over
own engineOne 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:

json
{"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.

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

json
{
  "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:

json
{"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 mock engine can inject one with a script line event:<name>.
  • On the own engine 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 mock engine does not replay a node after silence and does not use a node's own knowledge base.