Module 5: Multi-Agent Systems: Agents That Delegate Tasks

5. Designing the team: roles, handoffs, and contracts between agents

Description

By the end of this lesson you'll be able to write a specialist agent's role sheet — its scope, its exit criterion, what it receives, what it returns, what it can do, and what it does when it can't resolve something — you'll be able to turn that specialist's response into a structured output the orchestrator can interpret without guessing, and you'll be able to tell apart the three types of handoff that exist between agents, knowing which one n8n supports natively and how the other two get solved without stepping outside the pattern.

This matters because in lesson 4 you connected the agents and everything worked — on the happy path. A customer with a clear case, a specialist that resolves it, a clean response. The problem starts on day two, when the cases that aren't the happy path arrive: the customer didn't give the order number, the specialist checked and found nothing, the case turned out to belong to another domain, the action needed human approval. In every one of those cases the specialist returns something, and that something is free-form prose the orchestrator has to interpret. "I couldn't find the charge, maybe it's worth checking with another payment method" — does that mean the case is pending, that the customer needs to be asked something, or that the specialist is giving up? The orchestrator is going to interpret it, and sometimes it's going to interpret it wrong, and that's exactly the kind of failure that in production looks like "the agent said whatever." The contract is what eliminates that guessing.

Connection to the module: lesson 4 gave you the wire; this lesson shapes what travels through it. It's the direct continuation of Module 4's lesson 5 — a tool's contract: name, description, parameters — carried into the case where the tool is an agent and the response is no longer a piece of data but a judgment call. Lesson 6 takes this contract and uses one of its fields, the case's status, as the whole system's stopping condition.

A work order you can fulfill without asking questions

Think of two ways to send work to the company's mechanic shop.

The first: someone leaves a note on the counter that says "the white truck makes a noise." The mechanic is going to have to figure out which of the three white trucks, what kind of noise, when it happens, who reported it, and where to send the result. They're going to do the work, probably well, but they're going to spend half the time reconstructing the assignment — and if they guess wrong on any of it, they're going to do a good job on the wrong thing.

The second: a work order with fields. Vehicle: plate ABC-123. Reported by: logistics. Symptom: metallic noise when braking hard, since Monday. Priority: high. And at the bottom, a space where the mechanic writes the result with a checked box: repaired / needs a part / couldn't reproduce.

That second form has two halves and both matter. The top one is the input contract: what an assignment has to carry so it can be fulfilled without asking. The bottom one is the output contract: what shape the result takes, so whoever receives it can act without interpreting prose. The checked box is what lets the whole system work: whoever receives the completed order doesn't read a paragraph and decide what it means — they read a checkbox.

The same thing happens between agents, with one aggravating factor: the orchestrator's model is very good at interpreting prose, which is a trap. It's going to interpret whatever gets returned to it and produce a response that sounds good. Most of the time it'll get it right. And the times it doesn't, you're not going to have a way to detect it except by reading the conversation, because there was no error.

An agent's five contract clauses

An agent contract has five parts. All five fit on a half-page sheet, and writing it before configuring the node saves half the debugging work later.

1. Role and exit criterion

One sentence saying what scope it covers, and one saying when its work is done. You already worked on this in lesson 2: the exit criterion can't have an "or" separating domains.

Role: TuTienda's billing specialist.
Scope: unrecognized charges, amount questions, disputes, account
        statements.
Out of scope: product returns, shipping status,
        recommendations.
Exit criterion: the charge got explained, or a dispute
        got opened with its number, or it got logged what data is
        missing to resolve it.

That third "or" in the exit criterion is different from the ones you rejected in lesson 2: it doesn't separate domains, it separates outcomes of the same case. Every well-written contract has at least one failure outcome, and writing it is what stops the agent from inventing a happy ending when there isn't one.

2. Input contract

What minimum fields the assignment must carry so the specialist can start. It's not a wish list: it's the bare minimum without which nothing can be done.

Minimum input:
  - customer_id     (required)
  - amount          (required: the disputed charge's amount)
  - approximate_date (required: when the charge showed up)
  - description     (required: what the customer says happened)
Optional input:
  - order_id        (if the customer tied it to a purchase)

And here there's a design decision worth making explicit: is the input contract text, or is it fields?

  • A single text field (task), like in lesson 4. Simple to configure, flexible, and the orchestrator's model drafts an assignment in prose. The discipline of making sure it carries all the data lives in the $fromAI()'s description.
  • Several typed fields (customer_id, amount, approximate_date, description), each with its own $fromAI(). Stricter: the orchestrator has to produce each field separately, and if it can't find the amount in the conversation, that shows up visibly in the trace as an empty field instead of hidden inside a paragraph.

The second form is more robust and it's the one to reach for when the specialist makes costly decisions. It's comfortably implemented with lesson 4's sub-workflow mechanism, where the Execute Sub-workflow Trigger with Define Using Fields Below declares the fields and their types — the input contract stays written on the node, not just in your head. With AI Agent Tool it can also be done: you put several $fromAI() expressions inside the prompt text, one per field.

The practical rule: start with task as text, and move to typed fields as soon as you see in the trace that assignments recurrently arrive incomplete.

3. Output contract

What the specialist returns, with what fields. This is the one that pays off the most and gets forgotten the most.

A useful output contract for a support specialist has four fields:

{
  "status": "resolved | pending_info | out_of_scope | needs_human",
  "summary": "What it found and what it did, in two or three sentences, for the orchestrator to turn into a response to the customer.",
  "data": { "concrete result fields, if any" },
  "missing": ["what data is missing, if status is pending_info"]
}

The four status values aren't decorative — each one tells the orchestrator exactly what to do next:

statusWhat it meansWhat the orchestrator does
resolvedThe case is closed in this domainComposes the response and closes, or moves to the next topic
pending_infoA piece of data only the customer can give is missingAsks the customer what's in missing; doesn't delegate again until it has it
out_of_scopeThe assignment wasn't this domain'sRe-delegates to the right specialist (only once — lesson 6)
needs_humanA person is requiredTriggers the escalation path, doesn't retry

That fourth state deserves a note. needs_human doesn't replace Module 4's human review mechanism — where a sensitive tool literally stays halted until someone approves. They're two distinct, complementary things: human review is a structural barrier on a specific action; needs_human is a judgment call the specialist makes about the whole case. A case can need a human without any sensitive action being involved at all: a very angry customer, a legal situation, something the specialist doesn't understand.

How the structured output gets implemented. n8n's AI Agent node has an option to require a specific output format; turning it on reveals the ai_outputParser port, where you connect a Structured Output Parser sub-node with a JSON example or a schema. The agent then produces that shape instead of free text.

If for some reason that option isn't available on the node you're using, the alternative — less guaranteed but functional — is to ask for the format in the system prompt and be explicit:

# Fragment of the specialist's System Message (alternative without a parser)

  ALWAYS respond with a valid JSON object, with no text before or
  after, with exactly these fields:
  {
    "status": one of "resolved" | "pending_info" | "out_of_scope" | "needs_human",
    "summary": string,
    "data": object,
    "missing": array of strings
  }

The difference between the two routes is real: the structured parser validates the shape; the prompt only asks for it. With the prompt you're going to occasionally see a JSON with an explanation stuck before it. Use the parser when you can.

4. Authority contract

What this agent can do on its own and what it can't. It's the direct application of Module 4's trust boundaries, now with one more layer:

billing_specialist's authority:
  Can only: lookup_charge, get_customer_profile  (read)
  Can, logged: open_dispute                  (reversible action)
  Cannot: issue refunds, change the payment method, cancel
            subscriptions.
  Requires human approval: none of its current tools.
  Forbidden by prompt: promising amounts, resolution deadlines, or
            dispute outcomes.

Two things worth being clear on here. First: "cannot" and "forbidden by prompt" are different things and the contract must tell them apart. Cannot issue refunds is structural — that tool isn't connected to this agent — and it doesn't depend on the model obeying. Forbidden from promising amounts is textual, it lives in the system prompt, and it's strong but not guaranteed. Writing down which is which forces you to notice when you're trusting the prompt for something that should be structural.

Second: in a multi-agent system, authority is inherited downward, never upward. The orchestrator can call billing_specialist, but that doesn't give the orchestrator the ability to open disputes on its own — only the ability to ask someone to do it, with that someone applying its own rules. That's a valuable property of the pattern: every barrier gets enforced where the tool lives, not where the intent was born.

5. Failure contract

What it returns when it can't resolve something. It's the part almost nobody writes and the one that prevents the most problems.

billing_specialist's failure contract:
  - If a required assignment field is missing → status "pending_info",
    with the missing field in "missing". Doesn't make up the data.
  - If the assignment isn't billing's → status "out_of_scope",
    with "summary" indicating which domain it seems to belong to.
    Doesn't use any tool before reporting it.
  - If lookup_charge fails technically → status "needs_human",
    summary describing the error. Doesn't retry more than once.
  - If the case is billing's but exceeds its authority (the customer
    demands a refund) → status "needs_human".
  - Never returns a "resolved" without having run at least one
    tool from its domain.

