Determinism in Workflows
Temporal Workflows are executed differently than conventional code as they can be restored at any point. A Workflow can sleep for months, and even if your Worker crashes or Temporal Cluster is down, timers and timeouts are persisted and will fire as scheduled. As soon as your Worker and Cluster are back up, your code will appear to resume where it left off. This also means that sleeping or retrying code does not tie up the process - you can run thousands of timers off a single Worker.
import * as wf from '@temporalio/workflow';
const { yourActivity } = wf.proxyActivities({
// persisted
startToCloseTimeout: '1 week',
retry: {
// persisted
initialInterval: '1 day',
},
});
export async function ExampleWorkflow() {
let state = []; // mutable local state
while (true) {
await wf.sleep('30 days'); // persisted
state.push(yourActivity()); // activity results can be replayed
}
}
For this to be possible, Workflow code must be completely deterministic, meaning it does the exact same thing every time it is rerun. Determinism brings limitations: you can't just call an external service, get the current time, or generate a random number, as these are all dependent on the state of the world at the time they're called, and may produce different values. The Temporal SDKs come with a set of tools that allow you to overcome these limitations.
How a Workflow is executed
The Temporal TypeScript SDK runs each Workflow in a separate v8 isolate — a "sandbox" environment using Node's built in vm
with its own global variables, just like in the browser.
- When we need to defer execution (such as for a timer or activity), we simply destroy the
vm
context. - When we need to continue execution, Temporal Server sends over the Event History, and we replay through the code from the start until the end to restore state.
- The serialization takes time, which is why we recommend keeping Event History under 10,000 events. "Sticky" optimizations exist to make this faster for common situations.
- If the execution logic has changed enough to affect Event History, you need to patch new code.
- The Workflow runtime is completely deterministic: functions like
Math.random
,Date
, andsetTimeout
are replaced by deterministic versions, and the only way for a Workflow to interact with the world is via Activities. - When an Activity completes, its result is stored in the Workflow history to be replayed in case a Workflow is restored.
The SDK does not throw an exception to suspend execution (like React Suspense), nor does it use VM snapshotting (yet), nor does it do any AST magic.
Imports in Workflow code
Workflow code is bundled on Worker creation using Webpack, you may import any JS package, as long as it doesn't reference Node or DOM APIs.
Sources of non-determinism
Math.random
- replaced by the runtimeuuid4
- provided by the runtimeDate
- replaced by the runtimenew Date()
andDate.now()
are both set on the first invocation of the Workflow Task
WeakRef | FinalizationRegistry
- cannot be used, as GC is non-deterministic and the Workflow code may observe its effect; deleted by the runtime- Timers -
setTimeout
andclearTimeout
are replaced by the runtime.- We recommend you use the
@temporal/workflow
package's exportedsleep
function because it plays well with cancellation scopes:import { sleep } from '@temporalio/workflow'
- We recommend you use the
- Activities - use to run non-deterministic code; results are replayed from history
- Node built ins:
process
globalpath
module,fs
module
Deterministic examples
How Date
is deterministic:
import { sleep } from '@temporalio/workflow';
// this prints the *exact* same timestamp repeatedly
for (let x = 0; x < 10; ++x) {
console.log(Date.now());
}
// this prints timestamps increasing roughly 1s each iteration
for (let x = 0; x < 10; ++x) {
await sleep('1 second');
console.log(Date.now());
}