On this page

On this page

Agent coordination

Swarm

Swarm orchestrates many sub-agents from a Code Mode script. It is enabled by default, with an explicit opt-out. Use normal JavaScript control flow such as Promise.all, while, and if to fan out work, collect results, and make decisions.

There is no graph DSL and no separate workflow format. The program is the orchestration. Swarm adds awaitable collector children, structured results, bounded concurrency, and progress reporting to that program.

When to use Swarm

Use ordinary sessions_spawn announcing runs for one or a few children. Reserve Swarm for large parallel fan-out: several similar children (about five or more), typically driven by Code Mode agents.run with control flow such as Promise.all. Collector children (collect: true) send no completion notification and cannot be steered. Their results must be explicitly collected. Use outputSchema for structured results and groupId to group a batch.

Enable Swarm

Swarm needs no enablement setting. Omitted tools.swarm, an empty object, or an object that sets only limits all leave Swarm enabled. Code Mode remains separately opt-in, and normal tool policy still applies. Existing Codex sessions can retain an older tool catalog. See the fresh-session guidance below.

To opt out, disable Swarm in Settings → Agent Defaults → Tools, or set tools.swarm: false in openclaw.json:

json5
{  tools: {    swarm: false,  },}

swarm: { enabled: false } has the same effect while preserving configured limits. To re-enable Swarm, remove the explicit opt-out, set swarm: true or swarm: { enabled: true }, or enable it in Settings → Agent Defaults → Tools.

To tune the limits, use object form. These are the defaults. You only need to include values you want to change:

json5
{  tools: {    swarm: {      maxConcurrent: 32,      maxChildrenPerGroup: 50,      maxTotalPerGroup: 200,      waitTimeoutSecondsMax: 600,      defaultAgentId: "",    },  },}
Field Default Description
enabled true Enables collector features subject to tool policy; set false to opt out. Code Mode has additional requirements below.
maxConcurrent 32 Maximum collector children running concurrently in one swarm group's execution lane. Additional accepted children queue FIFO.
maxChildrenPerGroup 50 Maximum live collector children in one group.
maxTotalPerGroup 200 Maximum collector children a group may spawn over its lifetime. This is the runaway-spawn backstop.
waitTimeoutSecondsMax 600 Maximum timeout accepted by one agents_wait call. The call default is 30 seconds.
defaultAgentId "" Target agent used when a spawn omits agentId. An empty value uses the requesting agent. Existing sub-agent allowlists apply.

Numeric values must be positive integers. OpenClaw bounds maxConcurrent to 1–1000, maxChildrenPerGroup to 1–10000, maxTotalPerGroup to 1–100000, and waitTimeoutSecondsMax to 1–86400.

Each group has its own execution lane, independent of the parent's ordinary sub-agent limit. Budget for one model stream and one Code Mode worker isolate per running child. Raising maxConcurrent increases model and Gateway resource use; it does not raise maxChildrenPerGroup or maxTotalPerGroup.

You can override Swarm for one configured agent with agents.entries.*.tools.swarm. Per-agent values merge over the top-level setting. An agent's false or { enabled: false } disables Swarm for that agent. true or { enabled: true } enables it even if globally disabled. A limits-only per-agent object inherits global enablement, so it does not re-enable a global false.

Requirements

To use the agents.run, phase, and log guest globals, enable OpenClaw Code Mode and leave Swarm enabled:

json5
{  tools: {    codeMode: true,  },}

Code Mode exposes these globals, API.read("agents.d.ts"), and Swarm prompt hints only when its catalog contains the native OpenClaw sessions_spawn tool and the run's execution allowlist permits it. An MCP tool with the same name does not qualify. Tool profiles, allow/deny policy, provider rules, and sandbox policy can remove the native tool. If the Swarm API is absent, check Code Mode activation and Sub-agents.

Code Mode waits for collector results internally. Its agents.run() API does not require the standalone agents_wait tool to be allowed. The low-level tool flow needs both sessions_spawn and agents_wait allowed. Enabling Swarm never grants tools or bypasses policy.

defaultAgentId and per-run agentId values must name a configured target permitted by the requester's subagents.allowAgents policy. OpenClaw rejects an unknown or disallowed target instead of falling back to another agent.

Write a Swarm script

When the requirements are met, Code Mode exposes this guest API:

typescript
type AgentRunOptions = {  label?: string;  model?: string;  thinking?: string;  fastMode?: boolean | "auto";  agentId?: string;  schema?: Record<string, unknown>;  phase?: string;}; agents.run(prompt: string, options?: AgentRunOptions & { schema?: undefined }): Promise<string>;agents.run<T>(prompt: string, options: AgentRunOptions & { schema: Record<string, unknown> }): Promise<T>;phase(title: string): void;log(message: string): void;

Without schema, agents.run() resolves to the child's final text. With a JSON Schema, it resolves to the value submitted through the child's structured_output tool. A failed, killed, timed-out, or schema-invalid child rejects the promise with an error whose name is "SwarmAgentError" and whose runId, status, and message identify the failed child and outcome. There is no global SwarmAgentError constructor. Inspect the caught error's fields. Spawn or bridge failures can reject with other errors. Read the exact generated declarations and short orchestration idioms from API.read("agents.d.ts") inside Code Mode.

Use label for a recognizable child name in session transcripts. Use phase in the options to publish a phase immediately before that child starts, or call phase() when several children belong to the same stage. log() publishes a short progress note. Progress calls are fire-and-forget. They do not delay the script if the UI is unavailable.

Fan out in parallel with structured results

This example launches one researcher per topic, waits for every outcome, then asks a final child to synthesize the successful reports. Failed lanes stay in the result, even if synthesis also fails:

javascript
const reportSchema = {  type: "object",  properties: {    finding: { type: "string" },    evidence: { type: "array", items: { type: "string" } },    confidence: { type: "number" },  },  required: ["finding", "evidence", "confidence"],  additionalProperties: false,}; const topics = ["authentication", "storage", "recovery"];phase("Independent review"); const settled = await Promise.allSettled(  topics.map((topic) =>    agents.run(`Review the ${topic} path. Return one finding with evidence.`, {      label: `review-${topic}`,      thinking: "high",      fastMode: "auto",      schema: reportSchema,    }),  ),); const reports = [];const failures = [];for (const [index, outcome] of settled.entries()) {  if (outcome.status === "fulfilled") {    reports.push({ topic: topics[index], report: outcome.value });  } else {    failures.push({ topic: topics[index], error: String(outcome.reason) });  }} if (reports.length === 0) return { reports, failures }; phase("Synthesis");log(`Collected ${reports.length} reports; ${failures.length} lanes failed.`); try {  const synthesis = await agents.run(    `Reconcile these reports, explain disagreements, and disclose failed lanes:\n${JSON.stringify({ reports, failures })}`,    { label: "synthesis" },  );  return { synthesis, reports, failures };} catch (error) {  return { reports, failures, synthesisError: String(error) };}

Promise.allSettled preserves partial results while waiting for every child. Promise.all rejects on the first failure and does not collect the remaining outcomes for you. Keep completed work and report failed lanes. Do not respawn the batch automatically. A later provider failure can still prevent a final model reply, so retain the collected results for recovery.

OpenClaw starts up to maxConcurrent children for the group and queues the rest in submission order.

Code Mode separately bounds concurrent guest bridge calls with tools.codeMode.maxPendingToolCalls (default 16, maximum 128). Swarm launches, progress notes, and result waits queue automatically when those slots are full. Queued requests retain their original arguments across snapshot resumes. Stopping the run discards requests that have not been admitted. maxConcurrent still limits running children, and group child limits still apply. Ordinary tool calls and timers share this guest queue but have their own 128-request waiting quota, separate from the in-flight bridge cap. Swarm launches, notes, and result waits do not consume that quota and retain their existing group, VM memory, and snapshot limits. Exceeding the ordinary quota fails the synchronous frontier before any new calls from it are dispatched. Await smaller ordinary batches instead. See Code Mode for queue, cancellation, and resource-limit semantics.

Loop on a decision gate

Use a bounded while loop when each pass decides whether another pass is needed:

javascript
const gateSchema = {  type: "object",  properties: {    ready: { type: "boolean" },    reason: { type: "string" },    nextAction: { type: "string" },  },  required: ["ready", "reason", "nextAction"],  additionalProperties: false,}; let pass = 0;let decision = { ready: false, reason: "Not checked", nextAction: "Review" }; while (!decision.ready && pass < 4) {  pass += 1;  phase(`Decision pass ${pass}`);  decision = await agents.run(    `Check whether the release evidence is complete. Previous decision: ${JSON.stringify(decision)}`,    {      label: `release-gate-${pass}`,      schema: gateSchema,    },  );  log(decision.reason);} if (!decision.ready) {  throw new Error(`Gate still closed after ${pass} passes: ${decision.nextAction}`);} return decision;

Always bound decision loops. maxTotalPerGroup is the final safety backstop, not a substitute for a clear stopping condition.

Process the first child that finishes

agents.run() returns an ordinary promise, so Promise.race can react to the first Code Mode child. For harnesses that call the lower-level tools, agents_wait provides the same first-completion boundary: it returns as soon as at least one requested run completes, or when the bounded timeout expires. See Use Swarm from other harnesses for the complete drain loop.

How collector children behave

Collector children are ordinary isolated sub-agent sessions with a different completion path. They write a durable collector result for the parent to await instead of announcing or steering a reply back into the parent session. The accepted spawn receipt describes this path: collect the result with agents_wait, or await agents.run() in OpenClaw Code Mode. Do not use sessions_yield to wait for collector children. They do not send completion notifications.

Embedded and CLI-backed collector turns are not offered sessions_yield. If an override reaches the tool, it returns an error explaining that collector results are collected explicitly. A collector that nevertheless yields through another path is settled at its own terminal instead of pausing, so the turn finishes and its collected result is recorded for the waiter.

The target agent resolves in this order:

  1. agentId on the spawn or agents.run() call.
  2. tools.swarm.defaultAgentId.
  3. The requesting agent.

A dedicated, lean worker agent is useful when collector children need a smaller tool surface, cheaper model, or tighter sandbox policy. OpenClaw does not ship a built-in worker agent id. Configure one before naming it as the default. Harden that worker with tools.swarm: false in its per-agent configuration so it can be spawned but cannot start swarms from its own top-level sessions:

json5
{  tools: { swarm: { enabled: true, defaultAgentId: "worker" } },  agents: {    ownership: "explicit",    defaults: {      heartbeat: { agentId: "main" },      systemAgent: { agentId: "main" },    },    entries: {      main: {        workspace: "~/.openclaw/workspace",        subagents: { allowAgents: ["worker"] },      },      worker: { tools: { swarm: false } },    },  },  talk: { agentId: "main" },}

Collectors deny actions that require approval. A child never opens an operator approval prompt. A tool action that would require approval is denied, and the child can report that denial in its result so the script can decide what to do next.

For structured output, OpenClaw adds a synthetic structured_output tool to the child and validates its payload against the supplied JSON Schema. If a string-valued result fails validation, OpenClaw decodes it once as JSON and accepts the decoded value only if it matches the schema. Valid string results stay unchanged; integer literals outside JavaScript's safe range are preserved as strings. An invalid payload gets one corrective nudge. If no payload is submitted, or the retry still does not validate, the collector completion keeps the child's raw text, leaves structured unset, and includes schemaError. The low-level agents_wait result exposes those fields for explicit recovery logic.

Keep collector groups flat

Collector children can delegate recursively, but the usual orchestration idiom is to return work to the parent instead of expanding the collector tree:

javascript
const plan = await agents.run("Plan this job as independent tasks.", {  schema: {    type: "object",    properties: { tasks: { type: "array", items: { type: "string" } } },    required: ["tasks"],    additionalProperties: false,  },});return await Promise.all(plan.tasks.map((task) => agents.run(task)));

Nested collector children are discouraged for Swarm. Group caps, budgets, and observability all assume flat collector groups. Set agents.defaults.subagents.maxSpawnDepth: 1 when a workflow must enforce that shape.

Every child has one admission owner. Announce and interactive children use agents.defaults.subagents.maxChildrenPerAgent (default 5) and do not count collector children. Collector children use only maxChildrenPerGroup and maxTotalPerGroup. They do not consume the per-session child budget. The spawn depth guard still applies to both modes.

