跳转到内容

Recipe: Delegate to a Specialist (Sub-Agent Inputs and Outputs)

此内容尚不支持你的语言。

This recipe splits one job between two agents. A main agent talks to the customer and holds the facts (the order id, the conversation). A specialist agent, with its own prompt and knowledge base, answers one narrow question and hands back structured values. The main workflow then branches on those values with no extra model call.

Status: live in production: the sub_agent node, with inputs, outputs and success/failure edges.

  • Two agents in the same workspace, each with a published workflow. Here the main agent is 57 and the returns specialist is 64, which has a knowledge base with your returns policy.
  • A dashboard sign-in (Authorization: Bearer <token> plus x-huat-platform: customer) to edit workflows.
  • The order id reaches the main workflow as per-turn data (turnContext: { "orderId": "A-1042" } on sdkSendMessage), or from any earlier node.
sequenceDiagram
participant M as Main workflow (agent 57)
participant S as Specialist (agent 64, own session)
M->>S: contextTemplate text + inputs {orderId}
Note over S: Starts with an empty history.<br/>Sees only userMessage and the inputs.
S->>S: llm (knowledge base) sets decision, reason
S-->>M: resultVar [{status, text, values {decision, reason}}]
M->>M: set_variable, then switch on decision

A sub-agent run starts nearly empty. It does not see the main conversation, the main workflow’s variables, channel facts or attachments. It sees:

  • userMessage: whatever the target’s contextTemplate renders to, against the main workflow’s context;
  • one variable per entry in inputs (text only);
  • its own agent’s settings, knowledge base and published workflow.

There are two ways to get data across, and you can use both:

WayGood forCosts
contextTemplateProse a model reads: the customer’s question, a labelled summary, the conversation so far.Nothing extra; an llm node in the specialist reads it as the message.
inputsValues a non-LLM node needs as a variable: an id for an action, a value a switch tests.Nothing extra; without it you would need an extraction llm call in the specialist.

Step 1: The specialist’s workflow (agent 64)

Section titled “Step 1: The specialist’s workflow (agent 64)”

Two llm nodes write the values the main workflow asks for, then a response node ends the run. The labels approve, decline and need_info are chosen so that none contains another: matchAnyOf stores the label that appears earliest in the model’s answer, so a pair such as eligible and ineligible would be confused.

{
"input": {
"id": 90,
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": { "x": 0, "y": 0 }
},
{
"id": "decide",
"type": "llm",
"label": "Decide eligibility",
"config": {
"promptTemplate": "You check return requests against the returns policy in your reference material. Order {{orderId}}. Decide whether the request in the message is eligible for a return. Answer with exactly one word: approve, decline or need_info.",
"knowledgeBaseId": 33,
"matchAnyOf": ["approve", "decline", "need_info"],
"outputVar": "decision",
"streamOutput": false
},
"position": { "x": 0, "y": 100 }
},
{
"id": "explain",
"type": "llm",
"label": "Give the reason",
"config": {
"promptTemplate": "The decision on the return request in the message was {{decision}} under the returns policy in your reference material. In one sentence a customer can read, give the reason. If more information is needed, say exactly what.",
"knowledgeBaseId": 33,
"outputVar": "reason",
"streamOutput": false
},
"position": { "x": 0, "y": 200 }
},
{
"id": "done",
"type": "response",
"label": "Done",
"config": { "messageTemplate": "{{decision}}: {{reason}}" },
"position": { "x": 0, "y": 300 }
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "decide" },
{ "id": "e2", "source": "decide", "target": "explain" },
{ "id": "e3", "source": "explain", "target": "done" }
]
}
}

Send it with updateWorkflow, then publishWorkflow and attach it to agent 64. A target whose agent has no published workflow comes back as status: "error" at run time; saving the main workflow does not check this.

Step 2: The main workflow’s dispatch (agent 57)

Section titled “Step 2: The main workflow’s dispatch (agent 57)”
{
"id": "ask_returns",
"type": "sub_agent",
"label": "Ask the returns specialist",
"config": {
"targets": [
{
"agentId": 64,
"contextTemplate": "Customer request: {{userMessage}}",
"inputs": { "orderId": "{{turn.context.orderId}}" },
"outputs": ["decision", "reason"]
}
],
"resultVar": "returnsCheck",
"timeoutMs": 30000
},
"position": { "x": 0, "y": 100 }
}

returnsCheck is always an array, one entry per target, in targets order:

[
{
"agentId": 64,
"status": "ok",
"text": "approve: The item was delivered 9 days ago, within the 30-day window.",
"values": {
"decision": "approve",
"reason": "The item was delivered 9 days ago, within the 30-day window."
}
}
]

values appears only when status is ok. A requested output the specialist never set (or one over 16,384 characters) is left out and named in missingValues.

Copy the values into flat variables with a set_variable node (a template of a missing path renders empty text, so this never fails), then route with a switch:

[
{
"id": "unpack",
"type": "set_variable",
"label": "Unpack the result",
"config": {
"assignments": [
{
"variable": "checkStatus",
"kind": "template",
"source": "{{returnsCheck.[0].status}}"
},
{
"variable": "decision",
"kind": "template",
"source": "{{returnsCheck.[0].values.decision}}"
},
{
"variable": "reason",
"kind": "template",
"source": "{{returnsCheck.[0].values.reason}}"
}
]
},
"position": { "x": 0, "y": 200 }
},
{
"id": "route",
"type": "switch",
"label": "Route on the decision",
"config": {
"cases": [
{ "label": "unavailable", "expression": "checkStatus != \"ok\"" },
{ "label": "approve", "expression": "decision == \"approve\"" },
{ "label": "decline", "expression": "decision == \"decline\"" }
]
},
"position": { "x": 0, "y": 300 }
}
]

Edges from route: unavailable to a response such as “I can’t check returns right now; a colleague will follow up”, approve to the next step (an action that opens the return), decline to a response that quotes {{reason}}, and default (which covers need_info and anything unexpected) to a response that asks for what {{reason}} names.

Give ask_returns itself a success edge to unpack and a failure edge to the “unavailable” reply. Its failure edge is taken only for a configuration error (no targets, or more than 10); a specialist that fails or times out still takes success with status: "error" or "timeout", which is why the unavailable case checks checkStatus first.

LimitValue
Targets per node1 to 10, run at the same time
Timeout per target30 s by default, at most 120 s
inputs per target20; template 2,000 characters; value 16,384; all inputs 65,536 together
outputs per target20; value 16,384 characters; 65,536 together
Dispatch depth (a sub-agent of a sub-agent …)5
RetriesNone: a target that timed out may already have acted

Input and output names use letters, digits and underscores, start with a letter, and cannot be a reserved context key or start with _; updateWorkflow refuses them otherwise.

  • Each target runs as its own session with its own model calls, billed to the same workspace and reported against the specialist agent, so you can see what each specialist costs. The child runs under the same API key’s model allowlist as the main session.
  • The specialist’s run shares the main run’s traceId (workflowRun), and every outbound call it makes carries the same x-wetel-trace-id header, so one id finds the whole turn in your logs.
  • In the main run’s trace, the ask_returns row shows the dispatch; the specialist’s nodes are in the run of its own session, whose clientExternalId begins with sub-agent: and the main session’s id.
What you seeWhyFix
status: "error" and no partner call in the specialistThe target agent has no published workflow, or is in another workspace.Publish and attach the specialist’s workflow.
status: "error" from a bad inputAn input over its limit, or a template that cannot render. Only that target fails.Shorten the value; pass lists with {{jsonString list}}.
status: "timeout"The specialist took longer than timeoutMs.Raise timeoutMs (120 s at most) or make the specialist do less.
decision is in missingValuesThe specialist never set it (for example a branch skipped the node).Make every path of the specialist set each requested output.
A reply shows [object Object]A template rendered {{returnsCheck}} or {{returnsCheck.[0].values}} whole.Index a field: {{returnsCheck.[0].values.reason}}.
updateWorkflow refuses an input or output name (BAD_USER_INPUT)Reserved, starts with _, or listed twice.Rename it.
The specialist’s await_reply makes the target failA sub-agent cannot pause to ask the customer.Ask in the main workflow, then dispatch.
  • An eligible request reaches the approve branch, with decision and reason filled.
  • A request missing a detail takes default and the reply asks for it.
  • Unpublishing the specialist’s workflow makes the main workflow take unavailable, with no raw error in the reply.
  • The specialist’s session shows up separately in usage, and its run has the main run’s traceId.
  • No secret or signed URL is passed through inputs (they are stored in the specialist’s run context).
  • The specialist cannot hold a conversation: it runs once per dispatch and cannot pause for a reply.
  • It does not inherit the main conversation, channel facts or attachments; pass what it needs.
  • It does not retry a failed or slow specialist.
  • Mock-mode tests of sub-agents are live in production since 2026-10-24; a draft test with draftSideEffects: LIVE (workspace owner only) dispatches for real.