# Durable Execution: Flow Steps & Checkpoint

**Status:** Design proposal — not yet implemented.

This note sketches two related changes:

1. Give **flow** a real step model — a flow is a *task* made of *ordered
   steps* — so it becomes the deterministic-workflow engine it has always
   claimed to be (today it runs a single LLM step per event).
2. Introduce **`Checkpoint`**, a pluggable durability primitive that
   persists run progress and resumes after a crash. Store-backed by
   default; both flow and agent use it.

The two are designed together because a step boundary is the natural
place to checkpoint.

---

## Motivation

A flow or agent run is long, expensive, and has side effects partway
through (it sent an email at step 2, charged via x402 at step 4). Today
all in-flight state lives in process memory: a crash loses the run, and
re-running from the top repeats the side effects.

Durable execution means the run survives a crash and **continues from
where it stopped**, without re-doing completed steps.

This is squarely a distributed-systems concern — checkpoint state, replay
on restart, pluggable backend — i.e. go-micro's kind of problem, built on
primitives it already has (`store`, `WrapTool`, `call.ID`).

---

## What flow is today (for contrast)

`flow` is a concrete `*Flow` struct. Per broker event, `Execute` runs
**one** augmented-LLM turn (a single `Generate` with services as tools)
or dispatches the event to an agent, records one `Result`, and returns.
There is no notion of a task with ordered steps, no carried state, no
checkpoint. The step model below generalizes today's behavior: a flow
with one step == current flow.

---

## Core concepts

### State

What carries across steps. **A struct, not a map** — a typed `Data`
plus a `Stage` marker so you can always tell where a run is.

```go
type State struct {
    Stage string // name of the step the run is at — where it is
    Data  []byte // carried data, serialized; use Set / Scan
}

// Set replaces the data with the JSON encoding of v.
func (s *State) Set(v any) error
// Scan decodes the data into v (a pointer to the caller's struct).
func (s State) Scan(v any) error
```

The developer defines their own data struct and threads it through
with `Set`/`Scan` — type-safe at the edges, serializable in the middle
(which is what makes checkpointing possible). `Stage` is the readable
"where am I"; the engine also uses it as the resume point.

The trigger event seeds the first `State`.

### Step

The unit of a flow. **One kind** — a struct with a name, the action to
run, and an optional retry override. No per-kind constructors.

```go
type StepFunc func(ctx context.Context, in State) (State, error)

type Step struct {
    Name  string
    Run   StepFunc
    Retry int // optional per-step override of the flow's retry (0 = use flow default)
}
```

Common actions are **helpers that return a `StepFunc`**, dropped into
`Step.Run` — so there is still one `Step` type, and the actions compose:

```go
flow.Call(service, endpoint) StepFunc // one RPC to a service
flow.LLM(opts...)           StepFunc // one augmented-LLM turn
flow.Agent(name)            StepFunc // dispatch to a registered agent
// …or write your own StepFunc.
```

Steps are **authored by the developer** and run in order. That ordering
is the defining difference from an agent, where the *model* chooses the
steps.

### Run

The persisted record of one execution — what `Checkpoint` saves and
loads. Retained for success and failure (see retention below).

```go
type Run struct {
    ID      string       // durable run id (idempotency root)
    Flow    string       // flow name
    State   State        // carried data + Stage (where it is)
    Steps   []StepRecord // per-step status + outcome (history/audit)
    Status  string       // running | done | failed
    Started time.Time
    Updated time.Time
}

type StepRecord struct {
    Name    string
    Status  string // pending | in_progress | done | failed
    Attempts int   // how many tries this step took
    Result  string // short serialized outcome / summary
    Error   string
}
```

The resume point is `State.Stage` — there is no separate numeric cursor,
so there is one source of truth for "where it is."

### Checkpoint

The pluggable durability primitive. Persists and restores a `Run`.

```go
type Checkpoint interface {
    Save(ctx context.Context, run Run) error
    Load(ctx context.Context, runID string) (Run, bool, error)
    Delete(ctx context.Context, runID string) error
}
```

The built-in implementation is **store-backed** and on by default, keyed
in the store:

```
database "flow", table "{name}", key {runID}   →   JSON(Run)
```

Runs are confined to their own **store table** — database `flow`, one
table per flow name — via `store.Scope`, not a single shared table keyed
by prefix. `StoreCheckpoint(s, scope)` takes that scope; the flow passes
its name by default. `store.Scope` injects the database/table per
operation, so it doesn't mutate or race on the shared store (the way
`Init(Table(...))` would). Because it rides on `store.Store`, the storage
is also pluggable (Postgres, NATS KV, file) with no extra interface.

**Retention:** completed runs (success *and* failure) are **kept** by
default, so you have a durable history of what ran. `Delete` is only
called when the flow opts in with `flow.DeleteOnSuccess()` (failures are
always kept).

---

## The run loop

```
run := load(runID) or new Run{State: {Stage: steps[0].Name, ...}}

start := index of step named run.State.Stage
for i := start; i < len(steps); i++ {
    step := steps[i]
    run.Steps[i].Status = "in_progress"; checkpoint.Save(run)

    out, err := runWithRetry(ctx, step, run.State, retriesFor(step))
    run.Steps[i].Attempts = attemptsTaken
    if err != nil {
        run.Steps[i].Status = "failed"; run.Steps[i].Error = err
        run.Status = "failed"; checkpoint.Save(run)   // kept for audit
        return err                                    // resumable: retry resumes here
    }

    run.State = out
    run.Steps[i].Status = "done"
    if i+1 < len(steps) {
        run.State.Stage = steps[i+1].Name             // <-- checkpoint boundary
    } else {
        run.State.Stage = ""                          // finished
    }
    checkpoint.Save(run)
}

run.Status = "done"; checkpoint.Save(run)
// Delete only if flow.DeleteOnSuccess() was set.
```

On restart, `Load` returns the `Run`; the loop resumes at the step named
`run.State.Stage`, so completed steps are skipped — their effects already
happened and their output is already in `run.State.Data`.

### Retry

Flow-level by default, per-step override when needed (e.g. a tool that
times out):

```go
flow.Retry(2)                 // flow-level default for every step
flow.Step{Name: "charge", Run: …, Retry: 0}  // override: never retry this one
```

`retriesFor(step)` uses `step.Retry` if set, else the flow default.

### Idempotency (the honest part)

True exactly-once is impossible if a crash lands *inside* a step. What we
provide is at-least-once + a stable **idempotency key** per step:
`runID + stepName`. That key is passed to the tool as `call.ID`, so a
replayed call is recognized downstream and de-duplicated. Side-effecting
steps must cooperate (honor the key). The framework makes this
consistent; it cannot make it free.

Retry uses the same key, so a retried step is de-duplicated the same way.
This is where the existing `WrapTool` seam pays off: a durable wrapper
checks the checkpoint — if this `call.ID` already has a recorded result,
return it without re-calling.

---

## Agent reuse

The agent loop is the **self-directed** analogue and uses the same
`Checkpoint`. The difference is who authors the steps:

| | Steps authored by | Steps known | Durability |
|---|---|---|---|
| **flow** | developer | up front (ordered list) | checkpoint between steps |
| **agent** | the model | discovered at runtime | checkpoint each LLM turn + its tool calls |

For the agent, `Run.Steps` grows as the model acts, instead of being
predefined. One requirement: the agent must own its loop (today the
provider drives it), so it can `Save` between turns. That is the one
structural change on the agent side.

---

## Pluggability — two levels

1. **Storage (free today).** Built-in `Checkpoint` over `store.Store`;
   swap the store backend. Covers "checkpoint to my DB instead."
2. **Engine (future).** Because steps are now explicit and named, a flow
   can be mapped onto an external durable-execution engine — each `Step`
   becomes a Temporal activity / Restate handler — by providing an
   alternative runner. Most users only need level 1; level 2 exists so
   teams already running Temporal aren't forced off it.

The explicit step model is what makes level 2 possible later; we don't
build it now.

---

## Proposed API

```go
type Onboarding struct {
    Email       string `json:"email"`
    WorkspaceID string `json:"workspace_id"`
}

f := flow.New("onboard-user",
    flow.Trigger("events.user.created"),
    flow.Retry(2), // flow-level retry default
    flow.Steps(
        flow.Step{Name: "plan", Run: flow.LLM(flow.Prompt("Plan onboarding for {{.Email}}"))},
        flow.Step{Name: "workspace", Run: flow.Call("workspace", "Workspace.Create")},
        flow.Step{Name: "welcome", Run: flow.Agent("comms")},
    ),
    // Durable by default (store-backed); runs are retained for audit.
    flow.WithCheckpoint(flow.StoreCheckpoint(service.Options().Store, "onboard-user")),
)
f.Register(reg, broker, client)
```

A single-step flow keeps today's behavior, so this is additive.

---

## Decisions (resolved)

- **State is a struct, not a map** — typed `Data` + `Stage`. The
  developer defines the data struct; `Stage` doubles as the resume
  point, so there is one source of truth for position.
- **One `Step` kind** — a struct with `Name`, `Run`, and an optional
  `Retry`. Common actions are `StepFunc` helpers (`Call`, `LLM`,
  `Agent`), not separate step constructors.
- **Runs are retained** for success and failure by default;
  `flow.DeleteOnSuccess()` opts into cleanup (failures always kept).
- **Retry is a flow-level option** (`flow.Retry(n)`), with a per-step
  `Retry` field as a fine-grained override.

---

## Scope & phasing

1. **Step model in flow** (no durability yet): `State`, `Step`, ordered
   `Steps`, the run loop, retry. Single-step flows unchanged.
2. **`Checkpoint` + store-backed default**: persist/resume flow runs,
   retention.
3. **Agent durability**: move the agent loop in-package, reuse
   `Checkpoint`. Opt-in (`AgentDurable()`), default off — overkill for
   short interactive chats, essential for long unattended runs.
4. **Engine-level pluggability** (Temporal/Restate): only if demand.

Each phase is independently useful and shippable.
