Millstream vs Temporal
Temporal is a durable execution platform: a workflow is ordinary code whose event history is replayed to rebuild its state after a failure. Millstream is a pipeline platform: a declarative graph of steps connected by persisted streams of items, where some steps are code, some are LLM agents and some wait for a person.
At a glance
| Millstream | Temporal | |
|---|---|---|
| Definition | pipeline.yaml: triggers, streams, steps, CEL conditions |
Workflow and activity code, with SDKs in Go, Java, TypeScript, Python and other languages |
| Unit of durable work | One delivery of one item to one step | One workflow execution and its event history |
| Durability mechanism | A database item log with fenced leases; outputs publish when a delivery is done | Replay of the workflow's event history through deterministic workflow code |
| Code changes mid-run | Runs pinned to the content hash of their package; millstream rollback reactivates a revision |
Workflow code changes that affect in-flight executions need versioning |
| Agents | Agent actions with output schemas, a sandbox, transcripts, budgets and evals | Activities that call a model, written by you |
| Human decisions | review builtin with an inbox, editable fields, reason codes and timeouts |
Signals and updates; the inbox and its UI are yours to build |
| Laptop | millstream run: engine, node, SQLite, API and UI in one process |
The CLI's development server, with your workers beside it |
| Licence | Apache-2.0 core, commercial edition | MIT |
| Status | Pre-release: M1 to M5 built and tested | Released, widely used |
Where the models differ
Steps read and write streams
Millstream steps never reference each other: they read and write named streams. Several crawlers writing one stream merge their output, several steps reading one stream each receive every item, and inserting a step means pointing it at an existing stream:
dedupe:
action: dedupe
in: listings.raw
out: listings.unique
config: { key: "item.addressNorm + ':' + string(item.surfaceM2)", window: 30d }
In Temporal the same per-item fan-out is modelled by you, as child workflows or as activities over batches. Millstream records each item with its parents, so the UI shows the lineage of any report back to the listing it came from, and millstream replay --step score --since 7d runs a week of past items through a changed step.
No determinism rules on your code
Millstream never replays action code. An action is any program that reads JSON lines on stdin and writes them on stdout, so it may read the clock, call the network or change between releases. Idempotency comes from deterministic item ids and an idempotencyKey per delivery, stable across retries and new on replay. The trade-off is explicit: delivery is at-least-once, and an external system enforces the key.
Agents, people and cost are part of the model
An agent step declares its model, tools, network allowlist, maxTurns and maxCostUsd in its manifest. Its answer is validated against an output schema before it goes anywhere, and its transcript is stored with the attempt. A pipeline's budget.runUsd and budget.dayUsd are enforced by reservation at dispatch, so an agent delivery is leased only while the budget has room. A review step parks items in an inbox and measures the intervention rate per step. In Temporal each of these is application code you write and maintain.
When to choose Temporal
- Your workflows need general control flow: loops, data-dependent branching, recursion, a dynamic number of steps. Millstream's graph is static, and a cycle is a
checkerror. - You wait on timers and external events for days or months. Millstream's only first-class wait is a human review; a delay or a callback is a new run started by a cron or webhook trigger.
- Your team prefers workflows as code in its own language, with typed calls between workflows.
- You want a hosted service, or a platform with years of production use. Millstream is self-hosted only and has not had its first release.
Using them together
The two meet over HTTP. A Temporal activity can start a Millstream run through a webhook trigger, POST /hooks/<pipeline>/<trigger>, passing an Idempotency-Key header: a retried activity sending the same key gets back the run it already fired. In the other direction, a Millstream action declared with effects: external can start a Temporal workflow and use the delivery's idempotencyKey as the workflow id, so a retried delivery finds the workflow it started.