Crux
API Reference@crux/core

Task Lists

Structured task tracking with auto-completion, agent integration, and task worker primitives.

import {
  tasklist, getTaskList, getTaskListByPlan,
  deriveTaskListStatus,
  taskListAgent, taskWorker,
  createTaskListTool,
} from '@crux/core/tasks'
import type {
  TaskList, TaskListStatus, TaskListHandle, CreateTaskListInput,
  Task, TaskStatus, TaskUpdate, CreateTaskInput,
  TaskListAgent, TaskListAgentOptions,
  TaskWorker, TaskWorkerOptions,
} from '@crux/core/tasks'

Overview

Task lists are structured work tracking with live status updates. They persist via CruxStore adapters and emit instrumentation events for devtools observability.

Task lists are standalone entities. They can be associated with a plan, a thread, a session, or anything via the planId field or metadata. The planId field is a convenience for linking to a Plan, but task lists work independently without any plan association.

Agent integration primitives (taskListAgent, taskWorker) provide context injection and focused LLM tools for task management and execution.

Why Task Lists?

Task lists solve the "what is the agent doing right now?" problem. Without them, multi-step agent work is a black box — you know the agent started and eventually finished, but not what happened in between.

Task lists provide:

  1. Structured visibility. Each step has a status, progress message, and assignee. UIs can show real-time progress bars. Logs capture exactly which step failed and why.
  2. Auto-completion. You update individual tasks; the list status derives automatically. No manual bookkeeping to forget.
  3. Worker isolation. Each worker agent gets tools scoped to its assigned task — no risk of updating the wrong task. See Worker Pattern below.

Independence

Task lists are NOT tied to plans. The planId field is optional metadata for linking to a plan — but task lists work independently for any scenario that needs structured tracking.

Common uses without plans:

// Track work for a chat thread
const handle = await tasklist({
  metadata: { threadId: 'thread-abc' },
})

// Track a data processing pipeline
const handle = await tasklist({
  metadata: { pipelineId: 'etl-daily-2025-03-25' },
})

// Track user onboarding steps
const handle = await tasklist({
  metadata: { userId: 'user-123', flow: 'onboarding' },
})

TaskList API

tasklist(input)

Create a task list and return a fluent handle. The list is persisted immediately with status 'pending' — no lazy creation, which avoids race conditions from concurrent addTask() calls.

ParameterTypeDescription
inputCreateTaskListInputOptional planId association and metadata

Returns: Promise<TaskListHandle>

const handle = await tasklist({ planId: p.id })
await handle.addTask({ id: 'research', label: 'Research sources' })
await handle.updateTask('research', { status: 'completed' })

The planId is optional. Task lists work without any plan association:

// Standalone task list — no plan needed
const handle = await tasklist({
  metadata: { threadId: 'thread-abc', purpose: 'data-pipeline' },
})

getTaskList(taskListId)

Get a task list by ID. Self-heals stale status — if the persisted status doesn't match the derived status from tasks, it's corrected and persisted.

ParameterTypeDescription
taskListIdstringThe task list's ID

Returns: Promise<TaskList | null>

getTaskListByPlan(planId)

Find the task list associated with a plan.

ParameterTypeDescription
planIdstringThe plan's ID

Returns: Promise<TaskList | null>

deriveTaskListStatus(tasks)

Derive a task list's status from its tasks. Pure function — no side effects.

ParameterTypeDescription
tasksTask[]All tasks (including removed). Removed tasks are filtered out internally.

Returns: TaskListStatus (never returns 'discarded' or 'pending')

TaskListHandle

The fluent handle returned by tasklist(). All mutations trigger auto-completion evaluation.

MethodSignatureDescription
addTask(input)(CreateTaskInput) => Promise<Task>Add a new task with status 'pending'
updateTask(taskId, update)(string, TaskUpdate) => Promise<Task>Update status, progress, assignee, result, error, or duration
removeTask(taskId)(string) => Promise<void>Soft-delete a task. Removed tasks don't count for auto-completion
discard(reason?)(string?) => Promise<void>Discard the list. Cancels all pending/in_progress tasks
getTasks()() => Promise<Task[]>Get all non-removed tasks
getStatus()() => Promise<TaskListStatus>Get current status (self-heals if stale)
idstring (readonly)The task list's ID

