Best practices
Heuristics for building maintainable plan and task workflows — naming, scoping, tool selection, and lifecycle management.
These rules of thumb come from running plans and task lists in production. They are guidance, not enforcement — break them when your domain demands it, but understand the cost first.
Use Meaningful Task IDs
IDs like 'research', 'write-intro', 'review' are self-documenting. They make logs, devtools output, and worker bindings readable.
// Good
await handle.addTask({ id: 'research', label: 'Research sources' })
await handle.addTask({ id: 'write-intro', label: 'Write introduction' })
// Bad — unreadable in logs and devtools
await handle.addTask({ id: crypto.randomUUID(), label: 'Research sources' })A random UUID forces you to cross-reference the task list every time you see one in a trace. A meaningful ID lets you read the trace top-to-bottom.
Keep Plans Concise
Plans are intent documents, not implementation details. Keep them focused on what and why, not how:
// Good — concise intent
const p = await plan({
title: 'Cloud Migration Guide',
content: `## Goal
Help teams migrate from AWS to GCP.
## Audience
Platform engineers with AWS experience.
## Key Sections
1. Assessment checklist
2. Service mapping (AWS → GCP)
3. Migration patterns
4. Testing strategy`,
})
// Bad — too detailed, includes implementation
// content: '## Step 1\nOpen terminal. Run `gcloud init`...'When a plan starts including command-line invocations and code snippets, it has stopped being a plan and started being a task. Move that detail into the task description or the worker prompt.
Use Templates to Guide LLMs
Templates produce consistent plan structures without constraining the LLM:
const planTool = createPlanTool({
template: `## Goal\n[What we want to achieve]\n\n## Constraints\n[Budget, timeline, tech stack]\n\n## Success Criteria\n[How we know it's done]`,
})The bracketed placeholders signal which sections the LLM should fill, while leaving phrasing to the model.
Pick Focused Tools Per Role
Give each agent the minimum tools it needs:
| Role | Tools |
|---|---|
| Planner | createPlanTool |
| Orchestrator | taskListAgent().asTools() (full management) |
| Monitor | { listTasks } from taskListAgent().asTools() |
| Worker | taskWorker().asTools() (lifecycle only) |
| Reviewer | planAgent().asTools() with { getPlan } only |
// Orchestrator — full task management
const tools = taskListAgent(handle.id).asTools()
// Worker — lifecycle only, taskId bound
const tools = taskWorker(handle.id, 'research').asTools()
// Monitor — read-only
const { listTasks } = taskListAgent(handle.id).asTools()A worker that has access to addTask will eventually create tasks. A reviewer with updatePlan will eventually edit. Scope the tools to the role and you scope the failure modes.
Monitor via Devtools During Development
Plan and task events appear in the devtools dashboard with full trace correlation. Use crux dev during development to see:
- Plan creation and updates with content diffs
- Task status transitions and progress updates
- Worker tool calls and results
- Flow step hierarchy with timing
See the Devtools reference for setup.
Let Auto-Completion Work
Don't manually set task list status. Update individual task statuses and let deriveTaskListStatus() handle the rest:
// Good — update tasks, let list status derive
await handle.updateTask('research', { status: 'completed' })
await handle.updateTask('write', { status: 'completed' })
// List status auto-derives to 'completed'
// The only manual override is discard()
await handle.discard('User changed direction')Manually setting list status invites drift between the list view and the underlying tasks. The derived status is always consistent because it's a function of the tasks themselves.