API reference
Graph agents
Validate a graph agent's nodes and edges before saving it, and test it with scripted callers.
A graph agent is an agent whose conversation is a set of nodes joined by edges, rather than one prompt. You create and update one through the agent endpoints, with llm_agent.agent_type set to graph_agent and the graph under llm_agent.llm_config. Saving an agent with a broken graph is refused. This page has the extra endpoints: one checks a graph and lists every problem without saving anything, and the workflow tests run scripted callers through a saved graph. The graph agents guide describes node and edge types.
| Operation | Method and path | Minimum role |
|---|---|---|
| Validate a graph | POST /graph-agent/validate | member |
| List scenarios | GET /graph-agent/{agent_id}/tests/scenarios | viewer |
| Create a scenario | POST /graph-agent/{agent_id}/tests/scenarios | member |
| Get a scenario | GET /graph-agent/{agent_id}/tests/scenarios/{scenario_id} | viewer |
| Replace a scenario | PUT /graph-agent/{agent_id}/tests/scenarios/{scenario_id} | member |
| Delete a scenario | DELETE /graph-agent/{agent_id}/tests/scenarios/{scenario_id} | member |
| Suggest scenarios | POST /graph-agent/{agent_id}/tests/suggest | member |
| Start a test run | POST /graph-agent/{agent_id}/tests/runs | member |
| List test runs | GET /graph-agent/{agent_id}/tests/runs | viewer |
| Get a test run | GET /graph-agent/{agent_id}/tests/runs/{run_id} | viewer |
Validate a graph
POST /graph-agent/validate
Returns the errors and warnings for a graph. Errors would block saving the agent; warnings would not.
Auth: API key or session token. Role: member and up.
Send one of these shapes:
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
agent_config | body | object | one of | A whole agent configuration, as you would send it when creating an agent. Its tools are read from the configuration. |
llm_config | body | object | one of | Only the graph: nodes, current_node_id and the rest of the graph settings, such as global_edges (the safety transitions from every node). |
api_tools | body | object | no | With llm_config: {"tools": [{"name": "..."}]}, the tool names that nodes may reference. |
Request
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",
"prompt": "Greet the caller and find out what they need.",
"edges": [{"to_node_id": "ghost", "condition": "Caller wants order status"}]
},
{"id": "closing", "prompt": "Thank the caller and say goodbye.", "edges": []}
]
}
}'Response 200 OK
{
"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": "unreachable_node",
"message": "Node 'closing' has no transitions pointing to it",
"node_id": "closing"
}
]
}A graph with no problems returns {"valid": true, "errors": [], "warnings": []}. valid is true whenever errors is empty, even if there are warnings.
Each issue has severity, code and message, and where they apply node_id and path (a dotted path into the graph, such as nodes.1.edges.0). Codes you may see include:
| Code | Severity | Meaning |
|---|---|---|
no_nodes | error | The graph has no nodes. |
invalid_node | error | A node is not an object. |
missing_node_id | error | A node has no id. |
duplicate_node_id | error | Two nodes share an id. |
no_start_node | error | current_node_id is missing or names no node. |
invalid_node_type | error | A node's node_type is not one the platform knows. |
invalid_edges | error | A node's edges is not a list of objects. |
missing_transition_target | error | An edge, or a global edge, points to a node that does not exist. |
invalid_condition_type | error | An edge's condition_type is not one the platform knows. |
missing_event_name | error | An event edge has no event_name. |
invalid_dtmf_event | error | A keypad event name is malformed. |
unreachable_tool | error | A node's function_call names a tool that is not in api_tools. |
knowledge_base_no_vectors | error | A rag_config has no knowledge base ids. |
invalid_collect_dtmf | error | A node's keypad-entry settings are invalid. |
router_speaks | error | A router node sets a prompt or a static message. |
router_no_catch_all | error | A router node has no unconditional edge. |
router_event_edge | error | A router node has an event edge. |
router_collect_dtmf | error | A router node tries to collect keypad digits. |
router_cycle | error | Routers form a loop that never reaches a speaking node. |
invalid_variable_type | error | variable_types has an unknown type. |
invalid_agent_information | error | agent_information is not a string. |
invalid_global_edges | error | global_edges is not a list of objects. |
invalid_global_edge | error | A global edge has a condition_type (only intent edges can be global) or no condition. Its path is global_edges.<n>. |
empty_prompt | warning | An LLM node has no prompt. |
static_without_message | warning | A static node has no static_message. |
empty_intent_condition | warning | An intent edge on a node with several edges has no condition. |
duplicate_priorities | warning | Two rule edges of a node share a priority. |
unreachable_node | warning | No edge leads to the node. |
no_global_safety | warning | A node has an intent edge about danger, safety, an emergency or fraud, but the graph has no global_edges: a caller on any other node cannot take it. |
Expression and edge-parameter problems have further codes of their own. Treat any code you do not recognise by its severity.
Errors
| Status | Message |
|---|---|
| 400 | agent_config is not a graph agent: set llm_agent.agent_type to "graph_agent" |
| 400 | Send agent_config, or llm_config (with nodes) |
Workflow tests
A workflow test plays a scripted caller through a saved graph agent and checks where the conversation goes. Each caller line is routed the way the dashboard's test chat routes it: the agent's model picks the transition. When the agent has no usable model, a keyword rule decides instead and the run says so (routing is keyword). Nothing is dialled, no tool is called and no audio is involved, so a scenario that passes here can still behave differently on a real call.
Scenarios
GET /graph-agent/{agent_id}/tests/scenarios lists them. POST /graph-agent/{agent_id}/tests/scenarios creates one (201); GET, PUT and DELETE /graph-agent/{agent_id}/tests/scenarios/{scenario_id} read, replace and delete one.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | body | string | yes | Up to 200 characters. |
lines | body | array of strings | yes | What the caller says, in order (1 to 30 lines). event:<name> injects an event instead of a line. |
variables | body | object | no | The values the call starts with. |
expect | body | object | yes | At least one of final_node (node id), visits (node ids that must be on the path), avoids (node ids that must not be) and values (extracted values by name). |
curl -X POST https://api.kshyamai.space/graph-agent/3c90c3cc-0d44-4b50-8822-8dd25736052a/tests/scenarios \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{
"name": "Order status, happy path",
"lines": ["Where is my order ORD-3?", "Thanks, that is all"],
"expect": {"final_node": "closing", "visits": ["collect_order"], "values": {"order_id": "ORD-3"}}
}'Suggest scenarios
POST /graph-agent/{agent_id}/tests/suggest
Proposes one scenario for each path from the start node to a closing node (at most 12). The lines come from the edge conditions, reworded by the agent's model when it is available (phrased is llm, otherwise conditions). Send {"save": true} to store the proposals whose name is not taken, and {"phrase": false} to skip the model. The response has suggestions and saved. Read suggested lines before relying on them.
Start a test run
POST /graph-agent/{agent_id}/tests/runs
Runs in the background and answers 202 with the run. One run per agent at a time (409 otherwise).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
repeats | body | integer | no | How many times each scenario is played: 1 to 10, default 3. |
scenario_ids | body | array | no | Only these scenarios. Default: all. |
safety_probe | body | boolean | no | Default true: see below. |
emergency_line | body | string | no | The caller line the safety probe says. |
agent_replies | body | boolean | no | Default false. When true the model also speaks for the agent after each turn, so routing reads both sides. Slower. |
Read a test run
GET /graph-agent/{agent_id}/tests/runs lists the last 20 runs without detail. GET /graph-agent/{agent_id}/tests/runs/{run_id} returns one in full; poll it until status is done (or error).
| Field | Meaning |
|---|---|
routing, model | llm and the model that routed, or keyword when there was none. |
summary | Counts: scenarios passed, failed and flaky, conversations passed, nodes and edges never reached, safety probes failed. |
results[] | Per scenario: status (passed, failed, or flaky when only some repeats passed), passes, repeats, pass_rate, and runs[] with the path, the extracted values, each failed expectation in failures[], and per turn the routing decisions[] (from, to, by: llm, keyword or rule, reason, and global: the position of the global edge when one was taken). |
coverage | nodes.unreached and edges.unreached: what no scenario reached. |
safety | The safety probe. mode says how the safety transitions were found. global: the graph has global_edges; an emergency line is said on every node a caller speaks on, and the conversation must reach a global edge's target in that turn (probes[], with ok; a node is probed once per kind of safety edge). wording: there are none, so intent edges whose condition is about danger, safety, an emergency or fraud count; only the nodes that can take one within a single caller turn are probed, later_only lists nodes that reach one only in a later turn, and unreachable lists nodes from which none can ever be reached: treat it as a warning and add global_edges. none: nothing to probe. In safety_edges[], a global edge has from * and type global. |