---
summary: "Orchestrate concurrent sub-agents from Code Mode scripts with structured results, bounded fan-out, and live progress"
title: "Swarm"
sidebarTitle: "Swarm"
read_when:
  - You want a Code Mode script to fan out work across several agents
  - You need structured child results, decision gates, or first-completion pipelines
  - You are enabling or tuning tools.swarm limits
  - You want to observe collector children in chat
---

Swarm is an experimental, opt-in way to orchestrate many sub-agents from a
[Code Mode](/tools/code-mode) script. Use normal JavaScript or TypeScript
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.

## Enable Swarm

The recommended path is **Settings → Labs → Swarm** in the Control UI. The
toggle takes effect immediately and writes `tools.swarm.enabled` to your
configuration.

You can also enable Swarm directly in `openclaw.json`:

```json5
{
  tools: {
    swarm: {
      enabled: true,
      maxConcurrent: 8,
      maxChildrenPerGroup: 50,
      maxTotalPerGroup: 200,
      waitTimeoutSecondsMax: 600,
      defaultAgentId: "",
    },
  },
}
```

Boolean shorthand enables or disables the feature with all other values at
their defaults:

```json5
{
  tools: {
    swarm: true,
  },
}
```

| Field                   | Default | Description                                                                                                                    |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `enabled`               | `false` | Exposes collector-mode spawn options, `agents_wait`, and the Code Mode `agents.*` guest API.                                   |
| `maxConcurrent`         | `8`     | Maximum collector children running concurrently in one swarm group. Additional accepted children queue in FIFO order.          |
| `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`.

You can override Swarm for one configured agent with
`agents.entries.*.tools.swarm`. The per-agent object merges over the top-level
`tools.swarm` object.

## Requirements

The `agents.run`, `phase`, and `log` guest globals require both Swarm and
OpenClaw Code Mode:

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

Code Mode must also have effective access to `sessions_spawn`. Tool profiles,
allow/deny policy, provider rules, and sandbox policy can remove that tool.
See [Code Mode activation](/tools/code-mode#activation) and
[Sub-agents](/tools/subagents) if a script reports that `sessions_spawn` is
unavailable.

`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 Swarm is enabled, 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 a `SwarmAgentError`. 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 the dashboard and sidebar. 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 all of them, then
asks a final child to synthesize their structured reports:

```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 reports = await Promise.all(
  topics.map((topic) =>
    agents.run(`Review the ${topic} path. Return one finding with evidence.`, {
      label: `review-${topic}`,
      thinking: "high",
      fastMode: "auto",
      schema: reportSchema,
    }),
  ),
);

phase("Synthesis");
log(`Collected ${reports.length} independent reports.`);

return await agents.run(
  `Reconcile these reports and explain disagreements:\n${JSON.stringify(reports)}`,
  { label: "synthesis" },
);
```

`Promise.all` is the fan-out and fan-in boundary. 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`). For very
large groups, launch bounded batches below that limit and leave headroom for
`phase()`, `log()`, and child wait transitions. `maxConcurrent` limits running
children; it does not raise the guest bridge-call limit.

### 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](#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 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 swarm 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: {
    entries: {
      main: {
        default: true,
        subagents: { allowAgents: ["worker"] },
      },
      worker: { tools: { swarm: false } },
    },
  },
}
```

Collector approvals fail closed. 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. An
invalid or missing payload gets one corrective nudge. If 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.

### Children are leaves

Swarm children are leaves by default. The universal
`agents.defaults.subagents.maxSpawnDepth` guard prevents a child from spawning
its own children at the default depth of `1`. The usual orchestration idiom is
to return work to the parent, not spawn more work from a child:

```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 sub-agents are an operator opt-in through
`agents.defaults.subagents.maxSpawnDepth` and are discouraged for Swarm.
Group caps, budgets, and observability all assume flat collector groups.

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, children above `maxConcurrent` queue FIFO within their swarm
group, nested inside the global sub-agent lane. These concurrency layers queue
work rather than rejecting it. A collector spawn that exceeds either group 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, rendering each active collector group as one
dot per child with queued, running, done, or failed state. Accessible labels identify
each child and status; the Control UI also exposes them as dot tooltips. The widget
disappears after every group child reaches a terminal state.

The session sidebar keeps the normal parent/child tree. Expand the parent row to
inspect a collector child or open its transcript without losing the swarm hierarchy.

Collector results remain waitable until their group is archived. After every
member reaches its retention deadline, OpenClaw archives the group's children
as a batch so completed swarms do not remain in the live session tree.

## 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.

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

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 collectors 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 = [];

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") {
    throw new Error(launch.error ?? "Collector spawn was not accepted.");
  }
  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") {
      const detail = [item.error, item.schemaError, item.result].find(
        (value) => typeof value === "string" && value.trim(),
      );
      throw new Error(detail ?? `${item.runId}: ${item.status}`);
    }
    completed.push(item); // Process each result as soon as it finishes.
  }

  for (const failure of batch.errors ?? []) {
    pending.delete(failure.runId);
    throw new Error(`${failure.runId}: ${failure.error}`);
  }
}

return completed;
```

Each `agents_wait` call accepts 1–1000 run ids. 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. Completed records are idempotent, so passing an
already-completed run id returns its 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 and roadmap

Swarm v1 runs one-shot collector children; the planned `agents.session()` API
will add stateful multi-turn workers. Children currently run on the local
Gateway's sub-agent lane; cloud placement is planned as an explicit spawn
option. Saved workflow definitions and a graph DSL are not part of Swarm's
current direction.

## Related

- [Code Mode](/tools/code-mode) for the QuickJS guest runtime and activation rules
- [Sub-agents](/tools/subagents) for child policy, isolation, and session behavior
- [Multi-agent sandbox tools](/tools/multi-agent-sandbox-tools) for per-agent restrictions
- [Tools overview](/tools) for tool profiles and policy routing
