Crux
GuidesPlans & Tasks

Working with Task Lists

Structured task tracking with auto-completion, agent integration, assignees, and focused tools.

For full API signatures and type definitions, see the Task Lists reference.

What Is a Task List?

A task list is a set of structured work items with status tracking, progress updates, and auto-completion. Task lists are independent from plans — they can track work for a thread, session, pipeline, or anything with discrete steps. The optional planId field links a task list to a plan, but it's entirely optional.

Creating Task Lists

create-tasklist.ts
import { tasklist } from '@crux/core/tasks'

// Standalone — no plan association
const handle = await tasklist({
  metadata: { threadId: 'thread-abc', userRequest: 'Set up my project' },
})

// Linked to a plan
const handle = await tasklist({ planId: plan.id })

tasklist() persists the list immediately with status 'pending'. This is intentional — lazy creation would cause race conditions if multiple addTask() calls happen concurrently.

The TaskListHandle

tasklist() returns a TaskListHandle — the primary interface for all task operations:

const handle = await tasklist({ metadata: { threadId: 'abc' } })

handle.id                                    // string (generated UUID)

// Task CRUD
await handle.addTask({ id: 'research', label: 'Research sources' })
await handle.updateTask('research', { status: 'in_progress' })
await handle.removeTask('research')          // soft-delete

// Lifecycle
await handle.discard('User cancelled')       // cancels pending/in_progress tasks
await handle.getTasks()                      // excludes removed tasks
await handle.getStatus()                     // self-heals if stale

// Agent integration
handle.asContext()                           // Context for system message
handle.asTools()                             // { listTasks, addTask, updateTask, removeTask, discardTaskList }
handle.worker('research')                    // TaskWorker handle scoped to one task

Adding Tasks

Tasks require a meaningful user-provided ID and a human-readable label:

await handle.addTask({ id: 'research', label: 'Research competitor messaging' })
await handle.addTask({ id: 'hero', label: 'Write hero section' })
await handle.addTask({ id: 'cta', label: 'Write call-to-action' })

Optional fields:

await handle.addTask({
  id: 'write-intro',
  label: 'Write introduction',
  description: 'A compelling opening paragraph that hooks the reader.',
  assignee: { agent: 'writer', model: 'claude-sonnet-4-5-20250514' },
})

Use meaningful IDs like 'research', 'write-intro', 'review' — not UUIDs. The task list already has a UUID. Meaningful task IDs make logs, devtools output, and worker bindings readable.

Updating Tasks

// Start working
await handle.updateTask('research', { status: 'in_progress' })

// Report progress
await handle.updateTask('research', { progress: 'Found 3 relevant sources...' })

// Complete with result
await handle.updateTask('research', {
  status: 'completed',
  result: { competitors: ['Acme', 'Widget Co'] },
  durationMs: 4500,
})

// Reassign
await handle.updateTask('write', {
  assignee: { agent: 'senior-writer', model: 'claude-sonnet-4-20250514' },
})

The assignee is informational — it doesn't route work automatically. Your orchestration code reads the assignee and dispatches accordingly.

Progress Tracking

The progress field provides human-readable status updates during long-running tasks:

await handle.updateTask('write', {
  status: 'in_progress',
  progress: 'Writing section 1 of 4...',
})

// Later...
await handle.updateTask('write', {
  progress: 'Writing section 3 of 4...',
})

Progress strings are freeform. They're especially useful with reactive hooks — the UI updates in real time as the agent reports progress.

Auto-Completion

Task list status derives automatically from individual task statuses via deriveTaskListStatus():

All tasksList status
All completedcompleted
Any in_progressin_progress
Any failed (none in_progress)failed
All pendingpending
Mix of terminal statusesBased on worst outcome

The status self-heals on every read via getStatus(). If the stored status doesn't match derived status, it's corrected automatically.

await handle.updateTask('research', { status: 'completed' })
await handle.updateTask('write', { status: 'completed' })
await handle.updateTask('review', { status: 'completed' })