Context Injection

Both taskListAgent and taskWorker provide .asContext() which returns a Crux Context — a composable fragment that injects a markdown section into the system message at generation time.

Task List Agent Context

taskListAgent(id).asContext() renders a task summary with status icons:

## Tasks (2/4)
✓ Research sources — completed [researcher]
✓ Create outline — completed
⟳ Write draft — in_progress "Writing section 2 of 4..."
○ Review and polish — pending

Status icons: completed, in_progress, pending, failed/cancelled, skipped.

Task Worker Context

taskWorker(listId, taskId).asContext() renders the worker's specific assignment and guidelines:

## Your Assignment
**Task:** Write introduction (assigned to writer / claude-sonnet-4-20250514)
**Description:** Write a compelling introduction that hooks the reader
**Status:** pending

## Guidelines
- Call `startTask` when you begin working on this task.
- Use `reportProgress` to report what you are doing as you work.
- Call `completeTask` when finished, optionally providing a structured result.
- Call `failTask` if you encounter an unrecoverable issue, with an error message.

Task worker context defaults to priority 95 — the highest of any built-in context. This ensures the worker's assignment is always present, even under tight token budgets. Task list agent context defaults to priority 80.

Worker Pattern

The most common pattern is one agent assigned to one task. taskWorker provides focused tools (startTask, reportProgress, completeTask, failTask) with NO taskId parameter — it is bound at creation time.

This is better for LLMs because:

  • Fewer parameters. Each tool takes zero or one parameter. The LLM never needs to remember which task ID to pass.
  • Less confusion. A worker cannot accidentally update someone else's task. The scope is enforced at the API level.
  • Clearer intent. Tool names like startTask and completeTask map directly to the worker's lifecycle, instead of a generic updateTask with a status parameter.
// Orchestrator dispatches work to multiple workers
for (const task of tasks) {
  const worker = taskWorker(taskList.id, task.id)

  const workerPrompt = prompt({
    use: [planAgent.asContext(), worker.asContext()],
    tools: worker.asTools(),
    system: 'Complete your assigned task.',
  })

  await generate(workerPrompt, { model: myModel, input: {} })
}

Overriding Defaults

Custom Context Rendering

Both agent types support renderContext overrides:

// Task list agent — custom task summary
const agent = taskListAgent(taskList.id, {
  renderContext: (tasks) => {
    const done = tasks.filter(t => t.status === 'completed').length
    return `<tasks progress="${done}/${tasks.length}">\n${tasks.map(t =>
      `  <task id="${t.id}" status="${t.status}">${t.label}</task>`
    ).join('\n')}\n</tasks>`
  },
})

// Task worker — custom assignment rendering
const worker = taskWorker(taskList.id, 'research', {
  renderContext: (task, allTasks) =>
    `You are working on: "${task.label}"\n` +
    `Overall progress: ${allTasks.filter(t => t.status === 'completed').length}/${allTasks.length} tasks done.`,
})

Custom Worker Guidelines

Replace the default lifecycle instructions:

const worker = taskWorker(taskList.id, 'research', {
  guidelines: [
    'Start by calling `startTask`.',
    'Report findings as you discover them using `reportProgress`.',
    'When you have at least 5 sources, call `completeTask` with a JSON result.',
    'If no sources are found after exhaustive search, call `failTask`.',
  ].join('\n- '),
})

Custom Tool Descriptions

Wrap individual tools to override descriptions:

const { startTask, completeTask, ...rest } = worker.asTools()

const tools = {
  ...rest,
  startTask: {
    ...startTask,
    description: 'Signal that you are beginning the research phase.',
  },
  completeTask: {
    ...completeTask,
    description: 'Submit your research findings. Include a "sources" array in the result.',
  },
}

Agent Integration

Agent handles for task list management and task execution, plus a standalone creation tool. All follow the focused tools pattern — multiple dedicated tools instead of a single tool with an action parameter.

