# Execution Primitives (/docs/sdks/workflow-primitives)



An SDK workflow body is an ordinary function. In Go it has the signature
`func(ctx ductor.Ctx) error`; you register it on a `Client` and it becomes a
durable, replayable workflow without any orchestration code of your own:

```go title="place_order.go"
client.Workflow("place-order", ductor.WorkflowOpts{
    Trigger: ductor.Trigger{Event: "order.placed"},
}, func(ctx ductor.Ctx) error {
    order, err := ductor.Step(ctx, "charge-card", func() (Receipt, error) {
        return billing.Charge(ctx.Event())
    })
    if err != nil {
        return err
    }

    if err := ductor.Sleep(ctx, "settle-delay", 24*time.Hour); err != nil {
        return err
    }

    _, err = ductor.Step(ctx, "ship", func() (Shipment, error) {
        return fulfil.Ship(order)
    })
    return err
})
```

What makes this durable is not the function — it is the **step helpers** it calls.
Each one is a generator: the first time control reaches it, it *yields* an opcode
to the bridge and the invocation ends; on the next invocation it *replays* its
recorded result and returns immediately. The function runs from the top every
time, but each step executes exactly once.

## The Ctx handle [#the-ctx-handle]

`Ctx` is the per-invocation context handed to your workflow body
(`sdks/go/ductor/ctx.go`). It exposes exactly three things:

| Method      | Returns           | Use                                                            |
| ----------- | ----------------- | -------------------------------------------------------------- |
| `Event()`   | `map[string]any`  | The trigger event payload that started the run.                |
| `History()` | `[]MemoizedStep`  | The recorded results of steps from prior invocations.          |
| `Context()` | `context.Context` | The underlying request context for cancellation and deadlines. |

You rarely touch `History()` directly — the step helpers read it for you. It is
the raw material of replay: a slice of `MemoizedStep` records, each carrying a
deterministic step `ID`, the originating `Op`, and either a JSON `Output` or an
`Error`.

## The step primitives [#the-step-primitives]

Every helper takes the `Ctx`, a human-readable `name`, and produces (or replays)
a single opcode. The generic ones decode the replayed output into a type of your
choosing.

| Helper             | Signature (Go)                                     | Opcode yielded            | Purpose                                 |
| ------------------ | -------------------------------------------------- | ------------------------- | --------------------------------------- |
| `Step[T]`          | `Step(ctx, name, func() (T, error)) (T, error)`    | `step.run` / `step.error` | Run code once, capture its output.      |
| `Sleep`            | `Sleep(ctx, name, d time.Duration) error`          | `sleep`                   | Durable timer for a relative delay.     |
| `SleepUntil`       | `SleepUntil(ctx, name, t time.Time) error`         | `sleep`                   | Durable timer to an absolute time.      |
| `WaitForEvent[T]`  | `WaitForEvent(ctx, name, opts) (T, error)`         | `wait_for_event`          | Park until a matching event arrives.    |
| `WaitForSignal[T]` | `WaitForSignal(ctx, name, opts) (T, error)`        | `wait_for_signal`         | Park until a named signal is delivered. |
| `Invoke`           | `Invoke(ctx, name, opts) (json.RawMessage, error)` | `invoke_function`         | Start a child workflow run.             |

`Sleep`, `WaitForEvent`, and `WaitForSignal` map onto the same durable
timer/signal machinery the engine uses for native
[wait steps](/docs/concepts/dag-workflow-model#waits-signals-and-human-steps);
`Invoke` starts a child run the way a native `subworkflow` step does. The SDK is
just another way to express the same primitives.

## Deterministic step IDs [#deterministic-step-ids]

Each helper computes a step ID before it does anything else. The ID is
`sha256(name + ":" + index)`, truncated to its first 8 bytes and hex-encoded,
where `index` is the position of this step in the current invocation
(`stepID` in `ctx.go`):

```go
func stepID(name string, index int) string {
    h := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", name, index)))
    return hex.EncodeToString(h[:8])
}
```

The index is what keeps IDs stable across replays *and* unique when the same name
appears twice. A cursor advances by one every time a step helper is reached, in
program order — so as long as your workflow body reaches its steps in the same
order each invocation, every step lands on the same ID it had before.

<Callout type="warn" title="Determinism is a contract you keep">
  The ID scheme only works if the *sequence* of step calls is deterministic.
  Branching on the wall clock, a random number, or unmemoized external state can
  shift a step's index between invocations, break its ID, and re-run work that
  already ran. Read those values *inside* a `Step` so the result is captured in
  history and replayed identically.
</Callout>

## Memoization and replay [#memoization-and-replay]

Here is the mechanism, step by step. When a helper is reached:

1. It computes its deterministic ID from `name` and the current cursor index.
2. It scans `History()` for a memoized record with that ID.
3. **On a hit** — the step already ran on a prior invocation — it rehydrates the
   recorded value. `Step[T]` unmarshals the stored JSON `Output` into `T` and
   returns it; a recorded `Error` is returned as an error. &#x2A;*Your function is
   never called.**
4. **On a miss** — this is the first time the step is reached — it runs your
   function, marshals the result, and yields a `step.run` opcode (or `step.error`
   if the function failed). Yielding ends the invocation right there.

```text
invocation 1:  charge-card → MISS → run billing.Charge(), yield step.run, STOP
                             (engine persists the receipt to history)
invocation 2:  charge-card → HIT  → replay receipt, DO NOT re-charge
               settle-delay → MISS → yield sleep, STOP
invocation 3:  charge-card → HIT  → replay receipt
               settle-delay → HIT → timer elapsed, return
               ship         → MISS → run fulfil.Ship(), yield step.run, STOP
invocation 4:  all HIT, function returns nil → run_complete
```

The card is charged once, in invocation 1. Every later invocation replays that
receipt from history and moves on. That is what "durable" and "exactly once"
mean here: the workflow function is re-executed many times, but each *side
effect* happens exactly once because it is guarded by a memoized step.

<Callout type="info" title="How yielding works under the hood">
  Go has no generator syntax, so the SDK yields by panicking with an internal
  sentinel that `Workflow.Run` recovers (`safeCall` in `client.go`). You never
  see this — it is why a step helper "returns" by ending the whole invocation
  rather than the next line of your function. A clean return from the function
  yields the terminal `run_complete` opcode; a returned error yields
  `step.failed`.
</Callout>

## Related [#related]

<Cards>
  <Card title="Bridge protocol" href="/docs/sdks/bridge-protocol">
    The opcodes these primitives emit, the actions that carry them, and how the
    bridge verifies each request.
  </Card>

  <Card title="The DAG workflow model" href="/docs/concepts/dag-workflow-model">
    The native step vocabulary these primitives mirror — waits, signals,
    sub-workflows, and the guarantees behind them.
  </Card>
</Cards>
