Skip to main content

Build a supervisor multi-agent system

What you'll build: a content-request router that reads an incoming question, decides which of three specialist subagents should handle it — finance research, general research, or writing — and hands the request to that subagent's own prompt and tools, with the whole two-step run landing in one trace.

A supervisor (sometimes called a router or orchestrator) is a small model call whose only job is to classify: given a request, which of several specialists should take it? The pattern shows up anywhere one entry point needs to fan out to different tools, prompts, or personas depending on what's actually being asked — a support desk with a billing team and a technical team, a content pipeline with a data team and a copy team. This page builds the simplest real version of it: one classification call, one hand-off call.

Ported from langgraph — without langgraph

This page ports langchain-samples/assistants-demo's agents/supervisor/subagents.py. The original uses langgraph_supervisor.create_supervisor(...), a LangGraph state machine with unbounded loops and a ROUTE_TO/COMPLETE text protocol. This page replaces all of that with one response_format call and one runToolLoop() — no langgraph dependency, no string parsing, no cycles.

Extending to multi-hop

The source's LangGraph version can chain subagents — e.g. research Tesla's earnings, then write a LinkedIn post from that research. This page builds the one-hop case only. To add multi-hop, extend Step 5's loop: feed one subagent's output into a second router call. That's the hybrid-port case, not covered here.

Prefer a notebook?

supervisor_flow.ipynb is this whole page as one runnable notebook, written for a first-timer: a preflight cell that checks whether your models are on the right kind of connection, the four tools and four prompts built step by step, a live read of the one trace both calls land in, and four real ways to get routing wrong. It renders on GitHub with its saved output, so you can read it through before running anything.

1. Run the four tools standalone

The source assigns three subagents four tools total, defined once in agents/react_agent/tools.py and shared across all of them:

SubagentTools
finance_research_agentfinance_research, basic_research, get_todays_date
general_research_agentadvanced_research, get_todays_date
writing_agentbasic_research, get_todays_date

finance_research reads Yahoo Finance's own news endpoint; advanced_research and basic_research are two depths of the same Tavily search, the same two configurations Build a configurable ReAct agent uses. Run all four standalone first, before any of this touches AcruxCore:

import asyncio
import requests
from tavily import AsyncTavilyClient
from datetime import datetime

# Yahoo's news-stream endpoint needs two calls in this order: a GET for a session
# cookie, then the POST. Both need a browser-shaped User-Agent.
YAHOO_NEWS_URL = "https://finance.yahoo.com/xhr/ncp?queryRef=latestNews&serviceKey=ncp_fin"

def _yahoo_headlines(ticker_symbol: str, limit: int = 3) -> str:
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0"})
session.get("https://fc.yahoo.com", timeout=15)
res = session.post(YAHOO_NEWS_URL, timeout=20,
json={"serviceConfig": {"snippetCount": limit, "s": [ticker_symbol]}})
res.raise_for_status()
stream = res.json()["data"]["tickerStream"]["stream"]
return "\n\n".join(
f"{i['content']['title']}\n{i['content'].get('summary') or ''}".strip()
for i in stream[:limit]
)

async def _tavily_search(query, *, max_results, search_depth, include_images=False) -> list:
res = await AsyncTavilyClient().search(query, max_results=max_results,
search_depth=search_depth,
include_images=include_images)
return [{"title": r["title"], "url": r["url"]} for r in res["results"]]

async def finance_research(ticker_symbol: str):
return _yahoo_headlines(ticker_symbol)

async def advanced_research(query: str):
return await _tavily_search(query, max_results=10, search_depth="advanced")

async def basic_research(query: str):
return await _tavily_search(f"trending {query}", max_results=5,
search_depth="basic", include_images=True)

async def get_todays_date() -> str:
return datetime.now().strftime("%Y-%m-%d")

async def main():
print("=== finance_research(TSLA) ===")
print(await finance_research("TSLA"))
print()

print('=== advanced_research("LangGraph supervisor multi-agent pattern") ===')
results = await advanced_research("LangGraph supervisor multi-agent pattern")
print(f"{len(results)} result(s)")
for r in results[:3]:
print(f" - {r['title']} | {r['url']}")
print()

