| name | taskflow |
| description | Structured project/task management for OpenClaw agents — markdown-first authoring, SQLite-backed querying, bidirectional sync, CLI, Apple Notes integration. |
| metadata | {"openclaw":{"emoji":"📋","os":["darwin","linux"],"requires":{"bins":"[Truncated]","env":"[Truncated]"}}} |
TaskFlow — Agent Skill Reference
TaskFlow gives any OpenClaw agent a structured project/task/plan system with markdown-first authoring, SQLite-backed querying, and bidirectional sync.
Principle: Markdown is canonical. Edit tasks/*.md directly. The SQLite DB is a derived index, not the source of truth.
Security
OPENCLAW_WORKSPACE Trust Boundary
OPENCLAW_WORKSPACE is a high-trust value. All TaskFlow scripts resolve file paths from it, and the CLI and sync daemon use it to locate the SQLite database, markdown task files, and log directory.
Rules for safe use:
-
Set it only from trusted, controlled sources. The value must come from:
- Your own shell profile (
.zshrc, .bashrc, /etc/environment)
- The systemd user unit
Environment= directive in a template you control
- The macOS LaunchAgent
EnvironmentVariables dictionary you installed
Never accept OPENCLAW_WORKSPACE from:
- User-supplied CLI arguments or HTTP request parameters
- Untrusted config files read at runtime
- Any external input that has not been explicitly validated
-
Validate the path exists before use. Any script that reads OPENCLAW_WORKSPACE should confirm the directory exists before proceeding:
import { existsSync } from 'node:fs'
import path from 'node:path'
const workspace = process.env.OPENCLAW_WORKSPACE
if (!workspace) {
console.error('OPENCLAW_WORKSPACE is not set. Aborting.')
process.exit(1)
}
if (!existsSync(workspace)) {
console.error(`OPENCLAW_WORKSPACE path does not exist: ${workspace}`)
process.exit(1)
}
const safeWorkspace = path.resolve(workspace)
-
Do not construct paths from untrusted input. Even with a valid OPENCLAW_WORKSPACE, never concatenate unvalidated user input onto it (e.g. path.join(workspace, userSlug, '../../../etc/passwd')). Use path.resolve() and check that the resolved path starts with the workspace root:
function safeJoin(base, ...parts) {
const resolved = path.resolve(base, ...parts)
if (!resolved.startsWith(path.resolve(base) + path.sep)) {
throw new Error(`Path traversal attempt detected: ${resolved}`)
}
return resolved
}
-
Treat OPENCLAW_WORKSPACE as a local system path only. It must point to a directory on the local filesystem. Remote paths (NFS mounts, network shares) may work but are outside the tested configuration and could introduce TOCTOU (time-of-check/time-of-use) race conditions.
Setup
1. Set environment variable
Add to your shell profile (.zshrc, .bashrc, etc.):
export OPENCLAW_WORKSPACE="/path/to/your/.openclaw/workspace"
All TaskFlow scripts and the CLI resolve paths from this variable. Without it, they fall back to process.cwd(), which is almost never what you want.
See also: OPENCLAW_WORKSPACE Trust Boundary above for security requirements.
2. Link the CLI
ln -sf {baseDir}/scripts/taskflow-cli.mjs /opt/homebrew/bin/taskflow
3. Run the setup wizard
taskflow setup
The wizard handles the rest: creates workspace directories, walks you through adding your first project(s), initializes the database, syncs, and optionally installs the macOS LaunchAgent for periodic sync.
Alternative — manual setup:
Manual steps (if you prefer explicit control)
mkdir -p "$OPENCLAW_WORKSPACE/tasks" "$OPENCLAW_WORKSPACE/plans" "$OPENCLAW_WORKSPACE/memory" "$OPENCLAW_WORKSPACE/logs"
taskflow init
taskflow sync files-to-db
taskflow status
First Run
For agents (OpenClaw / AI)
When a user asks you to set up TaskFlow or you detect it has not been initialized:
- Detect state. Check for
$OPENCLAW_WORKSPACE/PROJECTS.md and $OPENCLAW_WORKSPACE/memory/taskflow.sqlite.
- If clean slate: Ask the user for their first project name and description, then run:
taskflow setup --name "Project Name" --desc "One-liner description"
Follow up by running taskflow status to confirm.
- If PROJECTS.md exists but no DB: Run
taskflow setup (it detects the state automatically and offers to init + sync).
- If both exist: Run
taskflow status — already set up.
- After setup, update
AGENTS.md with the new project slug so future sessions discover it via cat PROJECTS.md.
For humans (CLI)
taskflow setup
The interactive wizard will:
- Detect your existing workspace state
- Walk you through naming your first project(s)
- Create
PROJECTS.md and tasks/<slug>-tasks.md from templates
- Initialize the SQLite database and sync
- Offer to install the periodic-sync daemon (LaunchAgent on macOS, systemd timer on Linux) for automatic 60s sync
Non-interactive (scripted installs):
taskflow setup --name "My Project" --desc "What it does"
Passing --name skips all interactive prompts (daemon install is also skipped in non-interactive mode).
Directory Layout
<workspace>/
├── PROJECTS.md # Project registry (one ## block per project)
├── tasks/<slug>-tasks.md # Task list per project
├── plans/<slug>-plan.md # Optional: architecture/design doc per project
└── taskflow/
├── SKILL.md # This file
├── scripts/
│ ├── taskflow-cli.mjs # CLI entry point (symlink target)
│ ├── task-sync.mjs # Bidirectional markdown ↔ SQLite sync
│ ├── init-db.mjs # Bootstrap SQLite schema (idempotent)
│ ├── export-projects-overview.mjs # JSON export of project/task state
│ └── apple-notes-export.mjs # Optional: project state → Apple Notes (macOS only)
├── templates/ # Starter files for new projects
├── schema/
│ └── taskflow.sql # Full DDL
└── system/
├── com.taskflow.sync.plist.xml # Periodic sync (macOS LaunchAgent)
├── taskflow-sync.service # Periodic sync (Linux systemd user unit)
└── taskflow-sync.timer # Systemd timer (60s interval)
<workspace>/
└── taskflow.config.json # Apple Notes config (auto-created on first notes run)
Creating a Project
Follow this full checklist when creating a new project:
1. Add a block to PROJECTS.md
## <slug>
- Name: <Human-Readable Name>
- Status: active
- Description: One-sentence description of the project.
slug is lowercase, hyphenated (e.g., my-project). It becomes the canonical project ID everywhere.
- Valid status values:
active, paused, done.
2. Create the task file
Copy taskflow/templates/tasks-template.md → tasks/<slug>-tasks.md and update the project name in the heading.
The file must contain these five section headers in this order:
# <Project Name> — Tasks
## In Progress
## Pending Validation
## Backlog
## Blocked
## Done
3. Optionally create a plan file
Copy taskflow/templates/plan-template.md → plans/<slug>-plan.md for architecture docs, design decisions, and phased roadmaps. Plan files are not synced to SQLite — they are reference-only for the agent.
4. DB row (auto-created on first sync)
You do not need to manually insert into the projects table. The sync engine auto-creates the project row from PROJECTS.md on the next files-to-db run. If you want to be explicit via Node.js, use a parameterized statement:
db.prepare(`INSERT INTO projects (id, name, description, status)
VALUES (:id, :name, :description, 'active')`)
.run({ id: slug, name: projectName, description: projectDesc })
Task Line Format
Every task line follows this exact format:
- [x| ] (task:<id>) [<priority>] [<owner>] <title>
| Field | Details |
|---|
[ ] / [x] | Open / completed. Sync drives status from section header, not this checkbox. |
(task:<id>) | Task ID. Format: <slug>-NNN (zero-padded 3-digit). Sequential per project. |
[<priority>] | Required. Must come before owner tag. See priority table below. |
[<owner>] | Optional. Agent/model tag (e.g., codex, sonnet, claude). |
<title> | Human-readable task title. |
⚠️ Tag Order Rule
Priority tag MUST come before owner tag. The sync parser is positional — it reads the first [Px] bracket as priority, and the next [tag] as owner. Swapping them will misparse the task.
⚠️ Title Sanitization Rules
Task titles must be plain text only. Before writing any user-supplied string as a task title, apply the following rules:
-
Reject lines that look like section headers. A title may not start with one or more # characters followed by a space (e.g. # My heading, ## Done). These would corrupt the sync parser's section detection.
-
Reject the exact section header strings even without leading whitespace:
In Progress, Pending Validation, Backlog, Blocked, Done
- Comparison must be case-insensitive.
-
Escape or strip markdown special characters that have structural meaning in the task file:
| Character | Risk | Safe action |
|---|
# | Looks like a header | Strip or reject |
- (dash + space at line start) | Looks like a list item / task | Strip leading - |
[ ] / [x] | Looks like a checkbox | Escape brackets: \[ \] |
] / [ alone | Can corrupt (task:id) parse | Escape: \[ \] |
Newlines (\n, \r) | Creates multi-line titles | Strip / reject |
-
Maximum length. Titles should be ≤ 200 characters. Truncate or reject longer strings.
Example sanitization (Node.js):
function sanitizeTitle(raw) {
if (typeof raw !== 'string') throw new TypeError('title must be a string')
let title = raw.replace(/[\r\n]+/g, ' ').trim()
if (/^#{1,6}\s/.test(title)) {
throw new Error('Title may not start with a markdown heading (#)')
}
const BANNED_HEADERS = /^(in progress|pending validation|backlog|blocked|done)$/i
if (BANNED_HEADERS.test(title)) {
throw new Error('Title may not be a reserved section header name')
}
title = title
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]')
if (title.length > 200) {
throw ()
}
title
}
These rules apply whenever a task title comes from any external or user-supplied source (CLI args, API payloads, file imports). Titles hard-coded by agents in their own sessions are low-risk but should still avoid structural characters.
✅ Correct: - [ ] (task:myproject-007) [P1] [codex] Implement search
❌ Wrong: - [ ] (task:myproject-007) [codex] [P1] Implement search
Priority Levels (Configurable)
| Tag | Default Meaning |
|---|
P0 | Critical — must do now, blocks everything |
P1 | High — important, do soon |
P2 | Normal — standard priority (default) |
P3 | Low — nice to have |
P9 | Someday — no urgency, parking lot |
Priorities are configurable per-installation but the tags themselves (P0–P3, P9) are what the sync engine validates.
Optional Note Lines
A note can follow a task line as an indented - note: line:
- [ ] (task:myproject-003) [P1] [codex] Implement auth flow
- note: blocked on API key from vendor
Known limitation (v1): Notes are one-way. Removing or editing a note in markdown does not propagate to the DB. This is tracked for a post-MVP fix.
Example Task File Section
## In Progress
- [ ] (task:myproject-001) [P1] [codex] Wire up OAuth login
- note: PR open, needs review
## Backlog
- [ ] (task:myproject-002) [P2] Add rate limiting middleware
- [ ] (task:myproject-003) [P3] Write integration tests
Adding a New Task
-
Determine the next ID. Scan the task file for the highest existing <slug>-NNN and increment by 1. Or query SQLite using a parameterized statement (never interpolate the slug into SQL strings):
const db = new DatabaseSync(dbPath)
const row = db
.prepare(`SELECT MAX(CAST(SUBSTR(id, LENGTH(:slug) + 2) AS INTEGER)) AS max_seq
FROM tasks_v2
WHERE project_id = :slug`)
.get({ slug: projectSlug })
const nextSeq = (row.max_seq ?? 0) + 1
const nextId = `${projectSlug}-${String(nextSeq).padStart(3, '0')}`
⚠️ Never construct SQL by string interpolation. Use db.prepare() with named or positional parameters (? or :name) for all values that come from external input. This applies even for read-only queries.
-
Append the task line to the correct section (## Backlog for new work, ## In Progress if starting immediately).
-
Format the line using the exact format above. No trailing spaces. Priority tag before owner tag.
Updating Task Status
Move the task line from its current section to the target section in the markdown file.
| Target State | Move to Section |
|---|
| Started / picked up | ## In Progress |
| Needs human review | ## Pending Validation |
| Not started yet | ## Backlog |
| Waiting on dependency | ## Blocked |
| Finished | ## Done |
Also flip the checkbox: [ ] for active states, [x] for Done (and optionally Pending Validation).
The periodic sync (60s) will pick up the change and update SQLite automatically. To force an immediate sync:
node taskflow/scripts/task-sync.mjs files-to-db
Querying Tasks
Simple: Read the markdown file directly
cat tasks/<slug>-tasks.md
For a quick in-session view, just read the relevant section.
Advanced: Query SQLite
⚠️ SQL Safety Rule: Any query that incorporates a variable value (project slug, task ID, status string, etc.) must use parameterized statements — not string interpolation. The sqlite3 CLI examples below use only static, hardcoded literal values and are shown as diagnostic/inspection tools only. For programmatic use, always use the Node.js db.prepare() API with bound parameters.
sqlite3 CLI (static queries — for manual inspection only)
sqlite3 "$OPENCLAW_WORKSPACE/memory/taskflow.sqlite" \
"SELECT id, project_id, priority, title
FROM tasks_v2
WHERE status = 'in_progress'
ORDER BY priority, project_id;"
sqlite3 "$OPENCLAW_WORKSPACE/memory/taskflow.sqlite" \
"SELECT project_id, status, COUNT(*) AS count
FROM tasks_v2
GROUP BY project_id, status
ORDER BY project_id, status;"
Do not embed shell variables directly in the SQL string (e.g. WHERE project_id = '$SLUG'). That pattern is SQL injection waiting to happen. Use the Node.js API with parameters instead.
Node.js API — parameterized queries (required for programmatic use)
import { DatabaseSync } from 'node:sqlite'
import path from 'node:path'
const dbPath = path.join(process.env.OPENCLAW_WORKSPACE, 'memory', 'taskflow.sqlite')
const db = new DatabaseSync(dbPath)
db.exec('PRAGMA foreign_keys = ON')
const backlog = db
.prepare(`SELECT id, priority, title
FROM tasks_v2
WHERE project_id = :slug AND status = 'backlog'
ORDER BY priority`)
.all({ slug: 'my-project' })
const transitions = db
.prepare(`SELECT from_status, to_status, actor, at
FROM task_transitions_v2
WHERE task_id = ?
ORDER BY at`)
.all('my-project-007')
db.prepare(`UPDATE tasks_v2 SET status = :status, updated_at = datetime('now')
WHERE id = :id`)
.run({ : , : })
CLI Quick Reference
taskflow status
taskflow add taskflow "Implement quick add command" --priority P1 --owner codex
taskflow list taskflow
taskflow list --project "TaskFlow" --all
taskflow list task --status backlog,pending_validation --json
node taskflow/scripts/export-projects-overview.mjs
node taskflow/scripts/task-sync.mjs check
node taskflow/scripts/task-sync.mjs files-to-db
node taskflow/scripts/task-sync.mjs db-to-files
Apple Notes Export (Optional — macOS Only)
TaskFlow can maintain a live Apple Note with your current project status. The note is rendered as rich HTML and written via AppleScript.
taskflow notes
On first run (or during taskflow setup), a new note is created in the configured folder and its Core Data ID is saved to:
$OPENCLAW_WORKSPACE/taskflow.config.json
Config schema:
{
"appleNotesId": "x-coredata://...",
"appleNotesFolder": "Notes",
"appleNotesTitle": "TaskFlow - Project Status"
}
Important — never delete the shared note. The note is always edited in-place. Deleting and recreating it generates a new Core Data ID and breaks any existing share links. If the note is accidentally deleted, taskflow notes will create a new one and update the config automatically.
For hourly auto-refresh, add a cron entry:
0 * * * * OPENCLAW_WORKSPACE=/path/to/workspace /path/to/node /path/to/taskflow/scripts/apple-notes-export.mjs
Or install a dedicated LaunchAgent (macOS) targeting apple-notes-export.mjs with an hourly StartInterval of 3600.
This feature is entirely optional and macOS-specific. On other platforms, taskflow notes exits gracefully with a message.
Memory Integration Rules
These rules keep daily memory logs clean and prevent duplication.
✅ Do
❌ Do Not
- Never duplicate the backlog in daily memory files.
tasks/<slug>-tasks.md is the single source of truth for all pending work. Memory files should not list what's left to do.
- Do not track task state changes in memory (e.g., "Task 007 is now in progress"). Only note meaningful progress events or decisions.
- Do not create new tasks in memory files. Add them to the task file directly.
Pattern: Loading Project Context
At the start of a session involving a project:
cat PROJECTS.md — identify the project slug and status
cat tasks/<slug>-tasks.md — load current task state
cat plans/<slug>-plan.md — load architecture context (if it exists)
- Begin work. Record task ID references in memory at session end.
Periodic Sync Daemon
The sync daemon runs task-sync.mjs files-to-db every 60 seconds in the background. This means markdown edits are automatically reflected in SQLite within a minute.
- Logs:
logs/taskflow-sync.stdout.log and logs/taskflow-sync.stderr.log (relative to workspace)
- Lock: Advisory TTL lock in
sync_state table prevents concurrent syncs
- Conflict resolution: Last-write-wins per sync direction
Quickest install (auto-detects OS)
taskflow install-daemon
This detects your platform and installs the appropriate unit. On macOS it installs and loads the LaunchAgent; on Linux it writes systemd user units and enables the timer.
macOS — LaunchAgent (manual steps)
Templates: taskflow/system/com.taskflow.sync.plist.xml
- Copy
taskflow/system/com.taskflow.sync.plist.xml → ~/Library/LaunchAgents/com.taskflow.sync.plist
- Replace
{{workspace}} with the absolute path to your workspace (no trailing slash)
- Replace
{{node}} with the path to your node binary (which node)
- Load:
launchctl load ~/Library/LaunchAgents/com.taskflow.sync.plist
- Verify:
launchctl list | grep taskflow
Uninstall:
launchctl unload ~/Library/LaunchAgents/com.taskflow.sync.plist
rm ~/Library/LaunchAgents/com.taskflow.sync.plist
Linux — systemd user timer (manual steps)
Templates: taskflow/system/taskflow-sync.service and taskflow/system/taskflow-sync.timer
mkdir -p ~/.config/systemd/user
sed -e "s|{{workspace}}|$OPENCLAW_WORKSPACE|g" \
-e "s|{{node}}|$(which node)|g" \
taskflow/system/taskflow-sync.service > ~/.config/systemd/user/taskflow-sync.service
sed -e "s|{{workspace}}|$OPENCLAW_WORKSPACE|g" \
-e "s|{{node}}|$(which node)|g" \
taskflow/system/taskflow-sync.timer > ~/.config/systemd/user/taskflow-sync.timer
systemctl --user daemon-reload
systemctl --user enable --now taskflow-sync.timer
Verify:
systemctl --user status taskflow-sync.timer
journalctl --user -u taskflow-sync.service
Uninstall:
systemctl --user disable --now taskflow-sync.timer
rm ~/.config/systemd/user/taskflow-sync.{service,timer}
systemctl --user daemon-reload
Note: systemd user units require a login session. To run them without an interactive session (e.g. on a server), enable lingering: loginctl enable-linger $USER
Section Header → DB Status Map
| Markdown Header | DB status value |
|---|
## In Progress | in_progress |
## Pending Validation | pending_validation |
## Backlog | backlog |
## Blocked | blocked |
## Done | done |
Section headers are fixed. Do not rename them. The sync parser maps these exact strings.
Known Quirks
Things that work but might trip you up:
MAX(id) is lexicographic. Task IDs are text, so SELECT MAX(id) works only because IDs are zero-padded (-001, -002). If you create -1 instead of -001, sequencing breaks. Always zero-pad to 3 digits.
- Checkbox state is decorative. Status comes from which
## section a task lives under, not whether it's [x] or [ ]. The sync engine ignores the checkbox on read. On write-back, done tasks get [x], everything else gets [ ].
- Notes survive deletion. If you remove a
- note: line from markdown, the old note stays in the DB (COALESCE preserves it). This is intentional for v1 -- notes are one-way display. To truly clear a note, update the DB directly.
- Lock TTL is 60 seconds. If a sync crashes without releasing the lock, the next run will be blocked for up to 60s. The SIGTERM/SIGINT handlers try to clean up, but a
kill -9 won't. The lock auto-expires.
- Auto-project creation derives names from slugs. If sync encounters a task file with no matching
projects row, it creates one with a name like "My Project" from slug "my-project". The name might not be what you want -- fix it in PROJECTS.md and re-sync.
- Tag order is strict.
[P1] [codex] works. [codex] [P1] silently assigns codex as... nothing useful. Priority tag must come first.
Known Limitations (v1)
- Notes are one-way (markdown → DB). Removing a note in markdown does not clear it in DB.
db-to-files rewrites all project task files, even unchanged ones.
- One task file per project (1:1 mapping). Multiple files per project is post-MVP.
- Periodic sync daemon: macOS (LaunchAgent) and Linux (systemd user timer) are supported. Run
taskflow install-daemon to install.
- Node.js 22.5+ required (
node:sqlite). No Python fallback in v1.
Quick Cheat Sheet
New project: PROJECTS.md block + tasks/<slug>-tasks.md + optional plans/<slug>-plan.md
New task: taskflow add <project> "title" (or append manually to section)
Update status: Move line to correct ## section, flip checkbox if needed
Query simple: cat tasks/<slug>-tasks.md
Query complex: Use db.prepare('SELECT ... WHERE id = ?').all(id) — never interpolate variables into SQL
CLI status: taskflow status
CLI add: taskflow add dashboard "Fix cron panel" --priority P1 --owner codex
Force sync: node taskflow/scripts/task-sync.mjs files-to-db
Memory rule: Reference IDs in logs; never copy backlog into memory