Crux
GuidesPlans & Tasks

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:

  • pending
  • in_progress
  • completed
  • failed
  • skipped
  • cancelled

Ledger statuses are:

  • pending for empty ledgers or ledgers where all active tasks are pending.
  • in_progress when any active task is in progress.
  • completed when all active tasks are completed or skipped.
  • failed when at least one active task failed and none are in progress.
  • cancelled when terminal active tasks include cancellation, without failures.
  • discarded when 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 });

On this page