How the directives work
This advanced guide covers the internals of Workflow SDK directives. To start using Workflow SDK, see the getting started guide for your framework.
Workflows use special directives to mark code for transformation by the Workflow SDK compiler. The "use workflow" and "use step" directives apply the transformations required for durable execution.
Directives overview
Workflows use two directives to mark functions for special handling:
Key directives:
"use workflow": Marks a function as a durable workflow entry point"use step": Marks a function as an atomic, retryable step
These directives trigger the @workflow/swc-plugin compiler to transform your code in different ways depending on the execution context.
The three transformation modes
The compiler operates in three distinct modes, transforming the same source code differently for each execution context:
Comparison table
| Mode | Used In | Purpose | Runtime role | Required? |
|---|---|---|---|---|
| Step | Build time + your app code | Registers executable step functions; gives app code workflow IDs for start() | Imported by the combined flow handler, and applied to application code by the framework loader | Yes |
| Workflow | Build time | Bundles workflow orchestrators | Executed by .well-known/workflow/v1/flow | Yes |
| Detect | Build time | Discovers workflows, steps, and serialization classes without transforming code | Feeds the build's discovery phase and manifest | Yes (build-internal) |
Earlier releases had a separate client mode for application code. In 5.0 it merged into step mode, which produces the same app-code behavior (workflow functions throw on direct calls and carry workflowId for start()) while also registering step functions. Build integrations that passed mode: "client" now pass mode: "step".
Detailed transformation examples
Step Mode creates the registration bundle that the combined flow handler imports (it is not an HTTP route), and is also the transform framework loaders apply to your application code.
Input:
Output:
What happens:
- The
"use step"directive is removed - Step function bodies are kept completely intact (no transformation)
- Each step function is registered with the runtime via an inline IIFE (no imports needed)
- Step functions run with full Node.js/Deno/Bun access
- Workflow function bodies are replaced with an error throw, and a
workflowIdproperty is attached. Launch workflow functions withstart()instead of calling them directly. The ID identifies the workflow tostart() - A dead-code-elimination pass removes code reachable only from the replaced workflow bodies
Why no step transformation? Step functions execute in your main runtime with full access to Node.js APIs, the file system, databases, and other resources. They run normally without special handling.
ID format: Step IDs follow the pattern step//{filepath}//{functionName}, where the file path is relative to your project root.
Workflow Mode creates the workflow execution bundle served at /.well-known/workflow/v1/flow.
Input:
Output:
What happens:
- Step function bodies are replaced with calls to
globalThis[Symbol.for("WORKFLOW_USE_STEP")] - Workflow function bodies remain intact, so they execute deterministically during replay
- The workflow function gets a
workflowIdproperty for runtime identification - The
"use workflow"directive is removed
Why this transformation? When a workflow executes, it needs to replay past steps from the event log rather than re-executing them. The WORKFLOW_USE_STEP symbol 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: Suspends the replay and executes the step, usually inline in the same invocation. The runtime returns the run to the queue only when the invocation's inline budget is exhausted or its timeout approaches
ID format: Workflow IDs follow the pattern workflow//{filepath}//{functionName}. The workflowId property is attached to the function so start() works at runtime.
Detect Mode is a lightweight, non-transforming mode used during the build's discovery phase.
Input:
Output:
What happens:
- The code is not modified. Detect mode only walks the AST
- Discovered workflows, steps, and custom-serialization classes are emitted as a JSON manifest comment
- The build uses this to decide which files feed the step and workflow bundles
Why a separate mode? The build system first runs a fast regular expression pre-scan to find candidate files containing directive-like strings. It then runs detect mode on those candidates to validate them at the abstract syntax tree (AST) level. The plugin eliminates false positives, such as a directive-like string inside a template literal, because it only recognizes genuine directive statements.
Working without the app-code loader: Frameworks apply the step-mode transform to application code by default, which is what gives start(handleUserSignup) its automatic IDs and type safety. If your setup can't run the loader, you can instead construct workflow IDs manually using the pattern workflow//{filepath}//{functionName}, look them up in the build manifest, and pass them to start() as strings.
Generated files
When you build your application, the Workflow SDK generates a combined flow handler, an internal step registration bundle, and a webhook handler. Exact filenames vary by framework.
flow.js
Contains all workflow functions transformed in workflow mode. This file is imported by your framework to handle workflow execution requests at POST /.well-known/workflow/v1/flow.
How it's structured:
All workflow code is bundled together and embedded as a string inside flow.js. When a workflow needs to execute, this bundled code is run inside a Node.js VM (virtual machine) to ensure:
- Determinism: The same inputs always produce the same outputs
- Side-effect prevention: Direct access to Node.js APIs, the file system, the network, and other resources is blocked
- Sandboxed execution: Workflow orchestration logic is isolated from the main runtime
Build-time validation:
The workflow mode transformation validates your code during the build:
- Catches invalid Node.js API usage (like
fs,http,child_process) - Prevents imports of modules that would break determinism
Most invalid patterns cause build-time errors, catching issues before deployment.
What it does:
- Exports a
POSThandler that accepts Web standardRequestobjects - Executes bundled workflow code inside a Node.js VM for each request
- Handles workflow execution, replay, and resumption
- Returns execution results to the orchestration layer
Why a VM? Workflow functions must be deterministic to support replay. The VM sandbox prevents accidental use of non-deterministic APIs or side effects. All side effects should be performed in step functions instead.
__step_registrations.js
Contains all step functions transformed in step mode. The combined flow handler imports this module for its registration side effects.
What it does:
- Adds step functions to the runtime step registry
- Keeps step bodies in the full host runtime
- Makes registered steps available when a flow queue message includes
stepIdandstepName
This module must not be exposed as an HTTP endpoint.
Changed in 5.0: In 4.x, the step bundle was served as its own HTTP route at POST /.well-known/workflow/v1/step, with step messages delivered on a separate __wkf_step_* queue topic. v5 merged both into the combined flow handler. The step bundle became a registration module imported by flow.js, and step messages arrive on the shared workflow queue. See the v4 version of this page for the old layout.
webhook.js
Contains webhook handling logic for delivering external data to running workflows via createWebhook().
What it does:
- Exports a
POSThandler that accepts webhook payloads - Validates tokens and routes data to the correct workflow run
- Resumes workflow execution after webhook delivery
The webhook file structure varies by framework. Next.js generates webhook/[token]/route.js to use App Router's dynamic routing, while other frameworks generate a single webhook.js or webhook.mjs handler.
Why three modes?
The multi-mode transformation enables the Workflow SDK's durable execution model:
- Step mode (required): Bundles executable step functions that can access the full runtime and serves as the application-code transform that prevents direct workflow execution and enables type-safe
start()references - Workflow mode (required): Creates orchestration logic that can replay from event logs
- Detect mode (build-internal): Discovers directive-marked functions for the build without changing the code
This separation allows:
- Deterministic replay: Workflows can be safely replayed from event logs without re-executing side effects
- Sandboxed orchestration: Workflow logic runs in a controlled VM without direct runtime access
- Stateless execution: Your compute can scale to zero and resume from any point in the workflow
- Type safety: TypeScript supports workflow references passed to
start()
Determinism and replay
A key aspect of the transformation is maintaining deterministic replay for workflow functions.
Workflow functions must be deterministic:
- Same inputs always produce the same outputs
- No direct side effects (no API calls, no database writes, no file I/O)
- Can use seeded random/time APIs provided by the VM (
Math.random(),Date.now(), etc.)
Because workflow functions are deterministic and have no side effects, they can be safely rerun multiple times to calculate the next step. Workflow function bodies remain intact in workflow mode because they're pure orchestration logic.
Step functions can be non-deterministic:
- Can make API calls, database queries, etc.
- Have full access to Node.js runtime and APIs
- Results are cached in the event log after first execution
Learn more about Workflows and Steps.
ID generation
The compiler generates stable IDs for workflows and steps based on file paths and function names:
Pattern: {type}//{filepath}//{functionName}
Examples:
workflow//workflows/user-signup.js//handleUserSignupstep//workflows/user-signup.js//createUserstep//workflows/payments/checkout.ts//processPayment
Key properties:
- Stable: IDs don't change unless you rename files or functions
- Unique: Each workflow/step has a unique identifier
- Portable: Works across different runtimes and deployments
Although IDs can change when files are moved or functions are renamed, Workflow SDK functions assume atomic versioning in the World. This means changing IDs won't break old workflows from running, but will prevent runs from being upgraded and will cause your workflow/step names to change in observability across deployments.
Framework integration
These transformations are framework-agnostic. They output standard JavaScript that works anywhere.
For users: Your framework handles all transformations automatically. See the Getting Started guide for your framework.
For framework authors: Learn how to integrate these transformations into your framework in Building Framework Integrations.
Debugging transformed code
If you need to debug transformation issues, you can inspect the generated files:
- Inspect the generated output: Check the combined flow handler, step registration bundle, webhook handler, and emitted debug files.
- Check build logs: Most frameworks log transformation activity during builds
- Verify directives: Ensure
"use workflow"and"use step"are the first statements in functions - Check file locations: Transformations only apply to files in configured source directories