Custodian Squads
A Custodian Squad is a group of Custodian members deployed behind one chat app. Each member is a full Custodian (with its own model, prompt, tools, examples, and response schema), and the squad coordinates them.
Use a squad when one Custodian should route or pipeline work across specialists, such as billing, support, data analysis, or document workflows.
When to Use a Custodian Squad
- Different questions should be handled by different instructions.
- Each member has a clear topic area.
- One member needs a data source that others do not need.
- You want the response to show which member handled the request.
For a single Custodian with one prompt and one set of knowledge files, use the Custodian Class instead.
Core Objects
| Object | Purpose |
|---|---|
Custodian | A member: a full Custodian with model, prompt, tools, examples, response schema, and optional data source files. |
CustodianSquad | Groups multiple Custodians and defines the routing mode. |
CustodianSquadApp | The deployed squad app returned by squad.deploy(). Use it to chat with the squad. |
Create a Squad
from custodian_labs import Custodian, CustodianSquad
squad = CustodianSquad(
custodians=[
Custodian(
name="billing",
model="gpt-4o",
system_prompt="Answer billing questions.",
topics=["billing", "invoice", "refund", "payment"],
),
Custodian(
name="data",
model="gpt-4o",
system_prompt="Answer questions about the uploaded CSV file.",
topics=["data", "csv", "customer", "file"],
).add_data_source_file("sample_pii_data.csv"),
],
routing_mode="single",
)
app = squad.deploy()
reply = app.chat("How many people are in the uploaded file and which cities do they live in?")
if reply is not None:
print(reply.response)
print(reply.selected_agent)
Sharing is configured at deploy time on the container, never on the members:
app = squad.deploy(share_mode="password", share_password="s3cret")
How Routing Works
Each Custodian includes topics. The squad uses these topics, along with the user's message, to decide which member should handle the request.
The response may include selected_agent and handoff_path, which help you inspect which members participated.
Routing Modes
| Mode | Behavior | Use when |
|---|---|---|
single | Selects one member to answer the request. | Most squads where one specialist should handle each message. |
chain | Allows multiple handoffs within the squad, up to max_handoffs. | More complex conversations where members pass work between each other. |
workflow | Runs members in a fixed workflow_order pipeline. | Deterministic multi-step processes (this is what a visual builder would edit). |
Handoffs are chain-only.
can_handoff_tois only honored inchainmode. Insingleandworkflowmodes a member can never connect to another member, so anycan_handoff_tovalues are silently ignored/cleared before the squad is deployed.max_handoffsis likewise only meaningful inchainmode.
If you are unsure, start with routing_mode="single".
Chain mode
squad = CustodianSquad(
custodians=[
Custodian(name="triage", model="gpt-4o", system_prompt="Classify the issue.",
topics=["billing", "support"], can_handoff_to=["expert"]),
Custodian(name="expert", model="gpt-4o", system_prompt="Resolve the issue."),
],
routing_mode="chain",
max_handoffs=3,
)
app = squad.deploy()
reply = app.chat("A payment is stuck. Investigate it.")
print(reply.handoff_path) # e.g. ["triage", "expert"]
Workflow mode
squad = CustodianSquad(
custodians=[
Custodian(name="Researcher", model="gpt-4o", system_prompt="Gather the facts."),
Custodian(name="Analyst", model="gpt-4o", system_prompt="Produce the verdict."),
Custodian(name="Writer", model="gpt-4o", system_prompt="Write the final report."),
],
routing_mode="workflow",
workflow_order=["Researcher", "Analyst", "Writer"],
)
app = squad.deploy()
Tools on Members
Members are full Custodians, so each can declare its own tools. For example, give one member web search and page scraping:
Custodian(
name="researcher",
model="gpt-4o",
system_prompt="Search for current information and cite sources.",
tools=["web_search", "scrape_url"],
)
Available built-in tools:
| Tool | Purpose |
|---|---|
web_search | Search the web for current information about a topic. |
scrape_url | Extract the full content from a specific webpage URL. |
Per-Member Capabilities
Members are full Custodians, so each can declare its own few-shot examples and response_schema — the same way a standalone Custodian does:
squad = CustodianSquad(
custodians=[
Custodian(
name="analyst",
model="gpt-4o",
system_prompt="Classify the risk.",
examples=[{"user": "risk?", "assistant": "LOW"}],
response_schema={"type": "object", "properties": {"risk": {"type": "string"}}},
output_key="risk",
),
],
routing_mode="workflow",
workflow_order=["analyst"],
response_schema={"type": "object", "properties": {"report": {"type": "string"}}},
)
The squad-level response_schema (on CustodianSquad) governs the final output of the pipeline, independent of each member's own schema.
Add a Data Source to One Member
Call .add_data_source_file() on the member that should use the file:
Custodian(
name="data",
model="gpt-4o",
system_prompt="Answer questions about the uploaded CSV file.",
topics=["data", "csv", "customer", "file"],
).add_data_source_file("sample_pii_data.csv")
This keeps the file scoped to that member's role. For general RAG behavior, see Data Sources and RAG.
Sharing
A squad's sharing is configured at deploy time on the container, never on the members:
share_mode | Behavior |
|---|---|
"public" | Anyone with the link can chat. Default. |
"private" | Only the owner can access it. |
"password" | Access is protected by share_password. |
app = squad.deploy() # public (default)
app = squad.deploy(share_mode="private")
app = squad.deploy(share_mode="password", share_password="s3cret")
Squad Requirements
- Include at least one
Custodian. - Give every member a unique
name. - Use the same API key and base URL across all members.
- For
routing_mode="workflow", provideworkflow_order.
If a squad is misconfigured, the SDK raises a validation error before deployment.
Chat with a Squad
After deployment, call .chat() on the returned squad app:
reply = app.chat("Can you help with a refund question?")
if reply is not None:
print(reply.response)
print(reply.selected_agent)
print(reply.handoff_path)
Like regular apps, squad apps keep a session ID on the app instance. Use app.reset_session() to start a fresh conversation.
Next Steps
- Examples: see smaller SDK examples in one place.
- Chat Sessions: understand session behavior.
- Data Sources and RAG: add knowledge files to a Custodian or member.
- Error Handling: handle authentication, validation, and server errors.