{"slug": "build-a-multi-agent-system-with-openai-s-swarm-framework", "title": "Build a Multi-Agent System with OpenAI's Swarm Framework", "summary": "OpenAI's experimental Swarm framework enables developers to build a multi-agent customer support system that routes requests between specialist agents. The system uses a topic guardrail to filter irrelevant messages before a triage agent hands off billing or technical issues to dedicated agents. Swarm, which is not production-ready, demonstrates agent handoffs through function calls and runs locally with Python 3.10+.", "body_md": "# Build a Multi-Agent System with OpenAI's Swarm Framework\n\nRoute customer support requests between specialist agents using OpenAI's experimental Swarm framework, with a manual topic guardrail in front of it.\n\n[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)\n\n## What you'll build\n\nA 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.\n\n## Prerequisites\n\n- Python 3.10 or newer (Swarm relies on typing features that don't work on older versions)\n- An OpenAI API key with active billing (platform.openai.com/api-keys)\n- Basic comfort with Python functions and the OpenAI chat completions message format (\n`{\"role\": ..., \"content\": ...}`\n\n) `pip`\n\n,`git`\n\n, and a terminal\n\nA note on the framework: **Swarm** is OpenAI's own experimental, educational multi-agent framework (`openai/swarm`\n\non 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`\n\nclient 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.\n\nImportant: Swarm is **not** published on PyPI. There's an unrelated package called `swarm`\n\non PyPI (a distributed job queue tool), and `pip install swarm`\n\nwill install that instead and quietly break everything downstream. Install straight from GitHub.\n\n## 1. Set up the project\n\n```\nmkdir agent-handoffs && cd agent-handoffs\npython3 -m venv .venv\nsource .venv/bin/activate   # Windows: .venv\\Scripts\\activate\npip install git+https://github.com/openai/swarm.git\n```\n\nSet your API key as an environment variable rather than hardcoding it:\n\n```\nexport OPENAI_API_KEY=\"sk-...\"          # macOS/Linux\n# setx OPENAI_API_KEY \"sk-...\"          # Windows (new shell required after)\n```\n\nSwarm doesn't take the key directly, it builds an `openai.OpenAI()`\n\nclient internally, which reads `OPENAI_API_KEY`\n\nfrom the environment automatically.\n\nCreate `support_system.py`\n\n. Everything below goes in that one file, in order.\n\n## 2. Build the specialist agents\n\nStart with two agents that each know how to do one job and nothing else. In Swarm, `Agent`\n\nis a simple object with a name, a model, an instructions string, and a list of callable `functions`\n\nthe model can invoke as tools.\n\n``` python\nfrom swarm import Swarm, Agent\n\nclient = Swarm()\n\nbilling_agent = Agent(\n    name=\"Billing Agent\",\n    instructions=(\n        \"You help customers with billing issues: invoices, charges, and \"\n        \"subscription changes. Be concise and specific.\"\n    ),\n)\n\ntechnical_agent = Agent(\n    name=\"Technical Support Agent\",\n    instructions=(\n        \"You help customers troubleshoot technical problems with the product. \"\n        \"Ask clarifying questions if you need more detail.\"\n    ),\n)\n```\n\n## 3. Build the topic guardrail\n\nSwarm 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.\n\n``` python\nimport json\n\ntopic_guardrail_agent = Agent(\n    name=\"Topic Guardrail\",\n    model=\"gpt-4o-mini\",\n    instructions=(\n        \"Decide whether the user's message is a customer support question \"\n        \"about billing or technical issues. Jokes, homework, and general \"\n        \"chit-chat are not support related. Reply with ONLY raw JSON, no \"\n        'markdown fences, in this exact shape: {\"is_support_related\": true, '\n        '\"reasoning\": \"short reason\"}'\n    ),\n)\n\ndef check_support_topic(user_message: str) -> bool:\n    response = client.run(\n        agent=topic_guardrail_agent,\n        messages=[{\"role\": \"user\", \"content\": user_message}],\n    )\n    raw = response.messages[-1][\"content\"].strip()\n    raw = raw.removeprefix(\"``` json\").removeprefix(\"```\").removesuffix(\"```\").strip()\n    try:\n        parsed = json.loads(raw)\n        return bool(parsed.get(\"is_support_related\", False))\n    except json.JSONDecodeError:\n        return False\n```\n\n`gpt-4o-mini`\n\nis a reasonable default here: it's cheap and fast, and a yes/no classification doesn't need a frontier model. The `.removeprefix()`\n\n/`.removesuffix()`\n\ncleanup 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.\n\n## 4. Build the triage agent with handoffs\n\nIn Swarm, a handoff is nothing special, it's just a Python function that returns an `Agent`\n\ninstance 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.\n\n``` python\ndef transfer_to_billing():\n    \"\"\"Call this when the customer's issue is about billing, invoices, or charges.\"\"\"\n    return billing_agent\n\ndef transfer_to_technical():\n    \"\"\"Call this when the customer has a technical problem with the product.\"\"\"\n    return technical_agent\n\ntriage_agent = Agent(\n    name=\"Triage Agent\",\n    instructions=(\n        \"Determine whether the user needs billing help or technical support, \"\n        \"then call the matching transfer function. Don't try to solve the \"\n        \"problem yourself.\"\n    ),\n    functions=[transfer_to_billing, transfer_to_technical],\n)\n```\n\nThe 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.\n\n## 5. Run it\n\n``` python\ndef handle_message(user_message: str):\n    if not check_support_topic(user_message):\n        print(\"Blocked: this doesn't look like a support question.\")\n        return\n\n    response = client.run(\n        agent=triage_agent,\n        messages=[{\"role\": \"user\", \"content\": user_message}],\n    )\n    print(f\"Handled by: {response.agent.name}\")\n    print(response.messages[-1][\"content\"])\n\nif __name__ == \"__main__\":\n    handle_message(\"My last invoice charged me twice, can you fix it?\")\n```\n\nRun it:\n\n```\npython support_system.py\n```\n\n## Verify it works\n\nYou should see something like:\n\n```\nHandled by: Billing Agent\nI'm sorry about the duplicate charge. I can start a refund for the extra charge...\n```\n\n`response.agent.name`\n\nconfirms 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\")`\n\nand confirm it routes to the Technical Support Agent instead. Then try `handle_message(\"write me a poem about the ocean\")`\n\n, it should print the guardrail block message and never touch the triage agent at all.\n\nIf you want to see it handle a few cases in one run, loop over a list of test messages instead of calling `handle_message`\n\nonce, it's cheap since the guardrail model is small.\n\n## Troubleshooting\n\nor import errors that don't match anything in this tutorial: you likely ran`ModuleNotFoundError: No module named 'swarm'`\n\n`pip install swarm`\n\n, which grabs an unrelated PyPI package. Uninstall it (`pip uninstall swarm`\n\n) and install from GitHub as shown above.:`AuthenticationError: Incorrect API key provided`\n\n`OPENAI_API_KEY`\n\nisn't set in the shell that's running the script. Confirm with`echo $OPENAI_API_KEY`\n\n(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`\n\nfrom`check_support_topic`\n\n`raw`\n\nbefore 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=[...]`\n\non the triage`Agent`\n\nactually lists both transfer functions.\n\n## Next steps\n\nSwarm 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`\n\npackage), 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`\n\nrepo's `examples/`\n\ndirectory has a few more handoff patterns worth reading, including one with shared `context_variables`\n\npassed between agents.\n\n[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)· Senior Editor\n\nMariana 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.\n\n## Discussion 0\n\nNo comments yet\n\nBe the first to weigh in.", "url": "https://wpnews.pro/news/build-a-multi-agent-system-with-openai-s-swarm-framework", "canonical_source": "https://sourcefeed.dev/a/build-a-multi-agent-system-with-openais-swarm-framework", "published_at": "2026-07-10 17:42:57+00:00", "updated_at": "2026-07-10 17:48:53.655850+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["OpenAI", "Swarm", "Mariana Souza"], "alternates": {"html": "https://wpnews.pro/news/build-a-multi-agent-system-with-openai-s-swarm-framework", "markdown": "https://wpnews.pro/news/build-a-multi-agent-system-with-openai-s-swarm-framework.md", "text": "https://wpnews.pro/news/build-a-multi-agent-system-with-openai-s-swarm-framework.txt", "jsonld": "https://wpnews.pro/news/build-a-multi-agent-system-with-openai-s-swarm-framework.jsonld"}}