---
title: "Steps and determinism"
description: "The step primitives, how steps are identified, and the rules your workflow code follows."
---

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

# Steps and determinism

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](/workflows/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](/workflows/child-workflows)) |

Steps started together run in parallel: see [Parallel steps](/workflows/parallel-steps).

```ts
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
  third `send`.
- **Names are 1 to 256 bytes of UTF-8,** without NUL characters.
- **An explicit ID** (`{ id }` in TypeScript, `WithID` in 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 with `workflows.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 `Date` becomes an ISO string, and `undefined` becomes `null`. 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:** `dict` with string keys, `list`, `tuple` (as a list), `str`, `int`, finite `float`,
  `bool`, `None`, an aware `datetime` and a `date`. Anything else, such as a naive `datetime`,
  `Decimal`, `set` or `bytes`, 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 `now` and `uuid` steps** instead of the clock and random numbers.
- **In Go,** call steps only from the workflow's goroutine, or from a `Parallel`, `All` or `Race`
  branch 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 def` workflow 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 plain `def` workflow
  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](/workflows/retries).

Source: https://docs.ezghcloud.com/workflows/steps/index.mdx
