moxzi
Docs / Actor platforms / Temporal

Temporal

This host runs the actor-host contract, version 2, on top of a Temporal entity workflow per key (moxzi/Type/id) — Temporal's own per-workflow-id execution guarantee and its history replay are the per-key serialisation and the recovery story. A turn is a workflow.Go coroutine inside that one long-lived execution, not a separate workflow: the IC lets a parked actor keep taking other messages, so routing and the durable timer run beside the turn rather than blocking it. The code is moxzi/adapters/temporal (moxzi-temporal-host, Go, Temporal SDK v1.49.0), built by moxzi/conformance/hosts/temporal.json and driven by HOST=temporal scripts/host_matrix_gate.sh.

What Temporal owns, what Motoko owns#

OwnsWhoWhat
Identity, one actor per keythe entity workflowid moxzi/Type/id — Temporal's own per-workflow-id execution guarantee
One step at a time per keythe workflow's own step-mutex coroutinea capacity-1 buffered channel (stepMutex) held only around each /v2/begin/resume/remind activity call, never across a routed call's answer
A turn that parks and still takes messagesa workflow.Go coroutine per turn, inside the one entity executioncosts nothing extra in history beyond the signal that eventually unblocks it — no second workflow id to route to
Routed awaits, durablythis process, as ordinary workflow historya signal-with-start to the callee's own entity workflow, replied by a signal back; both replayed like any other event
Durable timerworkflow.NewTimercancelled and re-armed from each step's timer field; replay resumes it at its original fire time, not a fresh countdown
Placement, held keys, pending callsthe singleton moxzi/control workflowsignals to mutate, queries to read
Recovery after the host diesnothing to re-driveTemporal replays every open workflow's history against the reconnecting worker
Motoko execution, the heap, upgradesmoxzi-actor-host (Rust)everything past /v2/* — the workflow's activity does exactly one JSON POST per step

Two processes matter here: temporal server start-dev (the durable store — a restart of this host never restarts it, exactly like Restate's server for moxzi-restate-host) and one or more moxzi-actor-host services, one per actor type instance, same as every other host in this matrix.

Prerequisites#

Verified on this machine:

Walk through it#

/tmp/moxzi-tutorial-temporal/ is the scratch directory for this walk-through. Give the dev server its own directory — putting its SQLite file under a directory a harness (or your own script) wipes on start is exactly the "context deadline exceeded" bug the matrix gate hit and fixed on 2026-09-23 (docs/host-adapters.md).

Write and build the two actors#

Customer takes orders and, on chargeVia, awaits a Bank actor by principal text. Simplified from moxzi/conformance/fixtures/{customer,bank}.mo:

// customer.mo
import Prim "mo:⛔";
persistent actor {
  type Order = { id : Nat; total : Nat };
  var balance : Nat = 0;
  var orders : [Order] = [];
  var timerFired : Nat = 0;

  public func addOrder(order : Order) : async Nat {
    orders := Prim.Array_tabulate<Order>(orders.size() + 1, func (i : Nat) : Order {
      if (i < orders.size()) orders[i] else order
    });
    balance += order.total;
    orders.size()
  };
  public query func getBalance() : async Nat { balance };
  public query func orderCount() : async Nat { orders.size() };

  // A cross-actor await: `bank` is the Bank actor's principal, as text.
  public func chargeVia(bank : Text, amount : Nat) : async Nat {
    let b : actor { debit : (Nat) -> async Nat } = actor (bank);
    let v = await b.debit(amount);
    balance += v;
    balance
  };

  public func armTimer(delayNs : Nat64) : async () {
    ignore Prim.setTimer<system>(delayNs, false, func () : async () { timerFired += 1 });
  };
  public query func timerCount() : async Nat { timerFired };
};
// bank.mo
persistent actor {
  var debited : Nat = 0;
  public func debit(n : Nat) : async Nat { debited += n; n };
  public query func total() : async Nat { debited };
};
MOXZI_COMPILER=bin/compiler-canister/compiler.wasm MOXZI_LINKER=bin/linker-canister/linker.wasm \
  moxzi/target/release/moxzi build customer.mo -o customer.wasm
# customer.mo -> customer.wasm + customer.did (272261 bytes, 1 file(s), 65 steps, 0.79s)
MOXZI_COMPILER=bin/compiler-canister/compiler.wasm MOXZI_LINKER=bin/linker-canister/linker.wasm \
  moxzi/target/release/moxzi build bank.mo -o bank.wasm
# bank.mo -> bank.wasm + bank.did (249927 bytes, 1 file(s), 58 steps, 0.52s)

Start the Temporal dev server, in its own directory#

mkdir -p /tmp/moxzi-tutorial-temporal/server && cd /tmp/moxzi-tutorial-temporal/server
temporal server start-dev --headless --ip 127.0.0.1 --port 27233 --ui-port 28233 \
  --db-filename ./temporal-dev.db &

temporal operator cluster health --address 127.0.0.1:27233
# SERVING

Start the moxzi-actor-host services#

One service per type — --known Bank/1 prints Bank's principal so Customer can await it by text:

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29531 --state ./state-customer \
  --namespace tutorial --deployment crm --actor Customer=./customer.wasm --known Bank/1
# moxzi-actor-host: Bank/1 is 6lkaw-mxxfa-ayudl-lyjba-qiou3-hjvqo-deil4-f3r6l-apodg-jbfpw-uae
# moxzi-actor-host: moxzi-actor-host/2 on 127.0.0.1:29531, hosting Customer

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29532 --state ./state-bank \
  --namespace tutorial --deployment crm --actor Bank=./bank.wasm
# moxzi-actor-host: moxzi-actor-host/2 on 127.0.0.1:29532, hosting Bank

Build and start the Temporal Go host#

cd moxzi/adapters/temporal && go build -o /tmp/moxzi-tutorial-temporal/moxzi-temporal-host .

cd /tmp/moxzi-tutorial-temporal
PORT=29080 MOXZI_TEMPORAL_ADDRESS=127.0.0.1:27233 ./moxzi-temporal-host &
# 2026/09/23 22:32:49 INFO  Started Worker Namespace default TaskQueue moxzi-temporal-host WorkerID 38376@...
# moxzi-temporal-host: contract moxzi-actor-host/2; Temporal 127.0.0.1:27233 (namespace default); host-control on :29080

curl -s http://127.0.0.1:29080/healthz
# ok

Point placement at the services, and call it#

curl -X PUT http://127.0.0.1:29080/_moxzi/placement -H 'content-type: application/json' \
  -d '{"types":{"Customer":["http://127.0.0.1:29531"],"Bank":["http://127.0.0.1:29532"]}}'
# HTTP 204

curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/addOrder \
  -H 'content-type: application/json' -d '[{"id":1,"total":30}]'
# [1]   HTTP 200

A cross-actor await#

curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/chargeVia \
  -H 'content-type: application/json' \
  -d '["6lkaw-mxxfa-ayudl-lyjba-qiou3-hjvqo-deil4-f3r6l-apodg-jbfpw-uae", 5]'
# [35]  HTTP 200

curl -X POST http://127.0.0.1:29080/actors/Bank/1/method/total -H 'content-type: application/json' -d '[]'
# [5]

Customer's turn — a workflow.Go coroutine — parked on a blocking channel receive while the signal-with-start to Bank's own entity workflow was in flight; getBalance now reads 30 + 5 = 35.

Arm a timer, and watch it fire#

curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/armTimer \
  -H 'content-type: application/json' -d '[3000000000]'
curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [0]
sleep 6
curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

The 3-second due time became a workflow.NewTimer; on fire, the entity workflow's timer loop drove /v2/remind and the Motoko timer callback ran.

Kill it and bring it back#

Service-level restart — the Motoko heap is a file on disk, independent of Temporal:

kill <moxzi-actor-host Customer pid>          # 29531
moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29531 --state ./state-customer \
  --namespace tutorial --deployment crm --actor Customer=./customer.wasm --known Bank/1

curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]  — the durable heap on disk survived
curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/orderCount -H 'content-type: application/json' -d '[]'
# [1]
curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Host-level restart — kill only this host's worker+front process; the dev server (like Restate's server) is not part of kill_and_restart and keeps running:

kill <moxzi-temporal-host pid>

cd /tmp/moxzi-tutorial-temporal
PORT=29080 MOXZI_TEMPORAL_ADDRESS=127.0.0.1:27233 ./moxzi-temporal-host &

curl -s http://127.0.0.1:29080/healthz
# ok
curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]  — no re-PUT of placement: Temporal replayed the control and entity workflows' history

Drain, then upgrade#

curl -X POST http://127.0.0.1:29080/actors/Customer/1/admin/drain -d '{}'
# {"state":"Quiescent"}
curl -X POST http://127.0.0.1:29080/actors/Customer/1/admin/upgrade \
  -H 'content-type: application/json' \
  -d "{\"deploymentId\":\"crm-v2\",\"wasm\":\"$(base64 -i customer.wasm | tr -d '\n')\"}"
# {"state":"Active"}
curl -X POST http://127.0.0.1:29080/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]

Operating notes#

These notes are for the service form. In process, the actor's image lives in Postgres and nothing is written to a service's disk — see In process below.

In process#

The same actor also runs inside Temporal's own process through moxzi-rts, with Temporal keeping its image: see /docs/hosts/in-process/ for the contract.

In process on Temporal
Bindingcgo, inside the worker; each turn is a local activity
The actor's image lives inPostgres (the workflow history stays small)
Update latency vs Temporal's own durable actor (p50, macOS)1.68x
ConformanceHOST=temporal-inproc scripts/host_matrix_gate.sh

From moxzi/adapters/temporal/ (the exact commands and environment are in moxzi/conformance/hosts/temporal-inproc.json):

(cd ../../ && cargo build --release -p moxzi-rts)   # libmoxzi_rts first
go build -tags inproc -o out/moxzi-temporal-host .
MOXZI_TEMPORAL_ADDRESS=… DATABASE_URL=… MOXZI_IN_PROCESS=1 out/moxzi-temporal-host

The conformance matrix#

Temporal's column of the spec §61 matrix, real dev server, real process kills (moxzi/adapters/temporal/README.md, measured 2026-09-23 against compiler b7577666):

§61 scenarioTemporal
PersistencePASS
Process FailurePASS
Trap RollbackPASS
Actor IsolationPASS
Actor ParallelismPASS 1.16×
Duplicate InvocationPASS
AwaitPASS
Await Across RestartPASS
Upgrade With No AwaitsPASS
Upgrade With Outstanding AwaitPASS
Permanently Unresolved AwaitPASS
Timer Across PassivationPASS
Timer Across Process FailurePASS
Tiny Heap Mutation (206 B on an 8 MB heap)PASS
ICRC-72PASS
Upgrade CompatibilityPASS
total16/16

Reproduce it (starts its own temporal server start-dev on a scratch port with a private --db-filename — never a server this run did not start — plus two moxzi-actor-host instances per actor type, then drives all sixteen scenarios with real kills and restarts):

HOST=temporal scripts/host_matrix_gate.sh
On this pageWhat Temporal owns, what Motoko ownsPrerequisitesWalk through itWrite and build the two actorsStart the Temporal dev server, in its own directoryStart the moxzi-actor-host servicesBuild and start the Temporal Go hostPoint placement at the services, and call itA cross-actor awaitArm a timer, and watch it fireKill it and bring it backDrain, then upgradeOperating notesIn processThe conformance matrix