Skip to content

Sub-Agent Node

The sub_agent node dispatches the current turn to one or more other Wetel agents, each running as its own independent Session — its own turn history, its own cost tracking, its own tenant-scoped resources — rather than continuing in the calling (“master”) session. This is the building block for multi-specialist orchestration: a router/dispatcher agent that hands off to a billing specialist, a technical specialist, and a scheduling specialist, then combines what comes back.

Every entry in targets dispatches concurrently, not sequentially — this node fans out to all of them at once and best-effort-aggregates whatever comes back. One target failing or timing out never blocks or cancels the others.

See Workflows Overview for how this node type fits into the broader node/edge model.

SubAgentNodeConfig:

FieldTypeRequiredDescription
targetsarrayYesOne or more dispatch targets — see below. Must be non-empty.
targets[].agentIdnumberYesThe agent to dispatch to. Must belong to the same tenant as the calling agent — checked at execution time (not just when the workflow was saved), since an agent can be deleted or reassigned after the workflow was published.
targets[].contextTemplatestringYesA Handlebars template, rendered against the master session’s current execution context, that becomes the sub-agent’s opening message. This is intentionally author-templated, not an automatic full-history forward — give each target enough context to do its job, not the master’s entire conversation.
resultVarstringYesThe context variable the aggregated results are stored in — always an array, see below, regardless of how many targets you configure.
timeoutMsnumberNoPer-target timeout in milliseconds. Defaults to 30000 (30 seconds), clamped to a hard ceiling of 120000 (2 minutes). Applies independently per target — one target timing out doesn’t affect its siblings’ clocks.

ctx[resultVar] is always an array, in the same order as targets (not completion order), even for a single target:

Array<{
agentId: number;
status: "ok" | "timeout" | "error";
text?: string; // present only when status === 'ok'
error?: string; // present only when status !== 'ok'
}>;

A sub-agent dispatch is never retried — a target that timed out may already have run its own side-effecting TOOL/ACTION calls, and blindly re-running it is not safe.

A front-desk coordinator that classifies the message, then dispatches to a billing specialist and a technical specialist at the same time, combining both replies into one response:

{
"id": "dispatch_specialists",
"type": "sub_agent",
"label": "Dispatch to Specialists",
"config": {
"targets": [
{
"agentId": 42,
"contextTemplate": "A customer is asking about their bill: \"{{userMessage}}\". Their account email is {{customerEmail}}."
},
{
"agentId": 43,
"contextTemplate": "A customer has a technical question: \"{{userMessage}}\"."
}
],
"resultVar": "specialistAnswers",
"timeoutMs": 45000
},
"position": { "x": 300, "y": 260 }
}

Followed by an LLM node that synthesizes both replies (never interpolate the raw array directly into a template — see the gotcha below):

{
"id": "synthesize",
"type": "llm",
"label": "Combine Specialist Replies",
"config": {
"promptTemplate": "Billing specialist said: {{specialistAnswers.[0].text}}\nTechnical specialist said: {{specialistAnswers.[1].text}}\n\nCombine these into one helpful reply to: \"{{userMessage}}\"",
"outputVar": "combined_reply"
},
"position": { "x": 400, "y": 260 }
}
  • {{resultVar}} on its own renders [object Object], not an error. ctx[resultVar] is an array — a bare {{specialistAnswers}} in any downstream template silently produces the literal string [object Object], not a runtime error, since Handlebars has no auto-JSON-stringify for {{var}}. Always index into a specific entry ({{specialistAnswers.[0].text}}) or use the jsonString helper if you genuinely need the whole array serialized. See Best Practices for the general pattern.
  • A single-target dispatch still produces an array. resultVar is [{ agentId, status, text }], not a bare object — even with exactly one entry in targets, downstream templates must still index .[0].
  • One target’s status: 'error' or 'timeout' never fails the node itself, and never blocks the other targets. Check each entry’s own status before reading text — a failed entry has no text field, only error.
  • Every target agent must have a published workflow. A target whose agent has no workflowId (or an unpublished one) fails per-target with status: 'error' at execution time — it is not caught when you save or publish the master workflow. Check the target agents first when a specialist comes back as error with no partner call in its trace.
  • Dispatch depth is capped at 5. A sub-agent’s own workflow can itself contain a sub_agent node, but a chain deeper than 5 dispatches is rejected — this is what stops an accidental cycle (A → B → A …) from recursing forever.
  • A sub-agent session is fully independent — it has its own turn history and its own cost tracking, and does not automatically inherit anything from the master session beyond what you put in contextTemplate. One exception: a linked WebbyX One identity (and any product credentials linked to it — OmniChat, Nortia, aisCRM) is copied onto every dispatched sub-agent session automatically — a certified operation on a sub-agent’s own workflow that needs a session-scoped credential works without any extra wiring in contextTemplate.
  • Never retried on timeout or error. If you need a “try again if it fails” pattern, build that explicitly in your own workflow (e.g. a router branch that re-dispatches based on status) rather than expecting the node to retry for you.