Temporal workflows must be deterministic: replaying a workflow's persisted Event History against its code must produce the identical command sequence. Any change that reorders activities, adds or removes steps without versioning, branches on non-deterministic inputs (wall-clock time, random values, external state), or upgrades a library that changes internal behavior will break replay — silently, until a worker tries to resume an in-flight workflow in production.
The standard mitigation is the capture-then-replay pattern:
The Temporal Java SDK ships WorkflowReplayer and WorkflowExecutionHistory for exactly this purpose. This SDK exposes them as idiomatic Clojure through two namespaces: temporal.testing.history and temporal.testing.replayer.
For background, see the Temporal Testing Suite and Event History Walkthrough in the upstream docs.
┌─────────────────────────────────────────────────────────────┐
│ 1. Run workflow in test env (or production) │
│ 2. Capture history with history/fetch or history/->json │
│ 3. Save JSON fixture to test/resources/histories/ │
│ 4. In CI: replay fixture with replayer/replay-history │
│ or replayer/replay-all for batches │
│ 5. Fail build if replay throws │
└─────────────────────────────────────────────────────────────┘
The two namespaces map cleanly onto these phases:
| Namespace | Role |
|---|---|
temporal.testing.history | Load, capture, and serialize event histories |
temporal.testing.replayer | Replay histories against current workflow code |
Require both namespaces:
(require '[temporal.testing.history :as history]
'[temporal.testing.replayer :as replayer])
Store your history fixtures under test/resources/histories/ so they land on the test classpath:
(history/from-resource "histories/order-workflow.json")
Throws a clear ex-info with {:resource name} if the file is absent.
(history/from-file "test/resources/histories/order-workflow.json")
;; also accepts java.io.File and java.nio.file.Path
(history/from-json (slurp "order.json"))
;; optional 2-arity form overrides the embedded workflow ID
(history/from-json json "my-workflow-id")
After running a workflow in a temporal.testing.env or against a real server, fetch the history and save it:
(let [h (history/fetch client "workflow-id-123")]
(spit "test/resources/histories/order.json" (history/->json h)))
->json defaults to pretty-printed output. Pass false for compact:
(history/->json h false)
Every WorkflowExecutionHistory implements clojure.core.protocols/Datafiable:
(require '[clojure.datafy :as d])
(d/datafy (history/from-resource "histories/order-workflow.json"))
;; => {:workflow-id "order-123"
;; :run-id "abc-..."
;; :events [#object[...] ...]}
Full API: temporal.testing.history
The simplest form — no setup required:
(replayer/replay-history (history/from-resource "histories/order-workflow.json"))
With a custom data converter:
(replayer/replay-history h {:data-converter my-converter})
Throws ex-info with {:workflow-id "..." :cause #<Exception>} on any non-determinism error.
When replaying multiple histories, reuse a single replayer to avoid spinning up a TestWorkflowEnvironment per call:
(with-open [r (replayer/create)]
(replayer/replay r (history/from-resource "histories/order-workflow.json"))
(replayer/replay r (history/from-resource "histories/payment-workflow.json")))
create accepts an options map:
| Key | Description | Type | Default |
|---|---|---|---|
:dispatch | Explicit workflow dispatch table | map | auto-dispatch |
:data-converter | Custom data converter | DataConverter | default |
The returned value implements java.io.Closeable, so with-open handles shutdown automatically.
Full API: temporal.testing.replayer
For CI pipelines, load all your history fixtures and replay them in one call:
(ns my.replay-test
(:require [clojure.test :refer :all]
[clojure.java.io :as io]
[temporal.testing.history :as history]
[temporal.testing.replayer :as replayer]))
(deftest replay-all-fixtures
(let [dir (io/file "test/resources/histories")
histories (map history/from-file (.listFiles dir))]
(with-open [r (replayer/create)]
(let [{:keys [total failures]} (replayer/replay-all r histories {:fail-fast? false})]
(is (zero? (count failures))
(str "Non-determinism in " (count failures) "/" total " workflows: "
(map :workflow-id failures)))))))
replay-all returns a plain Clojure map:
{:total 5
:failures [{:workflow-id "order-123" :error #<NonDeterminismException>}]}
The :fail-fast? option (default false) controls whether replay stops at the first failure or collects all failures.
For a complete runnable example, see the replay-testing sample.
Replay testing and workflow versioning work together. Whenever you make a code change that would alter the command sequence of an in-flight workflow, you must guard it with temporal.workflow/get-version. A replay test will immediately catch you if you forget:
(require '[temporal.workflow :as w]
'[temporal.activity :as a])
;; Safely change from invoke → local-invoke without breaking existing histories
(let [version (w/get-version ::local-activity w/default-version 1)]
(cond
(= version w/default-version) @(a/invoke my-activity args)
(= version 1) @(a/local-invoke my-activity args)))
See the Versioning section in the Workflows guide for the full pattern and the upstream Versioning docs.
Upgrading the Temporal Java SDK can change default behavior through internal "SDK flags" recorded in workflow histories. For example, Temporal Java SDK 1.40 enables CANCEL_AWAIT_TIMER_ON_CONDITION and VERSION_WAIT_FOR_MARKER by default. Histories recorded by older SDK versions are still replayed using their original behavior, and this SDK's test suite pins that with a history recorded under 1.38 (using get-version and a timed await) that is replayed under 1.40. We recommend the same practice before any SDK upgrade: capture histories from the current release, then run them through [[temporal.testing.replayer]] after bumping.
Workflow code changes that require versioning:
Changes that do NOT require versioning:
temporal.testing.history| Function | Description |
|---|---|
from-json | Parse a JSON string into a WorkflowExecutionHistory |
from-file | Read a history from a file path, File, or Path |
from-resource | Read a history from a classpath resource |
fetch | Fetch a history from a live WorkflowClient |
->json | Serialize a history to a JSON string |
temporal.testing.replayer| Function | Description |
|---|---|
create | Create a reusable replayer handle (Closeable) |
replay | Replay a single history; throws on non-determinism |
replay-all | Batch replay; returns {:total n :failures [...]} |
replay-history | One-shot convenience: create → replay → close |
close | Async shutdown |
synchronized-stop | Blocking shutdown |
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |