OpenClaw tasks code analysis
Overview
OpenClaw is a TypeScript ESM monorepo whose root package exposes the openclaw CLI and builds a multi-channel AI gateway. The task subsystem is a shared background-run control plane: it records detached ACP, subagent, cron, CLI, and media-generation work; persists task and flow records in SQLite; exposes operator controls through CLI, chat, gateway protocol, and plugin runtime surfaces; and reconciles stale state in the gateway process.
Observed fact: tasks are not a scheduler or executor. Producers call task APIs when they already own background work. Evidence: docs/automation/tasks.md:11, docs/automation/tasks.md:15, src/tasks/detached-task-runtime-contract.ts:126, src/tasks/task-executor.ts:94.
Sources consulted
- Product docs:
docs/automation/tasks.md,docs/cli/tasks.md,docs/automation/taskflow.md,docs/automation/cron-jobs.md,docs/cli/cron.md. - Repository guidance: root
AGENTS.md,docs/AGENTS.md,src/gateway/AGENTS.md,src/gateway/protocol/AGENTS.md,src/plugins/AGENTS.md,src/agents/AGENTS.md,src/agents/tools/AGENTS.md. - Root metadata and commands:
README.md,CHANGELOG.md,package.json. - Task core:
src/tasks/task-registry.ts,task-registry.types.ts,task-registry.store*.ts,task-registry.maintenance.ts,task-registry.audit.ts,task-executor.ts,detached-task-runtime*.ts,task-status.ts. - Task Flow:
src/tasks/task-flow-*.ts,src/commands/flows.ts. - Entry points and producers:
src/cli/program/register.status-health-sessions.ts,src/commands/tasks.ts,src/gateway/server-methods/tasks.ts,src/auto-reply/reply/commands-tasks.ts,src/cron/service/timer.ts,src/agents/subagent-registry-run-manager.ts,src/agents/subagent-registry-lifecycle.ts,src/acp/control-plane/manager.core.ts,src/gateway/server-methods/agent.ts,src/agents/tools/media-generate-background-shared.ts. - Tests:
src/tasks/*.test.ts,src/commands/tasks*.test.ts,src/gateway/server-methods/tasks.test.ts,src/auto-reply/reply/commands-tasks.test.ts.
The docs index command was attempted earlier but dependency setup failed during a local postinstall step. Direct documentation and code evidence were used instead.
Repository map
| Path | Responsibility for this scope |
|---|---|
src/tasks/ | Task registry, task flow registry, persistence stores, lifecycle runtime facade, audit, maintenance, owner access, status formatting, domain view mapping, and tests. |
src/commands/ | CLI command implementations for openclaw tasks and openclaw tasks flow. |
src/cli/program/ | Commander wiring that registers task commands into the top-level CLI. |
src/gateway/ | Gateway startup starts the maintenance sweeper; server methods expose task RPC handlers; protocol schemas define typed request and response shapes. |
src/auto-reply/reply/ | Chat command handlers, including /tasks and status integration. |
src/cron/ | Scheduled-job producer that creates and finalizes runtime: "cron" task records. |
src/agents/ | Subagent, ACP spawn, status-tool, and media-generation producers and consumers. |
src/plugins/runtime/ | Plugin-facing task and flow runtime bindings constrained by owner session. |
docs/automation/ and docs/cli/ | User-facing model and operator command reference. |
Entry points
- CLI task commands:
src/cli/program/register.status-health-sessions.ts:381registersopenclaw tasks,list,audit,maintenance,show,notify,cancel, andflowsubcommands. Implementations live insrc/commands/tasks.tsandsrc/commands/flows.ts. - Gateway RPC:
src/gateway/server-methods.ts:49imports task handlers;src/gateway/server-methods/tasks.ts:133exposestasks.list,tasks.get, andtasks.cancel. Schemas are insrc/gateway/protocol/schema/tasks.ts. - Chat command:
src/auto-reply/reply/commands-handlers.runtime.ts:36includeshandleTasksCommand, implemented insrc/auto-reply/reply/commands-tasks.ts:126. - Gateway startup:
src/gateway/server-startup-early.ts:120configures task maintenance andsrc/gateway/server-startup-early.ts:124starts it. - Producer facade:
src/tasks/detached-task-runtime.ts:70andsrc/tasks/task-executor.ts:94are the preferred producer-facing lifecycle entry points. - Subagent producer:
src/agents/subagent-registry-run-manager.ts:521creates asubagenttask;src/agents/subagent-registry-lifecycle.ts:260finalizes it. - CLI/media producers:
src/gateway/server-methods/agent.ts:493createsclitasks for gateway-backed agent runs;src/agents/tools/media-generate-background-shared.ts:117createsclitasks for media generation. - Plugin runtime:
src/plugins/runtime/runtime-tasks.ts:178andsrc/plugins/runtime/runtime-tasks.ts:196create owner-bound task and flow runtimes.
Architecture notes
Subsystems and dependency direction
detached-task-runtime.tsis the public-ish facade for producers and plugins. It delegates to a registered detached runtime if one exists, otherwise totask-executor.ts. Evidence:src/tasks/detached-task-runtime.ts:33,src/tasks/detached-task-runtime.ts:55.task-executor.tsis the orchestration layer. It creates task records, creates one-task flows for eligible ACP/subagent records, retries blocked flows, finalizes by run id, and delegates cancellation to the registry or registered runtime. Evidence:src/tasks/task-executor.ts:46,src/tasks/task-executor.ts:94,src/tasks/task-executor.ts:290,src/tasks/task-executor.ts:683.task-registry.tsowns mutable in-memory state: task maps, secondary indexes, pending-delivery guard set, restore-on-first-use, persistence writes, lifecycle transitions, delivery, lookup, cancellation, and summary operations. Evidence:src/tasks/task-registry.ts:57,src/tasks/task-registry.ts:940,src/tasks/task-registry.ts:1490,src/tasks/task-registry.ts:1965.task-registry.store.tsabstracts persistence for tests and defaults to the SQLite store. Evidence:src/tasks/task-registry.store.ts:17,src/tasks/task-registry.store.ts:53,src/tasks/task-registry.store.ts:76.task-flow-registry.tsis a parallel registry for flow-level state with revision tracking and flow observer hooks. Evidence:src/tasks/task-flow-registry.ts:20,src/tasks/task-flow-registry.ts:84,src/tasks/task-flow-registry.ts:235.- Runtime-heavy dependencies are lazy-loaded where possible: delivery imports
sendMessagethroughtask-registry-delivery-runtime.ts, and cancellation imports ACP/subagent control throughtask-registry-control.runtime.ts. Evidence:src/tasks/task-registry-delivery-runtime.ts:1,src/tasks/task-registry-control.runtime.ts:1,src/tasks/task-registry.ts:1184,src/tasks/task-registry.ts:1912.
Composition and state
The task registry is not constructed as an instance. It is a module-level singleton with injectable store, delivery runtime, control runtime, and maintenance runtime seams for tests. This keeps call sites simple but requires careful test resets. Evidence: src/tasks/task-registry.ts:57, src/tasks/task-registry.store.ts:65, src/tasks/task-registry.test.ts:40, src/tasks/task-registry.maintenance.ts:116.
Core flows traced
1. Task creation and persistence
- A producer calls
createQueuedTaskRunorcreateRunningTaskRunthroughdetached-task-runtime.tsor directly throughtask-executor.ts. task-executor.tscallscreateTaskRecordwith normalized lifecycle fields.task-registry.ts:createTaskRecordcallsensureTaskRegistryReady, resolvesrequesterSessionKey,ownerKey,scopeKind, andagentId, checks parent-flow rules, deduplicates existing tasks, assigns a UUID, normalizes status and notification policy, updates indexes, persists, and emits observer events.- Persistence writes through
persistTaskUpsertinto the configured store; the default store uses SQLite tablestask_runsandtask_delivery_state.
Evidence: src/tasks/task-executor.ts:94, src/tasks/task-registry.ts:1490, src/tasks/task-registry.ts:1602, src/tasks/task-registry.store.sqlite.ts:390.
2. Run lifecycle updates by run id
- Producers report start, progress, completion, failure, timeout, or cancellation through
markTaskRunningByRunId,recordTaskProgressByRunId, orfinalizeTaskRunByRunId. updateTaskStateByRunIdfinds matching records by run scope, applies valid status transitions, updates summaries and timing, persists, syncs parent flow state, and triggers state-change or terminal delivery.- Additionally,
ensureListenersubscribes to agent lifecycle events and updates scoped tasks when the underlying run starts, ends, or errors.
Evidence: src/tasks/task-registry.ts:1430, src/tasks/task-registry.ts:1631, src/tasks/task-registry.ts:1743, src/tasks/task-registry.ts:1798.
3. Terminal delivery
maybeDeliverTaskTerminalUpdateexits early unless policy and status allow delivery.- A
tasksWithPendingDeliveryset suppresses duplicate concurrent delivery for the same task. - ACP duplicate records can be marked not applicable if another task is preferred for the same run.
- If parent-session review is needed or direct delivery is unavailable, the registry queues a system event and may request a heartbeat wake.
- Otherwise it lazy-loads
sendMessage, sends to the requester origin with an idempotency key, mirrors to the session, and updates delivery status. - On direct-send failure it attempts session-queued fallback and marks delivery failed.
Evidence: src/tasks/task-executor-policy.ts:110, src/tasks/task-registry.ts:1119, src/tasks/task-registry.ts:1125, src/tasks/task-registry.ts:1160, src/tasks/task-registry.ts:1184, src/tasks/task-registry.test.ts:1491, src/tasks/task-registry.test.ts:1689.
4. Cancellation
- CLI, Gateway RPC, or plugin runtime calls
cancelDetachedTaskRunById. - The executor gives a registered detached runtime first refusal, then falls back to
cancelTaskById. cancelTaskByIdrejects missing or terminal tasks, then cancels ACP viagetAcpSessionManager().cancelSession, subagents viakillSubagentRunAdmin, or records CLI cancellation directly.- Successful cancellation writes
status: "cancelled", timing, reason text, and terminal delivery.
Evidence: src/commands/tasks.ts:477, src/gateway/server-methods/tasks.ts:194, src/tasks/task-executor.ts:683, src/tasks/task-registry.ts:1870, src/gateway/server-methods/tasks.test.ts:185.
5. Maintenance and audit
- Gateway startup configures cron-store path and marks cron runtime as authoritative, then starts deferred and interval sweeps.
- Maintenance scans active tasks, reconciles them with runtime-specific backing state, recovers cron terminal outcomes from durable run logs when possible, marks unrecoverable stale records lost, stamps cleanup times, and prunes expired rows.
- Audit is read-oriented and produces findings for stale queued/running, lost, delivery-failed, missing-cleanup, and inconsistent timestamp records.
- CLI maintenance combines task maintenance, task-flow maintenance, and stale cron run session registry cleanup.
Evidence: src/gateway/server-startup-early.ts:120, src/tasks/task-registry.maintenance.ts:65, src/tasks/task-registry.maintenance.ts:767, src/tasks/task-registry.maintenance.ts:953, src/tasks/task-registry.maintenance.ts:1184, src/tasks/task-registry.audit.ts:92, src/commands/tasks.ts:584.
6. Task Flow mirroring and managed flow control
- Eligible single ACP/subagent tasks automatically get a one-task flow via
ensureSingleTaskFlow. - Task updates call
syncFlowFromTask, which projects task status into a mirrored flow status. - Managed flows are created explicitly with a controller id and revision. Updates use expected revision to detect conflicts.
- Flow cancellation records sticky cancel intent, cancels active linked tasks, then finalizes after children settle.
Evidence: src/tasks/task-executor.ts:46, src/tasks/task-executor.ts:64, src/tasks/task-flow-registry.ts:193, src/tasks/task-flow-registry.types.ts:12, src/tasks/task-executor.ts:355, src/tasks/task-flow-registry.audit.test.ts:114.
7. Operator and client reads
- CLI list/show uses
reconcileInspectableTasksandreconcileTaskLookupTokenso reads reflect stale-state projection before display. - Chat
/tasksusesbuildTaskStatusSnapshotto show active and recent current-session records, then aggregate same-agent fallback counts. - Gateway RPC maps internal task states to SDK-facing statuses and sanitizes task text before returning summaries.
Evidence: src/commands/tasks.ts:357, src/commands/tasks.ts:409, src/auto-reply/reply/commands-tasks.ts:84, src/tasks/task-status.ts:162, src/gateway/server-methods/tasks.ts:62, src/gateway/server-methods/tasks.test.ts:139.
Implementation patterns
- Strict discriminated string states:
TaskRuntime,TaskStatus,TaskDeliveryStatus,TaskNotifyPolicy, andTaskFlowStatusare closed unions intask-registry.types.tsandtask-flow-registry.types.ts. - Module singleton with test injection: registries keep module-level maps and expose
configure...Runtimeandreset...ForTestsseams. - Lazy heavy runtime loading: delivery and cancellation control imports are isolated in tiny runtime files.
- Owner-scoped access wrappers:
task-owner-access.tsandtask-flow-owner-access.tsfilter byownerKeybefore exposing plugin/runtime operations. - Run-id scoped updates: producers normally update by
runId, optionally constrained by runtime or session key. - Persistence-first state with in-memory indexes: task records are restored once, then maintained in maps and secondary indexes for run id, owner, flow id, and related session.
- Best-effort observer hooks: registry observer failures are logged or swallowed so observers cannot break writes.
- Explicit sanitization before user/client display:
task-status.tsstrips internal context and truncates text. - Preview/apply maintenance shape: CLI maintenance can explain decisions without mutating state.
- Colocated tests: most task files have nearby
*.test.tsfiles. - JSON output paths for automation: CLI commands support
--json, with lean JSON path tests insrc/commands/tasks-json.test.ts. - Gateway protocol schema first: RPC shapes live in
src/gateway/protocol/schema/tasks.tsand handlers validate params before operating.
Data and state
| State | Shape and owner | Evidence |
|---|---|---|
| Task record | TaskRecord has ids, runtime, owner, scope, session/run/flow links, status, delivery, notify, timestamps, summaries, error, and terminal outcome. | src/tasks/task-registry.types.ts:53 |
| Delivery state | TaskDeliveryState stores requester origin and last notified event time separately from main task rows. | src/tasks/task-registry.types.ts:47, src/tasks/task-registry.store.sqlite.ts:427 |
| Task registry store | SQLite file at $OPENCLAW_STATE_DIR/tasks/runs.sqlite with task_runs and task_delivery_state. | src/tasks/task-registry.paths.ts:24, src/tasks/task-registry.store.sqlite.ts:390 |
| Flow record | TaskFlowRecord has flow id, sync mode, owner, requester origin, revision, status, notify policy, goal, step, blocked state, JSON state/wait blobs, cancellation, and timing. | src/tasks/task-flow-registry.types.ts:24 |
| Flow registry store | SQLite flow_runs table with JSON columns for flow state and wait state. | src/tasks/task-flow-registry.store.sqlite.ts:248 |
| Indexes | In-memory indexes by run id, owner key, parent flow id, and related session key. | src/tasks/task-registry.ts:57 |
| Maintenance timers | Gateway starts a deferred sweep after 5 seconds and an interval sweep every 60 seconds. | src/tasks/task-registry.maintenance.ts:69, src/tasks/task-registry.maintenance.ts:1184 |
Error handling and observability
- Subsystem logging uses
createSubsystemLogger, for exampletasks/registry,tasks/executor, andtasks/task-flow-registry. - Restore failures are logged and surfaced through flow audit for task flows. Evidence:
src/tasks/task-flow-registry.ts:247,src/tasks/task-flow-registry.audit.test.ts:70. - Task delivery failure logs warning metadata, attempts session fallback, and records
deliveryStatus: "failed". Evidence:src/tasks/task-registry.ts:1208. - Observer failures are best-effort and do not block registry writes. Evidence:
src/tasks/task-registry.ts:243,src/tasks/task-flow-registry.ts:146. - Maintenance recovery hooks catch and warn instead of throwing into sweeps. Evidence:
src/tasks/detached-task-runtime.ts:132. - Gateway handlers validate request payloads and return protocol errors instead of throwing for invalid requests. Evidence:
src/gateway/server-methods/tasks.ts:133.
Testing and verification
- Core registry behavior:
src/tasks/task-registry.test.tscovers create/update/delivery/cancel/maintenance interactions. - Persistence:
src/tasks/task-registry.store.test.ts,src/tasks/task-flow-registry.store.test.ts, and path tests cover store behavior. - Audit and maintenance:
src/tasks/task-registry.audit.test.ts,src/tasks/task-registry.maintenance.issue-60299.test.ts, andsrc/tasks/task-flow-registry.audit.test.ts. - Operator CLI:
src/commands/tasks.test.tsandsrc/commands/tasks-json.test.ts. - Gateway RPC:
src/gateway/server-methods/tasks.test.ts. - Chat surface:
src/auto-reply/reply/commands-tasks.test.ts. - Recommended scoped verification for this subsystem is
pnpm test src/tasks,pnpm test src/commands/tasks.test.ts, and targeted gateway/chat tests. Repo guidance says usepnpm test <path-or-filter>, not raw Vitest. - General repo checks include
pnpm check:changed,pnpm check,pnpm build, andpnpm tsgo*lanes frompackage.json.
Security and trust boundaries
- Owner scope: owner wrappers prevent plugins from reading unrelated session-scoped tasks. Evidence:
src/tasks/task-owner-access.ts:13,src/plugins/runtime/runtime-tasks.ts:60. - Sanitization: task text is sanitized before chat and Gateway RPC display. Evidence:
src/tasks/task-status.ts:49,src/gateway/server-methods/tasks.test.ts:139. - Delivery target normalization: persisted requester origins are normalized on read/write. Evidence:
src/tasks/task-registry.store.sqlite.ts:97,src/tasks/task-flow-registry.store.sqlite.ts:79. - Storage permissions: SQLite task and flow stores create directories with
0700and files with0600. Evidence:src/tasks/task-registry.store.sqlite.ts:70,src/tasks/task-registry.store.sqlite.ts:445,src/tasks/task-flow-registry.store.sqlite.ts:52. - Cancellation authority: task cancellation can close ACP sessions and kill subagent runs; call sites must pass runtime config and should avoid exposing this to untrusted users. Evidence:
src/tasks/task-registry.ts:1912,src/tasks/task-registry.ts:1919.
Risks and sharp edges
- Module singleton state: the registry is easy to use but easy to leak between tests without resets.
- Delivery is best-effort and async: many delivery calls are fire-and-forget with
void, so tests need to flush async work and production must rely on audit for failures. - Read paths can reconcile projected state: CLI list/show use inspectable reconciliation, so read behavior can differ from raw store snapshots.
- Runtime-specific lost detection is subtle: cron, ACP, subagent, and CLI use different backing-state proofs. Incorrect producer metadata can cause false lost or retained records.
- Cancellation is not uniform: CLI cancellation marks the record; ACP/subagent cancellation attempts runtime action; registered plugin runtimes can override.
- Task Flow has two modes:
task_mirroredandmanagedshare records but have different ownership semantics. Mixing them incorrectly can cause confusing flow state. - Docs phrasing overstates notification universality: silent and not-applicable policies intentionally suppress task-specific notifications.
- SQLite availability depends on runtime support: the store imports
node:sqlitethroughrequireNodeSqlite; environment/runtime mismatches can affect persistence.
Divergences from documentation
The docs state that “When a task reaches a terminal state, OpenClaw notifies you,” but code gates delivery through shouldAutoDeliverTaskTerminalUpdate. Silent notification policy, subagent non-cancelled terminal behavior, non-terminal status, and non-pending delivery state suppress auto-delivery. Evidence: docs/automation/tasks.md:161, src/tasks/task-executor-policy.ts:110, src/tasks/task-registry.ts:1119. The guide should describe notification as policy-controlled.
Open questions and ambiguities
- Should task retention be configurable, or is the 7-day retention a fixed product contract?
- Should plugin-registered detached runtimes be documented as a public extension contract or kept as internal compatibility scaffolding?
- Should every producer provide
agentIdexplicitly, or is inference from owner/session keys sufficient? - Should flow managed-mode APIs become a first-class public SDK surface with examples beyond current plugin runtime bindings?
- Should terminal delivery failures be retried automatically, or is audit-driven operator intervention intentional?