๐Ÿ  Home โ† Docs Index
Open Beta ๐Ÿ“– Interactive Reader Raw .md llms.txt

Subagents & the Job Queue

GoClaw lets a parent agent delegate a scoped task to a child agent ("subagent") that runs asynchronously in its own isolated session. Every run is persisted to an embedded SQLite jobs table, so delegation survives process restarts, is auditable, and never leaks goroutines.

This document explains what a subagent is, how you trigger one, the guardrails that bound it, and the runtime machinery (job queue, EventBus, workers) that executes it.


1. What is a subagent?

Three terms, used the same way in the Web UI, the tools and this document:

A run answers a single, well-scoped task. The child runs in a fresh session (agent:<parent>:subagent:<id>), streams its answer, and reports back to the parent โ€” all without blocking the parent's own turn.

Use a subagent when the task is a distinct sub-problem that benefits from a different agent's persona, model, or toolset (e.g. a general assistant delegating a code change to a coding agent), or when you want fan-out/parallel work. Do not use one for a trivial single-tool call the parent can do itself โ€” a subagent spins up a full agent turn (LLM + tools) and has real cost.

Key properties:


2. How to trigger a subagent

There are two surfaces, both backed by the same SpawnSubagent entry point. The sessions_spawn and sessions_send tools start a run and send it a follow-up through the same functions (SpawnSubagent, SendSubagentMessage).

2.1 The subagents tool (LLM-facing)

A registered agent tool named subagents (internal/agent/tool_subagents.go) keeps registration (which subagents an agent has) apart from execution (running tasks). Registering never runs anything, and running never changes a registration.

Action Parameters Effect
create name, description, instructions, tools, model, agent_id, draft Register a subagent. Admins only. Refuses a name that already exists; draft: true shows it without saving.
update name, any of the above Change a subagent. Admins only.
delete name, agent_id Remove a subagent. Admins only.
list agent_id List an agent's subagents.
inspect name, agent_id Show one subagent. Its instructions are shown to admins only.
run agent_id, task, target_type, wait Start a run (spawn still works). agent_id may be omitted โ†’ auto-routing to an agent (below).
status run_id Report one run's state, timing, and error.
list_runs โ€” List this agent's active and recent runs.
follow_up run_id, message Send a further message to a run's session.
yield run_id Return the run's result if finished; otherwise report "still running".
stop run_id Cancel a queued/running run.

For registration, name is the subagent and agent_id the agent it belongs to (default: the agent of this conversation). Another agent's subagents can be listed, inspected or changed by an administrator only. For run, agent_id is the target: one of the agent's own subagents, or another agent's ID. If a subagent and a configured agent have the same name, target_type ("subagent" or "agent") must say which.

The execution actions only reach runs the calling agent started, and of those only the runs the caller may touch: the person who started the run, a human admin, or the system acting for no one (a cron or SOP run). A run that recorded no person belongs to the session that started it. Any other run answers "not found", so its task and result stay with it. A follow-up acts with the run's delegation: a caller acting for another person, or whose own grant does not cover the run's, is refused.

Example (as the model would call it):

{ "action": "run", "agent_id": "code-reviewer", "task": "Review merge request !42 for bugs" }

With wait (the default) the tool waits up to 60 seconds and returns the answer; a run that takes longer keeps going while the turn that started it runs (see ยง10). action is validated at runtime (not by the JSON schema), so an unknown action returns an error listing the valid ones.

If agent_id is omitted on run, GoClaw attempts embedding-based auto-routing (RouteToAgent) to pick the best-matching agent for the task; an explicit agent_id always wins. If no confident match is found, the tool asks the caller to pick one via agents_list or suggest_agent.

2.2 The /subagents slash command (human/channel-facing)

The same operations are available as a chat command, parsed by HandleSubagentCommand (internal/agent/subagent_commands.go) and wired into every message entry point โ€” chat channels, the WebSocket UI, the REST API, and the OpenAI-compatible endpoint:

/subagents spawn <agent_id> <task>
/subagents list
/subagents status <run_id>
/subagents stop <run_id>

3. Spawn policy โ€” the guardrails

Before any run starts, validateSubagentPolicy enforces per-parent limits declared in the parent agent's config, plus a system-wide safety cap. This is what prevents runaway "explosive chaining" loops.

Guardrail Config key (per agent) Default Meaning
Allow-list subagents.allow_agents (empty = allow all) Which other configured agents this parent may spawn. "*" = any. If set and the target is not listed, the spawn is denied. It does not apply to the agent's own subagents (agents/<name>.agent.md).
Max concurrency subagents.max_active 5 Max simultaneously queued + running children for this parent, its own subagents included.
Max depth subagents.max_depth 1 How deep the spawn chain may nest. Depth is counted from the number of :subagent: segments in the parent session ID, so a child cannot recurse past the limit.
System-wide cap (constant) 10 maxConcurrentSubagentsV2 โ€” a global semaphore bounding all concurrently executing subagents across every parent (prevents goroutine leaks).