print('=== basic_research("AI content marketing") ===')
results = await basic_research("AI content marketing")
print(f"{len(results)} result(s)")
for r in results[:3]:
print(f" - {r['title']} | {r['url']}")
print()

print("=== get_todays_date() ===")
print(await get_todays_date())

asyncio.run(main())

Call each one to verify the underlying services respond before wiring anything to the gateway:

=== finance_research(TSLA) ===
Tesla (TSLA) Explores China Split As SpaceX Merger Talk Draws Attention
Reports indicate Tesla (NasdaqGS:TSLA) has explored separating its China operations to address
regulatory and supply chain issues. The proposed structure could support a future merger with
SpaceX, although Elon Musk has firmly denied that such a merger is planned.

=== advanced_research("LangGraph supervisor multi-agent pattern") ===
10 result(s)
- LangGraph Multi-Agent Supervisor | https://reference.langchain.com/python/langgraph-supervisor
- supervisor-pattern · GitHub Topics · GitHub | https://github.com/topics/supervisor-pattern
- Building Multi-Agent Systems with LangGraph-Supervisor | https://dev.to/sreeni5018/building-multi-agent-systems-with-langgraph-supervisor-138i

=== basic_research("AI content marketing") ===
5 result(s)
- How to adopt AI for content marketing: 2026 guide | Airtable | https://www.airtable.com/articles/ai-content-marketing
- AI-Driven Content Strategy: The Future of Marketing Innovation - Aprimo | https://www.aprimo.com/blog/ai-driven-content-strategy-the-future-of-marketing-innovation
- AI Will Shape the Future of Marketing | https://professional.dce.harvard.edu/blog/ai-will-shape-the-future-of-marketing

=== get_todays_date() ===
2026-08-02

2. Get an API key

The scripts below need one AcruxCore key to sync the tools, create the prompts, render each one, and run the loop. Open Account & keys → New key, name it something like supervisor-multi-agent, and copy the value — it's shown once.

The "Copy your API key" dialog showing a freshly created key beginning acx_sk_, a Copy button, and a Node SDK snippet using it

export ACRUXCORE_API_KEY=acx_sk_...
export ACRUXCORE_BASE_URL=https://api.acruxcore.com/api/v1
export TAVILY_API_KEY=tvly-...

3. Create the four tools via the SDK decorator + tools.sync()

This is the first tutorial in this series to create tools the code-owned way: a Python function decorated with @acrux.tool is the tool — its docstring becomes the model-facing description, its signature becomes the JSON Schema, and tools.sync() reconciles that with the catalog. Per this design's docstring-in-code decision, every docstring below is the source repo's own text, kept verbatim — including its own typo ("finincial") and its own terse Args: None on get_todays_date — because this page's framing is "the tool owns its interface," and paraphrasing would contradict that:

"""
Create the four supervisor tools in the AcruxCore catalog via @acrux.tool + tools.sync().

Run once before supervisor_flow.py. Requires:
pip install acruxcore tavily-python requests
"""
import asyncio
from datetime import datetime
from typing import Optional, cast, Any

import requests
from acruxcore import AcruxCore, acrux
from tavily import AsyncTavilyClient

# _yahoo_headlines() and _tavily_search() are the two helpers from Step 1.


@acrux.tool
async def finance_research(ticker_symbol: str) -> Optional[list]:
"""Search for finance research, must be a ticker symbol. This tool is used to search for financial data and news from Yahoo Finance.
It will return related finincial news from Yahoo Finance for that given ticker symbol.

Args:
ticker_symbol (str): The ticker symbol of the company to research.
"""
return cast(Any, _yahoo_headlines(ticker_symbol))


@acrux.tool
async def advanced_research(query: str) -> Optional[list]:
"""Perform in-depth research with more results and deeper analysis. This tool
will return 10 results and go deeper for more information.

Args:
query (str): The query to search for.
"""
return cast(Any, await _tavily_search(query, max_results=10, search_depth="advanced"))


@acrux.tool
async def basic_research(query: str) -> Optional[list]:
"""This tool performs quick searches with little depth,
returning concise results ideal for basic research or quick queries.

Args:
query (str): The query to search for.
"""
return cast(Any, await _tavily_search(f"trending {query}", max_results=5,
search_depth="basic", include_images=True))


@acrux.tool
async def get_todays_date() -> str:
"""Quick tool to get today's date.
Args: None
"""
return datetime.now().strftime("%Y-%m-%d")


async def main() -> None:
async with AcruxCore() as hub:
results = await hub.tools.sync(
[finance_research, advanced_research, basic_research, get_todays_date]
)
for fn, r in zip([finance_research, advanced_research, basic_research, get_todays_date], results):
print(f"{fn.__name__}: tool_id={r.tool_id} v{r.version_number} committed={r.committed}")


if __name__ == "__main__":
asyncio.run(main())
finance_research: tool_id=c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f v2 committed=True
advanced_research: tool_id=bcf7f63f-7134-412b-8f16-5f745f077133 v1 committed=True
basic_research: tool_id=ab20959a-2651-4779-86f8-0714c967798e v1 committed=True
get_todays_date: tool_id=d0a800d5-a484-4d4f-a9e2-05a715775682 v2 committed=True

finance_research and get_todays_date came back as v2: this team's catalog already had a v1 for both, created by curl in Build a ReAct agent, with a plainer hand-written description. tools.sync() is idempotent per name — it found the existing tools and committed a new version carrying the source's real docstring, exactly the "code supersedes a dashboard/API version" behavior the SDK documents. Open Gateway → Tools → finance_research and both versions are visible, v1 tagged api and v2 tagged code:

finance_research tool detail page showing v2 defined in code with the verbatim source docstring, and v1 from the API with a shorter description

All four now appear in the catalog:

Tools list showing basic_research, advanced_research, web_research, get_todays_date, and finance_research among the team's tools

4. Create the router and subagent prompts over the API

Four prompts, all created by curl: the router, and the three subagents' own system prompts, quoted from agents/supervisor/supervisor_context.py — none collapsed into another, since their tool lists overlapping (finance_research_agent and writing_agent both use basic_research) doesn't make their prompts the same agent.

The router. The source's real supervisor prompt describes the three agents and a ROUTE_TO:/COMPLETE text protocol for a full multi-hop loop. Since this page reproduces one routing decision, not the loop, the version below keeps the source's own framing and agent descriptions verbatim and drops only the ROUTE_TO:/COMPLETE protocol text — that part is replaced by response_format at call time (Step 5), not by anything stored on this prompt.

Each prompt is created in two calls: first the shell (POST /prompts, name + description only), then a version (POST /prompts/:id/versions) with the actual messages and model. See Store prompts and tools via the API for a deeper walkthrough of this pattern.

curl -X POST "$ACRUXCORE_BASE_URL/prompts" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"content-supervisor","description":"Classifies an incoming content request and routes it to one of three specialist subagents."}'

curl -X POST "$ACRUXCORE_BASE_URL/prompts/2963439b-d01b-4a94-8345-9f0e50b7794d/versions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{
"model": "claude-haiku-direct",
"messages": [
{"role":"system","content":"You are the Executive Content Director orchestrating a team of specialized AI agents to produce exceptional content for clients.\n\nAvailable agents:\n- finance_research_agent: Specialized in financial data research and analysis using Yahoo Finance and other financial sources\n- general_research_agent: Expert at comprehensive web research on any topic using advanced search tools\n- writing_agent: Professional content writer that creates final polished content in any format\n\nRead the user'"'"'s request and decide which single agent should handle it next."},
{"role":"user","content":"{{ question }}"}
]
}'
{"id":"163c1c3a-181d-4568-9165-ace51278bb11","promptId":"2963439b-d01b-4a94-8345-9f0e50b7794d","versionNumber":1,"variables":["question"],"model":"claude-haiku-direct","aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}

claude-haiku-direct is a model registered against a direct Anthropic connection (POST /gateway/connections with provider: "anthropic", then POST /gateway/models pointing it at claude-haiku-4-5-20251001) — matching the source's own default, anthropic:claude-haiku-4-5, across all four prompts.

response_format requires a direct connection

The gateway translates response_format into a forced tool call for direct Anthropic connections and passes it through natively for direct OpenAI connections. For openai_compatible connections (e.g. OpenRouter), the gateway passes response_format through to the upstream as-is — whether the upstream honors it is outside the gateway's control. If the router in Step 5 returns free-text instead of JSON, your connection type is the first thing to check.

The three subagents, each with its own prompt name, its own real system prompt, and the tool ids tools.sync() returned above:

curl -X POST "$ACRUXCORE_BASE_URL/prompts" -H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"finance-research-agent","description":"Finance research subagent: Yahoo Finance news plus quick web lookups, for the content supervisor."}'

