Structuring the interview
Nodes, edges, branching, and validation.
A blueprint's graph is a directed acyclic graph of nodes and edges. Nodes hold questions; edges carry the respondent from one node to the next, sometimes conditionally. The shape of the graph determines what data you get back. This chapter walks through the design decisions, then the validation workflow.
Node types
Four type values exist on the replace_graph nodes map:
question- open-ended prose. The default. Use for anything you want a transcript-grade answer to. Probing fires on these nodes; they are where the agent earns its keep.stimulus- present material (image, video, prompt) and capture the reaction. Use when you want the agent to react to something, not to a typed question.form- structured input (rating, choice, short field). Use when you need typed data downstream - analytics, CRM sync, conditional branching on a known value.custom- escape hatch for non-standard interaction. Use sparingly; if you reach for it often you probably want a feature request.
A practical rule: most graphs are 70%+ question nodes, with form for the structured fields you'll branch on, and stimulus for moments that need a reference object. If your graph is half form nodes you have built a survey, not an interview.
The entry node
Every graph has exactly one entry node, named on replace_graph's entry_node field. Three things to hold:
- The entry node must exist as a key in the
nodesmap. The most common validation failure on a fresh draft is naming anentry_nodethat isn't innodes. - The entry node sets the tone. A cold "rate this 1–5" entry trains the respondent that the session is a survey; a warm open question trains them that it is an interview. They calibrate disclosure accordingly.
- The entry node often does double duty as consent or context. The blueprint's
intro_messageinsettingscovers the framing, but the entry node is where the conversation starts - and respondents who drop off do so within the first turn or two.
A short rule: write the entry node last, after the rest of the graph is in place. You will know better what it should set up.
Edges and conditions
Edges connect nodes. Each edge optionally has a condition that decides whether the edge fires. The condition field has two type values, and the choice between them is the most consequential design decision in the graph.
structured - typed comparison on a captured field. The condition has field, operator (one of eq, neq, gt, lt, gte, lte, contains), and value. Reach for structured when:
- The condition is a comparison on a known structured value, typically captured by an upstream
formnode. - You need deterministic, replayable branching - the same captured value always produces the same path.
- The branch logic affects analysis, because you will filter or compare cohorts on the field later. A
structuredcondition is also a structured field in the data.
agent_assessed - natural-language condition evaluated by the agent. The condition has an instruction written in free-form prose ("answer mentions pricing or cost", "the respondent indicated they have no children"). Reach for agent_assessed when:
- The condition lives in the substance of an open answer, not in a typed field.
- You are willing to accept some non-determinism for flexibility - the agent's judgement on a borderline answer may swing either way.
- The graph is exploratory and you would rather catch nuance than have replayable branching.
A side-by-side:
| Scenario | structured example | agent_assessed example | Recommended |
|---|---|---|---|
Branch on a 1–5 rating from a form node | field: "satisfaction", operator: "lt", value: 3 | instruction: "the respondent seems unsatisfied" | structured |
| Branch when respondent's answer mentions pricing | field: "topic", operator: "contains", value: "price" (only works if you captured topic) | instruction: "the respondent's answer mentions pricing or cost" | agent_assessed |
| Skip a section if the respondent has no children | field: "has_children", operator: "eq", value: false (form-captured) | instruction: "the respondent indicated they have no children" | structured |
| Trigger a deep-dive when emotional language surfaces | (no clean structured form) | instruction: "the respondent's answer carries strong emotional content" | agent_assessed |
There is a real cost to agent_assessed. It is flexible but slower - each evaluation is an LLM call - and analysis becomes harder because you cannot reliably filter cohorts on the branching condition after the fact. The branch happened, but the reason lives in the agent's judgement rather than in a column. Default to structured whenever a form node could capture the value cleanly.
A worked snippet - one agent_assessed branch with a default fallthrough, taken from the replace_edges reference:
{
"blueprint_id": "bp_7f3a_abc",
"edges": [
{ "from": "q3", "to": "q3a", "condition": { "type": "agent_assessed", "instruction": "answer mentions pricing or cost" }, "priority": 1 },
{ "from": "q3", "to": "q4", "condition": null, "priority": 0 }
]
}The priority field matters whenever multiple edges could fire from the same from node. The highest priority wins. Use it to fall through - priority: 1 for the conditional branch, priority: 0 for the default. An edge with condition: null always matches; pair it with the lowest priority so it only fires when nothing more specific does.
Branching patterns
Three patterns cover most real graphs. The names are useful for talking about graphs, not for anything in the API.
Skip-logic. Branch around a section that does not apply to this respondent. Common when an upstream form node captures a fact that makes a later section irrelevant.
A → B → C → D
↘_____↗ (skip C if condition)Use structured conditions where you can. The skip is deterministic, and the field that drove it is queryable later.
Deep-dive branch. When an answer warrants more probing, branch into a sub-section, then rejoin the main flow. This is the shape of most exploratory interviews - a single trigger answer opens a side path that closes back into the trunk.
A → B → C → D → E
↘ C1 → C2 ↗agent_assessed conditions earn their cost here. The trigger is usually substantive ("the respondent describes a specific failure"), and a structured field rarely captures the nuance.
Graceful exit. Let the respondent end early when they have signalled they are done or off-topic. An agent_assessed edge to a closing node lets the agent honour the signal without the respondent having to fight the script.
A → B → C → D → E
↘ EXIT (if respondent signals they need to go)Skip-logic protects time. Deep-dive captures depth. Graceful exit protects the relationship - and the response rate on the next interview you ask the same person to take.
Anti-patterns
Three to design out from the start, not patch afterwards.
- Forever-loops. An edge that conditionally returns to a previous node without a guaranteed exit. The graph is a DAG - no cycles allowed.
validate_graphwill flag a cycle, but design it out from the start. If you want a "ask again until they answer properly" loop, you wantmax_follow_ups_per_questionon the agent, not a graph edge. - Orphan nodes. Nodes in the
nodesmap that no edge reaches. Either delete them, or add the missing edge. Keepnodesandedgesin sync as you go. - Over-branching. More than two or three branches off a single node usually means the question is doing too much. Split it across multiple nodes, each with simpler branching. A node with five conditional outgoing edges is a debugging problem waiting to happen.
Validation and publish
The workflow once the draft is laid down:
- Call
validate_graph. It returns a result withvalid: booleanand anerrorsarray. - Read the errors. The common ones:
entry_nodenot present innodes, an edge referencing an unknown node, no path from entry to a terminal node, an orphan node that no edge reaches. - Fix and re-validate.
- Call
publish_blueprintto freeze the draft.
A valid: false response looks roughly like:
{
"valid": false,
"errors": [
{ "code": "unknown_node", "message": "edge references unknown node 'q3a'", "edge": { "from": "q3", "to": "q3a" } }
]
}Each error names the specific node or edge to look at. Fix in place via add_blueprint_node or replace_edges - no need to replay the whole graph for a one-line fix.
Published blueprints are immutable. To change a published blueprint, clone it as a new draft and republish - this preserves analysis on existing runs, which still reference the old version.