taskListAgent(taskListId, options?)

Create an agent handle for an existing task list. Provides context injection and tools for task list management.

ParameterTypeDescription
taskListIdstringThe task list's ID
optionsTaskListAgentOptions?Render overrides

Returns: TaskListAgent

MemberDescription
.taskListIdThe task list's ID (readonly)
.asContext(opts?)Returns a Context with task list summary (default priority: 80)
.asTools()Returns { listTasks, addTask, updateTask, removeTask, discardTaskList }

TaskListAgentOptions:

FieldTypeDescription
renderContext(tasks: Task[]) => stringOverride the default context rendering
const agent = taskListAgent(taskList.id)

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

// Full management:
const tools = agent.asTools()
prompt({ tools })

taskWorker(taskListId, taskId, options?)

Create an agent handle scoped to a single assigned task. The taskId is bound at creation — worker tools don't require it as a parameter, reducing LLM error surface.

ParameterTypeDescription
taskListIdstringThe task list's ID
taskIdstringThe specific task's ID
optionsTaskWorkerOptions?Render and guidelines overrides

Returns: TaskWorker

MemberDescription
.taskListIdThe task list's ID (readonly)
.taskIdThe task's ID (readonly)
.asContext(opts?)Returns a Context with task assignment and guidelines (default priority: 95)
.asTools()Returns { startTask, reportProgress, completeTask, failTask }

TaskWorkerOptions:

FieldTypeDescription
renderContext(task: Task, allTasks: Task[]) => stringOverride the default context rendering
guidelinesstringOverride the default worker guidelines
const worker = taskWorker(taskList.id, 'write-intro')
prompt({
  use: [worker.asContext()],
  tools: worker.asTools(),
})

createTaskListTool(options?)

Standalone tool for creating new task lists. Use when no task list exists yet. Optionally accepts a template string that is included in the tool description to guide LLM output format.

ParameterTypeDescription
options{ template?: string }?Optional template for LLM guidance

Returns: ToolDef

// Basic
const tools = { createTaskList: createTaskListTool() }

// With template
const tools = {
  createTaskList: createTaskListTool({
    template: 'Tasks should follow: research, draft, review, publish',
  }),
}

Types

TaskListHandle

The fluent handle returned by tasklist(). Extends TaskList data with methods for task management, context injection, and tool exposure.

MethodSignatureReturnsDescription
addTask(input)(CreateTaskInput) => Promise<Task>Promise<Task>Add a new task with status 'pending'
updateTask(taskId, update)(string, TaskUpdate) => Promise<Task>Promise<Task>Update status, progress, assignee, result, error, or duration
removeTask(taskId)(string) => Promise<void>Promise<void>Soft-delete a task (excluded from auto-completion)
discard(reason?)(string?) => Promise<void>Promise<void>Discard the list, cancel all pending/in_progress tasks
getTasks()() => Promise<Task[]>Promise<Task[]>Get all non-removed tasks
getStatus()() => Promise<TaskListStatus>Promise<TaskListStatus>Get current status (self-heals if stale)
asContext(opts?)(ContextOptions?) => ContextContextCreate a Context for system message injection
asTools()() => TaskListTools{ listTasks, addTask, updateTask, removeTask, discardTaskList }Returns focused tools for LLM agents
worker(taskId)(string) => TaskWorkerTaskWorkerCreate a worker handle scoped to a single task
idstring (readonly)The task list's ID

TaskList

FieldTypeDescription
idstringUUID, generated by tasklist()
planIdstring?Optional association with a plan, thread, session, or any entity
statusTaskListStatusCurrent derived status
metadataRecord<string, unknown>?Arbitrary user metadata
createdAtnumberUnix timestamp (ms)
updatedAtnumberUnix timestamp (ms)
completedAtnumber?Set when all tasks complete
discardedAtnumber?Set when the list is discarded
discardReasonstring?Reason provided when discarding

TaskListStatus

type TaskListStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'discarded'

Task