curl -X POST "$ACRUXCORE_BASE_URL/prompts/a672652c-8ef8-49ac-b674-b4a38bb6d8dd/versions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{
"model": "claude-haiku-direct",
"messages": [
{"role":"system","content":"You are an expert finance research assistant for a digital content agency.\nYou have access to the following tools: finance_research, basic_research, and get_todays_date. \nFirst get today'"'"'s date then continue. \nThe finance_research tool is used to search for financial data and news from Yahoo Finance. \nThe basic_research tool is used to search for general information. \nThe get_todays_date tool is used to get today'"'"'s date. \nWhen you are done with your research, return the research to the supervisor agent."},
{"role":"user","content":"{{ task }}"}
]
}'
{"id":"6bc66e79-5876-465a-a50f-4f138da54783","promptId":"a672652c-8ef8-49ac-b674-b4a38bb6d8dd","versionNumber":1,"variables":["task"],"model":"claude-haiku-direct","aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}

Then connect its three tools. Bindings live on the prompt, not on a version, so this is one PUT per tool with nothing to commit afterwards:

for TOOL in c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f ab20959a-2651-4779-86f8-0714c967798e d0a800d5-a484-4d4f-a9e2-05a715775682; do
curl -X PUT "$ACRUXCORE_BASE_URL/prompts/a672652c-8ef8-49ac-b674-b4a38bb6d8dd/tools/$TOOL" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"tool_alias":"production"}'
done

The other two subagents follow the exact same shape — create the prompt shell, commit a version with the system and user prompts, then connect its tools. You can do this from the dashboard (Prompts → New prompt, the Editor tab to commit a version, the Tools tab to connect the tools) or repeat the same curl calls. Here is what each one needs:

general-research-agent — tools: advanced_research + get_todays_date

You are an expert general research agent. You have access to the following tools: advanced_research and get_todays_date. First get today's date then continue to use the advanced_research tool to search for general information on the topic you are given to research, when your done you return the research to the supervisor agent. YOU MUST USE THE ADVANCED_RESEARCH TOOL TO GET THE INFORMATION YOU NEED

writing-agent — tools: basic_research + get_todays_date

You are an expert writing assistant. Your primary responsibility is to help draft, edit, and improve written content to ensure clarity, correctness, and engagement. You are strictly supposed to take in the content you are given and write the final content based on the requested format for the user, then return the final content to the supervisor agent.

Both use {{ task }} as the user prompt variable, same as the finance agent.

5. Run the two-step router → subagent flow

The flow has two steps:

  1. Classify. Render the router prompt and call the gateway with response_format set to a typed JSON schema — {"route_to": "finance_research_agent" | "general_research_agent" | "writing_agent"}. The model returns a structured answer like {"route_to": "finance_research_agent"}. This call carries no tools (response_format and tools are mutually exclusive on one request). Capture the trace id from x-gateway-trace-id.
  2. Dispatch. Render the subagent prompt the router picked, and run its tools via runToolLoop()/run_tool_loop() (curl loops by hand), passing that same trace id so the hand-off lands in one trace instead of two.

Run each script twice, with two genuinely different questions, to prove the routing is real:

./supervisor_flow.sh "Research Tesla (TSLA) latest stock news and tell me if investors should be worried."
./supervisor_flow.sh "What are the biggest trends in sustainable packaging for consumer goods in 2026?"

The first is a finance question; the second is a general-research question. If routing were hardcoded, both would land on the same subagent — they don't.