const status = await handle.getStatus() // 'completed' — auto-derived

Don't manually set the task list status. Update individual task statuses and let auto-completion handle the rest. The only exception is discard(), which explicitly overrides auto-completion.

Dynamic Tasks

Tasks can be added or removed mid-execution. Auto-completion re-evaluates after every mutation:

// Agent discovers it needs an extra step
await handle.addTask({ id: 'fact-check', label: 'Verify statistics' })

// Agent decides a task isn't needed
await handle.removeTask('fact-check')

removeTask() is a soft delete — the task remains in storage with a removedAt timestamp, but it's excluded from status derivation and getTasks() results.

Discarding

Call discard() to abandon a task list. All pending and in_progress tasks are automatically set to cancelled:

await handle.discard('User cancelled the operation')

const status = await handle.getStatus() // 'discarded'

Once discarded, the status is permanent. Auto-completion will not override it.

Agent Integration

taskListAgent

For existing task lists, taskListAgent() creates a handle without creating a new entity:

tasklist-agent.ts
import { taskListAgent } from '@crux/core/tasks'

const taskAgent = taskListAgent(handle.id)

// Full management access
prompt({
  use: [taskAgent.asContext()],
  tools: taskAgent.asTools(),
})

// Monitor only — give the LLM read access
const { listTasks } = taskAgent.asTools()
prompt({ tools: { listTasks } })

Context Injection

.asContext() injects a summary with status icons into the system message:

## Tasks (2/4)
✓ Research sources — completed
⟳ Write hero section — in_progress "Drafting v1..."
○ Write CTA — pending
○ Review — pending

Override with renderContext:

const taskAgent = taskListAgent(handle.id, {
  renderContext: (tasks) => {
    const summary = tasks.map(t => `- [${t.status}] ${t.label}`).join('\n')
    return `## Current Tasks\n${summary}`
  },
})

Focused Tools

.asTools() returns five dedicated tools:

ToolDescription
listTasksList all active tasks with status, progress, assignee
addTaskAdd a new task with ID, label, description, assignee
updateTaskChange status, progress, assignee, or record results
removeTaskSoft-delete a task (excluded from auto-completion)
discardTaskListAbandon the list, cancel pending tasks

Pick which tools each agent needs:

// Monitor only
const { listTasks } = taskAgent.asTools()

// Full management
const tools = taskAgent.asTools()

LLM-Driven Creation

When no task list exists yet, give the LLM a creation tool:

creation-tool.ts
import { createTaskListTool } from '@crux/core/tasks'

const taskListTool = createTaskListTool({
  template: 'Tasks should be granular: research, draft, review, publish',
})

prompt({
  tools: { createTaskList: taskListTool },
})

// After the LLM calls it:
const handle = taskListTool.created

After creation, switch to taskListAgent() for ongoing management.

Task Lists Without Plans

Task lists don't require plans. Common standalone patterns:

Thread-based work:

const handle = await tasklist({
  metadata: { threadId: 'thread-abc', userRequest: 'Set up my project' },
})
await handle.addTask({ id: 'init', label: 'Initialize project structure' })
await handle.addTask({ id: 'deps', label: 'Install dependencies' })

Pipeline tracking:

const handle = await tasklist({
  metadata: { sessionId: 'etl-run-42', pipeline: 'daily-sync' },
})
await handle.addTask({ id: 'extract', label: 'Extract from source API' })
await handle.addTask({ id: 'transform', label: 'Transform and validate' })
await handle.addTask({ id: 'load', label: 'Load into database' })

The planId field is just a convenience that enables getTaskListByPlan() lookups and useTaskList({ planId }) in the UI.

Devtools

Task list and task events are automatically captured by devtools instrumentation:

  • tasklist:created, tasklist:completed, tasklist:discarded — task list lifecycle
  • task:added, task:updated, task:removed — individual task mutations

These appear in the devtools dashboard under the Plans & Tasks view. See the Devtools reference for the full event list.

On this page