Two details worth knowing: max_active is counted per parent agent (all of that agent's sessions share the one budget), and the whole policy block is only applied when the parent's config is found โ€” if config.GetAgentConfig(parent) returns nil, all checks are skipped.

A separate switch governs agent-to-agent messaging (the "send a follow-up to a run" path, follow_up and SendSubagentMessage) when the run's target is another agent: tools.agent_to_agent.enabled must be true (it defaults to off), and tools.agent_to_agent.allow (if non-empty) must list the participating agents. A follow-up to one of the agent's own subagents does not need it. A follow-up runs the subagent with the same instructions, tools and model as its first run, and fails if the subagent has been deleted since. This gate is about messaging an existing run, not starting one.

Note: these limits are what stands between a prompt-injected message and a spawn storm. Registering, changing and deleting subagents needs an administrator, but starting a run does not, so allow_agents is the real boundary for runs on other agents โ€” keep it tight for agents exposed to untrusted channels. The agent's own subagents are bounded by max_active and by their narrowed tools.


4. Execution model

Runs are SubagentManagerV2's (subagent_v2.go): each run is a row in the SQLite jobs table, so it survives a restart and can be audited, its end is published on the EventBus and announced to the parent, and it has its own trace.

SpawnSubagent() โ€” the function both the tool and the slash command call โ€” delegates to SpawnSubagentV2 and converts the resulting Job back into a SubagentRun for callers; GetSubagentRun, ListSubagentRuns and StopSubagentRun read and stop jobs the same way. SubagentManager (subagent.go) is what remains of the first, in-memory manager: it holds the agent registry that a follow-up (Send, behind follow_up and sessions_send) runs on, and finds the run to follow up in the jobs table.


5. Lifecycle & state machine

A subagent run moves through these states (SubagentStatus):

stateDiagram-v2
    direction LR
    [*] --> queued : spawn
    queued --> running : acquire slot
    queued --> failed : stop / cancel
    running --> done : finish
    running --> failed : finish (error) / stop / cancel

A stopped or cancelled run ends failed, usually with the error killed (a stop records killed by user first; the run's own exit then overwrites it), or cancelled waiting for execution slot when it had not started. A run that reaches its one-hour limit (queued time included) ends the same way. The turn that started a run ending does not stop it. There is no separate killed state.

Under the hood, SpawnSubagentV2 performs these steps:

  1. Validate the spawn against the policy in ยง3.
  2. Insert a Job row (job_type="subagent", state="queued") with the parent/child context serialized into context_json.
  3. Launch a bounded goroutine. It acquires a slot from the global semaphore (cap 10); if the context is cancelled while waiting, the job is failed with "cancelled waiting for execution slot".
  4. Run โ€” flips the job to running, forwards the parent's RBAC identity, and calls the child agent's ProcessStream, draining it to a final result (DrainToResult).
  5. Persist the task + answer into the child's session history.
  6. Finish โ€” marks the job done (or failed on error or cancel), publishes an EventBus event, and, when it is done, announces to the parent (see next section).

The jobs row for a subagent is a ledger (state, result, audit, recovery) โ€” not a dispatch queue. Execution is driven directly by the spawning goroutine, not by a background poller. (Contrast this with SOP steps and memory jobs, ยง7.)


6. Completion: EventBus, yield, and announcements

When a run finishes, the parent finds out through either of two mechanisms:

The yield action (and YieldSubagentV2) uses the EventBus to wait efficiently: it subscribes to job.done/job.failed, and โ€” because publishing is non-blocking and drops events to a full subscriber channel โ€” also polls the DB every 3 seconds as a safety net, plus an overall timeout. This makes waiting correct even under bursty, high-concurrency fan-out.


7. Who actually runs the jobs?

The jobs table is shared by several kinds of async work, but they are executed by three distinct mechanisms. Keeping them straight is important โ€” they have different concurrency limits.

Executor Handles Concurrency Dispatch
Subagent manager (subagent_v2.go) subagent jobs Semaphore, cap 10 In-process goroutine per spawn (no poller)
MemoryWorker (memory_worker.go) fact_extract, memory_consolidate, graph_extract, graph_sync, belief_revision (an auto_learn_error row left by an older version is drained as a no-op) Sequential (one at a time) Polls queued jobs every 5 s
Tool WorkerPool (worker_pool.go) Tool calls (not jobs at all) tools.worker_pool_size (8), queue tools.worker_queue_size (64) Buffered channel with back-pressure

Two clarifications that the older version of this doc got wrong:

SOP steps also live in the jobs table (job_type="sop_step") and use the extra suspended state for human-in-the-loop approval, resuming via the EventBus and the parent_job_id link. SOPs are covered in Standard Operating Procedures.

The jobs table

Column Purpose
id Unique job ID (job-<unixnano>-<seq>).
parent_job_id Links a child job to its parent (used by SOP resume).
job_type subagent | sop_step | fact_extract | memory_consolidate | graph_extract | graph_sync | belief_revision; auto_learn_error only in rows left by an older version.
agent_id The agent that executes the job.
task The work description handed to the agent.
state queued | running | suspended | done | failed.
context_json Serialized context for resume (parent/child IDs, etc.).
result_json Output captured on completion.
error Failure reason, if any.
created_at / updated_at Timestamps.

8. Reliability & crash recovery

Because state lives in SQLite, a restart does not lose or double-run work:


9. Configuration example

Enable subagents on a parent agent and allow it to delegate to two workers:

agents:
  default: main
  list:
    - id: main
      subagents:
        allow_agents: [coding, research]  # who "main" may spawn ("*" = any)
        max_active: 5                      # concurrent children (default 5)
        max_depth: 1                       # nesting depth (default 1)
    - id: coding
    - id: research

tools:
  # Required to send a follow-up to a run on another agent (not to the agent's own subagents).
  agent_to_agent:
    enabled: true
    allow: [main, coding, research]

  # Tool-execution pool (separate from subagent concurrency).
  worker_pool_size: 8
  worker_queue_size: 64

10. Limits & gotchas