How the Multi-Agent Coordinator Roster Works in Claude Managed Agents

The multi-agent coordinator roster is a declarative array of RosterEntry objects that enables dynamic delegation by mapping agent capabilities to endpoints, allowing the coordinator's sendMessage function to route requests to the appropriate Claude Managed Agent via a configurable selector callback.

The anthropics/cwc-workshops repository provides a production-ready reference implementation for building scalable multi-agent systems. Central to this architecture is the multi-agent coordinator roster, which decouples agent discovery from invocation logic, letting you add or remove sub-agents by modifying a static configuration rather than changing dispatch code.

Roster Data Structure and Type Definitions

The roster is formally defined in production-ready-agent/starter/lib/types.ts as a collection of RosterEntry interfaces. Each entry acts as a service descriptor containing the metadata required to route requests and describe capabilities to the coordinator.

A RosterEntry consists of four required fields:

  • name: The unique identifier used by the selector logic (e.g., "research" or "legal").
  • skill: A capability tag describing what the agent does (e.g., "SEC-filing-research").
  • endpoint: The full URL or Claude Managed Agent ID where the sub-agent accepts requests.
  • description: A human-readable summary consumed by the coordinator's prompting layer or logging system.
// production-ready-agent/starter/lib/types.ts
export interface RosterEntry {
  name: string;
  skill: string;
  endpoint: string;
  description: string;
}

Agent Selection and Dispatch Logic

The dispatch mechanism lives in production-ready-agent/starter/lib/anthropic.ts within the sendMessage function. This implementation follows a three-phase pattern: selection, invocation, and normalization.

Selection Phase: The function accepts a selector callback alongside the roster array. The selector receives the raw userPrompt and returns the name string of the target agent. This design supports arbitrary routing strategies—semantic matching, keyword detection, or load-balancing—without altering core dispatch code.

Invocation Phase: Once the selector returns a name, sendMessage performs a lookup to find the matching RosterEntry in the roster array. It then constructs a request to the Claude Managed Agents API, injecting the entry's endpoint and skill into the payload.

Normalization Phase: After the sub-agent responds, the coordinator wraps the result with bookkeeping metadata (such as agentUsed and token consumption) before returning the final output to the caller.

// production-ready-agent/starter/lib/anthropic.ts (conceptual flow)
async function sendMessage({
  userPrompt,
  roster,
  selector,
}: {
  userPrompt: string;
  roster: RosterEntry[];
  selector: (prompt: string) => string;
}) {
  const targetName = selector(userPrompt);
  const entry = roster.find(r => r.name === targetName);
  
  // Dispatch to Claude Managed Agent
  const response = await fetch(entry.endpoint, {
    method: "POST",
    body: JSON.stringify({
      prompt: userPrompt,
      skill: entry.skill,
    }),
  });
  
  return {
    content: await response.text(),
    agentUsed: entry.name,
    skillUsed: entry.skill,
  };
}

Practical Implementation Examples

To implement a custom coordinator, define your roster as a static array and pass it to sendMessage with an appropriate selector. The following example creates a two-agent system for legal and summarization tasks:

// customRoster.ts
import { RosterEntry } from "./production-ready-agent/starter/lib/types";

export const legalRoster: RosterEntry[] = [
  {
    name: "legal",
    skill: "contract-review",
    endpoint: "https://api.anthropic.com/v1/managed_agents/contract-review",
    description: "Review legal contracts for compliance risks"
  },
  {
    name: "summary",
    skill: "document-summarizer",
    endpoint: "https://api.anthropic.com/v1/managed_agents/summarizer",
    description: "Generate executive summaries of long documents"
  },
];
// coordinator.ts
import { sendMessage } from "./production-ready-agent/starter/lib/anthropic";
import { legalRoster } from "./customRoster";

async function routeUserQuery(question: string) {
  const result = await sendMessage({
    userPrompt: question,
    roster: legalRoster,
    selector: (prompt) => prompt.includes("contract") ? "legal" : "summary",
  });
  
  console.log(`Agent ${result.agentUsed} processed the request.`);
  return result.content;
}

For load-balancing scenarios, implement a round-robin selector that maintains state across calls:

let lastIndex = 0;

function roundRobinSelector(_prompt: string, roster: RosterEntry[]): string {
  const entry = roster[lastIndex];
  lastIndex = (lastIndex + 1) % roster.length;
  return entry.name;
}

// Usage
await sendMessage({
  userPrompt: "Analyze Q3 earnings",
  roster: myRoster,
  selector: (prompt) => roundRobinSelector(prompt, myRoster),
});

Core Files in the Coordinator Architecture

Understanding the following files is essential for modifying or extending the multi-agent coordinator roster behavior:

Summary

  • The multi-agent coordinator roster is a static configuration array that eliminates hardcoded endpoints by describing each sub-agent's name, skill, endpoint, and description.

  • The sendMessage function in anthropic.ts executes a selector callback against the roster to determine which agent should handle a given prompt, enabling flexible routing strategies from simple keyword matching to complex load balancing.

  • Because the roster is pure data defined in types.ts, you can add new agents or modify capabilities without changing the core dispatch logic in the coordinator.

  • The reference implementation supports metadata tracking (such as agentUsed) for observability and billing reconciliation across distributed agent calls.

Frequently Asked Questions

How does the coordinator handle cases where no roster agent matches the query?

When the selector returns a name that does not exist in the roster array, the sendMessage function in anthropic.ts will fail to find a matching RosterEntry and typically throws an error or falls back to a default 'generalist' agent if your implementation includes such logic. You should validate selector output against roster.map(r => r.name) before dispatching or wrap the lookup in error handling that returns a graceful fallback response.

Can the roster be updated dynamically without restarting the coordinator?

Yes, because the roster is passed as an argument to sendMessage rather than being loaded from a static config file at startup, you can modify the RosterEntry array in memory between requests. For persistent changes, update the source module exporting the roster array; in a hot-reloaded development environment or serverless function, this takes effect on the next invocation without requiring a process restart.

What is the relationship between the skill field and actual tool execution?

The skill string in a RosterEntry serves as a semantic tag consumed by both the coordinator's prompting layer and the downstream Claude Managed Agent to determine which tools to enable. While the coordinator uses skill for routing decisions, the actual tool binding happens at the sub-agent's endpoint based on this identifier, ensuring that the research skill triggers SEC-filing tools while the valuation skill triggers financial modeling tools.

Is there a performance penalty for maintaining large rosters with many agents?

The roster lookup is an O(n) array search performed once per request in anthropic.ts. For rosters under 100 entries, the overhead is negligible; however, if you scale to hundreds of agents, you should refactor the roster into a Map<string, RosterEntry> indexed by name to achieve O(1) lookups and prevent latency accumulation in high-throughput coordinator deployments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →