Skip to content

Await Reply Node

This content is not available in your language yet.

The await_reply node asks the user something, ends the current turn, and continues the same workflow from that node’s successor when the next message on the session arrives. Nothing is held open while it waits — no job, no worker, no socket — and the front half of the graph does not re-run.

This is the node to reach for whenever a flow genuinely needs an answer before it can continue: picking one of several records a partner API returned, confirming a destructive action, collecting one missing field. Before this node existed, every inbound message restarted the graph at start, so “where were we?” had to be re-derived every turn — usually by paying an LLM node to read the conversation history back and guess.

See Workflow Overview for the node/edge envelope this fits into.

AwaitReplyNodeConfig:

FieldTypeRequiredDescription
promptTemplatestringYesThe question, as a Handlebars template — rendered and spoken exactly like a response node’s messageTemplate, so it is a real, persisted assistant message that shows up in the transcript.
saveAsstringYesThe context variable the answer lands in when the run resumes. Validated against the reserved-key list, same as any other node’s outputVar.
matchMode'free_text' | 'options'No (default free_text)free_text accepts any reply as the answer. options matches the reply against a fixed list — see below.
optionsFromstringoptions mode onlyDot-path into the run context naming the array to build the option list from, e.g. jobPostings.data.
optionIdFieldstringNo (default id)Field on each array element holding its identifier.
optionLabelFieldstringNo (default label)Field on each array element holding its display label. Labels are truncated to 80 characters when the list is rendered.
expiresAfterSecondsnumberNo (default 86400)How long the wait stays resumable. 24 hours by default, clamped to a hard ceiling of 7 days.
moodstringNoPassed through to the outbound message, same as ResponseNodeConfig.mood.

Routing lives on the edges, the same way it does for condition’s true/false and tool/action’s success/failure:

  • answered — a reply arrived, and in options mode it matched one of the offered options. Required on every await_reply node, in both modes.
  • unansweredoptions mode only: a reply arrived and matched nothing unambiguously. Required when matchMode is 'options'.

Both rules are enforced when the workflow is saved or published, not discovered mid-conversation. Flipping a node from free_text to options without adding an unanswered edge is rejected with:

AWAIT_REPLY node "ask_which_order" uses matchMode 'options' but has no outgoing
edge labeled 'unanswered' — a reply that matches none of the offered options
must have somewhere to go

unanswered is not required in free_text mode because it is unreachable there — every reply is by definition the answer, so the edge would be permanently dead.

Mode / outcomectx[saveAs] is…
free_textThe reply text, as a string.
options, answered edgeThe matched option object: { "slot": 2, "id": "19", "label": "Solution Architect" }.
options, unanswered edgeThe reply text, as a string — what the user actually typed, which is the only thing worth reasoning about on that branch.

So with "saveAs": "chosenOrder", the answered branch of an options node templates {{chosenOrder.id}} into an outbound call and {{chosenOrder.label}} to echo the choice back; on the unanswered branch the same variable holds the raw text to re-ask against.

Rendering the option list in your question

Section titled “Rendering the option list in your question”

In options mode the rendered list is exposed to promptTemplate as awaitReplyOptions, so the numbers the user sees are the same 1-based slots the matcher accepts:

Which role are you applying for?
{{#each awaitReplyOptions}}{{slot}}. {{label}}
{{/each}}
Reply with the number or the role name.

awaitReplyOptions exists for this one render only — it is never written into the run context and cannot be read by a later node.

The option list is rendered once, at the moment the question is asked, and stored server-side. Matching on resume reads only that stored list — never the live context, and never anything the inbound message claims. That ordering is the point: the set of acceptable answers is fixed when the question is asked, so a later message cannot widen it.

Matching runs in tiers, each of which requires exactly one winner — zero matches, or two or more, is “unmatched”, never a guess:

  1. Slot number — the whole message is a number matching a shown position (2). “I’ll take 2 of them” is not a slot pick.
  2. Exact id, case-insensitively. No punctuation folding — POD-40 and POD40 are different ids.
  3. Normalised label equality — case, accents, punctuation and trailing plurals folded.
  4. Unique whole-token containment, either direction, with a minimum length — “I’d like the Solution Architect role” matches the Solution Architect option, and so does a bare “architect”.

Whole tokens, never raw substrings, and no abbreviation or synonym expansion: "N/A" must not match "Solutio[n A]rchitect", "IT" must not match "arch[it]ect", "POD-40" must not match "POD-401". A clean trip down the unanswered edge — where your graph can re-ask — is always better than a confident write against a record nobody chose.

Practical limits: at most 20 options per question (extra entries are dropped), labels truncated to 80 characters. An optionsFrom path that resolves to a missing value, a non-array, or an empty array fails the node rather than silently degrading to a free-text question — put a condition node in front if “offer options if there are any, otherwise ask freely” is what you want.

Expiry: 24 hours, and nothing happens when it lapses

Section titled “Expiry: 24 hours, and nothing happens when it lapses”

A wait is resumable for expiresAfterSeconds (default 24 hours, ceiling 7 days), enforced at read time. Past that, the wait is closed and the next message simply starts a fresh run from start — exactly the behaviour you get today without this node.

An expired wait does not take the unanswered edge. The platform will not speak unprompted hours later into an abandoned conversation. unanswered means exactly one thing: a reply arrived and matched none of the options offered.

If your flow depends on a tighter guarantee — “chase the user after 10 minutes”, “cancel the held order if they don’t confirm” — build that timeout on your side (a scheduled job of your own that checks pendingAwaitReplies, then acts, e.g. via runWorkflowTask). Do not size a business SLA off the expiry window.

A shipping assistant that finds several orders for a customer and asks which one they mean before looking up its status. (All data below is fictional.)

{
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": { "x": 0, "y": 0 }
},
{
"id": "find_orders",
"type": "action",
"label": "Find Open Orders",
"config": {
"customActionId": 42,
"outputVar": "orderLookup",
"bodyTemplate": "{\"customerRef\": \"{{clientExternalId}}\"}"
},
"position": { "x": 0, "y": 100 }
},
{
"id": "ask_which_order",
"type": "await_reply",
"label": "Which Order?",
"config": {
"promptTemplate": "I found a few open orders on your account:\n\n{{#each awaitReplyOptions}}{{slot}}. {{label}}\n{{/each}}\nWhich one would you like an update on? Reply with the number.",
"saveAs": "chosenOrder",
"matchMode": "options",
"optionsFrom": "orderLookup.data",
"optionIdField": "order_id",
"optionLabelField": "description",
"expiresAfterSeconds": 86400,
"mood": "helpful"
},
"position": { "x": 0, "y": 200 }
},
{
"id": "fetch_status",
"type": "action",
"label": "Fetch Order Status",
"config": {
"customActionId": 43,
"outputVar": "orderStatus",
"bodyTemplate": "{\"orderId\": \"{{chosenOrder.id}}\"}"
},
"position": { "x": -150, "y": 320 }
},
{
"id": "report_status",
"type": "response",
"label": "Report Status",
"config": {
"messageTemplate": "{{chosenOrder.label}} is currently {{orderStatus.data.state}}, expected {{orderStatus.data.eta}}.",
"mood": "helpful"
},
"position": { "x": -150, "y": 420 }
},
{
"id": "reask",
"type": "response",
"label": "Could Not Match",
"config": {
"messageTemplate": "Sorry — I couldn't tell which order you meant. Could you reply with just the number from the list?",
"mood": "apologetic"
},
"position": { "x": 150, "y": 320 }
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "find_orders" },
{
"id": "e2",
"source": "find_orders",
"target": "ask_which_order",
"label": "success"
},
{
"id": "e3",
"source": "find_orders",
"target": "reask",
"label": "failure"
},
{
"id": "e4",
"source": "ask_which_order",
"target": "fetch_status",
"label": "answered"
},
{
"id": "e5",
"source": "ask_which_order",
"target": "reask",
"label": "unanswered"
},
{
"id": "e6",
"source": "fetch_status",
"target": "report_status",
"label": "success"
},
{
"id": "e7",
"source": "fetch_status",
"target": "reask",
"label": "failure"
}
]
}

Turn 1 runs start → find_orders → ask_which_order, speaks the question, and the run persists as AWAITING_INPUT. Turn 2 (the user replying “2”) re-enters at fetch_status with chosenOrder already populated — find_orders does not run again, and the partner API is not called twice.

Note reask above deliberately does not loop back into ask_which_order: the graph asks once, and a mismatched reply ends the turn with guidance instead. Routing it back to ask again is supported — see the ask-again note in Gotchas for what bounds that loop.

What carries across the pause, and what doesn’t

Section titled “What carries across the pause, and what doesn’t”

The resumed run seeds its context as { ...snapshot, ...freshTurnFacts, [saveAs]: answer } — fresh facts always win over stored ones.

Carried: the workflow’s own output variables (whatever your nodes wrote via outputVar/resultVar), capped at 256 KB serialized. A graph carrying more than that fails the node with a named error rather than silently truncating — if you’re extracting a large document mid-flow, drop it into a variable you don’t need after the pause, or pause before the extraction.

Not carried, because the resuming turn supplies them fresh: userMessage, conversationHistory, sessionId/tenantId/agentId, turn counters, and the per-turn attachment/channel facts (attachment, channel, attachmentUrl, attachmentMime, attachmentFilename). If your graph needs the attachment that arrived before the pause, copy the pieces you need into your own variable first.

A paused run has its own status, so “waiting on a human” is queryable rather than inferred:

  • workflowRun/workflowRuns report status: "AWAITING_INPUT" for the run that asked the question — it is not COMPLETED.
  • The resumed run is a separate WorkflowRun whose resumedFromRunId points back at the paused one, so the two halves of one logical step are followable.
  • pendingAwaitReplies(sessionId: Int) lists what is waiting right now, with nodeId, saveAs, matchMode, the exact options offered, and expiresAt. Omit sessionId for every open wait in your tenant.

The stored context a paused run will resume with is deliberately not exposed over the API, on any query.

  • The question ends the turn — by design, and it always sets turnComplete: true. An await_reply node’s outbound message is published with turnComplete: true regardless of what follows it in the graph, because the turn genuinely is over until a human replies. Any client deciding “the agent has finished” should watch turnComplete, not a quiet-period/silence timer — a timer that fires while an earlier node is still working will end the turn before the question is even spoken, and one that keeps waiting after turnComplete will hang until it times out.
  • An “ask, validate, ask again” loop is bounded by the person replying, not by maxVisits. The resumed turn is a new run, and maxVisits is a per-run bound, so its counters start empty — an unanswered branch routing back into the await_reply node that just asked will re-ask, once per turn, indefinitely. That is the intended behavior (a human can always stop answering), but it means the graph itself will not cut the loop off for you. If you want a hard “I’ve asked three times, escalate” rule, count the attempts yourself in a workflow variable and gate the back-edge on a condition node. Within a single run the question always ends the turn, so the node itself never runs twice in one run regardless of what maxVisits says.
  • One open question per session. A session can hold exactly one unanswered await_reply at a time — this is enforced at the database level, not merely unlikely. A second one is rejected with a clear error rather than two questions racing for the same reply.
  • Not usable inside a sub_agent dispatch. A sub-agent runs on a throwaway child session no human is watching, so a question asked there could never be answered. The node refuses up front and the parent records it as an error for that target. Put the question in the master workflow, before or after the dispatch.
  • Identity changes cancel the wait. If the session’s signed-in identity changes between the question and the reply — including gaining or losing one — the wait is cancelled rather than resumed, and the reply starts a fresh run. Someone else’s session never inherits an in-flight question.
  • Ending the session cancels any open wait, on every path that ends one.
  • Editing a published graph can strand a live wait. If you remove the answered/unanswered edge (or the node) while a conversation is paused on it, that conversation’s reply fails with an explicit “cannot resume … it has no outgoing edge labeled …” error rather than guessing. Ship graph edits when few conversations are mid-question, or accept that in-flight waits will fail once.
  • Regression scenarios can’t answer a question. The workflow regression harness has no scripted-reply format yet, so a scenario that reaches an await_reply node stops there at AWAITING_INPUT (cleanly — it no longer hangs to a misleading timeout). Cover the post-answer branches with their own scenario for now.
  • Response Node — the one-way version of speaking to the user, for when you don’t need an answer back.
  • Condition Node — the usual gate in front of an options-mode question (“do I actually have a list to offer?”).
  • Action Node — typically the node that produces the array an options question offers, and the one that consumes the chosen id afterwards.
  • Workflow Overview — full node/edge reference, including run limits.
  • Workflows API ReferencependingAwaitReplies, run statuses, and resumedFromRunId.
  • Events & SubscriptionsisFinal vs turnComplete.