A step is a named unit of work whose outcome EZGH records once. On every later invocation, the step returns its recorded outcome without running again.
Primitives
| TypeScript | Go | Python | What it does |
|---|---|---|---|
step.run(name, fn, opts?) |
workflows.Run(ctx, name, fn, opts...) |
ctx.step.run(name, fn, ...) |
Runs fn at least once and records its JSON result |
step.sleep(name, "3d"), step.sleepUntil(name, at) |
workflows.Sleep(ctx, name, d), workflows.SleepUntil(ctx, name, t) |
ctx.step.sleep(name, "3d"), ctx.step.sleep_until(name, when) |
A durable timer, at most a year |
step.now(name), step.uuid(name) |
workflows.Now(ctx, name), workflows.UUID(ctx, name) |
ctx.step.now(name), ctx.step.uuid(name) |
A time or UUID, recorded once and the same on every replay |
step.createCallback(name, opts?) |
workflows.CreateCallback(ctx, name, timeout) |
ctx.step.create_callback(name, ...) |
A URL that completes the step (Callbacks) |
step.invoke(name, { workflow, input }) |
workflows.Invoke[T](ctx, name, workflow, input) |
ctx.step.invoke(name, workflow=…, input=…) |
Runs a child workflow and waits for it (Child workflows) |
Steps started together run in parallel: see Parallel steps.
import { workflow } from "@ezgh/workflows"
export const onboarding = workflow("onboarding", async ({ input, step }) => {
const user = await step.run("load-user", () => db.users.get(input.userId))
await step.run("welcome-email", () => email.send(user.email, "Welcome!"))
await step.sleep("cooldown", "3d")
return step.run("follow-up", () => email.send(user.email, "How is it going?"))
})How steps are identified
- A step is identified by its name and how many times the name was used before in the run. A
loop calling
step.run("send", …)three times gets three distinct steps: the first, second and thirdsend. - Names are 1 to 256 bytes of UTF-8, without NUL characters.
- An explicit ID (
{ id }in TypeScript,WithIDin Go,id=in Python) names one step for the whole run, and matches^[A-Za-z0-9._:-]{1,128}$. Using the same explicit ID twice fails the run withworkflows.duplicate_step_name. - Steps pending at the same time need distinct names (or explicit IDs). Otherwise the run
fails with
workflows.duplicate_step_name.
Because of the counter, removing or adding a step with a repeated name shifts the later ones:
if you delete the first of three send steps from your code while runs are in progress, the
second send of those runs gets the recorded result of the old first one. Give steps in loops
explicit IDs, such as send-${recipient.id}, when the code may change while runs are in progress.
Values are JSON
A step’s result, a workflow’s input and output, and a callback’s body are JSON, at most 1 MiB each. The SDK passes a step’s result through JSON before returning it, so the first execution and every replay see the same value:
- TypeScript: a
Datebecomes an ISO string, andundefinedbecomesnull. A function, symbol or bigint fails the step without retrying. - Go: the result is encoded to JSON and decoded into the step’s type
T. - Python:
dictwith string keys,list,tuple(as a list),str,int, finitefloat,bool,None, an awaredatetimeand adate. Anything else, such as a naivedatetime,Decimal,setorbytes, fails the step without retrying.
A result over 1 MiB fails the step with workflows.output_too_large, without retrying. Keep large
data elsewhere and return a reference to it.
Rules for workflow code
- Code outside steps runs again on every invocation, so it must have no side effects and take the same path every time. Put every call to another system in a step.
- Use the
nowanduuidsteps instead of the clock and random numbers. - In Go, call steps only from the workflow’s goroutine, or from a
Parallel,AllorRacebranch with the branch’s context, and don’t depend on map iteration order. When an invocation suspends (at a sleep, a wait or parallel steps), the workflow’s goroutine ends, so its deferred functions run. - In Python, an
async defworkflow runs on an event loop of its own in a dedicated thread, and must not await objects bound to your server’s loop outside steps. A plaindefworkflow runs in a dedicated thread, and its step calls block. - A step’s function can’t call steps. Calling one from inside a step’s function is an error.
Inside a step, the step’s context gives its attempt number and an idempotency key; see Retries and idempotency.