OpenClaw tasks code analysis

Scope: the src/tasks background task ledger, Task Flow layer, and direct producers and operator surfaces. This is the working audit trail for engineering onboarding.

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

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

PathResponsibility 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

  1. CLI task commands: src/cli/program/register.status-health-sessions.ts:381 registers openclaw tasks, list, audit, maintenance, show, notify, cancel, and flow subcommands. Implementations live in src/commands/tasks.ts and src/commands/flows.ts.
  2. Gateway RPC: src/gateway/server-methods.ts:49 imports task handlers; src/gateway/server-methods/tasks.ts:133 exposes tasks.list, tasks.get, and tasks.cancel. Schemas are in src/gateway/protocol/schema/tasks.ts.
  3. Chat command: src/auto-reply/reply/commands-handlers.runtime.ts:36 includes handleTasksCommand, implemented in src/auto-reply/reply/commands-tasks.ts:126.
  4. Gateway startup: src/gateway/server-startup-early.ts:120 configures task maintenance and src/gateway/server-startup-early.ts:124 starts it.
  5. Producer facade: src/tasks/detached-task-runtime.ts:70 and src/tasks/task-executor.ts:94 are the preferred producer-facing lifecycle entry points.
  6. Subagent producer: src/agents/subagent-registry-run-manager.ts:521 creates a subagent task; src/agents/subagent-registry-lifecycle.ts:260 finalizes it.
  7. CLI/media producers: src/gateway/server-methods/agent.ts:493 creates cli tasks for gateway-backed agent runs; src/agents/tools/media-generate-background-shared.ts:117 creates cli tasks for media generation.
  8. Plugin runtime: src/plugins/runtime/runtime-tasks.ts:178 and src/plugins/runtime/runtime-tasks.ts:196 create owner-bound task and flow runtimes.

Architecture notes

Subsystems and dependency direction

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

  1. A producer calls createQueuedTaskRun or createRunningTaskRun through detached-task-runtime.ts or directly through task-executor.ts.
  2. task-executor.ts calls createTaskRecord with normalized lifecycle fields.
  3. task-registry.ts:createTaskRecord calls ensureTaskRegistryReady, resolves requesterSessionKey, ownerKey, scopeKind, and agentId, checks parent-flow rules, deduplicates existing tasks, assigns a UUID, normalizes status and notification policy, updates indexes, persists, and emits observer events.
  4. Persistence writes through persistTaskUpsert into the configured store; the default store uses SQLite tables task_runs and task_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

  1. Producers report start, progress, completion, failure, timeout, or cancellation through markTaskRunningByRunId, recordTaskProgressByRunId, or finalizeTaskRunByRunId.
  2. updateTaskStateByRunId finds 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.
  3. Additionally, ensureListener subscribes 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

  1. maybeDeliverTaskTerminalUpdate exits early unless policy and status allow delivery.
  2. A tasksWithPendingDelivery set suppresses duplicate concurrent delivery for the same task.
  3. ACP duplicate records can be marked not applicable if another task is preferred for the same run.
  4. If parent-session review is needed or direct delivery is unavailable, the registry queues a system event and may request a heartbeat wake.
  5. Otherwise it lazy-loads sendMessage, sends to the requester origin with an idempotency key, mirrors to the session, and updates delivery status.
  6. 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

  1. CLI, Gateway RPC, or plugin runtime calls cancelDetachedTaskRunById.
  2. The executor gives a registered detached runtime first refusal, then falls back to cancelTaskById.
  3. cancelTaskById rejects missing or terminal tasks, then cancels ACP via getAcpSessionManager().cancelSession, subagents via killSubagentRunAdmin, or records CLI cancellation directly.
  4. 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

  1. Gateway startup configures cron-store path and marks cron runtime as authoritative, then starts deferred and interval sweeps.
  2. 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.
  3. Audit is read-oriented and produces findings for stale queued/running, lost, delivery-failed, missing-cleanup, and inconsistent timestamp records.
  4. 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

  1. Eligible single ACP/subagent tasks automatically get a one-task flow via ensureSingleTaskFlow.
  2. Task updates call syncFlowFromTask, which projects task status into a mirrored flow status.
  3. Managed flows are created explicitly with a controller id and revision. Updates use expected revision to detect conflicts.
  4. 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

  1. CLI list/show uses reconcileInspectableTasks and reconcileTaskLookupToken so reads reflect stale-state projection before display.
  2. Chat /tasks uses buildTaskStatusSnapshot to show active and recent current-session records, then aggregate same-agent fallback counts.
  3. 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

Data and state

StateShape and ownerEvidence
Task recordTaskRecord 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 stateTaskDeliveryState 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 storeSQLite 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 recordTaskFlowRecord 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 storeSQLite flow_runs table with JSON columns for flow state and wait state.src/tasks/task-flow-registry.store.sqlite.ts:248
IndexesIn-memory indexes by run id, owner key, parent flow id, and related session key.src/tasks/task-registry.ts:57
Maintenance timersGateway 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

Testing and verification

Security and trust boundaries

Risks and sharp edges

  1. Module singleton state: the registry is easy to use but easy to leak between tests without resets.
  2. 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.
  3. Read paths can reconcile projected state: CLI list/show use inspectable reconciliation, so read behavior can differ from raw store snapshots.
  4. 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.
  5. Cancellation is not uniform: CLI cancellation marks the record; ACP/subagent cancellation attempts runtime action; registered plugin runtimes can override.
  6. Task Flow has two modes: task_mirrored and managed share records but have different ownership semantics. Mixing them incorrectly can cause confusing flow state.
  7. Docs phrasing overstates notification universality: silent and not-applicable policies intentionally suppress task-specific notifications.
  8. SQLite availability depends on runtime support: the store imports node:sqlite through requireNodeSqlite; 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