# Build a Multi-Agent System with OpenAI's Swarm Framework

> Source: <https://sourcefeed.dev/a/build-a-multi-agent-system-with-openais-swarm-framework>
> Published: 2026-07-10 17:42:57+00:00

# Build a Multi-Agent System with OpenAI's Swarm Framework

Route customer support requests between specialist agents using OpenAI's experimental Swarm framework, with a manual topic guardrail in front of it.

[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)

## What you'll build

A small customer support system: a triage agent reads an incoming message, decides whether it's a billing or technical issue, and hands off to the right specialist agent. A lightweight classifier agent acts as a topic guardrail, blocking anything unrelated to support before the triage agent even sees it. You'll run all of this locally with OpenAI's Swarm framework and confirm the handoff actually happened by checking which agent produced the final reply.

## Prerequisites

- Python 3.10 or newer (Swarm relies on typing features that don't work on older versions)
- An OpenAI API key with active billing (platform.openai.com/api-keys)
- Basic comfort with Python functions and the OpenAI chat completions message format (
`{"role": ..., "content": ...}`

) `pip`

,`git`

, and a terminal

A note on the framework: **Swarm** is OpenAI's own experimental, educational multi-agent framework (`openai/swarm`

on GitHub). It's explicitly not meant for production, there's no built-in tracing dashboard, no persistent sessions, no guardrail primitives. It's a thin, readable layer over the standard `openai`

client that demonstrates one core idea well: an agent can "hand off" a conversation to another agent by returning that agent from a function call. That's exactly what we need here, and it's small enough that you can read the entire source in twenty minutes if you want to know what's happening under the hood.

Important: Swarm is **not** published on PyPI. There's an unrelated package called `swarm`

on PyPI (a distributed job queue tool), and `pip install swarm`

will install that instead and quietly break everything downstream. Install straight from GitHub.

## 1. Set up the project

```
mkdir agent-handoffs && cd agent-handoffs
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install git+https://github.com/openai/swarm.git
```

Set your API key as an environment variable rather than hardcoding it:

```
export OPENAI_API_KEY="sk-..."          # macOS/Linux
# setx OPENAI_API_KEY "sk-..."          # Windows (new shell required after)
```

Swarm doesn't take the key directly, it builds an `openai.OpenAI()`

client internally, which reads `OPENAI_API_KEY`

from the environment automatically.

Create `support_system.py`

. Everything below goes in that one file, in order.

## 2. Build the specialist agents

Start with two agents that each know how to do one job and nothing else. In Swarm, `Agent`

is a simple object with a name, a model, an instructions string, and a list of callable `functions`

the model can invoke as tools.

``` python
from swarm import Swarm, Agent

client = Swarm()

billing_agent = Agent(
    name="Billing Agent",
    instructions=(
        "You help customers with billing issues: invoices, charges, and "
        "subscription changes. Be concise and specific."
    ),
)

technical_agent = Agent(
    name="Technical Support Agent",
    instructions=(
        "You help customers troubleshoot technical problems with the product. "
        "Ask clarifying questions if you need more detail."
    ),
)
```

## 3. Build the topic guardrail

Swarm has no built-in guardrail concept, so we build one the plain way: a cheap classifier agent that runs *before* triage and decides whether the message is worth routing at all. It replies with JSON, which we parse ourselves.

``` python
import json

topic_guardrail_agent = Agent(
    name="Topic Guardrail",
    model="gpt-4o-mini",
    instructions=(
        "Decide whether the user's message is a customer support question "
        "about billing or technical issues. Jokes, homework, and general "
        "chit-chat are not support related. Reply with ONLY raw JSON, no "
        'markdown fences, in this exact shape: {"is_support_related": true, '
        '"reasoning": "short reason"}'
    ),
)

def check_support_topic(user_message: str) -> bool:
    response = client.run(
        agent=topic_guardrail_agent,
        messages=[{"role": "user", "content": user_message}],
    )
    raw = response.messages[-1]["content"].strip()
    raw = raw.removeprefix("``` json").removeprefix("```").removesuffix("```").strip()
    try:
        parsed = json.loads(raw)
        return bool(parsed.get("is_support_related", False))
    except json.JSONDecodeError:
        return False
```

`gpt-4o-mini`

is a reasonable default here: it's cheap and fast, and a yes/no classification doesn't need a frontier model. The `.removeprefix()`

/`.removesuffix()`

cleanup exists because small models occasionally wrap JSON in a markdown code fence even when told not to, don't skip it, it's the most common way this guardrail silently breaks.

## 4. Build the triage agent with handoffs

In Swarm, a handoff is nothing special, it's just a Python function that returns an `Agent`

instance instead of a string. When the model calls that function, Swarm swaps the active agent and keeps running the conversation against it. Define the specialists first (already done above), then the transfer functions, then the triage agent that uses them.

``` python
def transfer_to_billing():
    """Call this when the customer's issue is about billing, invoices, or charges."""
    return billing_agent

def transfer_to_technical():
    """Call this when the customer has a technical problem with the product."""
    return technical_agent

triage_agent = Agent(
    name="Triage Agent",
    instructions=(
        "Determine whether the user needs billing help or technical support, "
        "then call the matching transfer function. Don't try to solve the "
        "problem yourself."
    ),
    functions=[transfer_to_billing, transfer_to_technical],
)
```

The docstring on each transfer function isn't decoration, Swarm sends it to the model as the function's description, same as any other tool. Vague docstrings are the number one reason triage picks the wrong specialist or doesn't hand off at all.

## 5. Run it

``` python
def handle_message(user_message: str):
    if not check_support_topic(user_message):
        print("Blocked: this doesn't look like a support question.")
        return

    response = client.run(
        agent=triage_agent,
        messages=[{"role": "user", "content": user_message}],
    )
    print(f"Handled by: {response.agent.name}")
    print(response.messages[-1]["content"])

if __name__ == "__main__":
    handle_message("My last invoice charged me twice, can you fix it?")
```

Run it:

```
python support_system.py
```

## Verify it works

You should see something like:

```
Handled by: Billing Agent
I'm sorry about the duplicate charge. I can start a refund for the extra charge...
```

`response.agent.name`

confirms the handoff actually happened, triage routed to billing, not itself. Swap the call at the bottom to `handle_message("my app crashes when I upload a file")`

and confirm it routes to the Technical Support Agent instead. Then try `handle_message("write me a poem about the ocean")`

, it should print the guardrail block message and never touch the triage agent at all.

If you want to see it handle a few cases in one run, loop over a list of test messages instead of calling `handle_message`

once, it's cheap since the guardrail model is small.

## Troubleshooting

or import errors that don't match anything in this tutorial: you likely ran`ModuleNotFoundError: No module named 'swarm'`

`pip install swarm`

, which grabs an unrelated PyPI package. Uninstall it (`pip uninstall swarm`

) and install from GitHub as shown above.:`AuthenticationError: Incorrect API key provided`

`OPENAI_API_KEY`

isn't set in the shell that's running the script. Confirm with`echo $OPENAI_API_KEY`

(macOS/Linux) before running, env vars set in one terminal tab don't carry to another.: the guardrail model wrapped its answer in a code fence or added a stray sentence before the JSON. Tighten the instructions further ("reply with nothing but the JSON object") or, if it keeps happening, print`json.JSONDecodeError`

from`check_support_topic`

`raw`

before parsing so you can see exactly what came back.**Triage agent answers the question directly instead of transferring**: this usually means the transfer function docstrings are too vague or the triage instructions don't clearly forbid answering directly. Make both more explicit, and double check`functions=[...]`

on the triage`Agent`

actually lists both transfer functions.

## Next steps

Swarm is deliberately minimal, there's no persistent session across separate script runs, no built-in tracing, no output guardrails. If you outgrow it, the next stop is OpenAI's Assistants API (via the standard `openai`

package), which adds persistent threads, built-in file retrieval, and hosted tool execution at the cost of more moving parts to configure. For now, try adding a third specialist agent (say, a "refunds" agent) and a third transfer function, or add a second guardrail that checks the specialist's *reply* before it's printed, useful if you're worried about a specialist agent giving out information it shouldn't. The `openai/swarm`

repo's `examples/`

directory has a few more handoff patterns worth reading, including one with shared `context_variables`

passed between agents.

[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)· Senior Editor

Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.

## Discussion 0

No comments yet

Be the first to weigh in.
