Skip to main content
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(...):
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:
Deliver an event to a run paused on waitForEvent the same way, for example from the webhook action that received the confirmation:
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):
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.