durable

js/task/durable.ts

fino:task/durable — durable tasks: fino:task executables whose runs execute inside a fino:workflow run.

A DurableTask is a Task in every consumer-facing way — it validates input, parses CLI argv, mounts as an AI tool, honors timeoutMs — but its handler runs inside a durable workflow execution: ctx.step() checkpoints results, ctx.sleep() and ctx.waitForSignal() park the run durably, and a resumed run replays from its last checkpoint instead of starting over.

Execution model

The handler re-executes from the top on every resume. Completed steps are replayed from the store by call order and id — they return their recorded results without re-running — so all side effects (including writer output that must not repeat) belong inside ctx.step(). Changing the sequence of step ids between runs of the same runId is a determinism error and the workflow runtime rejects it. ctx.env, ctx.cwd, ctx.prompt, and ctx.writer are per-drive values, not durable state.

run() drives a run to completion, waiting out timer parks in-process and honoring signalRun() deliveries. The start() / resume() / signalRun() surface performs exactly one drive per call and reports the parked state — schedulers (e.g. fino:jobs) own the timers in that mode.

Task-level suspend() is not available inside a durable handler: the AI-loop suspension signal would unwind through the workflow driver and persist the run as failed. Durable pauses use ctx.waitForSignal().

Without a store, runs checkpoint into a fresh in-memory store: replay semantics apply within the process, but nothing survives a restart.

import { durableTask } from 'fino:task/durable';
import { SqliteWorkflowStore } from 'fino:workflow';

const ingest = durableTask({
  name: 'ingest',
  store: () => SqliteWorkflowStore.open('/data/jobs.db'),
  run: async (input: { url: string }, ctx) => {
    const doc = await ctx.step('fetch', () => fetch(input.url).then((r) => r.text()));
    await ctx.sleep('cooldown', '5s');
    return await ctx.step('summarize', () => doc.slice(0, 100));
  },
});

await ingest.run({ url: 'https://example.com' }, { runId: 'ingest-42' });

Interfaces

interface DurableTaskContext extends Omit<TaskContext, 'step' | 'suspend'> {

Context passed to a durable task handler.

Extends the task context with the workflow's durable operations. The numeric AI-loop step index moves to aiStep so step() can be the workflow checkpoint method, and suspend() always throws — durable pauses use waitForSignal().

Readonly Properties

readonly aiStep?: number

AI-loop step index (the plain TaskContext step number).

readonly workflowRunId: string

Durable workflow run id backing this execution.

readonly state: WorkflowStateBag

Durable key/value state persisted with every checkpoint.

Methods

step<T>(id: string, fn: () => Promise<T> | T, opts?: { retry?: WorkflowRetryOptions; }): Promise<T>

Run fn once and checkpoint its result under id.

call< In, Out >(activity: Activity<In, Out>, input: In, opts?: { retry?: WorkflowRetryOptions; }): Promise<Out>

Invoke a workflow activity with validated input/output, checkpointed.

sleep(id: string, duration: number | string | Date): Promise<void>

Park the run durably until duration elapses.

waitForSignal<T = unknown>(name: string, opts?: { timeout?: number | string | Date; }): Promise<T>

Park the run durably until signalRun() delivers name.

suspend(opts?: { reason?: string; payload?: unknown; }): never

Always throws — durable pauses use waitForSignal().

interface DurableTaskOptions< Input = unknown, Output = unknown > extends Omit<TaskOptions<Input, Output>, 'run'> {

Options used to create a durable task.

Properties

store?: WorkflowStore | (() => WorkflowStore | Promise<WorkflowStore>)

Workflow store backing this task's runs — a store instance or a lazy factory resolved once on first use. Defaults to a per-task InMemoryWorkflowStore (checkpointing without persistence).

run: DurableTaskHandler<Input, Output>

interface DurableTaskRunOptions extends TaskRunOptions {

Options for direct durable execution — TaskRunOptions plus per-run durability overrides.

Properties

store?: WorkflowStore

Store override for this run (wins over the task-level store).

key?: string

Idempotency key recorded on the workflow run state.

interface DurableRunOptions {

Options for the one-drive control surface (start, resume, signalRun).

Properties

store?: WorkflowStore
key?: string
signal?: AbortSignal
writer?: TaskOutputWriter

interface DurableRunHandle {

JSON-serializable snapshot of a durable run after one drive.

Properties

runId: string
status: WorkflowStatus
waitingOn?: WorkflowWait
result?: unknown
error?: { message: string; stack?: string; }

Types

type DurableTaskHandler< Input, Output = unknown > = (input: Input, ctx: DurableTaskContext) => Output | Promise<Output>

Function that performs a durable task.

Classes

class DurableTask< Input = unknown, Output = unknown > extends Task<Input, Output> {

A Task whose handler runs inside a durable fino:workflow execution.

Construct via durableTask(). All Task surfaces (direct run(), CLI parse(), AI invoke()) work unchanged; durability is added underneath by a synthetic task handler that drives the workflow run.

Constructors

constructor(opts: DurableTaskOptions<Input, Output>)

Create a durable task. Prefer the durableTask() factory.

Methods

override run(rawInput: Input, options: DurableTaskRunOptions = {}): Promise<Output>

Run to completion, waiting out durable parks in-process.

Timer parks self-resume at their due time; signal parks wait for a matching signalRun() (or the park's own timeout). The workflow state is checkpointed at every transition, so a run interrupted here can still be resumed later from its store.

import { durableTask } from 'fino:task/durable';

const t = durableTask({ name: 'noop', run: async () => 'ok' });
await t.run(undefined, { runId: 'once' });
async start(rawInput: Input, opts: DurableRunOptions & { runId?: string; } = {}): Promise<DurableRunHandle>

Drive a run once: start it (or resume it when runId exists in the store) and return a serializable snapshot instead of waiting out parks.

import { durableTask } from 'fino:task/durable';

const t = durableTask({ name: 'nap', run: async (_i, ctx) => ctx.sleep('z', '1h') });
const handle = await t.start(undefined, {});
console.log(handle.status, handle.waitingOn?.type);
async resume(runId: string, opts: DurableRunOptions = {}): Promise<DurableRunHandle>

Resume a persisted run by one drive and return its snapshot.

import { durableTask } from 'fino:task/durable';

const t = durableTask({ name: 'nap', run: async (_i, ctx) => ctx.sleep('z', '1s') });
const handle = await t.resume('nap-run-1');
console.log(handle.status);
async signalRun(runId: string, name: string, payload?: unknown, opts: { store?: WorkflowStore; } = {}): Promise<void>

Deliver an external signal to a parked run and wake any in-process run() waiting on it. Does not drive the run — call resume() (or let the waiting run() continue) to process the delivery.

import { durableTask } from 'fino:task/durable';

const t = durableTask({ name: 'gate', run: async (_i, ctx) => ctx.waitForSignal('go') });
await t.signalRun('gate-run-1', 'go', { ok: true });

Functions

function durableTask< Input = unknown, Output = unknown >(opts: DurableTaskOptions<Input, Output>): DurableTask<Input, Output>

Create a durable task.

import { durableTask } from 'fino:task/durable';

const t = durableTask({
  name: 'compact',
  run: async (_input, ctx) => ctx.step('work', () => 42),
});