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 aworkflows/ directory next to functions/. One file,
one default-exported workflow(...):
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:
waitForEvent the same way, for
example from the webhook action that received the confirmation:
<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 ofstep, sleep, or waitForEvent calls must be
identical on every replay, for the same input and step outputs.
- Branch on
wf.inputand 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):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.