* docs: design note for flow steps + Checkpoint durable execution
* docs: fold in durable-execution decisions (State struct, single Step, run retention, retry)
* docs: rename State.Payload to State.Data
* feat(flow): ordered steps + Checkpoint durable execution
A flow can now be an ordered list of steps (a task with stages) instead
of a single LLM turn. State carries typed Data plus a Stage marker; each
step is checkpointed before and after via a pluggable Checkpoint
(store-backed by default), so a run survives a crash and resumes where it
stopped without re-running completed steps. Flow-level Retry with a
per-step override; runs retained for audit unless DeleteOnSuccess.
Step actions: Call (RPC), LLM (augmented turn), Dispatch (to an agent),
or any StepFunc. Single-step and agent-dispatch flows are unchanged.
* feat(flow): top-level re-exports + durable flow example
Expose the step/checkpoint API from the micro package (FlowSteps,
FlowStep, FlowState, FlowRetry, FlowWithCheckpoint, FlowCall/LLM/Dispatch,
Checkpoint, StoreCheckpoint) and add a runnable, key-free example
demonstrating crash + resume.
* docs: document durable flow steps (guide, README, CLI help)
* docs: blog post + changelog for durable workflows
* fix(flow): scope checkpoint keys by flow name (flow/{name}/runs/{id})
Run keys were flow/runs/{id} — a single global keyspace shared by every
flow on the default store. Namespace them by flow name so each flow's
state is kept apart. StoreCheckpoint now takes a scope argument (the flow
passes its name by default).
* feat(store): Scope handle; scope agent and flow state by name
Add store.Scope(s, database, table) — a store handle that confines every
operation to a database/table without mutating the shared store, so
co-located components don't clobber each other's table (the failure mode
of the global Init(Table(...)) approach).
Use it to keep each agent's memory and plan in its own table
(agent/{name}) and each flow's runs in its own (flow/{name}), instead of
one global table partitioned only by key prefix. Services already scope
by service name.
* feat: consistent state model — service store scoping, flow registry, list/history CLI
- service: scope store via store.Scope (database service / table name),
retiring the Init(store.Table(name)) global-mutation hack; bridge the
default store so handlers using store.DefaultStore stay isolated.
- flow: register in the registry as type=flow while running (with trigger
and step count), deregister on Stop. Live discovery, like agents.
- cli: micro flow list (registry), micro flow runs <name> (durable store),
micro agent history <name> (durable store). list = running, runs/history
= durable, mirroring the service model.
* test: mini-universe end-to-end harness + scheduled GitHub Action
internal/harness/universe boots a small but real go-micro world — four
services, a durable checkout flow that crashes at payment and resumes,
and a guardrailed agent with a tool wrapper reached over RPC — drives the
scenario, asserts the end state (10 checks), and shuts down. Everything
is real except the LLM (mocked), so it's deterministic and needs no key;
-provider anthropic runs it live. Exits non-zero on failure, so it's an
end-to-end test, not just a demo.
Adds .github/workflows/universe.yml (push/PR/daily/dispatch) running the
universe + existing harnesses on the mock provider, plus an opt-in job
that runs live when ANTHROPIC_API_KEY is set. 'make harness' runs them
locally.
* ci: run the live universe job against AtlasCloud (ATLASCLOUD_API_KEY)
* ci: run the live universe job only on schedule or manual dispatch
The deterministic mock job still runs on push/PR/daily; the live
(AtlasCloud) job runs daily and on manual workflow_dispatch only, so
changes don't burn API credits on every PR but can still be checked
against a real model on demand.
---------
Co-authored-by: Claude <noreply@anthropic.com>
5.7 KiB
layout, title, permalink, description
| layout | title | permalink | description |
|---|---|---|---|
| blog | Durable Workflows | /blog/24 | An event-driven workflow runs for minutes and has side effects partway through — it reserved stock, it charged a card. When the process dies mid-run, re-running from the top does it all again. Go Micro flows are now ordered, checkpointed steps that resume where they stopped. |
Durable Workflows
June 17, 2026 • Asim Aslam
A workflow that calls real services is rarely instant and rarely side-effect-free. It reserves inventory at step one, charges a card at step two, sends a confirmation at step three. Each of those changes the world. So when the process dies between step two and step three — a deploy, an OOM, a node going away — you can't just run it again from the top: that reserves twice and charges twice. And if the workflow was triggered by an event with no human watching, nobody noticed it died at all.
This is the oldest problem in distributed systems, and it has an established answer: durable execution — checkpoint progress as you go, and on restart resume from where you stopped instead of from the beginning. Go Micro flows now do this.
What a flow was, and what it is now
A flow used to run one augmented-LLM turn per event. Useful, but a single step — there was no notion of a task with stages, and nothing survived a crash.
A flow can now be an ordered list of steps — a task made of stages — and each step is checkpointed before and after. If the process dies mid-run, the run resumes at the step it stopped on, and the steps that already completed do not run again.
f := micro.NewFlow("checkout",
micro.FlowTrigger("events.order.placed"),
micro.FlowRetry(2),
micro.FlowSteps(
micro.FlowStep{Name: "reserve", Run: micro.FlowCall("inventory", "Inventory.Reserve")},
micro.FlowStep{Name: "charge", Run: micro.FlowCall("payment", "Payment.Charge")},
micro.FlowStep{Name: "confirm", Run: micro.FlowCall("orders", "Orders.Confirm")},
),
)
A single-step flow keeps working exactly as before; steps are additive.
How it resumes
State carries a typed payload plus a Stage marker — the name of the step the run is at. That marker is the single source of truth for "where it is," and it's the resume point. Before each step, the run is saved; after each step completes, the stage advances and the run is saved again. On restart, the engine loads the run and starts at Stage, so completed steps — and their side effects — are skipped.
Here is a run whose payment dependency is down on the first attempt:
first run:
reserve → inventory reserved
charge → payment dependency unavailable (crash)
run failed: payment gateway timeout
checkpoint: run 70643f61 is at step "charge" (status failed)
resume:
charge → payment captured
confirm → order confirmed
reserve ran 1 time(s) total — completed steps are not repeated on resume
f.Pending(ctx) lists incomplete runs after a restart; f.Resume(ctx, runID) continues one. The full example is examples/flow-durable — it needs no API key, because durability is the only thing on display.
The honest part
Exactly-once is impossible if a crash lands inside a step — you can't know whether the charge went through. What durable execution actually gives you is at-least-once delivery plus a stable idempotency key per step (runID + step name), so a replayed step is recognized and de-duplicated by the service receiving it. Side-effecting steps have to honor that key. A framework can make this consistent; it can't repeal the underlying reality, and claiming otherwise would be dishonest.
Where agents come in
Go Micro draws the line from Anthropic's taxonomy: workflows follow a predefined path; agents direct themselves. A flow is the workflow — you author the steps. An agent is the self-directed one — the model authors the steps at runtime. They are two kinds of control flow, and durability is orthogonal to both.
So a workflow step can hand off to an agent:
micro.FlowStep{Name: "resolve", Run: micro.FlowDispatch("support-agent")}
The deterministic part stays a durable flow; the open-ended part is an agent. The same Checkpoint that persists a flow run is the mechanism the agent's own loop will use to become durable too — that's the next step, and it's a bigger one, because it means the agent owning its loop rather than the provider driving it. What ships today is durable workflows that can call services and dispatch to agents.
No separate engine
The pluggability is the usual Go Micro shape. The built-in Checkpoint is store-backed — point the default store at Postgres or NATS KV and a run survives a real restart, no extra moving parts. Need more, or already run Temporal or Restate? Implement the Checkpoint interface and delegate to it; the explicit step model is what makes a flow mappable onto an external engine. Most teams need neither — the default is durable.
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
List(ctx context.Context) ([]Run, error)
}
That's the through-line. Durable execution isn't a workflow engine you adopt alongside your services; it's a store and an interface, and the workflow is still just an ordered list of steps you can read. Same as everything else in Go Micro — the abstraction is the service, and this is one more thing the substrate underneath it now handles.
See the Agents and Workflows guide for the full reference.