FieldTypeDescription
idstringUser-provided meaningful ID (e.g., 'research', 'write-intro')
taskListIdstringParent task list ID
labelstringHuman-readable task label
descriptionstring?Longer description of the task
statusTaskStatusCurrent task status
progressstring?Human-readable progress message (e.g., "Writing section 2...")
assignee{ agent?: string; model?: string }?Which agent/model is assigned
resultunknown?Structured result data from a completed task
errorstring?Error message if the task failed
durationMsnumber?How long the task took in milliseconds
createdAtnumberUnix timestamp (ms)
updatedAtnumberUnix timestamp (ms)
removedAtnumber?Set when soft-deleted via removeTask()

TaskStatus

type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'skipped' | 'cancelled'

ToolDef

The tool shape returned by all agent integration primitives. Compatible with AI SDK tool format.

interface ToolDef {
  description: string
  parameters: z.ZodType
  execute: (args: Record<string, unknown>) => Promise<string>
}

Input Types

interface CreateTaskListInput {
  planId?: string
  metadata?: Record<string, unknown>
}

interface CreateTaskInput {
  id: string        // user-provided meaningful ID
  label: string
  description?: string
  assignee?: { agent?: string; model?: string }
}

interface TaskUpdate {
  status?: TaskStatus
  progress?: string
  assignee?: { agent?: string; model?: string }
  result?: unknown
  error?: string
  durationMs?: number
}

Auto-Completion

After every task mutation (addTask, updateTask, removeTask), the list status is re-evaluated using deriveTaskListStatus():

ConditionDerived Status
All active tasks completed or skipped'completed'
Any task failed AND no tasks in_progress'failed'
All tasks removed (empty active list)'completed'
Otherwise'in_progress'

'discarded' is never derived — it's set explicitly via discard(). 'pending' is the initial status before any tasks change state.

Auto-Completion Walkthrough

Here is how the state machine works through a typical lifecycle:

1. tasklist()                  → list status: 'pending'
2. addTask('a')               → list status: 'pending'    (no task has started)
3. addTask('b')               → list status: 'pending'
4. updateTask('a', in_progress) → list status: 'in_progress' (a task started)
5. updateTask('a', completed)   → list status: 'in_progress' (b still pending)
6. updateTask('b', in_progress) → list status: 'in_progress'
7. updateTask('b', completed)   → list status: 'completed'   (all done!)

If task b fails instead:

6. updateTask('b', failed)    → list status: 'failed'     (a task failed, none in_progress)

If task b fails while a is still in progress:

5. updateTask('b', failed)    → list status: 'in_progress' (a is still working)
6. updateTask('a', completed) → list status: 'failed'      (b failed, nothing in_progress)

The 'failed' status only triggers when no tasks are in_progress. This prevents premature failure — if one task fails but others are still running, the list waits for them to finish before declaring failure.

Storage Keys

Task lists and tasks use prefixed keys in the CruxStore:

EntityKey FormatExample
TaskListtasklist:{id}tasklist:e5f6g7h8
Tasktask:{listId}:{taskId}task:e5f6g7h8:research

Usage Example

Full task list workflow — create a list, add tasks, use agent primitives for execution:

task-workflow.ts
import {
  tasklist,
  taskListAgent, taskWorker,
} from '@crux/core/tasks'

// 1. Create a task list (standalone or linked to a plan)
const handle = await tasklist({
  metadata: { purpose: 'blog-post-pipeline' },
})

// 2. Add tasks with meaningful IDs
await handle.addTask({ id: 'research', label: 'Research sources', assignee: { model: 'gpt-4.1' } })
await handle.addTask({ id: 'outline', label: 'Create outline' })
await handle.addTask({ id: 'write', label: 'Write draft' })
await handle.addTask({ id: 'review', label: 'Review and polish' })

// 3. Create agent handles for LLM integration
const taskListAgent = taskListAgent(handle.id)
const worker = taskWorker(handle.id, 'research')

// 4. Use in prompts
prompt({
  use: [worker.asContext()],
  tools: { ...worker.asTools(), ...taskListAgent.asTools() },
})

Task lists can also be linked to plans for combined intent + execution workflows. See the Plans reference for the plan API, and the Task Lists guide for patterns combining both.

On this page