Sub-agents: Spawn Isolated Background Agent Runs

Learn how to spawn isolated background agent runs that announce results back to the requester chat. Ideal for parallelizing research and long tasks without blocking the main run.

Read this when

  • You want background or parallel work via the agent
  • You are changing sessions_spawn or sub-agent tool policy
  • You are implementing or troubleshooting thread-bound subagent sessions
  • You are looking for the sub-agent page that matches your task

Sub-agents are background agent runs spawned from an existing agent run. Each one runs in its own session (agent:<agentId>:subagent:<uuid>) and, when finished, announces its result back to the requester chat channel. Every sub-agent run is tracked as a background task.

Goals:

  • Parallelize research, long tasks, and slow tool work without blocking the main run.
  • Keep sub-agents isolated by default (session separation, optional sandboxing).
  • Keep the tool surface hard to misuse: sub-agents do not get session or message tools by default.
  • Support configurable nesting depth for orchestrator patterns.

Note

Cost note: each sub-agent has its own context and token usage by default. For heavy or repetitive tasks, set a cheaper model for sub-agents and keep your main agent on a higher-quality model via agents.defaults.subagents.model or per-agent overrides. When a child genuinely needs the requester's current transcript, spawn it with context: "fork". Thread-bound subagent sessions default to context: "fork" because they branch the current conversation into a follow-up thread.

Slash command

/subagents inspects sub-agent runs for the current session:

/subagents list
/subagents log <id|#> [limit] [tools]
/subagents info <id|#>

/subagents info shows run metadata (status, timestamps, session id, transcript path, cleanup). /subagents log prints recent chat turns for a run; add the tools token to include tool-call/result messages (omitted by default). Use sessions_history for a bounded, safety-filtered recall view from within an agent turn, or inspect the transcript path on disk for the raw full transcript.

In the Control UI, parent sessions with recent child runs have an expandable sidebar row. The nested rows show child status and runtime, and selecting one opens that child's chat while preserving the parent hierarchy.

Thread binding controls

These commands work on channels with persistent thread bindings. See Thread supporting channels below.

/focus <subagent-label|session-key|session-id|session-label>
/unfocus
/agents
/session idle <duration|off>
/session max-age <duration|off>

Spawn behavior

Agents start background sub-agents with the sessions_spawn tool. Completions return as internal parent-session events; the parent/requester agent decides whether a user-facing update is needed.

When execution identity auditing is enabled, each native or ACP child receives a new immutable identity context. Its lineage links the exact parent context/run when available and records bounded references for the parent grant, local policy, runtime assurance, and target policy that constrained the spawn. Neither the private identity token nor task text appears in the tool schema, result, transcript-derived evidence, or public plugin API. External ACP-native actions without a callback remain explicitly unsupported even though the ACP spawn and child are observable.

Non-blocking, push-based completion

  • sessions_spawn returns a run id after startup is accepted, without waiting for the child task to finish. Spawns from an OpenClaw cloud worker can first wait for child provisioning and node enrollment.
  • On completion, the sub-agent reports back to the parent/requester session.
  • Agent turns that need child results should call sessions_yield after spawning required work. That ends the current turn and lets the completion event arrive as the next model-visible message.
  • Completion is push-based. Once spawned, do not poll /subagents list, sessions_list, or sessions_history in a loop just to wait for it to finish; check status on-demand only when debugging.
  • Child output is a report/evidence for the requester agent to synthesize. It is not user-authored instruction text and cannot override system, developer, or user policy.
  • On completion, OpenClaw best-effort closes tracked browser tabs/processes opened by that sub-agent session before the announce cleanup flow continues.

Completion delivery

  • OpenClaw hands completions back to the requester session through an agent turn with a stable idempotency key.
  • If the requester run is still active, OpenClaw first tries to wake/steer that run instead of starting a second visible reply path.
  • If an active requester cannot be woken, OpenClaw falls back to a requester-agent handoff with the same completion context instead of dropping the announce.
  • A successful parent handoff completes sub-agent delivery even when the parent decides no visible user update is needed.
  • Native sub-agents do not get the message tool. They return plain assistant text to the parent/requester agent; human-visible replies stay owned by the parent/requester agent's normal delivery policy.
  • If direct handoff cannot be used, delivery falls back to queue routing. A queued completion remains session_queued, rather than delivered, until the durable queue settles.
  • Automatic completion delivery retries for up to 30 minutes, starting around 15 seconds and capping the backoff at 5 minutes. Permanent failure or deadline expiry leaves the successful child task visibly blocked instead of discarding its result.
  • Blocked canonical results are retained for 7 days. Operators can retry or intentionally dismiss them from the Tasks page or with openclaw tasks retry / openclaw tasks dismiss; retry can duplicate a visible result after an ambiguous provider acknowledgement.
  • Delivery keeps the resolved requester route: thread-bound or conversation-bound completion routes win when available. If the completion origin only provides a channel, OpenClaw fills the missing target/account from the requester session's resolved route (lastChannel / lastTo / lastAccountId) so direct delivery still works.

Completion handoff metadata

The completion handoff to the requester session is runtime-generated internal context (not user-authored text) and includes:

  • Result, the latest visible assistant reply text from the child. Tool/toolResult output is not promoted into child results. Terminal failed runs do not reuse captured reply text.
  • Status, completed; ready for parent review / failed / timed out / unknown.
  • Compact runtime/token stats.
  • A review instruction telling the requester agent to verify the result before deciding whether the original task is done.
  • Follow-up guidance telling the requester agent to continue the task or record a follow-up when the child result leaves more action.
  • A final-update instruction for the no-more-action path, written in normal assistant voice without forwarding raw internal metadata.

Modes and ACP runtime

  • --model and --thinking override defaults for that specific run.
  • Use info/log to inspect details and output after completion.
  • For persistent thread-bound sessions, use sessions_spawn with thread: true and mode: "session".
  • If the requester channel does not support thread bindings, use mode: "run" instead of retrying an impossible thread-bound combination.
  • For ACP harness sessions (Claude Code, Gemini CLI, OpenCode, or explicit Codex ACP/acpx), use sessions_spawn with runtime: "acp" when the tool advertises that runtime. See ACP delivery model when debugging completions or agent-to-agent loops. When the codex plugin is enabled, Codex chat/thread control should prefer /codex ... over ACP unless the user explicitly asks for ACP/acpx.
  • OpenClaw hides runtime: "acp" until ACP is enabled, the requester is not sandboxed, and a backend plugin such as acpx is loaded. runtime: "acp" expects an external ACP harness id, or an agents.entries.* entry with runtime.type="acp"; use the default sub-agent runtime for normal OpenClaw config agents from agents_list.

Context modes

Non-thread native sub-agents begin in an isolated state unless the caller explicitly requests a fork of the current transcript. For thread-bound spawns, the behavior follows threadBindings.defaultSpawnContext, which has fork as its default. When the child needs to start with a blank context, pass context: "isolated" explicitly.

ModeWhen to use itBehavior
isolatedFresh research, independent implementation, slow tool work, or anything that can be briefed in the task textCreates a clean child transcript. Default for non-thread spawns; keeps token use lower.
forkWork that depends on the current conversation, prior tool results, or nuanced instructions already present in the requester transcriptBranches the requester transcript into the child session before the child starts.

Reserve fork for cases that truly need it. It serves context-sensitive delegation, not as a substitute for drafting a well-defined task prompt.

Tool: sessions_spawn

A sub-agent run is initiated with deliver: false on the global subagent lane, followed by an announce step and the posting of the announce reply to the requester chat channel.

Whether this is available hinges on the caller's effective tool policy. The built-in coding and messaging profiles come with sessions_spawn, sessions_yield, and subagents; minimal omits them. full grants access to every tool. For an agent on a custom narrower profile that should still delegate work, add those tools with tools.alsoAllow, or pick one of the profiles mentioned above. Even after the profile stage, channel/group, provider, sandbox, and per-agent allow/deny policies can still strip the tool. To verify the effective tool list, use /tools from the same session.

Defaults:

  • Model: native sub-agents take on the caller's model unless you override with agents.defaults.subagents.model (or per-agent agents.entries.*.subagents.model). ACP runtime spawns apply the same configured subagent model when one exists; otherwise the ACP harness falls back to its own default. An explicit sessions_spawn.model still takes precedence.
  • Thinking: native sub-agents take on the caller's thinking setting unless you override with agents.defaults.subagents.thinking (or per-agent agents.entries.*.subagents.thinking). ACP runtime spawns also apply agents.defaults.models["provider/model"].params.thinking for the chosen model. An explicit sessions_spawn.thinking still takes precedence.
  • Run timeout: to set a timeout for a specific native, ACP, or visible sub-agent run, pass runTimeoutSeconds. If omitted, OpenClaw uses agents.defaults.subagents.runTimeoutSeconds when it is configured; otherwise it defaults to 0 (no timeout). An explicit 0 turns off the timeout for that run.
  • Process lifetime: a detached OpenClaw sub-agent runs on its own lifecycle. A background task spawned inside an external CLI backend differs: it shares the parent CLI subprocess and terminates when that parent hits agents.defaults.timeoutSeconds.
  • Task delivery: native sub-agents get their delegated task in a [Subagent Task] message appended after any forked history. Inherited task envelopes are context, not the current child's assignment. The sub-agent system prompt carries runtime rules and routing context, not a hidden duplicate of the task.

Accepted native sub-agent spawns report their actual initialized context (fork or isolated), including isolated when a requested fork exceeds the parent-context size cap. They also include resolved child model metadata: resolvedModel holds the applied model ref and resolvedProvider holds the provider prefix when the ref has one.

Delegation prompt mode

agents.defaults.subagents.delegationMode governs prompt guidance only; it does not alter tool policy or enforce delegation. With no explicit setting, OpenClaw uses prefer in each agent's main session and suggest in every other session.

  • suggest: retain the standard prompt nudge to use sub-agents for larger or slower work.
  • prefer: instruct the agent to remain responsive and route anything more involved than a direct reply through sessions_spawn.

An explicit default or per-agent setting always wins, including suggest in a main session and prefer elsewhere. Per-agent overrides use agents.entries.*.subagents.delegationMode.

In prefer mode, hidden sub-agents handle internal legwork the user does not need to track. Work the user will watch or return to, or work with its own deliverable such as a URL, PR, or report, should use sessions_spawn with visible: true so it stays in the sidebar.

{
  agents: {
    defaults: {
      subagents: {
        delegationMode: "prefer",
        maxConcurrent: 4,
      },
    },
    entries: {
      coordinator: {
        default: true,
        subagents: { delegationMode: "prefer" },
      },
    },
  },
}

Tool parameters

  • task (string, required), The task description for the sub-agent.

  • taskName (string), Optional stable handle for identifying a specific child in later status output. Must match [a-z][a-z0-9_-]{0,63} and cannot be a reserved target such as last or all.

  • label (string), Optional short task title shown in UI lists (task ledger, session sidebar). Name the work being done, not the agent; it is set on the child session at run start.

  • agentId (string), When subagents.allowAgents permits it, launch under a different configured agent identifier.

  • cwd (string), Optional working directory for the child task. Native sub-agents still pull bootstrap files from the target agent's workspace; cwd affects only where runtime tools and CLI harnesses execute the delegated work. For visible sessions, paths outside configured agent workspaces demand operator.admin. With worktree: true, leaving out cwd inherits the parent's managed repository from the same agent when one exists; otherwise, the target agent's workspace applies.

  • runtime (subagent" | "acp, default: subagent), acp applies solely to external ACP harnesses (claude, droid, gemini, opencode, or an explicitly requested Codex ACP/acpx) and to agents.entries.* records whose runtime.type equals acp.

  • resumeSessionId (string), ACP-only. When runtime: "acp" is set, resumes an existing ACP harness session; ignored for native sub-agent launches.

  • streamTo (parent), ACP-only. Streams ACP run output to the parent session when runtime: "acp"; leave out for native sub-agent launches.

  • model (string), Replace the sub-agent model. Invalid values get skipped, and the sub-agent falls back to the default model, with a warning in the tool result.

  • runTimeoutSeconds (integer), Replace the configured run timeout for this child. Must be a non-negative integer; 0 turns off the timeout. Works for native, ACP, and visible sessions.

  • thinking (string), Replace the thinking level for the sub-agent run. Not supported with visible: true.

  • thread (boolean, default: false), When true is true, asks for channel thread binding on this sub-agent session.

  • mode (run" | "session, default: run), If thread: true holds and mode is absent, the default shifts to session. mode: "session" calls for thread: true. When thread binding is unavailable on the requester channel, go with mode: "run" instead. With visible: true, drop mode or stick with the default "run"; the visible session stays persistent. mode: "session" is not offered on this route.

  • cleanup (delete" | "keep, default: keep), "delete" stores the session right after announce (the transcript is still kept via rename).

  • sandbox (inherit" | "require, default: inherit), require blocks the spawn unless the target child runtime is sandboxed.

  • context (isolated" | "fork), fork copies the requester's current transcript into the child session. Native sub-agents only. Non-thread spawns default to isolated; thread-bound spawns follow threadBindings.defaultSpawnContext, which defaults to fork. Pass isolated explicitly to ensure clean context. Every native fork, hidden or visible, must point at the same agent as the requester.

  • visible (boolean, default: false), Set up a persistent dashboard session for work the user will watch or revisit, or when they request a thread. Visible spawns support only runtime: "subagent" and always retain the created session.

  • group (string), Optional custom sidebar group for a visible session; a fresh name creates the group. Omitted, empty, and whitespace-only values mean ungrouped and are also fine for hidden or ACP runs. A nonempty group calls for visible: true.

  • worktree (boolean, default: false), Set up a managed git worktree for the new dashboard session. Needs visible: true.

  • worktreeName (string), Optional managed-worktree name. Needs visible: true and worktree: true.

  • worktreeBaseRef (string), Optional git base ref for the managed worktree. Needs visible: true and worktree: true.

