---
title: Serializable Steps
description: Wrap non-serializable third-party objects (like AI model providers) inside step factory functions so they can cross the workflow boundary.
type: guide
summary: Return a callback from a step to defer construction of a non-owned class (AI SDK models, cloud SDK clients) until execution time, making them usable inside durable workflows.
related:
  - /docs/foundations/serialization
  - /docs/foundations/serialization#custom-class-serialization
  - /docs/foundations/workflows-and-steps#step-functions
---

# Serializable Steps



<CopyPrompt text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing the object (AI SDK model, cloud SDK client) into the workflow, export a factory that returns an async callback marked with &#x22;use step&#x22; which constructs and returns the object at execution time, for example `export function openai(...args) { return async () => { &#x22;use step&#x22;; return openaiProvider(...args); }; }`. Pass the factory across the workflow boundary — the compiler serializes the function reference, not the instance — and invoke it inside steps where full Node.js access is available. Keep the factory's constructor arguments serializable. Verify the workflow builds, replays deterministically, and the dependency is only instantiated during step execution." />

<Callout>
  This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
</Callout>

## When to use this pattern

Workflow functions run inside a sandboxed VM where every value that crosses a function boundary must be serializable. There are two ways to get a non-serializable object across that boundary, depending on whether you own the class:

* **You own the class** — implement the [`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE` protocol](/docs/foundations/serialization#custom-class-serialization). The instance becomes a first-class serializable value: you can pass it as a workflow input, return it from a step, and call `"use step"` instance methods on it directly. This is the right tool when the class is yours to modify.
* **You don't own the class** — you can't add methods to `openai("gpt-4o")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction in a `"use step"` factory function and pass the factory across the boundary. That's what this page covers.

## The Problem

AI SDK model providers — `openai("gpt-4o")`, `anthropic("claude-sonnet-4-20250514")`, etc. — return complex objects with methods, closures, and internal state. Passing one directly into a step causes a serialization error, and you can't bolt `WORKFLOW_SERIALIZE` onto a third-party class.

```typescript lineNumbers
import { openai } from "@ai-sdk/openai";
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";

export async function brokenAgent(prompt: string) {
  "use workflow";

  const writable = getWritable<UIMessageChunk>();
  const agent = new DurableAgent({
    // This fails — the model object is not serializable
    model: openai("gpt-4o"),
  });

  await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
}
```

## The Solution: Step-as-Factory

Instead of passing the model object, pass a **callback function** that returns the model. Marking that callback with `"use step"` tells the compiler to serialize the *function reference* (which is just a string identifier) rather than its return value. The provider is only instantiated at execution time, inside the step's full Node.js runtime.

```typescript lineNumbers
import { openai as openaiProvider } from "@ai-sdk/openai";

// Returns a step function, not a model object
export function openai(...args: Parameters<typeof openaiProvider>) {
  return async () => {
    "use step";
    return openaiProvider(...args); // [!code highlight]
  };
}
```

The `DurableAgent` receives a function (`() => Promise<LanguageModel>`) instead of a model object. When the agent needs to call the LLM, it invokes the factory inside a step where the real provider can be constructed with full Node.js access.

## How `@workflow/ai` Uses This

<Callout type="warn">
  `@workflow/ai`'s pre-wrapped providers and `DurableAgent` are deprecated. AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) resolves models from AI Gateway model strings (e.g. `"openai/gpt-4o"`), which usually removes the need for a model factory — see the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The serialization pattern on this page still applies to any non-serializable dependency you own (for example, cloud SDK clients).
</Callout>

The `@workflow/ai` package ships pre-wrapped providers for all major AI SDK backends. Each one follows the same pattern:

```typescript lineNumbers
// packages/ai/src/providers/anthropic.ts
import { anthropic as anthropicProvider } from "@ai-sdk/anthropic";

export function anthropic(...args: Parameters<typeof anthropicProvider>) {
  return async () => {
    "use step";
    return anthropicProvider(...args); // [!code highlight]
  };
}
```

This means you import from `@workflow/ai` instead of `@ai-sdk/*` directly:

```typescript lineNumbers
import { anthropic } from "@workflow/ai/anthropic";
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";

export async function chatAgent(prompt: string) {
  "use workflow";

  const writable = getWritable<UIMessageChunk>();
  const agent = new DurableAgent({
    model: anthropic("claude-sonnet-4-20250514"), // [!code highlight]
  });

  await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
}
```

## Writing Your Own Serializable Wrapper

Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.

```typescript lineNumbers
import type { S3Client as S3ClientType } from "@aws-sdk/client-s3";

// The arguments (region, bucket) are plain strings — serializable
export function createS3Client(region: string) {
  return async (): Promise<S3ClientType> => {
    "use step";
    const { S3Client } = await import("@aws-sdk/client-s3");
    return new S3Client({ region });
  };
}

// Usage in a workflow
export async function processUpload(region: string, key: string) {
  "use workflow";

  const getClient = createS3Client(region); // [!code highlight]
  // getClient is a serializable step reference, not an S3Client
  await uploadFile(getClient, key);
}

async function uploadFile(
  getClient: () => Promise<S3ClientType>,
  key: string
) {
  "use step";
  const client = await getClient(); // [!code highlight]
  // Now you have a real S3Client with full Node.js access
  await client.send(/* ... */);
}
```

## Why This Works

1. **Compiler transformation**: `"use step"` tells the SWC plugin to extract the function into a separate bundle. The workflow VM only sees a serializable reference (function ID + captured arguments).
2. **Closure tracking**: The compiler tracks which variables the step function closes over. Only serializable values (strings, numbers, plain objects) can be captured.
3. **Deferred construction**: The actual provider/client is only constructed when the step executes in the Node.js runtime — never in the sandboxed workflow VM.

## Key APIs

* [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — marks a function for extraction and serialization
* [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) — declares the orchestrator function
* [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) — AI SDK's durable agent (resolves models via AI Gateway strings; replaces `DurableAgent`)
* [Custom class serialization](/docs/foundations/serialization#custom-class-serialization) — the companion pattern for classes you own (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`)


---

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)