Skip to content

19. Managed teams

Build a newsletter team: an editor, a writer and a fact checker. The editor is the manager. It gets the task, decides who does what and in which order, reads what comes back, and says when the newsletter is good enough. You’ll read the log of every piece of work it handed out, and see what happens when it runs out of the work it may hand out. Use a managed team when a job needs several kinds of work and someone to judge the result.

You’ll learn

  • managed interactions: a manager that hands work to named team members
  • what each member sees: a fresh start every time, or its own ongoing conversation
  • reading what the manager did from the run
  • the limits on a team, and what the manager does when it reaches one
  • choosing between a router, sub-interactions and a managed team

New concepts: managed teams · Builds on: 18. Sub-interactions · Time: about 15 minutes · Cost: under $0.01 per newsletter on DeepSeek · Needs: nothing beyond the tutorials’ setup

The editor is an ordinary agent. Its prompt says how to run the team, and that its final reply is the newsletter:

maitento/agents/editor.mtr
model = @ref("deepseek.deepseek-v4-flash")
prompt = "You are a newsletter editor. You plan the issue, hand work to your team with the Dispatch tool, and decide when it is good enough. Your final reply must be the finished newsletter and nothing else: no notes about the edits."
summary = "Plans and edits the newsletter"

The writer and the fact checker are ordinary agents too. The managed interaction puts them together: the manager owns the task, and the agents are the team it can hand work to:

maitento/interactions/managed/newsletter.mtr
// A managed interaction: the manager gets the task and hands out work.
// Team members only see what the manager sends them.
task = "Produce a three-paragraph newsletter about %%topic%% for a general audience."
inputs.prompts = [
{
name = "topic"
required = true
}
]
manager = {
name = "Editor"
agent = @ref("editor")
}
agents = [
{
name = "Writer"
agent = @ref("writer")
// Private: the writer keeps its own ongoing conversation with the editor.
mode = "Private"
},
{
name = "FactChecker"
agent = @ref("fact-checker")
// OneTime: a fresh fact checker every time it is asked.
mode = "OneTime"
}
]
limits.maxDispatches = 8
duration.max = "00:06:00"

The manager gets a Dispatch tool. Each call names a team member and gives it a message. The member takes one turn on that message, and its reply is the result of the call. A member’s mode decides what it sees:

modeThe member sees…Use it for
OneTime (the default)the manager’s message, and nothing else, every timeindependent checks: a fresh pair of eyes
Privateits own ongoing conversation with the manager: every message it was sent and every replywork that’s revised: it remembers its draft
Groupthe shared conversation, like a round-robin where it speaks only when the manager hands it the turnmembers who need to hear each other

Done when: you can say which member remembers earlier work (the writer) and which starts fresh (the fact checker).

Run the scripts from examples/19-managed-teams/typescript, python or csharp, as in tutorial 1. After the run, the script prints each piece of work the editor handed out, with the first line of the reply, then the newsletter:

import { Maitento } from "@maitento/sdk";
const mt = new Maitento();
await mt.deploy("../maitento");
// An editor hands work to a writer and a fact checker, then signs off the newsletter.
const run = await mt.interactions.run("newsletter", { inputs: { topic: "why sleep matters" } });
// Each piece of work the editor handed out is a Dispatch call on the run. Its result is the member's reply.
const firstLine = (text: string | null | undefined) => (text ?? "").split("\n").find((line) => line.trim()) ?? "";
for (const call of run.context.toolCall.filter((c) => c.functionName === "Dispatch")) {
const { agent, message } = JSON.parse(call.argumentsJson) as { agent: string; message: string };
console.log(`Editor -> ${agent}: ${firstLine(message).slice(0, 90)}`);
const result = run.context.toolResult.find((r) => r.toolCallId === call.toolCallId);
if (result) {
const reply = await mt.interactionRuns.toolResult(run.id, result.sequence);
console.log(` ${firstLine(reply.result).slice(0, 100)}`);
}
}
console.log(`\n${run.output}`);
Editor -> Writer: Write a three-paragraph newsletter section on why sleep matters, for a general audience. R
**Why Sleep Matters More Than You Think**
Editor -> FactChecker: Please fact-check the following short newsletter section on sleep for accuracy. Verify eve
**Fact-check: "Why Sleep Matters More Than You Think"**
Editor -> Writer: Your sleep newsletter draft is approved with the following fact-check changes. Please retu
**Why Sleep Matters More Than You Think**
**Why Sleep Matters More Than You Think**
Sleep is far from idle time. While you rest, your body repairs tissue, strengthens the immune system and regulates
hormones that control appetite and stress. …
When sleep falls short, the effects show up quickly. …
The good news is that small, practical changes can make a real difference. …

A run takes about 30 seconds. What happened:

  1. The editor got the task and the Dispatch tool. Nobody told it the order: it chose a draft first, then a check.
  2. It sent the draft to the fact checker inside its message. The fact checker is OneTime, so the message is all it saw.
  3. It sent the fact checker’s corrections back to the writer. The writer is Private, so it still had its own draft and only had to change it.
  4. When it was happy, it replied with the newsletter, and that reply became the run’s output.

