Skip to main content
Before you wire up adapters or write phases, it helps to have a mental model of how the pieces fit together. This page defines each concept precisely so you can reason about what the library stores, when it stores it, and what happens when something goes wrong.

Job

A job is a single pipeline run identified by a jobId string you provide. Every piece of state — status, checkpoint, and log entries — is scoped to that ID. Two pipelines with different jobId values are completely independent, even if they share the same phase definitions and adapters.

Phase

A phase is the smallest unit of work. It is a plain TypeScript object with two fields:
  • name: string — unique within the phases array; used to look up the phase by name from the checkpoint
  • run: (ctx: PhaseContext<TState>) => Promise<PhaseResult<TState>> — your business logic
Phases are stateless objects — all mutable state flows through the Checkpoint<TState>. This makes them easy to test in isolation: construct a fake PhaseContext and call run directly.

Checkpoint

A checkpoint is a serialized snapshot of in-progress state. The storage layer persists it after every successful phase transition and whenever your phase calls ctx.saveState().
The checkpoint is what makes crash recovery possible. If your process dies mid-job, the next runPipeline call reads the existing checkpoint and resumes from exactly that point — no work is duplicated up to the last save.

PipelineStatus

Every job has one of five statuses:

StorageAdapter

The StorageAdapter is the interface between cl-pipelines and your database or key-value store. You implement five methods:
The library never accesses your database directly — it only calls these methods. This means you can back it with Convex, PostgreSQL, Redis, or the built-in in-memory adapter without changing any pipeline logic.

SchedulerAdapter

The SchedulerAdapter is the interface between cl-pipelines and your job queue or task scheduler. It has a single method:
After each successful phase, the library calls scheduleAdvance(jobId, 0) to trigger the next advance. You implement this by enqueuing a call to advancePhase in your scheduler (e.g. Convex’s ctx.scheduler.runAfter).

RetryMode

RetryMode controls what happens when you call runPipeline on a job that already has a checkpoint: When no retryMode is specified and a checkpoint exists, the library behaves as "resume". See the Retry Modes page for guidance on when to choose each.

PhaseResult

Every run function must return one of three result shapes:
Both { kind: "error" } returns and thrown exceptions preserve the checkpoint. This means retryMode: "resume" works correctly for all error scenarios — the job can always resume from the last safe state.

How the pieces connect