> ## Documentation Index
> Fetch the complete documentation index at: https://docs.restate.dev/llms.txt
> Use this file to discover all available pages before exploring further.

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.restate.dev/feedback

```json
{
  "path": "/ai/patterns/multi-agent",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# Multi-Agent Orchestration

> Route tasks between specialized agents with durable decisions. Coordinate agents within the same process using handoffs and tools.

export const GitHubLink = ({url}) => <div style={{
  marginTop: '-8px',
  marginBottom: '8px',
  textAlign: 'right'
}}>
    <a href={url} target="_blank" rel="noopener noreferrer" style={{
  fontSize: '0.75rem',
  color: '#6B7280',
  textDecoration: 'none',
  display: 'inline-flex',
  alignItems: 'center',
  gap: '3px',
  padding: '2px 6px',
  borderRadius: '3px',
  border: '1px solid #E5E7EB',
  backgroundColor: 'transparent',
  transition: 'all 0.2s ease'
}} onMouseOver={e => {
  e.target.style.color = '#6B7280';
  e.target.style.backgroundColor = '#F9FAFB';
}} onMouseOut={e => {
  e.target.style.color = '#6B7280';
  e.target.style.backgroundColor = 'transparent';
}}>
      <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
        <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.230 3.297-1.230.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
      </svg>
      View on GitHub
    </a>
  </div>;

export const GlobalTab = ({title, icon, children}) => {
  return <div>{children}</div>;
};

export const GlobalTabs = ({children, className = ''}) => {
  const [activeTab, setActiveTab] = useState(0);
  const tabs = React.Children.toArray(children).filter(child => child.type && child.type.name === 'GlobalTab');
  useEffect(() => {
    const savedLanguage = localStorage.getItem('language');
    if (savedLanguage) {
      const matchingIndex = tabs.findIndex(tab => tab.props.title === savedLanguage);
      if (matchingIndex !== -1) {
        setActiveTab(matchingIndex);
      }
    }
  }, [tabs]);
  useEffect(() => {
    const handleGlobalTabChange = event => {
      const targetTitle = event.detail.title;
      const matchingIndex = tabs.findIndex(tab => tab.props.title === targetTitle);
      if (matchingIndex !== -1 && matchingIndex !== activeTab) {
        setActiveTab(matchingIndex);
      }
    };
    window.addEventListener('globalTabChange', handleGlobalTabChange);
    return () => window.removeEventListener('globalTabChange', handleGlobalTabChange);
  }, [tabs, activeTab]);
  const handleTabClick = index => {
    setActiveTab(index);
    const title = tabs[index].props.title;
    localStorage.setItem('language', title);
    window.dispatchEvent(new CustomEvent('globalTabChange', {
      detail: {
        title
      }
    }));
  };
  return <div className={`tabs tabs tab-container ${className}`}>
            <ul className="not-prose mb-6 pb-[1px] flex-none min-w-full overflow-auto border-b border-gray-200 gap-x-6 flex dark:border-gray-200/10" data-component-part="tabs-list">
                {tabs.map((tab, index) => <li key={index} className="cursor-pointer">
                        <button className={index === activeTab ? "flex text-sm items-center gap-1.5 leading-6 font-semibold whitespace-nowrap pt-3 pb-2.5 -mb-px max-w-max border-b text-primary dark:text-primary-light border-current" : "flex text-sm items-center gap-1.5 leading-6 font-semibold whitespace-nowrap pt-3 pb-2.5 -mb-px max-w-max border-b text-gray-900 border-transparent hover:border-gray-300 dark:text-gray-200 dark:hover:border-gray-700"} data-component-part="tab-button" data-active={index === activeTab} onClick={() => handleTabClick(index)}>
                            {tab.props.icon && <img src={tab.props.icon} alt="" className="h-4 w-4 not-prose" noZoom />}
                            {tab.props.title}
                        </button>
                    </li>)}
            </ul>
            <div className="prose dark:prose-dark overflow-x-auto" data-component-part="tab-content">
                {tabs[activeTab]?.props.children}
            </div>
        </div>;
};

Many agent systems need a **router** that decides which specialist agent should handle a request. Restate makes these routing decisions durable: if the process crashes after the LLM picks an agent but before that agent responds, recovery skips the routing step and resumes the agent call.

<GlobalTabs>
  <GlobalTab title="Vercel AI" icon={"/img/languages/typescript.svg"} />

  <GlobalTab title="OpenAI Agents" icon={"/img/languages/python.svg"} />

  <GlobalTab title="Google ADK" icon={"/img/languages/python.svg"} />

  <GlobalTab title="Pydantic AI" icon={"/img/languages/python.svg"} />

  <GlobalTab title="Restate TS" icon={"/img/languages/typescript.svg"} />

  <GlobalTab title="Restate Py" icon={"/img/languages/python.svg"} />
</GlobalTabs>

## How it works

1. A router agent receives the request
2. An LLM decides which specialist to delegate to (persisted as a durable step)
3. The specialist agent processes the request
4. The router returns the result

Routing decisions, agent calls, and results are all recorded in the journal.

## Example: routing to specialist agents

Define specialist agents within the same process. The LLM picks the right one, and Restate ensures the decision sticks.

<GlobalTabs className={"hidden-tabs"}>
  <GlobalTab title="Vercel AI">
    With the Vercel AI, specialist agents are exposed as tools. The LLM decides which tool to call, and Restate durably persists the routing decision.

    ```typescript multi-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/multi-agent.ts#here"}  theme={null}
    async function runEligibilityAgent(model: LanguageModel, claim: InsuranceClaim){
      const { text } = await generateText({
        model,
        system:
            "Decide whether the following claim is eligible for reimbursement." +
            "Respond with eligible if it's a medical claim, and not eligible otherwise.",
        prompt: JSON.stringify(claim),
      });
      return text;
    }

    async function runFraudAgent(model: LanguageModel, claim: InsuranceClaim){
      const { text } = await generateText({
        model,
        system:
            "Decide whether the claim is fraudulent." +
            "Always respond with low risk, medium risk, or high risk.",
        prompt: JSON.stringify(claim),
      });
      return text;
    }

    const run = async (ctx: restate.Context, claim: ClaimInput) => {
      const model = wrapLanguageModel({
        model: openai("gpt-4o"),
        middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
      });

      const { text } = await generateText({
        model,
        prompt: `Claim: ${JSON.stringify(claim)}`,
        system:
          "Analyze the insurance claim and use your tools to decide whether to approve.",
        tools: {
          analyzeEligibility: tool({
            description: "Analyze claim eligibility.",
            inputSchema: InsuranceClaimSchema,
            execute: async (claim: InsuranceClaim) => runEligibilityAgent(model, claim),
          }),
          analyzeFraud: tool({
            description: "Analyze probability of fraud.",
            inputSchema: InsuranceClaimSchema,
            execute: async (claim: InsuranceClaim) => runFraudAgent(model, claim),
          }),
        },
        stopWhen: [stepCountIs(10)],
        providerOptions: { openai: { parallelToolCalls: false } },
      });

      return text;
    };
    ```

    <GitHubLink url="https://github.com/restatedev/ai-examples/tree/main/vercel-ai/tour-of-agents/src/multi-agent.ts" />

    <Accordion title="Try out multi-agent systems" icon="laptop">
      [Install Restate](/installation) and launch it:

      ```bash theme={null}
      npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
      restate-server
      ```

      Get the example:

      ```bash theme={null}
      restate example typescript-vercel-ai-tour-of-agents && cd typescript-vercel-ai-tour-of-agents
      npm install
      ```

      Export your [OpenAI API key](https://platform.openai.com/api-keys) and run the agent:

      ```bash theme={null}
      export OPENAI_API_KEY=sk-...
      ```

      ```bash theme={null}
      npx tsx ./src/multi-agent.ts
      ```

      Register the agents with Restate:

      ```bash theme={null}
      restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
      ```

      Start a request for a claim that needs to be analyzed by multiple agents:

      ```bash theme={null}
      curl localhost:8080/MultiAgentClaimApproval/run --json '{
          "date":"2024-10-01",
          "category":"orthopedic",
          "reason":"hospital bill for a broken leg",
          "amount":3000,
          "placeOfService":"General Hospital"
      }'
      ```

      In the UI, in the LLM responses, you can see that the agent called the sub-agents via tools.
    </Accordion>
  </GlobalTab>

  <GlobalTab title="OpenAI Agents">
    With the OpenAI Agents, you use `handoffs` for in-process agent delegation with automatic context sharing. The intake agent routes to the right specialist based on the claim type.

    You can use Virtual Object state to remember the last agent that handled a request, so the user can reconnect seamlessly on the next interaction.

    ```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/multi_agent.py#here"}  theme={null}
    medical_agent = Agent(
        name="MedicalSpecialist",
        handoff_description="I handle medical insurance claims from intake to final decision.",
        instructions="Review medical claims for coverage and necessity. Approve/deny up to $50,000.",
    )

    car_agent = Agent(
        name="CarSpecialist",
        handoff_description="I handle car insurance claims from intake to final decision.",
        instructions="Assess car claims for liability and damage. Approve/deny up to $25,000.",
    )


    intake_agent = Agent(
        name="IntakeAgent",
        instructions="Route insurance claims to the appropriate specialist",
        handoffs=[medical_agent, car_agent],
    )

    agent_dict = {
        "IntakeAgent": intake_agent,
        "MedicalSpecialist": medical_agent,
        "CarSpecialist": car_agent,
    }

    agent_service = restate.VirtualObject("MultiAgentClaimApproval")


    @agent_service.handler()
    async def run(ctx: restate.ObjectContext, claim: InsuranceClaim) -> str:
        # Store context in Restate's key-value store
        last_agent_name = await ctx.get("last_agent_name", type_hint=str) or "IntakeAgent"
        last_agent = agent_dict.get(last_agent_name, intake_agent)

        result = await DurableRunner.run(
            last_agent, f"Claim: {claim.model_dump_json()}", session=RestateSession()
        )

        ctx.set("last_agent_name", result.last_agent.name)
        return result.final_output
    ```

    <GitHubLink url="https://github.com/restatedev/ai-examples/blob/main/openai-agents/tour-of-agents/app/multi_agent.py" />

    <Accordion title="Try out multi-agent systems" icon="laptop">
      [Install Restate](/installation) and launch it:

      ```bash theme={null}
      restate-server
      ```

      Get the example:

      ```bash theme={null}
      restate example python-openai-agents-tour-of-agents && cd python-openai-agents-tour-of-agents
      ```

      Export your [OpenAI API key](https://platform.openai.com/api-keys) and run the agent:

      ```bash theme={null}
      export OPENAI_API_KEY=sk-...
      ```

      ```bash theme={null}
      uv run app/multi_agent.py
      ```

      Register the agents with Restate:

      ```bash theme={null}
      restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
      ```

      Start a request for a claim that needs to be analyzed by multiple agents:

      ```bash theme={null}
      curl localhost:8080/MultiAgentClaimApproval/session123/run --json '{
          "date":"2024-10-01",
          "category":"orthopedic",
          "reason":"hospital bill for a broken leg",
          "amount":3000,
          "placeOfService":"General Hospital"
      }'
      ```

      In the UI, you can see that the agent called the sub-agents and is waiting for their responses.

      Once all sub-agents return, the main agent continues and makes a decision.

      <Frame>
        <img src="https://mintcdn.com/restate-6d46e1dc/AlmW9-xJqv-0ObCA/img/tour/agents/openai/multi-agent.png?fit=max&auto=format&n=AlmW9-xJqv-0ObCA&q=85&s=7794631f7f74e55f97c7ec26b78dd8a8" alt="Multi-agent execution trace" width="1863" height="941" data-path="img/tour/agents/openai/multi-agent.png" />
      </Frame>

      The state now contains the last agent that was called, so you can continue the conversation directly with the same agent:

      <Frame>
        <img src="https://mintcdn.com/restate-6d46e1dc/AlmW9-xJqv-0ObCA/img/tour/agents/openai/multi-agent-state.png?fit=max&auto=format&n=AlmW9-xJqv-0ObCA&q=85&s=df1d8554ae49643f51834be86a117d45" alt="Multi-agent state" width="1342" height="531" data-path="img/tour/agents/openai/multi-agent-state.png" />
      </Frame>
    </Accordion>
  </GlobalTab>

  <GlobalTab title="Google ADK">
    With the Google ADK, you use `sub_agents` for agent routing within the same app. The intake agent delegates to the right specialist based on the claim type.

    ```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/google-adk/tour-of-agents/app/multi_agent.py#here"}  theme={null}
    # AGENTS
    # Determine which specialist to use based on claim type
    medical_agent = Agent(
        model="gemini-2.5-flash",
        name="medical_specialist",
        description="Reviews medical insurance claims for coverage and necessity.",
        instruction="Review medical claims for coverage and necessity. Approve/deny up to $50,000.",
    )

    car_agent = Agent(
        model="gemini-2.5-flash",
        name="car_specialist",
        description="Assesses car insurance claims for liability and damage.",
        instruction="Assess car claims for liability and damage. Approve/deny up to $25,000.",
    )

    agent = Agent(
        model="gemini-2.5-flash",
        name="intake_agent",
        instruction="Route insurance claims to the appropriate specialist",
        sub_agents=[car_agent, medical_agent],
    )

    # Enables retries and recovery for model calls and tool executions
    app = App(name=APP_NAME, root_agent=agent, plugins=[RestatePlugin()])
    runner = Runner(app=app, session_service=RestateSessionService())

    agent_service = restate.VirtualObject("MultiAgentClaimApproval")


    @agent_service.handler()
    async def run(ctx: restate.ObjectContext, claim: InsuranceClaim) -> str | None:
        events = runner.run_async(
            user_id=ctx.key(),
            session_id=claim.session_id,
            new_message=Content(
                role="user",
                parts=[Part.from_text(text=f"Claim: {claim.model_dump_json()}")],
            ),
        )
        return await parse_agent_response(events)
    ```

    <GitHubLink url="https://github.com/restatedev/ai-examples/blob/main/google-adk/tour-of-agents/app/multi_agent.py" />

    <Accordion title="Try out multi-agent systems" icon="laptop">
      [Install Restate](/installation) and launch it:

      ```bash theme={null}
      restate-server
      ```

      Get the example:

      ```bash theme={null}
      restate example python-google-adk-tour-of-agents && cd python-google-adk-tour-of-agents
      ```

      Export your [Google API key](https://aistudio.google.com/app/apikey) and run the agent:

      ```bash theme={null}
      export GOOGLE_API_KEY=your-api-key
      ```

      ```bash theme={null}
      uv run app/multi_agent.py
      ```

      Register the agents with Restate:

      ```bash theme={null}
      restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
      ```

      Start a request for a claim that needs to be analyzed by multiple agents:

      ```bash theme={null}
      curl localhost:8080/MultiAgentClaimApproval/user123/run --json '{
          "amount": 3000,
          "category": "orthopedic",
          "date": "2024-10-01",
          "placeOfService": "General Hospital",
          "reason": "hospital bill for a broken leg",
          "sessionId": "session-123"
      }'
      ```

      In the UI, you can see that the agent called the sub-agents and is waiting for their responses.

      Once all sub-agents return, the main agent continues and makes a decision.

      <Frame>
        <img src="https://mintcdn.com/restate-6d46e1dc/Nqw04BBNfCiHrELc/img/tour/agents/adk/multi-agent.png?fit=max&auto=format&n=Nqw04BBNfCiHrELc&q=85&s=dcdfcf0240e0a2b023b138c329d84f5c" alt="Multi-agent execution trace" width="1615" height="692" data-path="img/tour/agents/adk/multi-agent.png" />
      </Frame>
    </Accordion>
  </GlobalTab>

  <GlobalTab title="Pydantic AI">
    With Pydantic AI, specialist agents are wrapped in `RestateAgent` and exposed as tools on the intake agent. The intake agent uses tool calls to route to the right specialist based on the claim type, and Restate durably persists the routing decision.

    ```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/multi_agent.py#here"}  theme={null}
    medical_agent = Agent(
        "openai:gpt-4o-mini",
        system_prompt="Review medical claims for coverage and necessity. Approve/deny up to $50,000.",
    )
    restate_medical_agent = RestateAgent(medical_agent)

    car_agent = Agent(
        "openai:gpt-4o-mini",
        system_prompt="Assess car claims for liability and damage. Approve/deny up to $25,000.",
    )
    restate_car_agent = RestateAgent(car_agent)

    intake_agent = Agent(
        "openai:gpt-4o-mini",
        system_prompt="Route insurance claims to the appropriate specialist using the available tools.",
    )


    @intake_agent.tool
    async def consult_medical_specialist(
        _run_ctx: RunContext[None], claim: InsuranceClaim
    ) -> str:
        """Route to the medical specialist for medical insurance claims."""
        result = await restate_medical_agent.run(claim.model_dump_json())
        return result.output


    @intake_agent.tool
    async def consult_car_specialist(
        _run_ctx: RunContext[None], claim: InsuranceClaim
    ) -> str:
        """Route to the car specialist for car insurance claims."""
        result = await restate_car_agent.run(claim.model_dump_json())
        return result.output


    restate_intake_agent = RestateAgent(intake_agent)
    agent_service = restate.Service("MultiAgentClaimApproval")


    @agent_service.handler()
    async def run(_ctx: restate.ObjectContext, claim: InsuranceClaim) -> str:
        result = await restate_intake_agent.run(f"Claim: {claim.model_dump_json()}")
        return result.output
    ```

    <GitHubLink url="https://github.com/restatedev/ai-examples/blob/main/pydantic-ai/tour-of-agents/app/multi_agent.py" />

    <Accordion title="Try out multi-agent systems" icon="laptop">
      [Install Restate](/installation) and launch it:

      ```bash theme={null}
      restate-server
      ```

      Get the example:

      ```bash theme={null}
      restate example python-pydantic-ai-tour-of-agents && cd python-pydantic-ai-tour-of-agents
      ```

      Export your [OpenAI API key](https://platform.openai.com/api-keys) and run the agent:

      ```bash theme={null}
      export OPENAI_API_KEY=sk-...
      ```

      ```bash theme={null}
      uv run app/multi_agent.py
      ```

      Register the agents with Restate:

      ```bash theme={null}
      restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
      ```

      Start a request for a claim that needs to be analyzed by multiple agents:

      ```bash theme={null}
      curl localhost:8080/MultiAgentClaimApproval/run --json '{
          "date":"2024-10-01",
          "category":"orthopedic",
          "reason":"hospital bill for a broken leg",
          "amount":3000,
          "placeOfService":"General Hospital"
      }'
      ```

      In the UI, you can see that the agent called the sub-agents and is waiting for their responses.

      Once all sub-agents return, the main agent continues and makes a decision.
    </Accordion>
  </GlobalTab>

  <GlobalTab title="Restate TS">
    With the Restate SDK, you implement routing by having the LLM pick a specialist (exposed as tools), then calling the LLM again with the specialist's prompt. Both the routing decision and the specialist call are durable steps.

    ```typescript multi-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/typescript-restate-only/tour-of-agents/src/multi-agent.ts#here"}  theme={null}
    const SPECIALISTS = {
      billingAgent: {
        description: "Expert in payments, charges, and refunds",
        prompt:
          "You are a billing support agent specializing in payments, charges, and refunds.",
      },
      accountAgent: {
        description: "Expert in login issues and security",
        prompt:
          "You are an account support agent specializing in login issues and security.",
      },
      productAgent: {
        description: "Expert in features and how-to guides",
        prompt:
          "You are a product support agent specializing in features and how-to guides.",
      },
    } as const;

    type Specialist = keyof typeof SPECIALISTS;

    async function answer(ctx: Context, { message }: { message: string }) {
      // 1. First, decide if a specialist is needed
      const routingDecision = await ctx.run(
        "Pick specialist",
        // Use your preferred LLM SDK here - specify agents as tools
        async () => llmCall(message, createTools(SPECIALISTS)),
        { maxRetryAttempts: 3 },
      );

      // 2. No specialist needed? Give a general answer
      if (!routingDecision.toolCalls || routingDecision.toolCalls.length === 0) {
        return routingDecision.text;
      }

      // 3. Get the specialist's name
      const specialist = routingDecision.toolCalls[0].toolName as Specialist;

      // 4. Ask the specialist to answer
      const { text } = await ctx.run(
        `Ask ${specialist}`,
        async () =>
          llmCall([
            { role: "user", content: message },
            { role: "system", content: SPECIALISTS[specialist].prompt },
          ]),
        { maxRetryAttempts: 3 },
      );

      return text;
    }
    ```

    <GitHubLink url="https://github.com/restatedev/ai-examples/blob/main/typescript-restate-only/tour-of-agents/src/multi-agent.ts" />

    <Accordion title="Try out multi-agent systems" icon="laptop">
      [Install Restate](/installation) and launch it:

      ```bash theme={null}
      restate-server
      ```

      Get the example:

      ```bash theme={null}
      restate example typescript-restate-tour-of-agents && cd typescript-restate-tour-of-agents
      npm install
      ```

      Export your API key:

      ```bash theme={null}
      export OPENAI_API_KEY=sk-...
      ```

      ```bash theme={null}
      npx tsx ./src/multi-agent.ts
      ```

      Register the services with Restate:

      ```bash theme={null}
      restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
      ```

      Send a request:

      ```bash theme={null}
      curl localhost:8080/AgentRouter/answer \
        --json '{"message": "I was charged twice for my subscription last month"}'
      ```
    </Accordion>
  </GlobalTab>

  <GlobalTab title="Restate Py">
    With the Restate SDK, you implement routing by having the LLM pick a specialist (exposed as tools), then calling the LLM again with the specialist's prompt. Both the routing decision and the specialist call are durable steps.

    ```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/python-restate-only/tour-of-agents/app/multi_agent.py#here"}  theme={null}
    # Create the routing service
    router = restate.Service("AgentRouter")

    # Our team of AI specialists
    SPECIALISTS = {
        "BillingAgent": "Expert in payments, charges, and refunds",
        "AccountAgent": "Expert in login issues and security",
        "ProductAgent": "Expert in features and how-to guides",
    }


    @router.handler()
    async def answer(ctx: restate.Context, question: Question) -> str | None:
        """Classify request and route to appropriate specialized agent."""

        # 1. First, decide if a specialist is needed
        routing_decision = await ctx.run_typed(
            "Pick specialist",
            llm_call,  # Use your preferred LLM SDK here
            RunOptions(max_attempts=3),
            messages=f"""You are a customer service routing system. 
            Choose the appropriate specialist, or respond directly if no specialist is needed. 
            {question.message}""",
            tools=[tool(name=name, description=desc) for name, desc in SPECIALISTS.items()],
        )

        # 2. No specialist needed? Give a general answer
        if not routing_decision.tool_calls:
            return routing_decision.content

        # 3. Get the specialist's name
        specialist = routing_decision.tool_calls[0].function.name or "ProductAgent"

        # 4. Ask the specialist to answer
        response = await ctx.run_typed(
            f"Ask {specialist}",
            llm_call,
            RunOptions(max_attempts=3),
            messages=f"""You are a {SPECIALISTS.get(specialist)} specialist."
            Answer the question: {question.message}""",
        )

        return response.content
    ```

    <GitHubLink url="https://github.com/restatedev/ai-examples/blob/main/python-restate-only/tour-of-agents/app/multi_agent.py" />

    <Accordion title="Try out multi-agent systems" icon="laptop">
      [Install Restate](/installation) and launch it:

      ```bash theme={null}
      restate-server
      ```

      Get the example:

      ```bash theme={null}
      restate example python-restate-tour-of-agents && cd python-restate-tour-of-agents
      ```

      Export your API key:

      ```bash theme={null}
      export OPENAI_API_KEY=sk-...
      ```

      ```bash theme={null}
      uv run app/multi_agent.py
      ```

      Register the services with Restate:

      ```bash theme={null}
      restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
      ```

      Send a request:

      ```bash theme={null}
      curl localhost:8080/AgentRouter/answer \
        --json '{"message": "I was charged twice for my subscription last month"}'
      ```
    </Accordion>
  </GlobalTab>
</GlobalTabs>

## Handing off to remote agents

When agents need to scale independently, run on different platforms, or be developed by different teams, you can deploy them as separate Restate services. Restate makes cross-service calls look like local function calls while providing end-to-end durability and failure recovery.

See the [guide on calling remote agents](/ai/patterns/remote-agents) implementation guide for full examples of remote agent routing.
