Workflow and job code should not care whether it runs on a Cloudflare Durable Object or in a test process. Adapters solve this by providing a stable interface that the execution engine depends on. Swap the implementation and the same workflow code runs unchanged.
The workflow engine uses this to persist step results, workflow state, and recovery metadata. Every Workflow.step() result is written here before the step’s value is returned to the caller.
When a workflow pauses (sleep or retry delay), the orchestrator calls schedule(resumeAt). When it completes, it calls cancel() to clear any pending alarm.
instanceId is the stable identifier for the Durable Object instance. now() returns the current time in milliseconds and is used for all internal timestamp calculations, making it controllable in tests.
These are the production implementations, backed by the Durable Object’s native storage and alarm APIs.
import { createDOStorageAdapter, createDOSchedulerAdapter, createDurableObjectRuntime,} from "@durable-effect/workflow";
createDurableObjectRuntime(state) composes all three adapters into a single RuntimeLayer using the DO’s DurableObjectState. This is what createDurableWorkflows() uses internally — you do not need to call these functions directly when using the engine factory.
The in-memory adapters let you run workflows in a plain Node.js or Vitest environment with no Durable Objects involved.
import { createInMemoryRuntime } from "@durable-effect/workflow";
createInMemoryRuntime returns a layer and a handle. The layer provides all three adapters. The handle gives you control over simulated time and lets you inspect internal state.
interface TestRuntimeHandle { // Inspect state getStorageState(): Effect<InMemoryStorageState> getSchedulerState(): Effect<InMemorySchedulerState> getCurrentTime(): Effect<number> // Control time advanceTime(ms: number): Effect<void> setTime(time: number): Effect<void> // Control alarms shouldAlarmFire(): Effect<boolean> clearAlarm(): Effect<void>}
Testing example
import { Effect } from "effect";import { createInMemoryRuntime } from "@durable-effect/workflow";const { layer, handle } = await Effect.runPromise( createInMemoryRuntime({ initialTime: Date.now() }));// Run your workflow under the in-memory layer// ...// Advance simulated time past a sleep durationawait Effect.runPromise(handle.advanceTime(60_000));// Check whether an alarm is dueconst shouldFire = await Effect.runPromise(handle.shouldAlarmFire());// Inspect what is stored in DO storageconst storageState = await Effect.runPromise(handle.getStorageState());
createInMemoryRuntime is re-exported from @durable-effect/core as createInMemoryRuntime alongside lower-level helpers createInMemoryStorage and createInMemoryScheduler if you need to compose adapters individually.
Adapters are pure Effect services. They are injected into the rest of the engine via layers, and nothing in the engine imports them directly. This means any implementation that satisfies the interface works — you can write a custom adapter for a different storage backend without changing any workflow code.