Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); docs: apply Vercel technical writing standards by TooTallNate · Pull Request #3704 · vercel/workflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .changeset/technical-writing-audit.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
10 changes: 5 additions & 5 deletions .claude/agents/docs-writer.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,10 +31,10 @@ You are an expert technical writer specializing in developer documentation for t
- Highlight only the most relevant code to the concept being taught
- In examples showing workflows calling steps, put workflow code before step code
- Use proper type annotations to encourage best practices (e.g., `getWritable<MyType>()`)
- Remove type annotations when not needed (e.g., when just calling `.close()`)
- Remove type annotations when not needed (e.g., when calling `.close()`)

6. **Example-Driven Teaching**: Support explanations with working code examples that:
- Start simple and build incrementally
- Start with the minimum required code and build incrementally
- Show real-world use cases
- Include terse, focused comments that add value
- Use meaningful variable names that self-document intent
Expand DownExpand Up@@ -74,7 +74,7 @@ You are an expert technical writer specializing in developer documentation for t
- Use pipe syntax with double quotes for edge labels: `A -->|"label"| B`
- Highlight terminal states or key components with purple: `style NodeId fill:#a78bfa,stroke:#8b5cf6,color:#000`
- Place all `style` declarations at the end of the diagram
- Keep diagrams simple and readable - split into multiple diagrams if needed
- Keep diagrams focused and readable - split them into multiple diagrams if needed
- Add a legend or callout explaining highlighted nodes when appropriate

**When Creating New Documentation:**
Expand All@@ -98,10 +98,10 @@ You are an expert technical writer specializing in developer documentation for t
- Reference real implementation code when showing how features work internally

**Quality Checklist Before Finalizing:**
- Can a developer understand and use this feature after reading just the first example?
- Can a developer understand and use this feature after reading the first example?
- Is every technical term defined or linked to its definition?
- Are code examples syntactically correct and following project conventions?
- Does the explanation flow logically from simple to complex?
- Does the explanation flow logically from basic to complex?
- Have you eliminated all emojis and em-dashes?
- Is the writing concise without sacrificing clarity?
- Does the tone match canonical documentation like the directives guide?
Expand Down
133 changes: 67 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/README.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
# Workflow SDK Docs
# Workflow SDK docs