That last line is a small safeguard worth a lot: it's the between-agents equivalent of Module 4's common mistake — "the agent that says it acted without having acted." A specialist that returns resolved without having called any tool is answering from memory, and the orchestrator has no way of noticing unless you explicitly forbid it and verify it in the trace.

Worked example: the complete sheet and its implementation

Here's what a specialist's role sheet looks like, complete, in the format worth keeping alongside the workflow:

┌─ ROLE SHEET ──────────────────────────────────────────────────┐
│ Agent:    billing_specialist                                  │
│ Type:     AI Agent Tool                                       │
│ Called by: triage_agent                                       │
│                                                               │
│ ROLE AND EXIT                                                 │
│   Scope: charges, amounts, billing, disputes.                 │
│   Out:   orders, shipments, returns, recommendations.         │
│   Finishes when: the charge got explained, or there's a       │
│     dispute open with a number, or it got logged which data   │
│     is missing.                                                │
│                                                               │
│ INPUT (task field, self-contained text)                       │
│   Required: customer_id, amount, approximate_date,            │
│                description                                    │
│   Optional:    order_id                                       │
│                                                               │
│ OUTPUT (structured JSON)                                      │
│   status:  resolved | pending_info | out_of_scope |           │
│            needs_human                                        │
│   summary: 2-3 sentences, no greetings, for the orchestrator  │
│            to compose with                                    │
│   data:    { dispute_id?, charge_found?, matched_order? }     │
│   missing: [ ] when status = pending_info                     │
│                                                               │
│ AUTHORITY                                                     │
│   Read:  lookup_charge, get_customer_profile                  │
│   Action:   open_dispute                                      │
│   No access: refunds, payment method change                  │
│   Forbidden by prompt: promising amounts or deadlines         │
│                                                               │
│ FAILURE                                                       │
│   Missing data → pending_info (never makes it up)             │
│   Other domain → out_of_scope, without using tools            │
│   Technical error → needs_human, a single retry                │
│   Exceeds authority → needs_human                              │
│   Never resolved without having used a tool                   │
│                                                               │
│ LIMITS                                                         │
│   Max Iterations: 6                                           │
│   No own memory                                                │
└───────────────────────────────────────────────────────────────┘

That sheet translates almost line by line into the node's configuration. The OUTPUT block becomes the Structured Output Parser:

# Sub-node: Structured Output Parser
# (connected to billing_specialist's ai_outputParser port,
#  after turning on the specific output format option)

# JSON example defining the expected shape:
{
  "status": "resolved",
  "summary": "The $1,200 charge from 07/18 doesn't match any purchase on file for customer C-9931. Dispute D-8842 was opened, review within 48 business hours.",
  "data": {
    "dispute_id": "D-8842",
    "charge_found": false,
    "matched_order": null
  },
  "missing": []
}

And the FAILURE and AUTHORITY blocks become the last lines of the System Message:

# Final fragment of billing_specialist's System Message

  Result rules:
  - If you're missing customer_id, amount, or an approximate date,
    return status "pending_info" and list exactly what's missing in
    "missing". Don't make up any data.
  - If the assignment isn't billing's, return status "out_of_scope"
    and indicate in "summary" which domain it seems to belong to.
    Don't use any tool in that case.
  - If the customer demands a refund or compensation, return status
    "needs_human". Don't promise anything.
  - If a tool fails, retry exactly once; if it fails again, return
    status "needs_human" with the error in "summary".
  - Never return status "resolved" without having used at least one
    of your tools.

On the other side, the orchestrator needs to know how to read that. Its system prompt gains a section:

# Fragment of triage_agent's System Message

  How to interpret a specialist's response (status field):
  - "resolved": use the summary to compose your response to the customer.
  - "pending_info": ask the customer exactly what's in "missing",
    in a natural tone. Don't delegate again until the customer
    responds.
  - "out_of_scope": delegate to the specialist the summary points
    to. If you already re-delegated once on this same topic, don't
    try again: tell the customer you're going to escalate the case.
  - "needs_human": don't retry and don't delegate to another one.
    Let the customer know a team member is going to follow up and
    close the turn.

What to expect. A customer writes: "I got charged something weird last month." The orchestrator delegates to billing_specialist with a task that honestly says the customer reports an unknown charge but didn't give an amount or a date. The specialist, following its failure contract, doesn't call any tool — it has nothing to search with — and returns:

{
  "status": "pending_info",
  "summary": "The customer reports an unknown charge but didn't give the amount or an approximate date; without that data it's not possible to look up the charge.",
  "data": {},
  "missing": ["amount", "approximate_date"]
}

The orchestrator reads pending_info, looks at missing, and responds to the customer: "Sure, I can help with that. Do you remember roughly how much the charge was and on what date it showed up?". It didn't delegate again, didn't make up an amount, didn't say it was looking into it. And most importantly: that decision didn't come from the model interpreting a paragraph well — it came from reading a field with a value from a closed list of four.

Compare that against what would have happened with no contract. The specialist would have returned something like "I couldn't find any charge with the available information. It would be useful to know the exact amount." The orchestrator would have interpreted it, probably correctly. Or it could have concluded there's no weird charge at all and told the customer so, which is a plausible reading of that text and completely wrong.

Handoffs: the three types

A "handoff" is the moment work passes from one agent to another. There are three distinct forms and it's worth not confusing them, because n8n supports one natively and the other two get built on top of that one.

Type 1 — Delegate and return

The orchestrator calls the specialist, the specialist works, returns a result, and control goes back to the orchestrator. It's the only one that happens natively when you connect an AI Agent Tool, because it's what any tool does: it gets called, it returns, and whoever called it keeps going.

triage_agent ──calls──► billing_specialist
             ◄─result──
             (keeps reasoning)

When it applies. Always, by default. It's 90% of a support system's handoffs.

Type 2 — Redirect

The specialist determines the case isn't its own and reports it; the orchestrator re-delegates to the right one. It isn't a direct handoff between specialists: it goes through the orchestrator.

triage_agent ──calls──► order_specialist
             ◄─ out_of_scope: "this is billing's" ──
triage_agent ──calls──► billing_specialist
             ◄─result──

How it gets implemented. With the output contract's status: "out_of_scope" field, plus the instruction in the orchestrator's prompt for what to do with it. No extra node needed.

Why it goes through the orchestrator and not directly. Because the direct path — having order_specialist treat billing_specialist as a tool — creates the possibility of cycles: A calls B, B decides it was A's, A calls B again. Lesson 6 deals with that. Lesson 3's starting rule — workers don't delegate to other workers — exists precisely to make this redirect safe by construction: if every redirect goes through the orchestrator, the orchestrator can count how many there've been and cut it off.

Type 3 — Transfer of control

The specialist takes over the conversation and keeps it for several turns, without going back to the orchestrator on every message. It's a pattern that exists in some code-based agent frameworks, where the "handoff" changes who persistently handles things.

In n8n, this doesn't happen natively, and it's worth saying so clearly instead of inventing syntax. An AI Agent Tool can't keep the conversation: it's a tool, it gets called and it returns. If you genuinely need that behavior — a specialist that guides a seven-step process across several turns — you have two honest paths:

Path A: emulate it with state in the orchestrator's memory. The orchestrator keeps a field like "current conversation mode" and its system prompt says: while that mode is active, delegate every message to that specialist without re-evaluating. The specialist returns, along with its result, whether that mode stays active or ended. It works, it's visible in the trace, and the orchestrator never loses the ability to step in — which is normally good.

Path B: ask yourself if you really need it. In the vast majority of support systems, transfer of control adds nothing over delegate-and-return: the orchestrator delegates to the same specialist again on the next turn and the effect is the same, with the advantage that it can change its mind if the customer switches topics halfway through. Transfer of control gets paid for with rigidity.

The handoff table

TypeNative in n8n?How it gets implementedWhen to use it
Delegate and returnYesConnect the specialist to the ai_tool portBy default, always
RedirectYes, built on top of the previous onestatus: "out_of_scope" field + policy in the orchestrator's promptWhen domains have fuzzy boundaries
Transfer of controlNoState in the orchestrator's memory (Path A)Long guided processes; verify first that you need it

Who owns the state during a handoff

One last piece, short but decisive. When work passes from one agent to another, what happens to what's known so far?

The rule is lesson 3's, and now it has a name: the orchestrator is the sole source of truth for the conversation's state. The specialist doesn't accumulate state between calls. Everything it knows, it knows because it came in the assignment.

The practical consequences are three, and all three are good:

  1. A specialist can be called twice with two different assignments without them getting mixed up. No cross-contamination between calls.
  2. It can be tested in isolation. You send it an assignment, you see what it returns. Reproducible.
  3. When something goes wrong, there's a single place to look at the state. You don't have to reconstruct what each agent believed.

And there's exactly one cost: the orchestrator has to do the work of including in each assignment whatever's needed. That work gets paid for in tokens — the assignment is longer — and it's exactly the kind of expense lesson 7 is going to teach you to measure and bound.

Common mistakes

Writing the output contract with no failure outcome (conceptual). What happens: someone defines status with two values, resolved and error. The specialist, faced with a case where a piece of data is missing, has to choose between those two: error sounds too severe for "the amount is missing," so it returns resolved with a summary saying it found nothing. The orchestrator reads resolved and tells the customer everything's fine. Why it happens: when designing, you think about the happy path and about technical failure, and forget the intermediate states, which in customer support are the majority. How to spot it: check which status values actually showed up in a hundred executions; if 100% are resolved, your enumeration is too poor to describe what's happening. How to fix it: this lesson's four states — resolved, pending_info, out_of_scope, needs_human — cover a support system well; add others if your domain calls for them, but never leave fewer than three.

Letting the specialist return prose "because the orchestrator understands it" (conceptual). What happens: it works in testing, it works in the demo, and it fails in production in cases where the prose is ambiguous. The worst part is that it fails silently and non-reproducibly: the same text can get interpreted differently across two runs. Why it happens: the orchestrator's model is genuinely good at interpreting, and in the twenty tests you ran it got it right twenty times. How to spot it: take your specialist's free-text outputs and ask yourself, for each one, whether a careful reader could interpret it two different ways; the ones that can are time bombs. How to fix it: structured output with a status field of closed values; the prose summary still exists, but only to draft the response to the customer, never to decide the flow.

Confusing needs_human with Module 4's human review mechanism (conceptual). What happens: someone implements needs_human as an output field and considers the problem of sensitive actions solved, without putting any tool behind an actual review. The day the model decides to call a refund tool, nothing stops it — the needs_human field only exists if the model produces it. Why it happens: the two mechanisms have similar names and pursue related goals. How to spot it: ask yourself what happens if the model gets it wrong; if the answer is "it returns a different status and the action runs anyway," your barrier is textual, not structural. How to fix it: the two things coexist — Module 4's human review protects specific actions and doesn't depend on the model's judgment; needs_human is a judgment about the whole case that's useful for routing, not for blocking.

Keeping the contract in your head instead of in a file (practical). What happens: the contract exists, it works, and it lives split across the specialist's system prompt, the $fromAI()'s description, and the orchestrator's prompt. Three months later someone changes one of those three things without knowing the other two depended on it, and the system starts failing in a way nobody connects to that change. Why it happens: the contract has no home of its own in n8n; it's implicit across three different fields. How to spot it: ask someone on the team what the four possible status values of a specialist are; if they have to open three nodes to answer, the contract isn't documented. How to fix it: keep the role sheet — like the worked example's — alongside the workflow, in an n8n canvas note or in the repository where you version your flows, and treat it as the source of truth all three fields come from.

Asking the specialist for fields it can't fill (practical). What happens: the output contract includes an estimated_resolution_days field, and the specialist fills it in every single time, because a model asked for a number produces a number. That number is made up. Why it happens: when designing the contract it's easy to include fields that would be useful without checking whether any tool actually provides them. How to spot it: for every field in your output contract, point to which tool its value comes from; whatever doesn't come from any tool comes from the model, and it's probably a hallucination with formatting. How to fix it: every output contract field must be traceable to a tool result or to an explicit judgment the prompt authorizes; if not, remove it or mark it as optional and nullable.

