Engineering · Jean-François Arcand
Run Atmosphere workflows on Temporal, by swapping one dependency
The durable-execution engine behind Workflow.run() is now a
ServiceLoader seam. The first external engine to plug into it: Temporal.
Atmosphere ships a small durable-workflow primitive: an ordered list of named
steps over an application-owned state type, checkpointed after every step
through the CheckpointStore SPI. Its signature move is
hibernation as a return, not a parked thread — a step that needs a
human returns hibernate(state), the call returns, and a later
run() picks up at the step after the last completed one, even in
a different JVM when the store is persistent:
var workflow = new Workflow<>(
"doc-pipeline", "coord-123",
List.of(
step("ingest", s -> StepOutcome.advance(s + ":ingested")),
step("review", s -> StepOutcome.hibernate(s)), // wait for a human
step("publish", s -> StepOutcome.done(s + ":published"))),
store);
var result = workflow.run("doc-42");
// → WorkflowResult.Hibernated — no thread held while we wait for the human.
// Later — same JVM or a fresh one, same store + coordinationId:
var resumed = workflow.run(null);
// → WorkflowResult.Completed; only 'publish' runs.
That in-tree step engine is deliberately boring: a loop, a retry budget per
step, a snapshot per step. But plenty of teams already operate a durable
execution platform — Temporal most commonly — and asking them to run a second
workflow engine next to it is a hard sell. So the engine behind
Workflow.run() is now a seam. Every call resolves a
DurableExecutionProvider via ServiceLoader; the
in-tree engine is the always-on fallback, and an external adapter takes over
when one is registered and actually reachable:
<!-- The workflow above does not change. This line is the whole migration: -->
<dependency>
<groupId>org.atmosphere</groupId>
<artifactId>atmosphere-checkpoint-temporal</artifactId>
</dependency> Who owns what
With the adapter on the classpath, Temporal owns what Temporal is good at:
per-step retries (your step's maxRetries() and
retryDelay() translate to Temporal retry policies with fixed
backoff), step timeouts, and the execution history — every run shows up in
the Temporal Web UI with its input, its per-step activities, and its terminal
result, next to whatever else your organization already runs there.
Atmosphere keeps what Atmosphere promised: the CheckpointStore
snapshot trail, hibernation, and cross-restart resume. The adapter writes the
exact trail the in-tree engine writes — same metadata keys, same
seed snapshot, same resume rule — so the two engines share one checkpoint
contract. A test pins the two trails as equivalent — the same ordered sequence
of (last-completed-step, done) entries the resume rule reads.
One design choice does most of the work here: your steps execute as Temporal
activities in the JVM that called run(),
against the live step lambdas. Application state never crosses the Temporal
payload boundary — which means adding the adapter imposes
no serialization constraints on your state type. Nothing
about S needs to become a DTO.
What it deliberately does not do
Honesty section. Because steps run against live lambdas, a run orphaned by a
JVM restart fails its next activity fast (SessionNotFound)
rather than hanging — and the application resumes by calling
run() again, which continues after the last checkpointed step.
That is the same restart contract as the in-tree engine, deliberately. What
the adapter does not give you is cross-JVM continuation of an
in-flight run — a worker fleet picking up mid-run — and it does not do
wall-clock scheduling: firing run() on a schedule remains your
host's job. Availability is runtime truth, too: the Temporal provider is
selected only after a health-checked connection succeeds, so an unreachable
server means the in-tree engine runs, automatically, with no configuration.
Proof, not promises
The adapter's test suite drives Workflow.run() end-to-end on
Temporal's in-process test service: completion, hibernate-and-resume with
exact step-execution counts, retry budgets honored to the attempt, explicit
failures propagated verbatim, snapshot-trail parity with the in-tree engine,
and fallback when no server is reachable. One assertion is worth calling
out: a step calls Activity.getExecutionContext() — which throws
anywhere except inside a real Temporal activity — so the suite can't pass on
a silent fallback.
On top of that sits a Playwright lane in CI that boots an actual
temporal server start-dev, runs a workflow through the adapter,
then opens Temporal's own Web UI and asserts what an operator would see: the
execution row, the AtmosphereTemporalWorkflow type, the
atmosphere-workflow task queue, the per-step activities, and
the terminal result. The first live run completed in 243 ms, and the
history reads exactly like the unit tests said it would.
The agent side of the story: the session tape
Temporal's UI answers "what did the engine do" — retries, timeouts,
history. For AI agents there is a second question: what did the
model see and say? Atmosphere records that separately. Flip
atmosphere.ai.tape.enabled (off by default) and every streaming
AI session records its typed event stream — prompts, completions, tool
calls — as an append-only, per-run tape, bounded in memory
or crash-durable on SQLite.
A recorded run can be reconstructed after the fact with no model call — including a whole multi-agent coordination tree, linked run-to-run. And the Atmosphere Console makes it visual: a Tape tab lists recorded runs and their step streams, and replaying a coordinator run draws the coordination tree as an interactive node graph — coordinator to agents — where clicking a node opens that run's steps. The tab only appears when a recorder is actually installed at runtime, the read endpoints are admin-gated and default-deny, and because each completed run is a (prompt → completion) record, the tape doubles as chat-format training data for distilling a smaller model.
Engine history in Temporal's UI, agent history in Atmosphere's Console — both from the same one-dependency posture.
The same seam is open for DBOS and Restate adapters — same interface, same contract, same parity tests to pass.
- atmosphere-checkpoint-temporal — module README
- Durable Checkpoints reference
- Session Tape reference
- Session Tape & Replay tutorial
- github.com/Atmosphere/atmosphere
— JFA