Check out the docs [here](https://workflow-sdk.dev/)
47 changes: 23 additions & 24 deletions docs/content/docs/v4/ai/chat-session-modeling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ Chat sessions in AI agents can be modeled at different layers of your architectu

While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.

## Single-Turn Workflows
## Single-turn workflows

Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.

Expand DownExpand Up@@ -81,7 +81,7 @@ export async function POST(req: Request) {

<Tab value="Client">

Chat messages need to be stored somewheretypically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.

```typescript title="app/chats/[id]/page.tsx" lineNumbers
"use client";
Expand DownExpand Up@@ -138,14 +138,13 @@ This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.

In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.

Persisting the turn is usually done through either:
Persist the turn through one of these methods:

- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.

## Multi-Turn Workflows
## Multi-turn workflows

A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.

Expand DownExpand Up@@ -191,7 +190,7 @@ export async function chat(initialMessages: UIMessage[]) {
tools: flightBookingTools,
});

// Use run ID as the hook token for easy resumption
// Use run ID as the hook token for resumption
const hook = chatMessageHook.create({ token: runId });
let turnNumber = 0;

Expand DownExpand Up@@ -254,7 +253,7 @@ export async function writeStreamClose(writable: WritableStream<UIMessageChunk>)

<Tab value="API Routes">

Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.

```typescript title="app/api/chat/route.ts" lineNumbers
import { createUIMessageStreamResponse, type UIMessage } from "ai";
Expand DownExpand Up@@ -493,13 +492,13 @@ In this pattern, the workflow owns the entire conversation session. All messages

The client hook processes these markers by:

1. Iterating through message parts in order
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
3. Deduplicating against optimistic sends from the initial message
1. Iterate through message parts in order.
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
3. Deduplicate against optimistic sends from the initial message.

This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.

## Choosing a Pattern
## Choosing a pattern

| Consideration | Single-Turn | Multi-Turn |
|--------------|-------------|------------|
Expand All@@ -509,13 +508,13 @@ This ensures the conversation displays as User → AI → User → AI regardless
| Workflow time horizon | Minutes | Hours to indefinitely |
| Observability scope | Per-turn traces | Full session traces |

**Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and fullsession observability, which becomes increasingly valuable as your agent matures.
**Multi-turn is recommended for most production usecases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.

**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.

## Multiplayer Chat Sessions
## Multiplayer chat sessions

The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.

<Tabs items={['System Event', 'External Service', 'Multiple Users']}>

Expand All@@ -542,7 +541,7 @@ export async function POST(req: Request) {

<Tab value="External Service">

External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.

```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
Expand DownExpand Up@@ -591,9 +590,9 @@ export async function POST(

</Tabs>

## Related Documentation
## Related documentation

- [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
10 changes: 5 additions & 5 deletions docs/content/docs/v4/ai/defining-tools.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,11 +14,11 @@ related:

This page covers the details for some common patterns when defining tools for AI agents using Workflow SDK.

Using WorkflowAgent, we model most tools as steps. These can be anything from a simple function call to a entire multi-day long workflow.
Using WorkflowAgent, we model most tools as steps. These can range from a single function call to an entire multi-day workflow.

## Accessing message context in tools

Just like in regular AI SDK tool definitions, tool in WorkflowAgent are called with a first argument of the tool's input parameters, and a second argument of the tool call context.
As with regular AI SDK tool definitions, tools in WorkflowAgent receive the tool's input parameters as the first argument and the tool call context as the second.

When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

Expand All@@ -34,9 +34,9 @@ async function getWeather(
}
```

## Writing to Streams
## Writing to streams

As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step just to call `getWritable()` for writing custom data parts to the stream.
As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools), it's common to use a step only to call `getWritable()` for writing custom data parts to the stream.

This can be made generic, by creating a helper step function to write arbitrary data to the stream:

Expand All@@ -53,7 +53,7 @@ async function writeToStream(data: any) {
}
```

## Step-Level vs Workflow-Level Tools
## Step-level vs workflow-level tools

Tools can be implemented either at the step level or the workflow level, with different capabilities and constraints.

Expand Down
22 changes: 11 additions & 11 deletions docs/content/docs/v4/ai/human-in-the-loop.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@ Workflow SDK's [webhook](/docs/api-reference/workflow/create-webhook) and [hook]

If you need to react to external events programmatically, see the [hooks](/docs/foundations/hooks) documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

## How It Works
## How it works

<Steps>

Expand All@@ -45,17 +45,17 @@ The workflow receives the approval data and resumes execution.

</Steps>

While this demo will use a clientside button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.
While this demo uses a client-side button for human approval, you could instead create a webhook and send the approval link over email or Slack to resume the agent.

## Creating a Booking Approval Tool
## Creating a booking approval tool

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

<Steps>

<Step>

### Define the Hook
### Define the hook

Create a typed hook with a Zod schema for validation:

Expand All@@ -78,7 +78,7 @@ export const bookingApprovalHook = defineHook({

<Step>

### Implement the Tool
### Implement the tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

Expand DownExpand Up@@ -126,14 +126,14 @@ export const flightBookingTools = {
```

<Callout type="info">
Note that the `defineHook().create()` function must be called from within a workflow context, not from within a step. This is why `executeBookingApproval` does not have `"use step"` - it runs in the workflow context where hooks are available.
Call `defineHook().create()` from within a workflow context, not from within a step. `executeBookingApproval` does not have `"use step"` because it runs in the workflow context where hooks are available.
</Callout>

</Step>

<Step>

### Create the API Route
### Create the API route

Create a new API endpoint that the UI will call to submit the approval decision:

Expand All@@ -158,7 +158,7 @@ export async function POST(request: Request) {

<Step>

### Create the Approval Component
### Create the approval component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

Expand DownExpand Up@@ -253,7 +253,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr

<Step>

### Show the Tool Status in the UI
### Show the tool status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

Expand DownExpand Up@@ -332,7 +332,7 @@ export default function ChatPage() {

</Steps>

## Using Webhooks Directly
## Using webhooks directly

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:

Expand DownExpand Up@@ -367,7 +367,7 @@ The webhook URL can be called directly with a POST request containing the approv
- Payment provider callbacks
- Email-based approval links

## Related Documentation
## Related documentation

- [Hooks & Webhooks](/docs/foundations/hooks) - Complete guide to hooks and webhooks
- [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook) - Webhook configuration options
Expand Down
Loading
Loading