Warning

Channel-delivery parameters (target, channel, to, threadId, replyTo, transport) are rejected by sessions_spawn. Native sub-agents send their most recent assistant turn back to the caller, while external delivery remains with the parent or requesting agent.

With visible: true, group, model, cwd, and a same-agent context: "fork" are supported. This durable mode suits coding, multi-step tasks, or outcomes the user might revisit, guide, or retain; it shows up in the sidebar when the web UI is present and functions even without it. Passing group atomically places the new session in that sidebar group; leaving it out or blank keeps it ungrouped. A sandboxed target confines cwd to that agent's workspace. Non-admin callers can employ cwd only within a configured agent workspace. With worktree: true, omitting cwd takes the same-agent parent's live managed repository and creates a separate worktree. Other spawns rely on the target agent workspace; for a different repository, have the operator launch the session from a registered project. Do not swap a rejected persistent spawn for the synchronous openclaw agent CLI, whose command deadline defaults to 600 seconds. Thread binding, mode: "session", thinking overrides, lightContext, and attachment staging are not available on this path because visible sessions are persistent dashboard sessions created via sessions.create. The default mode: "run", empty attachments, and an empty attachAs.mountPath are accepted without altering that behavior. The new dashboard child inherits the requester's effective tool-policy ceiling before its first turn. Session listing and addressing follow tools.sessions.visibility; the default tree scope covers the current session and its own spawn subtree, while the main session can reach every same-agent session unless self or the sandbox spawned-only clamp applies. See Session tools and Managed worktrees.

When a call fails with Parameters require visible=true, drop the named group or worktree options to preserve the hidden or ACP runtime. To create a visible session instead, use visible: true with runtime: "subagent" and omit mode, thread, thinking, lightContext, attachments, attachAs, swarm options, and the ACP-only streamTo and resumeSessionId. Worktree names and base refs also need worktree: true. Adding visible: true alone does not make an ACP call compatible.

A visible spawn is credited to the requesting agent: that agent becomes the new session's creator and initial owner, shown with its configured identity name and avatar in the sidebar. The accepted result doubles as a receipt with childSessionKey, runId, a Control UI sessionUrl (omitted when the Control UI is disabled), and an owner record. When acknowledging the spawn in a channel, put the session URL on the first line and Owner: <label> on the second so the user can open the session and see who is responsible. Owners can be reassigned later; see Multi-user mode.

Task names and targeting

taskName is a model-facing handle for orchestration, not a session key. Use it for stable child names such as review_subagents, linux_validation, or docs_update when a coordinator may need to inspect that child later.

Target resolution accepts exact taskName matches and unambiguous prefixes. Matching is scoped to the same active/recent target window used by numbered /subagents targets, so a stale completed child does not make a reused handle ambiguous. If two active or recent children share the same taskName, the target is ambiguous; use the list index, session key, or run id instead.

The reserved targets last and all are not valid taskName values because they already have control meanings.

Tool: sessions_yield

Ends the active model turn and blocks until runtime events arrive, with sub-agent completions being the main case. Call this after launching any required child work whenever the parent cannot form a final answer until those completions land.

sessions_yield serves as the waiting primitive. Avoid swapping it out for polling loops built on subagents, sessions_list, sessions_history, shell sleep, or process checks that merely watch for child completion.

The optional message field carries private context for the resumed turn. Use acknowledgment to supply a waiting reply when an interactive parent turn would otherwise finish silently. That acknowledgment is not emitted from sub-agent, heartbeat, or silent turns, nor does it stand in for a reply or message already sent during the turn. This host-owned waiting status sidesteps message-tool-only source suppression; ordinary model replies stay private unless the model sends them through the message tool.

On native Codex harness turns, wait_agent holds the current turn open and is meant for an intentional same-turn wait when the next immediate step depends on the child. Choose sessions_yield instead when a native child's result should bring the parent back in a later turn.

Only reach for sessions_yield when the session's effective tool list actually includes it. Certain minimal or custom tool profiles may offer sessions_spawn and subagents without exposing sessions_yield; in that situation, do not fabricate a polling loop just to await completion.

A sub-agent can also yield on its own to wait on external work, like a remote job or a long-running task it does not drive. That pauses the child run rather than finishing it, so the requester receives no completion event yet and continues waiting. A plugin can later resume that same run by invoking api.runtime.subagent.run with the paused sessionKey, instead of spawning a sibling. The requester is notified once such a follow-up completes normally; a follow-up that yields again leaves the run paused and the requester waiting.

Automatic continuation applies only to the plugin runtime API described above. Ordinary follow-ups via routes not tracked as sub-agent runs neither resume the paused run nor notify its requester. Explicit subagents steering differs: it intentionally replaces the yielded run and carries on with the same child session.

Among plugin runtime follow-ups, continuation holds for those using default delivery. A follow-up that provides its own requester or completion-delivery context is requesting its own audience, so it executes as a separate sibling and delivers there instead. The paused run stays resumable, and a later default follow-up still continues it.

When active children exist, OpenClaw inserts a compact runtime-generated Active Subagents prompt block into normal turns so the requester can view the current child sessions, run ids, statuses, labels, tasks, and taskName aliases without polling. The task and label fields in that block are quoted as data, not instructions, because they can come from user/model-provided spawn arguments.

Tool: subagents

Enumerates spawned sub-agent runs and background-task records owned by the requester session tree. The task rows cover native sub-agents, ACP runs, Gateway CLI/media work, and cron executions. It is scoped to the current requester; a child can only see its own controlled children.

Use subagents for on-demand status and debugging. Use sessions_yield to wait for completion events.

Use action: "cancel" with a taskId returned by action: "list" to stop a task. Cancellation is confined to the controlled session tree; a leaf sub-agent cannot cancel work owned by another session.

Thread-bound sessions

When thread bindings are enabled for a channel, a sub-agent can remain bound to a thread so follow-up user messages in that thread keep routing to the same sub-agent session.

Thread supporting channels

A channel supports persistent thread-bound subagent sessions (sessions_spawn with thread: true) when it registers a conversation binding adapter. Bundled channels with that support: Discord, iMessage, Matrix, and Telegram. Discord and Matrix default to creating a child thread; Telegram and iMessage default to binding the current conversation. Use the per-channel threadBindings config keys for enablement, timeouts, and spawnSessions.

Quick flow

Spawn

sessions_spawn with thread: true (and optionally mode: "session").

Bind

OpenClaw creates or binds a thread to that session target in the active channel.

Route follow-ups

Replies and follow-up messages in that thread route to the bound session.

Inspect timeouts

Use /session idle to inspect/update inactivity auto-unfocus and /session max-age to control the hard cap.

Detach

Use /unfocus to detach manually.

Manual controls

CommandEffect
/focus <target>Bind the current thread (or create one) to a sub-agent/session target
/unfocusRemove the binding for the current bound thread
/agentsList active runs and binding state (binding:<id>, unbound, or bindings unavailable)
/session idleInspect/update idle auto-unfocus (focused bound threads only)
/session max-ageInspect/update hard cap (focused bound threads only)

Config switches

  • Global default: session.threadBindings.enabled, session.threadBindings.idleHours, session.threadBindings.maxAgeHours.
  • Channel override and spawn auto-bind keys are adapter-specific. See Thread supporting channels above.

See Configuration reference and Slash commands for current adapter details.

Allowlist

  • agents.entries.*.subagents.allowAgents (string[]), The agent ids that may be selected as targets through explicit agentId are listed here (setting ["*"] permits any configured target). By default, only the requester is allowed. Should you define a list yet still want the requester to launch itself via agentId, add the requester's id to that list.

  • agents.defaults.subagents.allowAgents (string[]), When the requester agent doesn't supply its own subagents.allowAgents, this default target-agent allowlist is applied.

  • agents.defaults.subagents.requireAgentId (boolean, default: false), Any sessions_spawn invocation that leaves out agentId gets blocked (this enforces explicit profile selection). The per-agent override is agents.entries.*.subagents.requireAgentId.

  • agents.defaults.subagents.announceTimeoutMs (number, default: 120000), Each gateway agent announce delivery attempt has a timeout of this many milliseconds. Values must be positive integers and are capped at the platform-safe timer limit. Because transient retries occur, the total announce wait can exceed a single configured timeout.

When the requester session operates in a sandbox, sessions_spawn refuses targets that would run outside a sandbox.

Discovery

To see which agent ids are presently allowed for sessions_spawn, run agents_list. The response lists each agent's effective model and embedded runtime metadata, letting callers tell apart OpenClaw, Codex app-server, and other configured native runtimes.

Every allowAgents entry has to reference a configured agent id found in agents.entries.*. ["*"] expands to any configured target agent plus the requester. If an agent config gets removed but its id stays in allowAgents, sessions_spawn rejects that id and agents_list leaves it out. Execute openclaw doctor --fix to purge stale allowlist entries, or create a minimal agents.entries.* entry when the target should stay spawnable while adopting defaults.

Auto-archive

  • Once agents.defaults.subagents.archiveAfterMinutes elapses (default 60), sub-agent sessions are archived automatically.
  • Archiving relies on sessions.delete and renames the transcript to *.deleted.<timestamp> (kept in the same folder).
  • cleanup: "delete" archives right after announce (the transcript is still preserved via the rename).
  • Auto-archive is best-effort; if the gateway restarts, pending timers are lost.
  • Configured run timeouts do not trigger auto-archive; they merely halt the run. The session lingers until auto-archive occurs.
  • Depth-1 and depth-2 sessions both receive identical auto-archive treatment.
  • Browser cleanup is handled independently of archive cleanup: tracked browser tabs/processes are best-effort closed at run completion, even when the transcript/session record is retained.

Nested sub-agents

Sub-agents cannot, by default, spawn their own sub-agents (maxSpawnDepth: 1). Enabling one nesting level requires setting maxSpawnDepth: 2, which yields the orchestrator pattern: main → orchestrator sub-agent → worker sub-sub-agents.

{
  agents: {
    defaults: {
      subagents: {
        maxSpawnDepth: 2, // allow sub-agents to spawn children (default: 1, range 1-5)
        maxChildrenPerAgent: 5, // max active children per agent session (default: 5, range 1-20)
        maxConcurrent: 8, // global concurrency lane cap (default: 8)
        runTimeoutSeconds: 900, // default timeout for sessions_spawn (0 = no timeout)
        announceTimeoutMs: 120000, // per-call gateway announce timeout
      },
    },
  },
}

Depth levels

DepthSession key shapeRoleCan spawn?
0agent:<id>:mainMain agentAlways
1agent:<id>:subagent:<uuid>Sub-agent (orchestrator when depth 2 allowed)Only if maxSpawnDepth >= 2
2agent:<id>:subagent:<uuid>:subagent:<uuid>Sub-sub-agent (leaf worker)Never

Announce chain

Results travel upward through the chain:

  1. A depth-2 worker completes, then announces to its parent, the depth-1 orchestrator.
  2. The depth-1 orchestrator gets that announce, merges the results, finishes, and announces to main.
  3. Main receives the announce and passes it to the user.

Each level sees announces exclusively from its immediate children.

Note

Operational guidance: launch child work once and await completion events rather than constructing poll loops around sessions_list, sessions_history, /subagents list, or exec sleep commands. sessions_list and /subagents list keep child-session relationships tied to active work: live children stay attached, ended children remain visible for a brief recent window, and stale store-only child links are disregarded once their freshness window passes. This stops old spawnedBy / parentSessionKey metadata from reviving ghost children after a restart. If a child completion event arrives after you've already sent the final answer, the proper response is the exact silent token NO_REPLY / no_reply.

Tool policy by depth

  • When a child is spawned, it captures the requester's effective sender policy. Senderless child runs and authenticated operator resumes keep that snapshot even if toolsBySender changes later; current global, agent, provider, sandbox, and sub-agent restrictions still hold. A new external channel turn aimed at the child instead re-resolves the current sender policy.
  • Role and control scope get written into session metadata at spawn time. This prevents flat or restored session keys from accidentally regaining orchestrator privileges.
  • Depth 1 (orchestrator, when maxSpawnDepth >= 2): receives sessions_spawn, subagents, sessions_list, sessions_history so it can spawn children and check their status. All other session/system tools are denied.
  • Depth 1 (leaf, when maxSpawnDepth == 1): no session tools (the current default behavior).
  • Depth 2 (leaf worker): no session tools, since sessions_spawn is always denied at depth 2. Further child spawning is impossible.

Per-agent spawn limit

Each agent session, regardless of depth, can hold no more than maxChildrenPerAgent active child sessions at once, with 5 being the default. This cap stops a single orchestrator from spawning an uncontrolled number of descendants.

Cascade stop

Halting a depth-1 orchestrator also halts every one of its depth-2 children automatically:

  • Issuing /stop in the main chat terminates all depth-1 agents and propagates the stop down to their depth-2 descendants.

Authentication

Authentication for sub-agents is determined by agent id, not by session type:

  • The key for the sub-agent session is agent:<agentId>:subagent:<uuid>.
  • The local auth overlay comes from that agent's agentDir.
  • Shared auth profiles are added as a fallback; when conflicts arise, agent profiles take precedence over shared ones.

Because the merge is additive, shared profiles remain available as fallbacks at all times. Fully isolated auth per agent is not yet possible.

Announce

Sub-agents communicate results through an announce step:

  • The announce step executes inside the sub-agent session, not the requester session.
  • An exact ANNOUNCE_SKIP response suppresses the announce output.
  • For runs that require completion, an exact child NO_REPLY response or no output counts as a missing deliverable passed to the requester/parent for display or retry; it is not treated as silent delivery.
  • Paths that are optional, duplicate, already visible, or otherwise not required may use exact NO_REPLY to remain intentionally quiet.

Delivery varies with requester depth:

  • Top-level requester sessions make a follow-up agent call with external delivery (deliver=true).
  • Nested requester subagent sessions get an internal follow-up injection (deliver=false), letting the orchestrator compose child results within the session.
  • If a nested requester subagent session no longer exists, OpenClaw falls back to that session's requester when one is available.

For top-level requester sessions, completion-mode direct delivery first resolves any bound conversation/thread route and hook override, then fills missing channel-target fields from the requester session's stored route. This ensures completions land on the correct chat/topic even when the completion origin only specifies the channel.

Child completion aggregation stays scoped to the current requester run when building nested completion findings, which stops stale prior-run child outputs from entering the current announce. Announce replies keep thread/topic routing when channel adapters support it.

Announce context

Announce context is standardized into a stable internal event block:

FieldSource
Sourcesubagent or cron
Session idsChild session key/id
TypeAnnounce type + task label
StatusDerived from runtime outcome (ok, error, timeout, or unknown), not inferred from model text
Result contentLatest visible assistant text from the child
Follow-upInstruction describing when to reply vs stay silent

Terminal failed runs report failure status without replaying captured reply text. Tool/toolResult output is not promoted into child result text.

Stats line

Announce payloads end with a stats line, even when wrapped:

  • Runtime (e.g. runtime 5m12s).
  • Token usage (input/output/total).
  • Estimated cost when model pricing is configured (models.providers.*.models[].cost).
  • sessionKey, sessionId, and transcript path so the main agent can fetch history via sessions_history or inspect the file on disk.

Internal metadata is meant for orchestration only; user-facing replies should be rewritten in normal assistant voice.

Why prefer sessions_history

sessions_history is the safer orchestration path for reading a child's transcript from within an agent turn:

  • Redacts credential/token-like text even when general-purpose log redaction is disabled.
  • Truncates long text blocks (4000 chars per block) and drops thinking signatures, reasoning replay payloads, and inline image data.
  • Caps returned messages at 80 KB; older rows can be dropped or an oversized row replaced with [sessions_history omitted: message too large].
  • Use nextOffset when present to page backward through older transcript windows.
  • Returns structured history rather than /subagents log's plain chat lines. Reasoning tags, <relevant-memories> / <relevant_memories> scaffolding, and tool-call XML can remain in message text: sessions_history does not apply the log command's assistant prose sanitizer. See Session tools for the recall guarantees.
  • Raw on-disk transcript inspection is the fallback when you need the full byte-for-byte transcript.

Tool policy

Sub-agents use the same profile and tool-policy pipeline as the parent or target agent first. After that, OpenClaw applies the sub-agent restriction layer.

Sub-agents always lose gateway, agents_list, session_status, cron, message, sessions_send, and the conversations_* tools regardless of depth or role (system-level/interactive tools, direct delivery surfaces, or tools the main agent should coordinate). This hard-deny layer is derived from the persisted sub-agent session envelope on every turn, including resumed and visible dashboard sessions; ordinary allow/alsoAllow entries cannot override it. Hidden launches also disable message before tool construction as defense in depth. Leaf sub-agents (default depth-1 behavior, and always at depth 2) additionally lose subagents, sessions_list, sessions_history, and sessions_spawn, so sub-agent communication stays on the announce chain.

sessions_history remains a bounded, redacted recall view here too, it is neither a raw transcript dump nor a prose-only rendering.

When maxSpawnDepth >= 2, depth-1 orchestrator sub-agents additionally receive sessions_spawn, subagents, sessions_list, and sessions_history so they can manage their children.

Override via config

{
  agents: {
    defaults: {
      subagents: {
        maxConcurrent: 1,
      },
    },
  },
  tools: {
    subagents: {
      tools: {
        // deny wins
        deny: ["gateway", "cron"],
        // if allow is set, it becomes allow-only (deny still wins)
        // allow: ["read", "exec", "process"]
      },
    },
  },
}

tools.subagents.tools.allow acts as a terminal allow-only filter. It can restrict the tool set that has already been resolved, but it cannot restore a tool that tools.profile removed. As an example, tools.profile: "coding" includes web_search/web_fetch yet omits the browser tool. To grant browser automation to coding-profile sub-agents, add browser at the profile stage:

{
  tools: {
    profile: "coding",
    alsoAllow: ["browser"],
  },
}

Use per-agent agents.entries.*.tools.alsoAllow: ["browser"] when browser automation should be available to only one agent.

Concurrency

Sub-agents rely on a dedicated in-process queue lane:

  • Lane name: subagent
  • Concurrency: agents.defaults.subagents.maxConcurrent (default 8)

Retained blocked completions also shield the gateway from unbounded fan-out. OpenClaw issues a warning when the delivery backlog hits 25 and halts new subagent spawns at 50 until operators retry or dismiss enough retained deliveries. It does not discard results to free up space.

Liveness and recovery

OpenClaw does not interpret the absence of endedAt as definitive evidence that a sub-agent remains active. Unended runs older than the stale-run window (2 hours, or the configured run timeout plus a short grace period, whichever is longer) no longer count as active/pending in /subagents list, status summaries, descendant completion gating, and per-session concurrency checks.

Following a gateway restart, stale unended restored runs are pruned unless their child session is marked abortedLastRun: true. Restart-aborted runs stay registered for the sub-agent orphan recovery flow: stale runs are finalized without a resume, while fresh child sessions receive a synthetic resume message before the aborted marker is cleared.

Automatic restart recovery is bounded per child session. If the same sub-agent child is accepted for orphan recovery repeatedly inside the rapid re-wedge window, OpenClaw persists a recovery tombstone on that session and stops auto-resuming it on later restarts. Run openclaw tasks maintenance --apply to reconcile the task record, or openclaw doctor --fix to clear stale aborted recovery flags on tombstoned sessions.

Note

If a sub-agent spawn fails with Gateway PAIRING_REQUIRED / scope-upgrade, check the RPC caller before editing pairing state. Internal sessions_spawn coordination dispatches in process when the caller is already running inside the gateway request context, so it does not open a loopback WebSocket or depend on the CLI's paired-device scope baseline. Callers outside the gateway process still use the WebSocket fallback as client.id: "gateway-client" with client.mode: "backend" over direct loopback shared-token/password auth. Remote callers, explicit deviceIdentity, explicit device-token paths, and browser/node clients still need normal device approval for scope upgrades.

Stopping

  • Sending /stop in the requester chat aborts the requester session and stops any active sub-agent runs spawned from it, cascading to nested children.

Limitations

  • Direct announce attempts are best-effort, but admitted session-queued completion handoffs and their owner/task projections survive gateway restarts in the shared SQLite state database.
  • Sub-agents still share the same gateway process resources; treat maxConcurrent as a safety valve.
  • sessions_spawn returns { status: "accepted", runId, childSessionKey } when startup is accepted, without waiting for the child task to finish. Cloud-worker spawns can wait for provisioning before returning this receipt.
  • Sub-agent context only injects AGENTS.md (no SOUL.md, IDENTITY.md, USER.md, MEMORY.md, or BOOTSTRAP.md). Its ## Tools section carries environment-specific notes. Codex-native subagents follow the same boundary through native AGENTS.md discovery, while parent-only persona, identity, and user files are injected as turn-scoped collaboration instructions so children do not clone them.
  • Maximum nesting depth is 5 (maxSpawnDepth range: 1-5). Depth 2 is recommended for most use cases.
  • maxChildrenPerAgent caps active children per session (default 5, range 1-20).
6,189 words · updated Sep 1, 2026