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.
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 |