> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Durable Workflows

> Multi-step processes that survive restarts: completed results replay, sleeps span days, and events resume a paused run.

A workflow is a TypeScript function that composes `step`, `sleep`, and
`waitForEvent` calls. The engine persists every step result, so a
workflow survives server restarts and deploys. Completed steps never
re-run. A 7-day `sleep` costs nothing while it waits. An external event
wakes a paused run exactly where it stopped.

Use a workflow when a process spans more time than one function call
should hold:

* onboarding sequences
* agent pipelines with human approval gates
* billing dunning
* report generation with retries

## Writing a workflow

Workflows live in a `workflows/` directory next to `functions/`. One file,
one default-exported `workflow(...)`:

```ts theme={null}
// workflows/onboarding.ts
import { workflow } from "@pylonsync/functions";

export default workflow("onboarding", async (wf, ctx) => {
  // A completed step returns its recorded output during replay.
  const user = await wf.step("load-user", () =>
    ctx.runQuery("getUser", { id: wf.input.userId }),
  );

  await wf.step("send-welcome", () =>
    ctx.email.send({
      to: user.email,
      subject: "Welcome!",
      text: "Glad you're here.",
    }),
  );

  // Pauses the workflow. No machine time is spent waiting.
  await wf.sleep("24h");

  // Pauses until POST /api/workflows/<id>/event delivers this event.
  const confirmation = await wf.waitForEvent("email_confirmed");

  return { done: true, confirmation };
});
```

Each step's closure runs with a full action `ctx`: `ctx.runQuery`,
`ctx.runMutation`, `ctx.llm`, `ctx.email`, and `ctx.scheduler`. It runs
under the same idle timeout and cancellation semantics as any action.

## Starting and driving

Start a workflow from any mutation or action. `ctx.workflows.start`
returns the instance id immediately, and the engine's background driver
takes over from there:

```ts theme={null}
// functions/onSignup.ts
export default mutation({
  async handler(ctx, args) {
    const user = await ctx.db.insert("User", args);
    const { id } = await ctx.workflows.start("onboarding", {
      userId: user.id,
    });
    return { userId: user.id, onboardingWorkflow: id };
  },
});
```

Deliver an event to a run paused on `waitForEvent` the same way, for
example from the webhook action that received the confirmation:

```ts theme={null}
await ctx.workflows.sendEvent(workflowId, "email_confirmed", { ok: true });
```

Steps execute through the background job queue, and sleeps wake on
schedule. SQLite stores state in `<app-db>.workflows.db`. Postgres
stores state in shared application tables. The admin API drives the same engine for operators:
`POST /api/workflows/start`, `POST /api/workflows/<id>/event` (both
with `Authorization: Bearer $PYLON_ADMIN_TOKEN`).

Inspect runs at `GET /api/workflows` or `GET /api/workflows/<id>`
(step-by-step results, timings, retry counts), or cancel a run with
`POST /api/workflows/<id>/cancel`.

## The determinism contract

On every advance, the whole workflow function re-runs from the top.
Completed steps replay from their recorded outputs. This gives you plain
TypeScript control flow (branches, loops, early returns), with one rule:

**The sequence of `step`, `sleep`, or `waitForEvent` calls must be
identical on every replay, for the same input and step outputs.**

* Branch on `wf.input` and on step outputs freely. Both are stable.
* Never branch on wall-clock time, randomness, or external state read
  outside a step. Put those reads inside a step, then branch on its
  recorded output.
* Step names must be unique within a run. The replay cache is
  name-keyed, so a mismatch fails the run loudly rather than reusing
  the wrong output.

## Retries and failure

A throwing step fails the current advance. The engine retries the same
step (default 3 attempts, configurable per workflow):

```ts theme={null}
export default workflow("sync-crm", handler, { maxRetries: 5 });
```

Once retries are exhausted, the run lands in `failed` with the error and
the step that caused it, inspectable via the API. Code between steps
should be side-effect free.

Workflow step execution is at least once. A step can finish an external
side effect and stop before Pylon records its result. Pylon can then run
that step again. Make step bodies idempotent when they call an external
system.

## Multiple replicas

Postgres replicas share workflow state. A replica takes a short lease
before it advances a run. It renews the lease while the handler runs. A
lease token prevents a stale worker from replacing newer state. Another
replica resumes the run after an expired lease.

SQLite workflow state is local to one machine. Use Postgres for
horizontal scaling.
