---
title: How the Directives Work
description: Deep dive into the internals of how Workflow SDK directives transform your code.
type: conceptual
summary: Learn how the compiler transforms directive-annotated code into three execution modes.
prerequisites:
  - /docs/how-it-works/understanding-directives
related:
  - /docs/foundations/workflows-and-steps
---

# How the Directives Work



<Callout>
  This is an advanced guide that dives into internals of the Workflow SDK directive and is not required reading to use workflows. To use the Workflow SDK, check out the [getting started](/docs/getting-started) guides for your framework.
</Callout>

Workflows use special directives to mark code for transformation by the Workflow SDK compiler. This page explains how `"use workflow"` and `"use step"` directives work, what transformations are applied, and why they're necessary for durable execution.

## Directives overview

Workflows use two directives to mark functions for special handling:

{/* @skip-typecheck: incomplete code sample */}

```typescript
export async function handleUserSignup(email: string) {
  "use workflow"; // [!code highlight]

  const user = await createUser(email);
  await sendWelcomeEmail(user);

  return { userId: user.id };
}

async function createUser(email: string) {
  "use step"; // [!code highlight]

  return { id: crypto.randomUUID(), email };
}
```

**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:

<Mermaid
  chart="flowchart LR
    A[&#x22;Source Code<br/>with directives&#x22;] --> B[&#x22;Step Mode&#x22;]
    A --> C[&#x22;Workflow Mode&#x22;]
    A --> D[&#x22;Client Mode&#x22;]
    B --> E[&#x22;step.js<br/>(Step Execution)&#x22;]
    C --> F[&#x22;flow.js<br/>(Workflow Execution)&#x22;]
    D --> G[&#x22;Your App Code<br/>(Enables `start`)&#x22;]"
/>

### Comparison table

| Mode     | Used In       | Purpose                                    | Output API Route               | Required?  |
| -------- | ------------- | ------------------------------------------ | ------------------------------ | ---------- |
| Step     | Build time    | Bundles step handlers                      | `.well-known/workflow/v1/step` | Yes        |
| Workflow | Build time    | Bundles workflow orchestrators             | `.well-known/workflow/v1/flow` | Yes        |
| Client   | Build/Runtime | Provides workflow IDs and types to `start` | Your application code          | Optional\* |

\* Client mode is **recommended** for better developer experience: it provides automatic ID generation and type safety. Without it, you must manually construct workflow IDs or use the build manifest.

## Detailed transformation examples

<Tabs items={["Step Mode", "Workflow Mode", "Client Mode"]}>
  <Tab value="Step Mode">
    **Step Mode** creates the step execution bundle served at `/.well-known/workflow/v1/step`.

    **Input:**

    {/* @skip-typecheck: incomplete code sample */}

    ```typescript
    export async function createUser(email: string) {
      "use step";
      return { id: crypto.randomUUID(), email };
    }
    ```

    **Output:**

    {/* @skip-typecheck: incomplete code sample */}

    ```typescript
    export async function createUser(email: string) {
      return { id: crypto.randomUUID(), email };
    }
    (function(__wf_fn, __wf_id) { // [!code highlight]
        var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); // [!code highlight]
        __wf_reg.set(__wf_id, __wf_fn); // [!code highlight]
        __wf_fn.stepId = __wf_id; // [!code highlight]
    })(createUser, "step//workflows/user.js//createUser"); // [!code highlight]
    ```

    **What happens:**

    * The `"use step"` directive is removed.
    * The function body is kept completely intact (no transformation).
    * The function is registered with the runtime via an inline immediately invoked function expression (IIFE), with no imports needed.
    * Step functions run with full Node.js, Deno, or Bun access.

    **Why no transformation?** Step functions execute in your main runtime with full access to Node.js APIs, file system, databases, etc. They don't need any special handling; they run normally.

    **ID Format:** Step IDs follow the pattern `step//{filepath}//{functionName}`, where the filepath is relative to your project root.
  </Tab>

  <Tab value="Workflow Mode">
    **Workflow Mode** creates the workflow execution bundle served at `/.well-known/workflow/v1/flow`.

    **Input:**

    {/* @skip-typecheck: incomplete code sample */}

    ```typescript
    export async function createUser(email: string) {
      "use step";
      return { id: crypto.randomUUID(), email };
    }

    export async function handleUserSignup(email: string) {
      "use workflow";
      const user = await createUser(email);
      return { userId: user.id };
    }
    ```

    **Output:**

    {/* @skip-typecheck: incomplete code sample */}

    ```typescript
    export async function createUser(email: string) {
      return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//workflows/user.js//createUser")(email); // [!code highlight]
    }

    export async function handleUserSignup(email: string) {
      const user = await createUser(email);
      return { userId: user.id };
    }
    handleUserSignup.workflowId = "workflow//workflows/user.js//handleUserSignup"; // [!code highlight]
    ```

    **What happens:**

    * 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 function gets a `workflowId` property 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](/docs/how-it-works/event-sourcing) rather than re-executing them. The `WORKFLOW_USE_STEP` symbol is a special runtime hook that:

    1. Checks if the step has already been executed (in the event log)
    2. If yes: Returns the cached result
    3. If no: Triggers a suspension and enqueues the step for background execution

    **ID Format:** Workflow IDs follow the pattern `workflow//{filepath}//{functionName}`. The `workflowId` property is attached to the function to allow [`start()`](/docs/api-reference/workflow-api/start) to work at runtime.
  </Tab>

  <Tab value="Client Mode">
    **Client Mode** transforms workflow functions in your application code to prevent direct execution.

    **Input:**

    {/* @skip-typecheck: incomplete code sample */}

    ```typescript
    export async function handleUserSignup(email: string) {
      "use workflow";
      const user = await createUser(email);
      return { userId: user.id };
    }
    ```

    **Output:**

    {/* @skip-typecheck: incomplete code sample */}

    ```typescript
    export async function handleUserSignup(email: string) {
      throw new Error("You attempted to execute ..."); // [!code highlight]
    }
    handleUserSignup.workflowId = "workflow//workflows/user.js//handleUserSignup"; // [!code highlight]
    ```

    **What happens:**

    * Workflow function bodies are **replaced** with an error throw.
    * The `workflowId` property is added (same as workflow mode).
    * Step functions are not transformed in client mode.

    **Why this transformation?** Workflow functions cannot be called directly from application code; they must be started using [`start()`](/docs/api-reference/workflow-api/start). The error prevents accidental direct execution while the `workflowId` property allows the `start()` function to identify which workflow to launch.

    The IDs are generated exactly like in workflow mode to ensure they can be directly referenced at runtime.

    <Callout type="info">
      **Client mode is optional:** While recommended for better developer experience (automatic IDs and type safety), you can skip client mode and instead:

      * Manually construct workflow IDs using the pattern `workflow//{filepath}//{functionName}`
      * Use the workflow manifest file generated during build to lookup IDs
      * Pass IDs directly to `start()` as strings

      All framework integrations include client mode as a loader by default.
    </Callout>
  </Tab>
</Tabs>

## Generated files

When you build your application, the Workflow SDK generates three handler files in `.well-known/workflow/v1/`:

### `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, and the network 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 `POST` handler that accepts Web standard `Request` objects
* 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

<Callout type="info">
  **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](/docs/foundations/workflows-and-steps#step-functions) instead.
</Callout>

### `step.js`

Contains all step functions transformed in **step mode**. This file is imported by your framework to handle step execution requests at `POST /.well-known/workflow/v1/step`.

**What it does:**

* Exports a `POST` handler that accepts Web standard `Request` objects
* Executes individual steps with full runtime access
* Returns step results to the orchestration layer

### `webhook.js`

Contains webhook handling logic for delivering external data to running workflows via [`createWebhook()`](/docs/api-reference/workflow/create-webhook).

**What it does:**

* Exports a `POST` handler that accepts webhook payloads
* Validates tokens and routes data to the correct workflow run
* Resumes workflow execution after webhook delivery

**Note:** 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:

1. **Step mode** (required): Bundles executable step functions that can access the full runtime.
2. **Workflow mode** (required): Creates orchestration logic that can replay from event logs.
3. **Client mode** (optional): Prevents direct execution and enables type-safe workflow references.

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 works with workflow references (when using client mode).

## 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 re-run multiple times to calculate what the next step should be. This is why workflow function bodies remain intact in workflow mode: 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](/docs/how-it-works/event-sourcing) after first execution

Learn more about [Workflows and Steps](/docs/foundations/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//handleUserSignup`
* `step//workflows/user-signup.js//createUser`
* `step//workflows/payments/checkout.ts//processPayment`

**Key properties:**

* **Stable**: IDs don't change unless you rename files or functions.
* **Unique**: Each workflow or step has a unique identifier.
* **Portable**: IDs work across different runtimes and deployments.

<Callout type="info">
  Although IDs can change when files are moved or functions are renamed, Workflow SDK functions assume [atomic versioning](/docs/foundations/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.
</Callout>

## 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](/docs/getting-started) guide for your framework.

**For framework authors**: Learn how to integrate these transformations into your framework in [Building Framework Integrations](/docs/how-it-works/framework-integrations).

## Debugging transformed code

If you need to debug transformation issues, you can inspect the generated files:

1. **Look in `.well-known/workflow/v1/`**: Check the generated `flow.js`, `step.js`, `webhook.js`, and other emitted debug files.
2. **Check build logs**: Most frameworks log transformation activity during builds.
3. **Verify directives**: Ensure `"use workflow"` and `"use step"` are the first statements in functions.
4. **Check file locations**: Transformations only apply to files in configured source directories.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)