Task Runner Skill
A persistent, daemon-style task queue. Users add tasks at any time. A dispatcher runs on every
heartbeat to check the queue and execute pending work via subagents. Tasks accumulate, complete,
and are archived — the queue itself never closes.
Two Operating Modes
This skill has two distinct modes with different triggers and behaviors:
| Mode | Trigger | Purpose |
|---|
| INTAKE | User message containing task intent | Parse message → add tasks to queue → confirm → immediately run DISPATCHER |
| DISPATCHER |
After INTAKE (primary) · Heartbeat/cron (backup) | Read queue → dispatch pending tasks → report completions |
Both modes read and write the same persistent queue file.
A1 — Triggers
Mode 1: INTAKE (user message)
Activate INTAKE mode when the user's message matches any of the following patterns:
| Pattern | Examples |
|---|
| Explicit task add | "add task", "add these tasks", "task:", "new task" |
| Delegation |
"do this for me", "do these for me", "handle these", "can you do X" |
| Framing | "I need you to", "help me with", "I need", "I want you to" |
| List framing | "task list", "my tasks", "queue these", "work on these" |
| Control commands | "skip T-03", "retry T-02", "mark T-01 done", "cancel T-04" |
| Status check | "show tasks", "task status", "what's in the queue", "what are my pending tasks" |
| Compound ask | Any message with 2+ distinct action items (bullets, numbers, "and also", "then") |
Do NOT activate INTAKE for:
- - Pure single-question lookups answered in one sentence ("what time is it?")
- Scheduling-only requests with no actual task ("remind me in 20 min")
- Single web search requests ("google X")
- The heartbeat systemEvent (that's DISPATCHER mode)
Mode 2: DISPATCHER (inline after INTAKE, heartbeat, or cron)
Activate DISPATCHER mode when triggered by:
- - Immediately after INTAKE — runs in the same turn, right after tasks are queued (primary path)
- INLINECODE0 check during a heartbeat poll (backup: catches retries and completions)
- systemEvent:
"TASK_RUNNER_DISPATCH: check queue and run pending tasks" (backup) - Any scheduled/cron trigger registered for task-runner (backup)
Configuration
| Variable | Location | Default | Description |
|---|
| INLINECODE2 | TOOLS.md | INLINECODE3 | Directory for queue file and deliverables |
| INLINECODE4 |
TOOLS.md |
2 | Max tasks running simultaneously |
|
TASK_RUNNER_MAX_RETRIES | TOOLS.md or env |
3 | Max retry attempts before marking blocked |
|
TASK_RUNNER_ARCHIVE_DAYS | TOOLS.md |
7 | Days after which done/blocked tasks are archived |
How to configure — add to TOOLS.md:
CODEBLOCK0
Queue file path: ${TASK_RUNNER_DIR}/task-queue.json
(single persistent file, NOT dated — accumulates all tasks over time)
A3 — Outputs
| Output | Path / Channel | Description |
|---|
| Queue file | INLINECODE12 | Single persistent queue; all tasks |
| Per-task completion message |
Chat notification | Sent immediately when a task finishes (done or blocked) |
| Deliverable files | Task-specific paths | Files produced by tasks (when applicable) |
| INTAKE confirmation | Chat | Sent after adding tasks to queue |
Mode 1: INTAKE — Step-by-Step
Goal: Convert user message into structured task objects, append to queue, confirm.
Step 0 — First Run Setup (auto-configure on first use)
Run this check before anything else, every INTAKE invocation:
CODEBLOCK1
Idempotency rule: Step 0 only fires on true first run (queue file absent).
It will never double-register the heartbeat entry or create duplicate cron jobs.
Step 1 — Load queue
CODEBLOCK2
Step 2 — Parse tasks from message
Split user message into individual tasks using these cues:
- - Numbered lists (1., 2., 3.)
- Bulleted lists (-, *, •)
- Explicit separators ("first", "also", "and then", "next")
- Compound sentences with multiple imperatives
- Single task: entire message is one task
Step 3 — Assign IDs
Continue from lastId in the queue file:
- - If
lastId = "T-05", next task is INLINECODE15 - If
lastId = null, start at INLINECODE17 - Format:
T-NN (zero-padded, minimum 2 digits; expand to 3 when N > 99)
Step 4 — Build task objects
For each parsed task, create a JSON object (schema in references/queue-schema.md):
- - Set
id, description, goal, status = "pending", INLINECODE24 - Set
retries = 0, maxRetries from config - Leave execution fields null
Step 5 — Append to queue and save
CODEBLOCK3
Step 6 — Confirm to user
CODEBLOCK4
For multiple tasks:
CODEBLOCK5
Then immediately run DISPATCHER mode (Steps 1–5 below) in the same turn.
Do not exit and wait for the next heartbeat. Tasks must start executing immediately.
The heartbeat/cron dispatcher is a backup for retries and completion checks — not the primary execution path.
Step 7 — Handle control commands
| Command | Action |
|---|
| INLINECODE27 | Set status = "skipped"; save; confirm |
| INLINECODE28 |
Reset status = "pending", retries = 0; save; confirm |
|
cancel T-NN | Set status = "skipped", blocked_reason = "cancelled by user"; save; confirm |
|
mark T-NN done | Set status = "done", completed_at = now; save; confirm |
|
show tasks /
task status | Read queue; render status table (see A5 templates) |
Mode 2: DISPATCHER — Step-by-Step
Goal: Check queue, dispatch pending tasks, track running tasks, report completions.
Step 1 — Load queue
CODEBLOCK6
Step 2 — Check for work
CODEBLOCK7
Step 3 — Check running tasks for completion
For each task with status = "running":
CODEBLOCK8
Step 4 — Dispatch pending tasks
CODEBLOCK9
Subagent instructions template:
CODEBLOCK10
Step 5 — Save and exit
CODEBLOCK11
If any notifications were sent (done/blocked), this is an active heartbeat response.
If only silent dispatching occurred, this is still a heartbeat response (not HEARTBEAT_OK).
Only return HEARTBEAT_OK when there was truly nothing to do (no pending, no running tasks).
A5 — Output Format Templates
INTAKE confirmation (single task)
CODEBLOCK12
INTAKE confirmation (multiple tasks)
CODEBLOCK13
Task status table (on demand)
CODEBLOCK14
Task done notification
CODEBLOCK15
Task blocked notification
CODEBLOCK16
Task skipped
CODEBLOCK17
A6 — Heartbeat Integration
Heartbeat and cron setup is automatic. Step 0 of INTAKE mode handles this on first use —
no manual configuration required.
Role of heartbeat/cron (backup only)
Tasks are dispatched immediately after INTAKE — heartbeat and cron are backups only.
The backup dispatcher handles:
- - Retry dispatch: tasks that failed and were reset to pending
- Completion checks: polling running subagent sessions for done/blocked status
- Recovery: tasks that were pending when no user message triggered INTAKE
Users should never need to wait for a heartbeat for a freshly added task.
What gets configured automatically
HEARTBEAT.md entry (injected on first INTAKE):
CODEBLOCK18
Backup cron job (registered on first INTAKE):
CODEBLOCK19
Manual setup (if needed)
If for any reason auto-setup did not run (e.g., queue file was pre-created externally),
delete ${TASK_RUNNER_DIR}/task-queue.json and send any task — Step 0 will fire.
A7 — Success Criteria
INTAKE mode succeeds when:
- 1. All tasks from user message parsed and assigned IDs
- Tasks appended to queue file (file saved to disk)
- Confirmation sent to user with task IDs and count
- DISPATCHER mode triggered immediately in the same turn
- Subagents spawned for pending tasks before INTAKE turn ends
DISPATCHER mode succeeds when:
- 1. Queue file read without error
- All running tasks checked for completion (done/blocked notifications sent as needed)
- Pending tasks dispatched up to
maxConcurrent slots - Queue file saved with updated states
- User notified for every task that reached a terminal state this cycle
Ongoing system health:
- - Queue file is never corrupted (always valid JSON)
- Tasks older than
archiveDays days with terminal status are archived/removed - INLINECODE37 always increments (no ID reuse)
- INLINECODE38 respected before any task is marked blocked
Edge Cases
| Situation | Behavior |
|---|
| Queue file missing (first run) | Run Step 0 auto-setup: create dir, init queue, register heartbeat + cron; notify user |
| Queue file missing (manually deleted) |
Step 0 re-fires: re-initializes queue; does NOT re-register heartbeat/cron (idempotent check) |
| Queue file corrupt/invalid JSON | Log error, notify user, do not overwrite; ask user to inspect |
| Task description is ambiguous | Assign
unknown type; dispatcher will attempt classification + fallback |
|
maxConcurrent already reached | Dispatcher skips dispatching; checks again next heartbeat |
| User adds task while dispatcher is running | Race-safe: dispatcher reads, processes, writes atomically per cycle |
| Task depends on another task's output | Set
blocked_reason = "depends on T-NN-1 which is pending/blocked" |
| User says "retry T-NN" | Reset to pending, retries = 0, strategies_tried = [] |
| All tasks blocked | Notify user: "All tasks are blocked. Review unblock instructions above." |
| 20+ tasks added at once | Dispatcher dispatches in batches of
maxConcurrent; all tasks eventually run |
| Subagent session ID lost | Mark task as pending again; will re-dispatch next cycle |
| Archive: done tasks > archiveDays old | Move to
${TASK_RUNNER_DIR}/archive/YYYY-MM.json; remove from main queue |
A8 — File Organization
CODEBLOCK20
Queue file schema is documented in references/queue-schema.md.
References
- -
references/queue-schema.md — Queue JSON format (complete field reference) - INLINECODE46 — Task type catalog and strategy selection
- INLINECODE47 — Verification logic per task type
- INLINECODE48 — Trigger test cases (positive and negative)
任务运行器技能
一个持久的、守护进程风格的任务队列。用户可以随时添加任务。调度器在每个心跳周期运行,检查队列并通过子代理执行待处理的工作。任务会累积、完成并被归档——队列本身永不关闭。
两种运行模式
该技能具有两种不同的模式,具有不同的触发条件和行为:
| 模式 | 触发条件 | 目的 |
|---|
| 接收模式 | 包含任务意图的用户消息 | 解析消息 → 将任务添加到队列 → 确认 → 立即运行调度器 |
| 调度器模式 |
接收模式之后(主要)· 心跳/定时任务(备用) | 读取队列 → 分发待处理任务 → 报告完成情况 |
两种模式都读取和写入同一个持久化队列文件。
A1 — 触发条件
模式 1:接收模式(用户消息)
当用户消息匹配以下任一模式时,激活接收模式:
| 模式 | 示例 |
|---|
| 明确添加任务 | 添加任务、添加这些任务、任务:、新任务 |
| 委托 |
帮我做这个、帮我做这些、处理这些、你能做X吗 |
| 框架表述 | 我需要你、帮我、我需要、我希望你 |
| 列表框架 | 任务列表、我的任务、排队这些、处理这些 |
| 控制命令 | 跳过T-03、重试T-02、标记T-01完成、取消T-04 |
| 状态查询 | 显示任务、任务状态、队列里有什么、我有哪些待处理任务 |
| 复合请求 | 包含2个以上不同行动项的任何消息(项目符号、数字、还有、然后) |
以下情况不要激活接收模式:
- - 纯单问题查询,一句话就能回答(现在几点了?)
- 仅涉及日程安排、没有实际任务的请求(20分钟后提醒我)
- 单个网络搜索请求(搜索X)
- 心跳系统事件(那是调度器模式)
模式 2:调度器模式(接收模式后内联执行、心跳或定时任务)
在以下情况触发时激活调度器模式:
- - 接收模式之后立即执行 — 在同一轮次中运行,就在任务入队之后(主要路径)
- 心跳轮询期间检查HEARTBEAT.md(备用:捕获重试和完成情况)
- 系统事件:TASKRUNNERDISPATCH: 检查队列并运行待处理任务(备用)
- 为任务运行器注册的任何计划/定时任务触发(备用)
配置
| 变量 | 位置 | 默认值 | 描述 |
|---|
| TASKRUNNERDIR | TOOLS.md | ~/.openclaw/tasks/ | 队列文件和交付物的目录 |
| TASKRUNNERMAX_CONCURRENT |
TOOLS.md | 2 | 同时运行的最大任务数 |
| TASK
RUNNERMAX_RETRIES | TOOLS.md 或环境变量 | 3 | 标记为阻塞前的最大重试次数 |
| TASK
RUNNERARCHIVE_DAYS | TOOLS.md | 7 | 已完成/已阻塞任务归档前的天数 |
如何配置 — 添加到 TOOLS.md:
任务运行器
TASK
RUNNERDIR=~/.openclaw/tasks/
TASK
RUNNERMAX_CONCURRENT=2
TASK
RUNNERMAX_RETRIES=3
TASK
RUNNERARCHIVE_DAYS=7
队列文件路径: ${TASKRUNNERDIR}/task-queue.json
(单个持久化文件,不按日期命名——随时间累积所有任务)
A3 — 输出
| 输出 | 路径/通道 | 描述 |
|---|
| 队列文件 | ${TASKRUNNERDIR}/task-queue.json | 单个持久化队列;所有任务 |
| 每个任务的完成消息 |
聊天通知 | 任务完成时立即发送(完成或阻塞) |
| 交付物文件 | 任务特定路径 | 任务产生的文件(如适用) |
| 接收模式确认 | 聊天 | 将任务添加到队列后发送 |
模式 1:接收模式 — 逐步说明
目标: 将用户消息转换为结构化的任务对象,追加到队列,确认。
步骤 0 — 首次运行设置(首次使用时自动配置)
每次调用接收模式时,在执行其他任何操作之前运行此检查:
检查 ${TASKRUNNERDIR}/task-queue.json 是否存在
如果文件不存在:
→ 这是首次运行。在继续之前静默自动配置所有内容。
[1] 创建目录:
执行: mkdir -p ${TASKRUNNERDIR}
[2] 初始化队列文件:
写入 ${TASKRUNNERDIR}/task-queue.json 使用默认结构:
{ lastId: null, tasks: [], archivedCount: 0 }
[3] 注册心跳条目:
读取 HEARTBEAT.md(如果缺失则创建)
如果 任务运行器调度器 尚未在文件中:
追加以下块(前面加一个空行):
## 任务运行器调度器
每次心跳:检查 ${TASKRUNNERDIR}/task-queue.json
- 如果存在待处理或运行中的任务 → 运行调度器模式(任务运行器技能)
- 如果没有待处理任务 → HEARTBEAT_OK(跳过)
写入更新后的 HEARTBEAT.md
[4] 注册备用定时任务:
调用 cron 工具,参数:
action: add
job:
name: 任务运行器调度器
schedule: { kind: every, everyMs: 900000 }
payload: { kind: systemEvent, text: TASKRUNNERDISPATCH: 检查队列并运行待处理任务 }
sessionTarget: main
enabled: true
[5] 通知用户:
⚙️ 任务运行器已初始化。
心跳调度器已在 HEARTBEAT.md 中注册。
备用定时任务已注册(每15分钟运行一次)。
您的任务将自动执行。
→ 然后继续执行下面的正常接收模式步骤。
如果文件已存在:
→ 完全跳过步骤 0。直接进入步骤 1。
幂等性规则: 步骤 0 仅在真正的首次运行时触发(队列文件不存在)。
它永远不会重复注册心跳条目或创建重复的定时任务。
步骤 1 — 加载队列
读取 ${TASKRUNNERDIR}/task-queue.json
如果文件不存在:
使用默认结构初始化(参见 references/queue-schema.md)
设置 lastId = null
步骤 2 — 从消息中解析任务
使用以下线索将用户消息拆分为单个任务:
- - 编号列表(1.、2.、3.)
- 项目符号列表(-、*、•)
- 明确分隔符(首先、还有、然后、接下来)
- 包含多个祈使句的复合句
- 单个任务:整条消息就是一个任务
步骤 3 — 分配 ID
从队列文件中的 lastId 继续:
- - 如果 lastId = T-05,下一个任务是 T-06
- 如果 lastId = null,从 T-01 开始
- 格式:T-NN(零填充,最少2位;当 N > 99 时扩展为3位)
步骤 4 — 构建任务对象
为每个解析出的任务创建一个 JSON 对象(模式见 references/queue-schema.md):
- - 设置 id、description、goal、status = pending、added_at
- 设置 retries = 0、maxRetries 来自配置
- 执行字段留空
步骤 5 — 追加到队列并保存
将新的任务对象追加到 queue.tasks[]
更新 queue.lastId 为最后分配的 ID
将更新后的队列文件写入磁盘
步骤 6 — 向用户确认
已添加 T-06:[描述]。现在开始...
对于多个任务:
📋 已将 3 个任务添加到队列:
• T-06:[描述]
• T-07:[描述]
• T-08:[描述]
现在启动调度器...
然后立即在同一轮次中运行调度器模式(下面的步骤 1-5)。
不要退出并等待下一个心跳。任务必须立即开始执行。
心跳/定时任务调度器是重试和完成检查的备用方案——不是主要的执行路径。
步骤 7 — 处理控制命令
| 命令 | 操作 |
|---------