The full script threads one trace across the router call and the subagent's tool loop — see supervisor_flow.sh on GitHub for the complete source.

./supervisor_flow.sh "Research Tesla (TSLA) latest stock news and tell me if investors should be worried."
Question: Research Tesla (TSLA) latest stock news and tell me if investors should be worried.
Step A -- routed to: finance_research_agent (trace ed61263e-3b02-4cd3-8b9f-5c1c00080aca)

-> get_todays_date({})
2026-08-01
-> finance_research({"ticker_symbol":"TSLA"})
[{"title":"SpaceX stock hits new closing low in rocky week ahead of earnings"},{"title":"Tesla reportedly considering a spin off of its China business"},{"title":"Tesla Rival BYD Ramping Up Sales On O
Step B -- finance_research_agent: Based on the latest financial news for Tesla (TSLA) as of August 1, 2026, here's my analysis:

### Key Findings:
- **Tesla considering a spin-off of its China business** - potentially significant restructuring news
- **BYD ramping up overseas sales** - intensifying competitive pressure in the EV market

### Should Investors Be Worried?
Mixed signals, cautious approach recommended. Monitor earnings reports and official statements
about the China spin-off, and watch for the impact of BYD's overseas growth on Tesla's market
share. Not necessarily a red flag for panic selling at this point.

(2 model turn(s), trace ed61263e-3b02-4cd3-8b9f-5c1c00080aca)
./supervisor_flow.sh "What are the biggest trends in sustainable packaging for consumer goods in 2026?"
Question: What are the biggest trends in sustainable packaging for consumer goods in 2026?
Step A -- routed to: general_research_agent (trace 60754ee4-221d-4a53-94a3-09ccdff534e7)

-> get_todays_date({})
2026-08-01
-> advanced_research({"query":"biggest trends sustainable packaging consumer goods 2026"})
[{"title":"Food packaging trends 2026: Sustainability, digital innovation & consumer-centric solutions","url":"https://www.foodingredientsfirst.com/news/food-packaging-trends-sustainability-digital-in
Step B -- general_research_agent: Perfect! I've completed my research on the biggest trends in
sustainable packaging for consumer goods in 2026:

1. Food Packaging & Sustainability Focus, 2. Digital Innovation Integration, 3. Eco-Friendly
Materials Leadership, 4. Consumer-Centric Design, 5. Circular Economy Models, 6. Market Growth,
7. Compliance & Regulations.

(2 model turn(s), trace 60754ee4-221d-4a53-94a3-09ccdff534e7)

Same script, different question — a finance ticker question routed to finance_research_agent and called the real Yahoo Finance tool; a general trends question routed to general_research_agent and called the real Tavily-backed advanced_research. Two different subagents, two different tool sets, one classification decision each.

6. Inspect the trace

Open Observability → Traces and the general-research run above shows up as one trace with 5 spans: the router's llm call, the subagent's own llm call, the get_todays_date and advanced_research tool calls in between, and a final llm call with the answer — all on claude-haiku-4-5-20251001, the model both the router prompt and the subagent prompts are bound to:

Trace detail showing 5 spans: two LLM calls, get_todays_date and advanced_research tool calls, all on claude-haiku-4-5-20251001

Expanding the first LLM span — the router call — shows the real input (the supervisor system prompt plus the question) and the real output: a bare JSON object, {"route_to": "general_research_agent"}, no free text around it, because response_format constrained it:

Expanded router LLM span showing the supervisor system prompt as input and a bare JSON object with route_to set to general_research_agent as output

And the advanced_research tool span shows the real query the subagent chose and the real Tavily results it got back — proof the hand-off carried the subagent's own tools into the same trace the router opened:

Expanded advanced_research tool span showing the real search query and real Tavily result titles/URLs as output

Two model calls, two different prompts, four spans of real tool and model activity between them — one trace, exactly as the classification note at the top promised.

What's next

  • Build a configurable ReAct agent — more on the basic_research/advanced_research Tavily split this page's subagents reuse.
  • Build a ReAct agent — the baseline finance_research + get_todays_date tool pair, without a gateway or a router in front of them.
  • API details: see Prompts, Tools, and Traces in the API Reference.