Exercises

Exercise 1 — Write the role sheet. TuTienda adds a shipping_specialist that handles everything related to a shipment in progress: tracking the package with the carrier, managing address changes before dispatch, and reporting lost packages. It has three tools: track_shipment (read, checks the carrier's API), update_delivery_address (action, only works if the package hasn't left the distribution center), and report_lost_package (action, opens an investigation with the carrier). Write its complete role sheet with the five clauses.

See solution
┌─ ROLE SHEET ──────────────────────────────────────────────────┐
│ Agent:    shipping_specialist        Type: AI Agent Tool      │
│                                                               │
│ ROLE AND EXIT                                                 │
│   Scope: tracking shipments in progress, address change       │
│     before dispatch, lost or undelivered packages.            │
│   Out:  returns, warranties, charges, recommendations.        │
│   Finishes when: the customer knows where their package is,   │
│     or the address got updated, or an investigation got       │
│     opened for a lost package, or it got logged which data    │
│     is missing.                                                │
│                                                               │
│ INPUT                                                          │
│   Required: customer_id, order_id or tracking_number,         │
│                intent (track | change_address |                │
│                report_lost)                                    │
│   Optional:    new_address (required if intent is             │
│                change_address)                                 │
│                                                               │
│ OUTPUT                                                          │
│   status:  resolved | pending_info | out_of_scope |           │
│            needs_human                                        │
│   summary: 2-3 sentences, no greetings                        │
│   data:    { shipment_status?, eta?, new_address_confirmed?,  │
│              investigation_id? }                              │
│   missing: [ ]                                                │
│                                                               │
│ AUTHORITY                                                      │
│   Read:  track_shipment                                       │
│   Action:   update_delivery_address (only before               │
│             dispatch), report_lost_package                    │
│   No access: refunds, order cancellation, reshipping           │
│   Forbidden by prompt: promising an exact delivery date        │
│     (can only repeat the carrier's own estimate, citing        │
│     it as an estimate)                                         │
│                                                               │
│ FAILURE                                                         │
│   Missing order_id / tracking_number → pending_info            │
│   Intent = change_address but the package already shipped →   │
│     resolved with summary explaining it couldn't be done, or   │
│     needs_human if the customer insists                        │
│   Missing new_address when intent requires it → pending_info   │
│   Carrier API doesn't respond after one retry →                │
│     needs_human                                                 │
│   Assignment from another domain → out_of_scope, without       │
│     using tools                                                 │
│   Never resolved without having used at least one tool          │
│                                                               │
│ LIMITS                                                          │
│   Max Iterations: 5      No own memory                        │
└───────────────────────────────────────────────────────────────┘

Two decisions worth noting. First: update_delivery_address "only before dispatch" isn't a rule the prompt should be left to enforce on its own — if the tool itself rejects the change once the package has shipped, the barrier is structural and much better. The prompt describes the rule; the tool enforces it.

Second: the case "the package already shipped and the address can't be changed" is resolved, not a failure. The case is closed: the answer is that it can't be done. Marking everything the customer doesn't like as a failure is a common mistake that makes the resolved state lose its meaning.

Why it works: the sheet forces you to decide things that, if you don't decide them now, the model is going to decide in production, one at a time and differently each time.

Exercise 2 — Interpret the status. The orchestrator receives these four responses from specialists. For each one, say what the orchestrator should do next and what it should NOT do:

(a) {"status": "pending_info", "missing": ["order_id"], "summary": "The customer mentions an order but didn't give the number."} (b) {"status": "out_of_scope", "summary": "The customer is asking about a charge on their card, not a shipment. Belongs to billing."} (c) {"status": "needs_human", "summary": "The customer is threatening legal action over the delay."} (d) {"status": "resolved", "summary": "The package is in transit, estimated delivery 07/24.", "data": {"eta": "2026-07-24"}}

See solution

(a) Should ask the customer for the order number, in a natural tone: "Could you share the order number? It usually starts with #." Should not delegate to the same specialist again with the same assignment — the result would be identical — nor try to guess the order by checking something else.

(b) Should delegate to billing_specialist with an assignment reformulated for that domain. Should not forward the same task as-is — it was drafted for shipping — nor call the specialist that reported the out_of_scope again. And it should count this re-delegation: if there's already been one on this same topic, the policy is to close and escalate (lesson 6).

(c) Should tell the customer the case is moving to a team member, and trigger whatever escalation path the system has. Should not retry, should not delegate to another specialist looking for a better answer, and should not try to resolve the underlying complaint on its own.

(d) Should compose the response to the customer with the summary in its own voice, and if the original message had another topic pending, delegate that one. Should not quote the JSON, should not make up precision the data doesn't have ("it arrives on the 24th at 3 p.m."), and should not promise the date as certain if the specialist marked it as an estimate.

Why it works: in all four cases the orchestrator's decision came from reading a field, not from interpreting prose. That's the entire value of the output contract — and notice that in three of the four cases, what it should NOT do is retry, which is exactly the behavior that produces lesson 6's loops.

Exercise 3 — Find the hallucinated fields. A team proposes this output contract for a warranty_specialist that has two tools: lookup_purchase (returns purchase date, product, and price) and create_repair_request (creates a repair request and returns its number). Point out which fields can't be filled reliably and why.

{
  "status": "resolved",
  "summary": "...",
  "data": {
    "warranty_valid": true,
    "days_remaining": 143,
    "repair_request_id": "R-2291",
    "estimated_repair_cost": 0,
    "estimated_repair_days": 7,
    "customer_satisfaction_risk": "low"
  }
}
See solution

Traceable to a tool, and therefore legitimate:

  • warranty_valid and days_remaining — come from the purchase date lookup_purchase returns plus the warranty length, which is a known business rule. Legitimate, though the date math would be better done in a deterministic sub-workflow instead of leaving it to the model (Module 4, lesson 6): a model subtracting dates is an unnecessary risk.
  • repair_request_id — comes directly from create_repair_request. Legitimate.

Hallucinated or unreliable:

  • estimated_repair_cost — neither tool returns a cost. If the warranty covers it, the value 0 could be a valid business rule, but then it needs to be written explicitly into the prompt and limited to that case; if it doesn't cover it, the model is going to make up a figure.
  • estimated_repair_days — no tool provides it. The model is going to produce a plausible number, typically 7, 10, or 15, and the orchestrator is going to tell the customer as if it were a fact.
  • customer_satisfaction_risk — it's a subjective judgment with no source at all. It could be legitimate if the prompt explicitly defines the criterion ("high if the customer mentions prior dissatisfaction or threatens to leave") and if the orchestrator only uses it for routing, never to say something to the customer. As it stands, it's a field that sounds like data and is an opinion.

The fix: remove estimated_repair_cost and estimated_repair_days, or get a tool that genuinely provides them; and if customer_satisfaction_risk stays, define its criterion in writing and mark it as internal.

Why it works: the test "which tool does this value come from?" is quick and catches the problem before a customer receives a made-up repair date. A model never leaves a field blank if you ask it to fill it in.

Summary and next step

You now know how to design the team, not just wire it up. An agent contract has five clauses: role and exit criterion (with at least one failure outcome), input contract (the minimum fields, in self-contained text or in typed fields), output contract (status with closed values, summary to compose with, data for concrete facts, missing for what's absent), authority contract (what's structural and what's textual), and failure contract (what it returns when it can't, including the prohibition on returning resolved without having used any tool). The status is the piece that makes the orchestrator decide by reading a field instead of interpreting prose. And of the three handoff types, n8n natively supports delegate-and-return; the redirect gets built on top of it with out_of_scope; and transfer of control doesn't exist natively and is almost never needed.

Before moving on you should be able to: write a five-clause role sheet for a new specialist; list the four output states and say what the orchestrator should do with each one; tell a structural restriction apart from a textual one in the authority contract; and spot an output contract field no tool can fill.

What's still pending is the danger this contract makes possible. Now that a specialist can say out_of_scope and the orchestrator can re-delegate, there's a path by which the system hands the same case from one agent to another and never reaches a response. And now that every specialist has its own agentic loop inside the orchestrator's loop, there's the possibility that one of them iterates until it exhausts its budget without anyone noticing. A multi-agent system's stopping conditions are lesson 6's whole topic.

Resources