Blog · 13 October 2026 · 3 min read
Building a graph agent for a payment reminder
When one prompt is not enough, lay the call out as nodes and edges. A small worked example, and what the engine does with it.
A single prompt is a fine way to run a conversation until the order of steps starts to matter. A payment reminder is a good example: confirm who you are talking to, state the amount, then do one of three different things depending on the answer. Ask a model to hold all of that in one prompt and it will mostly comply. "Mostly" is the problem.
A graph agent makes the structure explicit. The conversation is a set of nodes, joined by edges, and the engine keeps track of which node the call is on.
The pieces
A graph agent is an ordinary agent whose llm_agent has agent_type: "graph_agent". Three kinds of node cover almost everything:
| Node type | What it does |
|---|---|
llm (the default) | The model speaks, from the agent's general instructions plus this node's own prompt. |
static | A fixed message is spoken. No model request. |
router | Never speaks. Picks the next node at once. |
Edges come in kinds too. An expression edge is a rule over variables and costs nothing to evaluate. An intent edge is described in words ("the caller says they have already paid") and is chosen by the model. An unconditional edge always holds, which makes it the catch-all a router needs.
A reminder in five nodes
{
"agent_type": "graph_agent",
"agent_flow_type": "streaming",
"llm_config": {
"provider": "openai",
"model": "gpt-4.1-mini",
"agent_information": "You are calling from Example Bank about a card payment. Be brief and polite.",
"current_node_id": "confirm",
"nodes": [
{"id": "confirm", "prompt": "Confirm you are speaking to {customer_name}.", "edges": [
{"to_node_id": "remind", "condition": "The caller confirms their identity"},
{"to_node_id": "wrong_person", "condition": "The caller is someone else"}
]},
{"id": "remind", "prompt": "Say that {amount} is due on {due_date} and ask when they plan to pay.", "edges": [
{"to_node_id": "promise", "condition": "The caller gives a date", "parameters": {"promised_date": "string"}},
{"to_node_id": "paid", "condition": "The caller says they have already paid"}
]},
{"id": "promise", "node_type": "static", "static_message": "Thank you. We have noted that. Goodbye.", "edges": []},
{"id": "paid", "node_type": "static", "static_message": "Thank you. We will check and update our records. Goodbye.", "edges": []},
{"id": "wrong_person", "node_type": "static", "static_message": "Sorry to have troubled you. Goodbye.", "edges": []}
]
}
}Two details are worth noticing. The edge into promise has parameters: when the call crosses it, the date the caller gave is captured as promised_date. And the three closing nodes have no outgoing edges, which is how a graph says "the call ends here".
What the engine does with it
After each caller turn, on the current node:
- Expression and unconditional edges are tried first, in priority order. The first that holds is taken. No model request is made.
- Otherwise the intent edges are offered to the model as a set of functions, with one more for "stay here". The function it picks is the edge taken, and its arguments become the edge's parameters.
- Otherwise the call stays on the node.
So a graph built mostly from rules routes instantly, and a graph built from intents pays for one short extra request per turn. If that request fails or is slow, the call stays where it is.
Validate before you save
POST /graph-agent/validate checks a graph and returns errors, which block saving, and warnings, which do not. It catches the mistakes that are easy to make by hand: an edge to a node that does not exist, a router with no catch-all, a node nothing leads to.
What you get back
A finished call reports node_path, the node ids in the order visited, and node_extracted_data, the values collected on the way:
{
"node_path": ["confirm", "remind", "promise"],
"node_extracted_data": {"promised_date": "5 November"}
}That is the part we like most. You do not have to read a transcript to find out which branch a call took.
Limits to know
- You can try a graph in the dashboard's test chat before any call. Without model credentials the test chat chooses intent edges by a keyword rule, so it behaves more simply than a live call.
- On a live call, event edges other than keypad events are not delivered.
- The routing step does not report a real confidence value.
Every field is described in the graph agents guide.