Worker Agents
The worker pattern — one agent assigned to one task with focused, scope-bound tools.
The Problem
When an agent manages multiple tasks, the LLM must remember which task ID to pass, pick the right status value, and avoid accidentally updating the wrong task. This creates a large error surface:
// Generic tool — LLM must get the taskId right every time
updateTask({ taskId: 'research', status: 'in_progress' })
updateTask({ taskId: 'research', progress: 'Found 3 sources...' })
updateTask({ taskId: 'research', status: 'completed', result: { ... } })The Worker Pattern
A worker is an agent assigned to exactly one task. The taskId is bound at creation — all worker tools operate on that specific task without the LLM needing to specify which task.
// Worker tools — no taskId needed, intent is clear from the tool name
startTask()
reportProgress({ message: 'Found 3 sources...' })
completeTask({ result: { ... } })Fewer parameters, clearer names, enforced scope. The LLM cannot accidentally update the wrong task.
Creating Workers
Two ways to create a worker:
import { taskWorker } from '@crux/core/tasks'
// From a standalone function
const worker = taskWorker(taskListId, 'research')
// From a TaskListHandle (shorthand)
const worker = handle.worker('research')Both are equivalent — .worker() is a convenience method on TaskListHandle.
Worker Context
.asContext() injects the task assignment and guidelines into the system message at priority 95 (higher than plans at 80):
## Your Assignment
**Task:** Research competitor messaging (assigned to researcher)
**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.Combine with plan context for full visibility:
import { planAgent } from '@crux/core/plan'
import { taskWorker } from '@crux/core/tasks'
const planAgent = planAgent(planId)
const worker = taskWorker(handle.id, 'research')
prompt({
use: [
planAgent.asContext(), // priority 80 — overall project context
worker.asContext(), // priority 95 — specific assignment
],
tools: worker.asTools(),
system: 'You are a research assistant. Complete your assigned task.',
})Under token pressure, the assignment (priority 95) is preserved first, then the plan (priority 80).
Worker Tools
.asTools() returns four lifecycle tools:
| Tool | What it does |
|---|---|
startTask | Mark the task as in_progress. Call when you begin working. |
reportProgress | Update the progress field with a human-readable message. |
completeTask | Mark as completed, optionally with a structured result. |
failTask | Mark as failed with an error message. |
None require a taskId parameter — it's bound at creation.
const { startTask, reportProgress, completeTask, failTask } = worker.asTools()When a worker calls completeTask or failTask, auto-completion re-evaluates the task list status. If all tasks are completed, the list auto-completes.
Full Lifecycle Example
import { planAgent } from '@crux/core/plan'
import { taskWorker } from '@crux/core/tasks'
import { prompt, generate } from '@crux/ai'
import { openai } from '@ai-sdk/openai'
const model = openai('gpt-4o') // or any AI SDK model
const planAgent = planAgent(planId)
const worker = taskWorker(handle.id, 'research')
const draftPrompt = prompt({
use: [planAgent.asContext(), worker.asContext()],
tools: worker.asTools(),
system: 'You are a research assistant. Complete your assigned task.',
})
// The worker agent will:
// 1. See the plan in its system message (overall project context)
// 2. See its specific assignment (what task to do)
// 3. Call startTask() to mark it in progress
// 4. Call reportProgress() as it works
// 5. Call completeTask() when done (triggers auto-completion check)
await generate(prompt, { model, input: {} })Overriding Guidelines
Replace the default guidelines with task-specific instructions:
const worker = taskWorker(handle.id, 'research', {
guidelines: 'Start immediately. Report progress every 30 seconds. Include source URLs in result.',
})Custom Context Rendering
Replace the default assignment format:
const worker = taskWorker(handle.id, 'research', {
renderContext: (task, allTasks) => {
const total = allTasks.length
const done = allTasks.filter(t => t.status === 'completed').length
return `Your task: ${task.label}\nOverall: ${done}/${total} complete`
},
})Wrapping Tools
Override tool descriptions or add validation by wrapping:
const { completeTask, ...rest } = worker.asTools()
const tools = {
...rest,
completeTask: {
...completeTask,
description: 'Submit your research findings. The result MUST include a "sources" array.',
async execute(args: Record<string, unknown>) {
const result = args.result as Record<string, unknown>
if (!result?.sources) {
return JSON.stringify({ error: 'Result must include a sources array' })
}
return completeTask.execute(args)
},
},
}Fan-Out Pattern
The most common orchestration pattern: create a task for each piece of work, then fan out workers:
const tasks = await handle.getTasks()
// Run workers sequentially
for (const task of tasks) {
const worker = taskWorker(handle.id, task.id)
await generate(
prompt({
use: [planAgent.asContext(), worker.asContext()],
tools: worker.asTools(),
system: 'Complete your assigned task.',
}),
{ model, input: {} },
)
}
// Or in parallel
await Promise.all(
tasks.map(async (task) => {
const worker = taskWorker(handle.id, task.id)
await generate(
prompt({
use: [planAgent.asContext(), worker.asContext()],
tools: worker.asTools(),
system: 'Complete your assigned task.',
}),
{ model, input: {} },
)
}),
)See Pipeline & Flow Patterns for more orchestration examples.