Skip to content

5. Agents That Work as a Team

With multi-agent setups, you can have multiple agents collaborate within a single session. On this page, you will build a coordinator that leads a researcher (web research via the Jina MCP) and a reporter (draft creation via the Gmail MCP), and have the team handle the job of “researching the news and summarizing it into an email draft.”

This page introduces the following Managed Agents features:

FeatureRole
Multi-agent (multiagent)Configuration that gives an agent a list (roster) of agents it can delegate to
Session threadsAn independent execution context for each subagent. They run in parallel

Purely in terms of functionality, you could achieve the same thing by giving a single agent both the Jina and Gmail MCP servers. Still, splitting the work across a team pays off in the three patterns the official documentation calls out:

  • Parallelization — fan out independent subtasks at the same time and have the coordinator integrate the results
  • Specialization — route work to agents with dedicated prompts and tools. By not giving one agent every capability, you can scope tools and credentials to individual agents
  • Escalation — hand only the hard subtasks to a more capable model

The setup on this page includes all three. The coordinator (claude-sonnet-5) can neither search the web nor touch email; the researcher (claude-haiku-4-5) can only use Jina, and the reporter (claude-haiku-4-5) can only use Gmail. Each agent runs in an independent context called a session thread, so the researcher’s long logs never pollute the coordinator’s conversation.

Create a project, move into its folder, and install the Claude SDK.

Terminal window
uv init team-agent
cd team-agent
uv add anthropic
  1. Create setup_team.py in the project folder and save the following content.

    setup_team.py
    import json
    from anthropic import Anthropic
    client = Anthropic()
    ###############################
    # 1. Find the vaults that hold the Jina and Gmail credentials
    ###############################
    vault_ids = []
    for vault in client.beta.vaults.list():
    for cred in client.beta.vaults.credentials.list(vault_id=vault.id):
    url = getattr(cred.auth, "mcp_server_url", "") or ""
    if ("mcp.jina.ai" in url or "gmailmcp.googleapis.com" in url) and vault.id not in vault_ids:
    vault_ids.append(vault.id)
    print(f"Found credential: vault={vault.id} ({vault.display_name}) / {url}")
    if not vault_ids:
    raise SystemExit("No Jina or Gmail credentials found.")
    ###############################
    # 2. Create an environment (allow network access to MCP servers)
    ###############################
    environment = client.beta.environments.create(
    name="Team-environment",
    config={
    "type": "cloud",
    "networking": {"type": "limited", "allow_mcp_servers": True},
    },
    )
    print(f"environment: {environment.id}")
    ###############################
    # 3. Create the researcher (has only the Jina MCP)
    ###############################
    researcher = client.beta.agents.create(
    name="Team Researcher",
    model={"id": "claude-haiku-4-5", "speed": "standard"},
    description="A researcher that investigates topics via web search and article reading.",
    system=(
    "You are a researcher. Search for the requested topic with search_web, "
    "and read the full text of promising articles with read_url. "
    "Return your findings as a concise English summary with source URLs."
    ),
    mcp_servers=[{"name": "jina", "type": "url", "url": "https://mcp.jina.ai/v1"}],
    tools=[
    {
    "type": "mcp_toolset",
    "mcp_server_name": "jina",
    "default_config": {"enabled": True, "permission_policy": {"type": "always_allow"}},
    }
    ],
    )
    print(f"researcher: {researcher.id}")
    ###############################
    # 4. Create the reporter (has only the Gmail MCP, with pre-execution approval)
    ###############################
    reporter = client.beta.agents.create(
    name="Team Reporter",
    model={"id": "claude-haiku-4-5", "speed": "standard"},
    description="A reporter that creates Gmail drafts.",
    system=(
    "You are a reporter. Turn the content you are given into a readable English email "
    "and create it as a Gmail draft. Do not send any email."
    ),
    mcp_servers=[{"name": "gmail", "type": "url", "url": "https://gmailmcp.googleapis.com/mcp/v1"}],
    tools=[
    {
    "type": "mcp_toolset",
    "mcp_server_name": "gmail",
    "default_config": {"enabled": True, "permission_policy": {"type": "always_ask"}},
    }
    ],
    )
    print(f"reporter: {reporter.id}")
    ###############################
    # 5. Create the coordinator (no external permissions; delegates to the researcher and reporter)
    ###############################
    coordinator = client.beta.agents.create(
    name="Research Team Coordinator",
    model={"id": "claude-sonnet-5", "speed": "standard"},
    description="A coordinator that leads the researcher and the reporter.",
    system=(
    "You are the coordinator of a research team. "
    "Delegate research to Team Researcher and email draft creation to Team Reporter. "
    "You yourself can neither search the web nor operate email. "
    "When researching multiple topics, run the researcher in parallel. "
    "Finally, integrate the results and report back to the user in English."
    ),
    tools=[
    {
    "type": "agent_toolset_20260401",
    "default_config": {"enabled": True},
    "configs": [
    {"name": "web_search", "enabled": False},
    {"name": "web_fetch", "enabled": False},
    ],
    }
    ],
    multiagent={
    "type": "coordinator",
    "agents": [
    {"type": "agent", "id": researcher.id},
    {"type": "agent", "id": reporter.id},
    ],
    },
    )
    print(f"coordinator: {coordinator.id}")
    with open("ids_team.json", "w") as f:
    json.dump(
    {
    "vault_ids": vault_ids,
    "environment_id": environment.id,
    "coordinator_id": coordinator.id,
    },
    f,
    indent=2,
    )
    print("Saved the IDs to ids_team.json")

    There are three key points.

    • multiagent.agents is the list of delegation targets (the roster). Simply registering existing agents by ID turns them into a team (delegation is limited to one level deep, and you can register up to 20 agents)
    • Tools and credentials are scoped per agent. Only the researcher can touch Jina, and only the reporter can touch Gmail. The coordinator also has the built-in web_search/web_fetch tools individually disabled, so it cannot reach the outside world on its own
    • Different models for different roles — only the coordinator, which handles integration and judgment, uses claude-sonnet-5; the workers use claude-haiku-4-5
  2. Run the script.

    Terminal window
    uv run setup_team.py
  1. Create run_team.py in the project folder and save the following content. Replace TO_ADDRESS at the top with your own email address.

    run_team.py
    import json
    from anthropic import Anthropic
    # Recipient of the email draft (replace with your own email address)
    TO_ADDRESS = "your email address here"
    client = Anthropic()
    with open("ids_team.json") as f:
    ids = json.load(f)
    ###############################
    # 1. Create a session (combining the coordinator, environment, and vaults)
    ###############################
    session = client.beta.sessions.create(
    agent=ids["coordinator_id"],
    environment_id=ids["environment_id"],
    vault_ids=ids["vault_ids"],
    )
    print(f"session: {session.id}")
    print(f"Console: https://platform.claude.com/workspaces/default/sessions/{session.id}")
    print("Chat with the agent team. Type exit to quit.\n")
    def text_of(content) -> str:
    return "".join(block.text for block in content if block.type == "text")
    ###############################
    # 2. Send and receive one turn (show the team's activity; confirm approvals with y/N)
    ###############################
    def run_turn(text: str) -> None:
    pending_tools = {} # event_id -> (tool name, input, thread ID)
    stream = client.beta.sessions.events.stream(session_id=session.id)
    client.beta.sessions.events.send(
    session.id,
    events=[{"type": "user.message", "content": [{"type": "text", "text": text}]}],
    )
    for event in stream:
    if event.type == "session.thread_created":
    print(f"[Thread created: {event.agent_name}]")
    elif event.type == "agent.thread_message_sent":
    summary = text_of(event.content).replace("\n", " ")[:80]
    print(f"[→ request to {event.to_agent_name}] {summary}...")
    elif event.type == "agent.thread_message_received":
    summary = text_of(event.content).replace("\n", " ")[:80]
    print(f"[← result from {event.from_agent_name}] {summary}...")
    elif event.type == "agent.mcp_tool_use":
    if getattr(event, "evaluated_permission", None) == "ask":
    thread_id = getattr(event, "session_thread_id", None)
    pending_tools[event.id] = (event.name, event.input, thread_id)
    else:
    print(f"[Tool: {event.name}]")
    elif event.type == "agent.message":
    for block in event.content:
    if block.type == "text":
    print(block.text)
    elif event.type == "session.status_idle":
    if event.stop_reason.type == "requires_action":
    for event_id in event.stop_reason.event_ids:
    if event_id not in pending_tools:
    continue # skip approval requests that have already been answered
    name, tool_input, thread_id = pending_tools.pop(event_id)
    print("\n----- Approval request -----")
    print(f"Tool: {name}")
    print(f"Input: {json.dumps(tool_input, ensure_ascii=False, indent=2)}")
    answer = input("Allow execution? [y/N] ").strip().lower()
    confirmation = {
    "type": "user.tool_confirmation",
    "tool_use_id": event_id,
    "result": "allow" if answer == "y" else "deny",
    }
    if thread_id:
    confirmation["session_thread_id"] = thread_id
    if answer != "y":
    confirmation["deny_message"] = "The user denied execution."
    client.beta.sessions.events.send(session.id, events=[confirmation])
    print("Response sent. Continuing...\n")
    else:
    break
    elif event.type == "session.status_terminated":
    print("The session has terminated.")
    raise SystemExit(1)
    stream.close()
    ###############################
    # 3. Send the first task, then keep the conversation going
    ###############################
    run_turn(
    "Research two of today's generative AI news stories and summarize each in 2-3 lines. "
    f"Then put those summaries into an email draft addressed to {TO_ADDRESS} (do not send it)."
    )
    while True:
    user_input = input("\nYou> ").strip()
    if user_input == "exit":
    break
    if not user_input:
    continue
    run_turn(user_input)
    print("\nDone")

    What differs from the earlier interactive scripts is that this one displays the multi-agent-specific events.

    EventMeaning
    session.thread_createdA subagent thread was created
    agent.thread_message_sentThe coordinator sent a request to a subagent
    agent.thread_message_receivedA subagent’s result arrived at the coordinator

    Also, approval requests from a subagent (the reporter) are cross-posted to the main session stream. When you reply with user.tool_confirmation, attach the session_thread_id of the originating request. Because the same approval request can arrive twice, the script skips IDs that have already been answered.

  2. Run the script. You can watch the delegation flow as events, and when the draft is being created you will see the same approval request as in 3. An Agent That Works with Gmail.

    Terminal window
    uv run run_team.py
    session: sesn_012EBVy3rLmecShnTGTU1PxZ
    Chat with the agent team. Type exit to quit.
    [Thread created: Team Researcher]
    [→ request to Team Researcher] Research two of today's (as of the current date) generative AI news stories...
    I have asked the researcher to investigate. Once the results arrive, I will review them and ask the Reporter to create the email draft.
    [← result from Team Researcher] I have completed the research on generative AI news as of July 5, 2026. Here are the two stories...
    [Thread created: Team Reporter]
    [→ request to Team Reporter] Create a Gmail draft (do not send it) addressed to (your email address)...
    ----- Approval request -----
    Tool: create_draft
    Input: {
    "body": "Hello,\n\nHere is today's roundup of important generative AI news....",
    "subject": "Today's Generative AI News Roundup (July 5, 2026)",
    "to": [
    "(your email address)"
    ]
    }
    Allow execution? [y/N] y
    Response sent. Continuing...
    [← result from Team Reporter] The Gmail draft has been created. [Details] - To: (your email address) - Subject: Today's Generative AI News Roundup...
    Team Researcher's research and Team Reporter's email draft are both complete. Here is a summary of the results.
    ...
    You> exit
    Done
  3. In Gmail’s “Drafts” folder, you will find the news roundup draft the team created. If you open the session page in the console, you can also follow the exchanges between the coordinator and each thread on screen.

  • Just register existing agents by ID in multiagent.agents (the roster) and you have a team the coordinator can delegate to. Agents are reusable building blocks.
  • Tools and credentials are scoped per agent. You can divide work with least privilege: the coordinator gets no external permissions, the researcher gets only Jina, and the reporter gets only Gmail (vaults apply to the whole session, so a single attachment is enough).
  • Subagents run in parallel as session threads, and you can observe them through the session.thread_created and agent.thread_message_sent/received events.
  • Subagents’ approval requests are also cross-posted to the main session, so the human approval gate can be managed in one place.

Great work! Please share your thoughts so far in a post on X.

Share your thoughts on X