After admission, collector children execute in subagent:swarm:<schedulerGroupKey>, capped by the group's resolved tools.swarm.maxConcurrent (default 32). Additional children queue FIFO. They do not consume the parent's ordinary subagent:<immediate session> slots, which remain capped by agents.defaults.subagents.maxConcurrent (default 8). An ordinary child spawned by a collector uses the collector's own session lane. A collector spawn that exceeds either group admission cap is rejected with the relevant config key in the error.

Observe a Swarm

Keep the parent session open in Chat while a swarm is active. The Control UI and native Android, iOS, and macOS chat surfaces show a compact Swarm progress widget between the transcript and composer.

In the Control UI, cards show queued, running, completed, and failed or stopped counts with visible status markers. The combined count includes failures, timeouts, and cancelled children; the summary does not report these outcomes separately. Click or tap Child details, or activate it with the keyboard, to expand available child names, status icons, and run durations. The view shows up to four active groups plus the latest completed group, with an explicit count when more groups are active. Each card displays at most 64 markers and 64 child details. Its counts include every accepted group member.

The latest completed group's counts remain visible after the children finish, including when the parent fails before writing its final response. Groups whose children all succeed use a compact completion row. Activate the row to expand child details and the final-response reminder. Groups with running, queued, failed, or stopped children keep their visible status markers and counts. These are child outcomes, not confirmation that the parent produced a synthesis. Counts come from retained collector records, so reloading the page or cleaning up a child session does not reduce the reported total. They expire with the existing collector retention policy. This is not a permanent execution archive.

Native Android, iOS, and macOS chat surfaces still show active-only phase-grouped grids, capped at 256 markers per phase with an overflow count. Accessible labels identify each child's status. Native clients present killed and timed-out children as failed. Native groups leave the widget when none of their children are queued or running. The native widget disappears when no active groups remain.

Collector children appear in their session transcripts. They have no session-sidebar rows. Their activity and unread failures still contribute to the parent’s sidebar ring and attention signals. Persistent spawned sessions and forks keep their normal sidebar nesting.

Delete-mode collector children can clean up their child sessions immediately after completion while retaining their waitable results. Those collector records remain available until the group is archived after every member reaches its retention deadline. Retained child sessions are archived as a batch at that point.

Resetting a child session durably revokes completed runs' cleanup before changing that session, so a delayed cleanup retry cannot delete its replacement. Reset fails if completion is still settling or revocation cannot be saved. If reset fails or the Gateway stops after revocation is saved, the original session may remain with that cleanup disabled. Collector results and task outcomes keep their normal retention, and active reset continuations keep running.

Stop a Swarm

Use Stop in the parent chat to cancel a running swarm. A Stop targeting a specific parent run also cancels its associated collector children and their nested descendants. Collector mode changes result delivery, not cancellation scope. Successful cancellation prevents selected queued children from starting as running siblings stop. It does not cancel work from unrelated parent turns.

Stop also waits for the selected children's execution and queued-launch cleanup. A child's task can show a terminal status while that cleanup is still settling.

If Stop times out or reports incomplete descendant cancellation, inspect the remaining work with subagents using action: "list" and retry cancellation for those children. A stopped parent alone does not confirm that every child stopped, and a failed request can leave cleanup pending.

Already-accepted children remain independent when the parent completes normally, yields, or times out. If the parent is no longer active, cancel the child tasks directly. See Sub-agent stopping for exact-run and session-wide Stop scopes.

Use Swarm from other harnesses

You can use Swarm without OpenClaw Code Mode. Its core tools are harness-independent: start collector children with sessions_spawn({ collect: true }) and drain them with bounded agents_wait calls. Both tools must be allowed by the effective tool policy. Default-on Swarm does not add them to a restrictive tool profile or allowlist.

Codex Code Mode automatically exposes eligible dynamic OpenClaw tools under tools.*. It does not use OpenClaw's guest API or require tools.codeMode, but tools.swarm must still be enabled. Codex harness agents_wait calls support the full 600-second timeout.

