Skip to main content
A phase is the core building block of a cl-pipelines pipeline. Each phase is a plain TypeScript object with a name and a run function — no classes, no decorators, no inheritance. Because phases are stateless objects, you can define them at module scope, pass them around, and test them in isolation without any special setup.

Phase shape

The name field is how the library looks up a phase from the checkpoint. It must be unique within the array you pass to runPipeline or advancePhase — if two phases share a name, the first match wins.

PhaseContext

Your run function receives a PhaseContext<TState> with everything it needs to read state, write logs, and persist mid-phase progress:
string
The ID of the currently running job. Useful if your phase needs to look up related records by the same ID.
Checkpoint<TState>
The checkpoint as it existed when this phase started. Read checkpoint.state to access state passed from the previous phase (or initialState on the first run).
(message: string, level?: string) => Promise<void>
Appends a LogEntry to the job’s log via StorageAdapter.appendLog. Default level is "info". Log entries are visible through the UI components and storage._inspect() in tests.
(state: TState) => Promise<void>
Writes a new checkpoint mid-phase. The stored checkpoint will have nextPhase set to the current phase name, so a crash and re-run re-enters this phase from the saved position. Use this inside loops over large datasets.

PhaseResult

Every run function must return one of three variants:
Return { kind: "next", nextPhase, state } to advance to another phase. The nextPhase string must match a phase name in your array. The state value becomes ctx.checkpoint.state in the next phase.
Throwing an exception from run has the same effect as returning { kind: "error" } — status is set to "error" and the checkpoint is preserved. You don’t need to wrap every async call in try/catch unless you want to produce a richer error message.

Mid-phase checkpointing with ctx.saveState

For phases that process large datasets in a loop, you can call ctx.saveState() after each item (or batch). If the process crashes, the next run re-enters the same phase from the last saved position rather than starting the whole phase over.
When saveState is called, the stored checkpoint has nextPhase set to "processItems" — so a crash and re-run enters the same phase with the saved chunkIndex, skipping already-processed items.
Always initialize chunkIndex (or equivalent) in your TState type and read it from ctx.checkpoint.state at the start of your loop. That way the phase is idempotent whether it starts fresh or resumes mid-loop.

Testing a phase in isolation

Because phases are plain objects, you can call run directly in a test with a fake context: