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.

— JFA