Response Node
The response node is the only way a workflow talks back to the user directly. It renders a Handlebars template against the current execution context and sends the result as the agent’s reply for this turn.
See Workflow Overview for the node/edge envelope this fits into.
Config fields
Section titled “Config fields”| Field | Type | Required | Description |
|---|---|---|---|
messageTemplate | string | Yes | The Handlebars template rendered into the outgoing message. Supports {{variable}} interpolation as well as block helpers — {{#if}} and {{#each}} — over context variables. |
mood | string | No | A free-form hint describing the emotional tone of this reply (e.g. "helpful", "concerned", "neutral"). Passed through to the message record; if omitted, defaults to "neutral". |
structuredContentTemplate | string | No | An optional second Handlebars template, rendered independently of messageTemplate, whose output must be JSON matching a closed vocabulary — see Structured content below. |
Structured content (structuredContentTemplate)
Section titled “Structured content (structuredContentTemplate)”structuredContentTemplate lets a response node attach an optional structured payload alongside its plain-text reply — a card, a table, or a list — without replacing the text. A client that doesn’t render it (which is every client except the Wetel dashboard today — see below) just shows text as normal.
- Rendered via
interpolateJson, not plain interpolation. Because the output must be valid JSON, use{{jsonString someVar}}for any interpolated value that could contain a quote, a newline, or other characters that would otherwise corrupt the JSON — the same convention aswebhook’sbodyTemplateandtool/action’sargsTemplate. See Best Practices for the fulljsonStringrationale. - Closed vocabulary — exactly 3 shapes, nothing else:
type StructuredContent =| {kind: "card";title: string;subtitle?: string;fields: { label: string; value: string }[];imageUrl?: string;}| { kind: "table"; columns: string[]; rows: string[][] }| { kind: "list"; items: string[]; ordered?: boolean };
- Fails safe, always. If the template is omitted, fails to compile, renders to something that isn’t valid JSON, or renders valid JSON that doesn’t match one of the 3 shapes above, the node does not fail —
structuredContentis simplynullfor that message, and the plain-textmessageTemplatereply still sends as normal. Never rely onstructuredContentbeing present; always design for a client that only readstext. - Dashboard-only consumer today. The Wetel dashboard’s own agent-testing chat surface renders
card/table/listalongside the plain-text reply. The<vai-avatar>embed SDK and any REST/SDK integration readingMessageDtodirectly can read the field (it’s a normalJSONGraphQL scalar, see Sessions API Reference) but nothing renders it there yet. - No interactive/button variant yet. This is read-only structured display content — there’s no way (yet) for a rendered widget to submit a value back into the conversation.
Worked example — structured content
Section titled “Worked example — structured content”{ "id": "respond_order_status", "type": "response", "label": "Respond with Order Status", "config": { "messageTemplate": "Here's your order status.", "structuredContentTemplate": "{\"kind\":\"card\",\"title\":\"Order #{{orderResult.id}}\",\"subtitle\":\"{{orderResult.status}}\",\"fields\":[{\"label\":\"Placed\",\"value\":\"{{jsonString orderResult.placedAt}}\"},{\"label\":\"ETA\",\"value\":\"{{jsonString orderResult.eta}}\"}]}" }, "position": { "x": 300, "y": 200 }}Rendering details worth knowing
Section titled “Rendering details worth knowing”messageTemplateis rendered with plain-text (no-escape) interpolation — the same rendering convention used byWebhookNodeConfig.bodyTemplateandToolNodeConfig.argsTemplate, as opposed to HTML-escaping. Values are inserted as-is. If you’re hand-authoring a JSON envelope as your outgoing text (e.g.{"message": "...", "type": "..."}), any variable going inside a JSON string literal needs thejsonStringhelper, not a bare{{variable}}— see Best Practices: building a hand-authored JSON envelope inside a text field.- Block helpers work as expected:
{{#if orderFound}}Your order is on the way.{{else}}I couldn't find that order.{{/if}}and{{#each items}}{{this}}, {{/each}}are both valid, since these are standard Handlebars built-ins. - Sending the message and recording it are not optional side effects you configure — every
responsenode execution publishes the rendered text to the live session subscription and persists it as a message. If the template fails to render (a template syntax error, or referencing a variable in a wayinterpolateTextcan’t resolve), the node throws, and the workflow run is recorded as failed rather than silently sending nothing. - A
responsenode’s own outgoing edge (if it has one) is followed unconditionally —responseis not a branching node type. It typically connects straight to anotherresponse, a further processing step, or anend_sessionnode.
Worked example
Section titled “Worked example”A generic order-status assistant renders a different message depending on whether an earlier tool node found a matching order, using {{#if}}/{{else}}:
{ "id": "respond_order_status", "type": "response", "label": "Respond with Order Status", "config": { "messageTemplate": "{{#if orderFound}}Your order #{{orderId}} is currently {{orderStatus}}.{{else}}I couldn't find an order matching that — could you double check the order number?{{/if}}", "mood": "helpful" }, "position": { "x": 300, "y": 200 }}Here, orderFound and orderStatus would have been written into the execution context by an earlier tool node’s outputVar (or by a follow-up llm node that shaped the tool’s raw JSON result into flatter fields). A minimal linear workflow tying this together:
{ "nodes": [ { "id": "start", "type": "start", "label": "Start", "config": {}, "position": { "x": 0, "y": 0 } }, { "id": "lookup_order", "type": "tool", "label": "Look Up Order", "config": { "connectorId": 7, "toolName": "get_order_status", "outputVar": "orderResult", "argsTemplate": "{\"orderQuery\": \"{{userMessage}}\"}" }, "position": { "x": 0, "y": 100 } }, { "id": "respond_order_status", "type": "response", "label": "Respond with Order Status", "config": { "messageTemplate": "{{#if orderResult.found}}Your order #{{orderResult.id}} is currently {{orderResult.status}}.{{else}}I couldn't find an order matching that — could you double check the order number?{{/if}}", "mood": "helpful" }, "position": { "x": 0, "y": 200 } }, { "id": "end", "type": "end_session", "label": "End", "config": {}, "position": { "x": 0, "y": 300 } } ], "edges": [ { "id": "e1", "source": "start", "target": "lookup_order" }, { "id": "e2", "source": "lookup_order", "target": "respond_order_status" }, { "id": "e3", "source": "respond_order_status", "target": "end" } ]}Note this example accesses orderResult.found / orderResult.id / orderResult.status — nested property access is fine here because messageTemplate is rendered by a real Handlebars engine, unlike a condition node’s expression, which uses a much more restrictive grammar with bare variable names only. Don’t confuse the two templating/evaluation contexts when moving between node types.
Next steps
Section titled “Next steps”- Workflow Overview — full node/edge reference and publishing flow.
- LLM Node — the usual source of a
responsenode’s dynamic content, especially for voice-facing replies usingapplyVoiceFormatting. - Router Node and Condition Node — commonly precede a
responsenode on each branch. - Best Practices — templating gotchas across all node types.
- Examples — complete end-to-end workflows.
- Workflows API Reference — mutations for creating and publishing workflows.