Tasks
Track structured work with dynamic or typed task ledgers.
When an agent works through multi-step jobs, you want to see which step is running, what finished, and what failed, without parsing chat history. tasks() creates a task ledger for exactly that. Ledgers can stand alone or attach to a plan handle or plan ID.
import { z } from "zod";
import { task, tasks } from "@use-crux/core/tasks";
const work = await tasks({
title: "Import job",
metadata: { jobId: "import-42" },
});Dynamic Ledgers
Dynamic ledgers are best when task IDs come from user input, model output, or runtime data.
await work.add({
id: "extract",
label: "Extract CSV rows",
metadata: { file: "customers.csv" },
});
await work.start("extract");
await work.progress("extract", "Parsed 1,200 rows");
await work.complete("extract", { rows: 1200 });Task IDs must be unique within the ledger, and results and metadata must be JSON-safe.
Defined Ledgers
Defined ledgers are best when the task set is known at author time. IDs are inferred from object keys, and result schemas infer complete() payloads.
const work = await tasks({
items: {
research: task("Research channels", {
result: z.object({ channels: z.array(z.string()) }),
}),
draft: task("Draft copy", {
result: z.object({ markdown: z.string() }),
}),
},
});
await work.complete("research", { channels: ["partners", "community"] });Defined ledgers do not accept dynamic add() calls. Use a dynamic ledger when new task IDs are discovered at runtime.
Lifecycle
Use lifecycle methods instead of editing status fields directly:
await work.start("draft");
await work.progress("draft", "Writing intro and offer");
await work.complete("draft", { markdown: "# Launch\n\n..." });Each terminal transition applies to a separate active task:
await work.fail("fact-check", "Source data was unavailable");
await work.skip("legal-review", "Covered by existing review");
await work.cancel("publish", "Launch paused");
await work.remove("cleanup");Terminal tasks are immutable through lifecycle methods. Removed tasks do not count toward the ledger status.
Status Derivation
Task statuses are:
pendingin_progresscompletedfailedskippedcancelled
Ledger statuses are:
pendingfor empty ledgers or ledgers where all active tasks are pending.in_progresswhen any active task is in progress.completedwhen all active tasks are completed or skipped.failedwhen at least one active task failed and none are in progress.cancelledwhen terminal active tasks include cancellation, without failures.discardedwhen the whole ledger is abandoned.
Errors
Task mutations throw typed lifecycle errors with structured fields:
try {
await work.add({ id: "research", label: "Duplicate research task" });
} catch (error) {
if (error instanceof Error && error.name === "DuplicateTaskIdError") {
// error.taskListId and error.taskId are available on the structural error.
}
}A duplicate add() rejects with DuplicateTaskIdError, including when concurrent writers race to add the same ID.
Common errors include TaskListNotFoundError, TaskNotFoundError, DuplicateTaskIdError, TaskRemovedError, TaskListDiscardedError, InvalidTaskTransitionError, TaskResultValidationError, and TaskJsonValueError.
Existing Ledgers
Bind an existing ledger by ID:
const work = tasks.ref(taskListId);
const current = await work.get();List ledgers, optionally scoped to a plan:
const ledgers = await tasks.list({ plan: launchPlan });