"use workflow" and "use step") as the foundation for its durable execution model. Directives provide the compile-time semantic boundary necessary for workflows to suspend, resume, and maintain deterministic behavior across replays.
This page explores how directives enable this execution model and the design principles that led us here.
Workflows and Steps Primer
The Workflow DevKit has two types of functions: Step functions are side-effecting operations with full Node.js runtime access. Think of them like named RPC calls - they run once, their result is persisted, and they can be retried on failure:await fetchUserData(userId) is called:
- If already executed: Returns the cached result immediately from the event log
- If not yet executed: Suspends the workflow, enqueues the step for background execution, and resumes later with the result
The Core Challenge
This execution model enables powerful durability features - workflows can suspend for days, survive restarts, and resume from any point. However, it also requires a semantic boundary in the code that tells the compiler, runtime, and developer that execution semantics have changed. The challenge: how do we mark this boundary in a way that:- Enables compile-time transformations and validation
- Prevents accidental use of non-deterministic APIs
- Allows static analysis of workflow structure
- Feels natural to JavaScript developers
Why Directives?
JavaScript directives have precedent for changing execution semantics within a defined scope:"use strict"(ECMAScript 5, TC39-standardized) changes language rules to make the runtime faster, safer, and more predictable"use client"and"use server"(React Server Components) define an explicit boundary of “where” code gets executed"use workflow"(Workflow DevKit) defines both “where” code runs (in a deterministic sandbox) and “how” it runs (deterministic, resumable, sandboxed execution)
"use workflow", it:
- Bundles the workflow and its dependencies into code that can be run in a sandbox
- Restricts access to Node.js APIs in that sandbox
- Enables static analysis to generate UML diagrams/visualizations
- Signals to the developer that you are entering a different execution mode
How Directives Work
The"use workflow" and "use step" directives trigger different transformations depending on the compilation mode:
Step Mode Transformation
Input:- The
"use step"directive is removed - The function body remains intact (no transformation)
- The function is registered with the runtime using a stable ID
- Step functions run with full Node.js access
Workflow Mode Transformation
Input:- Step function bodies are replaced with calls to
globalThis[Symbol.for("WORKFLOW_USE_STEP")] - Workflow function bodies remain intact—they execute deterministically during replay
- The
WORKFLOW_USE_STEPsymbol is a special runtime hook that:- Checks if the step has already been executed (in the event log)
- If yes: Returns the cached result
- If no: Triggers a suspension and enqueues the step for background execution
Client Mode Transformation
Input:- Workflow function bodies are replaced with an error throw
- The
workflowIdproperty is added (same as workflow mode) - This prevents accidental direct execution while allowing
start()to identify which workflow to launch
The useStep Implementation
ThecreateUseStep function in step.ts shows how step calls work during workflow replay:
- Queue the step invocation with a unique correlation ID
- Subscribe to the event log looking for matching step events
- If step_completed found: Resolve immediately with cached result
- If no matching event: The promise never resolves, causing a
WorkflowSuspensionto be thrown - The suspension handler enqueues the step for actual execution
Determinism and the VM Sandbox
Workflow functions execute in a Node.js VM context with restricted access to non-deterministic APIs. Fromworkflow.ts:
- Seeded random:
Math.random()is deterministic based on the run ID - Fixed timestamp:
Date.now()returns a consistent value during replay - Blocked APIs: Direct access to
fetch,setTimeout, file I/O, etc. is prevented
Why Not Other Approaches?
We explored several alternatives before settling on directives:Generator-Based API
- Unfamiliar syntax for most JavaScript developers
- Can’t use Promise.all directly
- Still no compile-time sandboxing
File System Conventions
- Too opinionated for diverse ecosystems
- Doesn’t support publishable npm packages
- Migration requires restructuring projects
Decorators
- Not yet TC39 standard
- Requires class boilerplate
- Makes workflows look like regular runtime code when they’re compile-time declarations
What Directives Enable
Because"use workflow" defines a compile-time semantic boundary, we can provide:
- Build-Time Validation: The compiler catches invalid patterns before deployment
- Static Analysis: Generate UML/DAG diagrams without executing code
- Durable Execution: Workflows can safely suspend and resume
- Future Optimizations: Smaller serialized state, smarter scheduling based on workflow structure
Closing Thoughts
Directives aren’t about syntax preference—they’re about expressing semantic boundaries."use workflow" tells the compiler, developer, and runtime that this code is deterministic, resumable, and sandboxed.
This clarity enables the Workflow Development Kit to provide durable execution with familiar JavaScript patterns, while maintaining the compile-time guarantees necessary for reliable workflow orchestration.