Edge conditions¶
A Hadron chatbot has two ways to decide where the conversation goes
next: deterministic edge conditions (the routing engine
evaluates a rule against memory and chat state) and
LLM-controlled routing (the LLM signals onTrack: false or
returns a next_stage). They overlap in capability but answer
different questions, and reaching for the right one changes how
predictable, debuggable, and cheap your bot is.
This page explains the model and helps you pick. For the broader conversation/stage/goal hierarchy these edges live in, see Conversation routing. For how to actually build a conditional flow in the portal, see Build a conditional conversation flow.
The two routing modes¶
| Mode | Decided by | When it fires | Cost per turn |
|---|---|---|---|
| Edge condition | The routing engine, evaluating a JSONLogic expression | Every turn the engine considers the edge (on_complete, on_enter, or always) |
A handful of memory reads — cheap |
| LLM routing | The LLM, returning onTrack: false, an explicit next_stage, or h-chat-push-goal |
Every turn the LLM runs | One LLM call — costly |
Both run on the same per-turn loop. The engine evaluates eligible
edges first; the LLM's respond({ next_stage }) and onTrack
signal then layer on top. The two modes are not alternatives —
production bots use both.
When to use which¶
Use edge conditions when the criterion is mechanical:
- "Skip the profile-questionnaire stage if
memory.business_typealready exists." - "After 20 turns in the same stage, escalate to a fallback conversation."
- "On
chat.message_count > 0, mark the welcome as already-sent." - "Detour to billing if
memory.subscription_status === 'expired'."
These decisions don't need judgment — there's data, there's a rule, the rule fires or it doesn't. Cementing them as edges makes the bot predictable (the same state always leads to the same routing), debuggable (every fired edge is recorded in route history), and cheap (no LLM call to make the decision).
Use LLM routing when the criterion needs judgment:
- "Has the user said something that suggests they actually want
to talk about pricing instead of features?" —
onTrack: false - a fallback edge.
- "Did the user just confirm they're done, or are they hedging?"
— the LLM returns
next_stage: "wrap-up"only when it reads a clear yes. - "Is the user asking about a topic this conversation can handle?" — the LLM evaluates intent against the conversation's goal description.
These calls require reading the conversation, weighing tone, and reasoning about intent. JSONLogic can't do that. The LLM can.
A useful mental model: conditions filter eligibility, the LLM picks among eligible options. A conversation with three "escape" edges, all with conditions like `chat.stage_message_count
5`, will only present those edges to the LLM after the user has exchanged five messages in the current stage. The LLM never sees the edges that aren't yet eligible — it can't accidentally fire them too early.
What an edge looks like¶
An edge attaches to a stage and points at another stage or conversation. It carries condition, timing, and behavior:
{
"target": "conversations:profile-building",
"condition": { "missing": ["memory.business_type"] },
"timing": "on_enter",
"behavior": "detour",
"label": "Fill profile if business type is missing",
"priority": 10
}
Field meanings:
target— the destination, by stagelocor conversation URN. Edges crossing into a different conversation switch the active routing context.condition— a JSONLogic expression evaluated against memory + chat state.nullmeans "always fires."timing— when the engine considers the edge:on_complete— when the current stage finishes (most common for forward progression).on_enter— when entering the source stage (used by Prerequisite edges to gate entry).always— every turn, regardless of stage transition (used by Escape and Fallback edges).behavior— what firing means:transition— permanent move; the previous stage is left.detour— push the destination onto the goal stack, with a return point baked in. Pop returns to where the user was.label— human-readable description shown in the editor.priority— lower fires first when multiple edges qualify on the same turn.
Variables you can read¶
Conditions read from a small set of scoped variables. Anything outside these scopes is undefined.
| Scope | What's in it |
|---|---|
memory.* |
Fields on the agent's memory the chat reads from. Cascades from edge-memory through the user's per-agent memory using deepest-wins semantics — an override on a specific node beats a value at the root. |
chat.* |
Per-chat state: chat.message_count, chat.stage_message_count, the current chat.conversation, chat.stage. |
agent.* |
Agent config and state — locale, persona name, capabilities. |
message.data.* |
Fields the LLM extracted into the latest respond({ data }) call. |
now(), today() |
ISO timestamp and YYYY-MM-DD date for time-based conditions. |
message.data.exception and message.data.exceptions are
reserved — they hold the agent-exceptions surface (deferred
feature). Don't use these names for application data.
The portal authors a curated subset¶
The condition builder in the portal exposes a curated set of
operators — ==, !=, <, <=, >, >=, in, and the
unary missing. The runtime evaluator supports the full
JSONLogic operator set including arithmetic (+, -, *, /),
nested operators, and combinators (and, or, !).
When an edge has a condition the UI can't represent — typically because someone hand-edited a JSONLogic expression through the GraphQL API — the editor falls back to a raw JSON view so nothing is silently lost. Round-tripping through the structured editor preserves the condition exactly.
This asymmetry is intentional: the UI optimizes for ops who don't write code; the engine doesn't constrain you when you do.
What's deferred¶
The edge-conditions feature shipped its v1 in the portal builder (hadron-portal #174) and the runtime evaluator. Capabilities still on the roadmap:
- UI authoring of arithmetic and nested operators. Today the evaluator supports them; the structured editor doesn't. Use the raw-JSON fallback or the GraphQL API.
- Cross-memory conditions. A condition cannot read from a
sibling memory or another agent's memory today. Pre-compute the
value in your extraction spec and read it from
memory.*instead. - Custom operators. Agents cannot register JSONLogic predicates. Same workaround.
- Per-user time zones for
now()/today(). Both run in the agent's configured timezone (or wall-clock UTC). Per-user tz is a future spec.
Related¶
- Edge conditions reference —
exhaustive operator list, scope semantics, the cascade rule for
memory.*reads, encrypted-memory behavior, and time-based worked examples. - Conversation routing — the broader topics / conversations / stages / goals model.
- Build a conditional conversation flow — walk through building a two-stage conditional flow in the portal.
- Building a chatbot agent — agent setup the conditional flows assume is already done.