Every dispatch is kept on the run: a Dispatch tool call in context.toolCall, with agent and message in its arguments, and the member’s reply as its result. The result’s text is fetched by its sequence number, with interactionRuns.toolResult. The portal’s Runs page shows the same: open the run to read each dispatch and reply.

Done when: you see at least two dispatches, one to the writer and one to the fact checker, then the newsletter.

3. Break it on purpose: run out of dispatches

Section titled “3. Break it on purpose: run out of dispatches”

A team needs limits, or a manager could hand out work for ever. Allow the editor just one dispatch. In newsletter.mtr, change limits.maxDispatches = 8 to limits.maxDispatches = 1, and run the script again:

Editor -> Writer: Write a three-paragraph newsletter item for a general audience on why sleep matters. Requi
**The Quiet Work of Sleep**
Editor -> FactChecker: Please fact-check the following short newsletter item for accuracy. Verify: (1) the claim
You have made all 1 dispatches this interaction allows. Finish with what you have.
**The Quiet Work of Sleep**
Sleep is sometimes treated as optional, but it quietly underpins nearly everything we do. …

The editor still asked for a fact check, but the call was refused. The refusal came back as the call’s result, so the editor finished with what it had: a newsletter nobody checked. The run still ends Finished, and nothing in it is an error. A limit like this is soft: the manager is told, and decides what to do.

A team has three limits:

LimitDefaultWhen it’s reached
limits.maxDispatches30 (at most 200)the next Dispatch is refused, and the manager is told to finish
context.maxToolCallLoops10the manager’s tools are taken away for the rest of its turn, and it must answer
duration.maxnonethe run stops with status Error and Interaction timed out, with no newsletter

duration.warn tells the agents when time is getting short, before duration.max stops them.

Change maxDispatches back to 8 before you go on.

Done when: the second dispatch was refused, and the newsletter still came back.

You’ve now seen three ways for agents to share work. They answer different questions:

RouterSub-interactionsManaged team
Who decidesa router picks one specialist per turnan agent starts runs as it sees fita manager hands out each piece
Runs at onceone specialistmany, in parallelone dispatch at a time
What helpers seethe whole conversationtheir own inputs, in their own runthe manager’s message, or their own conversation
Good forquestions that each belong to one expertmany independent pieces of the same kindwork that’s drafted, checked and revised
In the runroutingDecisionschild runs, each naming its parentDispatch calls in the run’s context

A managed team works in one run, one dispatch at a time. When the pieces can run at the same time, give the manager sub-interactions as well: a managed interaction can have a swarm block too.

For a conversation where agents and a person talk things over, use a round-robin instead.

Done when: you can say which shape fits a support desk that sends billing questions to one agent and technical ones to another. (A router.)

Every file in this tutorial’s project:

On GitHub: examples/19-managed-teams · TypeScript · Python · C#

All the files in this project (4)
model = @ref("deepseek.deepseek-v4-flash")
prompt = "You are a newsletter editor. You plan the issue, hand work to your team with the Dispatch tool, and decide when it is good enough. Your final reply must be the finished newsletter and nothing else: no notes about the edits."
summary = "Plans and edits the newsletter"
  • The output is the editor’s notes about the newsletter, not the newsletter: the manager’s final reply is the run’s output. Say in its prompt that its final reply must be the finished piece and nothing else, as editor.mtr does.
  • The editor writes the newsletter itself, with no dispatches: a manager can do the work alone if its prompt lets it. Tell it to hand work out with the Dispatch tool.
  • “There is no agent called …”: the manager used a name that isn’t in agents. The names it may use are the names in the interaction, not the agent files’ names.
  • The run ends Error with Interaction timed out: the team needed longer than duration.max. Raise it.
  • The run ends Error, and its failure details end with [api:model-keys:none-for-provider]: the tenant has no DeepSeek key. Add one (Model keys), or change model in the three agent files.

What you built: a newsletter team where an editor hands work to a writer and a fact checker, and decides when it’s done; a log of every piece of work it handed out; and a run where the team hit its limit.

The concepts:

  • Managed teams: a manager owns the task and hands work to named members with Dispatch. Its final reply is the answer.
  • Member modes: OneTime starts fresh, Private keeps its own conversation with the manager, Group sees the shared one.
  • Limits: maxDispatches is soft (the manager is told), duration.max is hard (the run stops).
  • Choosing a shape: a router picks one expert, sub-interactions fan out in parallel, a manager plans and judges.

Challenge: give the team a headline writer. It should start fresh each time, and the editor should put a headline on top of the newsletter. Print which members the editor asked, in order. A solution in TypeScript, Python and C# is in examples/19-managed-teams/solution:

The editor handed out work 3 times: Writer, FactChecker, HeadlineWriter
Sleep: The Maintenance Your Body Needs
Sleep is easy to treat as optional — the thing we cut when the day runs short — but the body treats it as
essential. …

Next: 20. Memory gives an agent a memory that lasts from one run to the next.