Skip to main content

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

ObjectPurpose
CustodianA member: a full Custodian with model, prompt, tools, examples, response schema, and optional data source files.
CustodianSquadGroups multiple Custodians and defines the routing mode.
CustodianSquadAppThe 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

ModeBehaviorUse when
singleSelects one member to answer the request.Most squads where one specialist should handle each message.
chainAllows multiple handoffs within the squad, up to max_handoffs.More complex conversations where members pass work between each other.
workflowRuns 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_to is only honored in chain mode. In single and workflow modes a member can never connect to another member, so any can_handoff_to values are silently ignored/cleared before the squad is deployed. max_handoffs is likewise only meaningful in chain mode.

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:

ToolPurpose
web_searchSearch the web for current information about a topic.
scrape_urlExtract 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_modeBehavior
"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", provide workflow_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