Codex records its dynamic tool catalog when a native thread starts. A thread created without agents_wait cannot gain that reader just by enabling Swarm or upgrading OpenClaw, so collector spawn fields remain unavailable on that thread. For an ordinary unlocked chat, use /new or /reset to start with current tools. For a supervised, model-locked Chat, open the Control UI's global New Session page and select a concrete Codex-backed model to start a separate ordinary session. Keep the supervised Chat intact: /new, /reset, and parent-linked New chat are blocked there. The new session still needs a tool policy that permits both collector tools.

With the currently supported Codex runtime, dynamic OpenClaw tool results reach Code Mode as JSON text. Parse each result before reading fields. Codex also serializes dynamic tool calls, so Promise.all does not submit several sessions_spawn calls concurrently. Launch collector children in a bounded loop. Already-accepted children can still run while later launches are submitted.

javascript
function parseToolResult(value) {  if (typeof value !== "string") return value;  return JSON.parse(value);} const tasks = [  "Check the authentication path.",  "Check the storage path.",  "Check the recovery path.",];const launches = [];const failures = []; for (const [index, task] of tasks.entries()) {  const launch = parseToolResult(    await tools.sessions_spawn({      task,      collect: true,      label: `review-${index + 1}`,    }),  );  if (launch.status !== "accepted") {    failures.push({ task, error: launch.error ?? "Collector spawn was not accepted." });    continue;  }  launches.push(launch);} const pending = new Set(launches.map((launch) => launch.runId));const completed = []; while (pending.size > 0) {  const ids = [...pending].slice(0, 1000);  const batch = parseToolResult(    await tools.agents_wait({      ids,      timeoutSeconds: 30,    }),  );   // Rotate this bounded window behind ids that have not been checked yet.  for (const runId of ids) {    if (pending.delete(runId)) pending.add(runId);  }   for (const item of batch.completed) {    pending.delete(item.runId);    if (item.status !== "done") {      failures.push(item);    } else {      completed.push(item); // Process each result as soon as it finishes.    }  }   for (const failure of batch.errors ?? []) {    pending.delete(failure.runId);    failures.push(failure);  }} return { completed, failures };

Drain the pending set before synthesizing the successful results and reporting failures. A rejected launch or failed child must not discard results from other accepted children. Keep the returned run IDs for recovery. Do not repeat successful launches or automatically rerun failed work.

Each agents_wait call accepts 1–1000 run ids. Use awaitResults: true to keep collection owned until all authorized requested collectors settle, without an observer polling timeout. It is mutually exclusive with timeoutSeconds; child and agent-run deadlines, tool watchdogs, cancellation, and ownership checks still apply. In OpenClaw Code Mode this also makes the enclosing cell required, so the runtime—not repeated model calls—waits for registry completion events. An ordinary call keeps its existing first-completion and timeout behavior.

It returns:

typescript
type AgentsWaitResult = {  completed: Array<{    runId: string;    status: "done" | "failed" | "killed" | "timeout";    result: string;    structured?: unknown;    error?: string;    schemaError?: string;    sessionKey: string;    label?: string;    usage?: { inputTokens: number; outputTokens: number };  }>;  pending: string[];  errors?: Array<{    runId: string;    error: "not_found" | "not_owner";  }>;};

A completed item can contain partial structured data and still have status: "failed" when the provider or runtime fails afterward. In that case, error is the authoritative terminal failure. Recovery code should prefer a nonblank error, then schemaError, then a nonblank result, and finally a run/status fallback.

Individual failed items do not make a mixed agents_wait poll a top-level tool error. The poll remains a successful JSON result so callers can process its completed, pending, and errors arrays independently.

The call returns immediately when any requested child is already complete, when at least one pending child completes, when no valid pending ids remain, or when its timeout expires. Passing an already-completed run id returns the same saved result again. Only the spawning session or its authorized parent chain can wait on a collector.

This is bounded long polling, not a busy status loop. Keep passing only the remaining run ids until pending is empty. Collector mode supports native OpenClaw sub-agents. It does not support ACP runtime, thread binding, visible sessions, or persistent session mode.

Limits

Swarm runs one-shot collector children. There is no stateful multi-turn worker API. Children run on the local Gateway in their group's Swarm lane, and spawns have no cloud-placement option. Saved workflow definitions and a graph DSL are not part of Swarm's current direction.

Was this useful?