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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/content/docs/ai/defining-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Just like in regular AI SDK tool definitions, tool in DurableAgent are called wi
When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:

```typescript title="tools.ts" lineNumbers
import { Experimental_Agent as Agent } from "ai";
import type { ModelMessage } from "ai";

async function getWeather(
{ city }: { city: string },
{ messages, toolCallId }: { messages: ModelMessage[], toolCallId: string }) { // [!code highlight]
Expand Down Expand Up @@ -65,6 +68,9 @@ Tools can be implemented either at the step level or the workflow level, with di
Tools can also combine both by starting out on the workflow level, and calling into steps for I/O operations, like so:

```typescript title="tools.ts" lineNumbers
import { sleep } from "workflow";
import type { LanguageModel, ModelMessage } from "ai";

// Step: handles I/O with retries
async function performFetch(url: string) {
"use step";
Expand Down
3 changes: 3 additions & 0 deletions docs/content/docs/ai/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ The core code that makes all of this happen is quite simple. Here's a breakdown
Our API route makes a simple call to [AI SDK's `Agent` class](https://ai-sdk.dev/docs/agents/overview), which is a simple wrapper around [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.

```typescript title="app/api/chat/route.ts" lineNumbers
import { Experimental_Agent as Agent } from "ai";
import type { LanguageModel } from "ai";

export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const agent = new Agent({ // [!code highlight]
Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/ai/message-queueing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ If you just need basic multi-turn conversations where messages arrive between tu
The `prepareStep` callback runs before each step in the agent loop. It receives the current state and can modify the messages sent to the model:

```typescript lineNumbers
import type { ModelMessage, LanguageModel } from "ai";

interface PrepareStepInfo {
model: string | (() => Promise<LanguageModel>); // Current model
stepNumber: number; // 0-indexed step count
Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/ai/sleep-and-delays.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ Aside from providing `sleep()` as a tool, there are other use cases for Agents t
When hitting API rate limits, use `RetryableError` with a delay:

```typescript lineNumbers
import { RetryableError } from "workflow";

async function callRateLimitedAPI(endpoint: string) {
"use step";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ async function multiToolAgentWorkflow(userQuery: string) {

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

async function searchProducts({ query }: { query: string }) {
Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/api-reference/workflow-api/get-run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export async function POST(req: Request) {
You can also target specific sleep calls by correlation ID:

```typescript lineNumbers
import { getRun } from "workflow/api";

const run = getRun("my-run-id"); // @setup
const { stoppedCount } = await run.wakeUp({
correlationIds: ["wait_abc123"],
Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/api-reference/workflow/define-hook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export const approvalHook = defineHook({
Tokens are used to identify a specific hook and for resuming a hook. You can customize the token to be more specific to a use case.

```typescript lineNumbers
import { defineHook } from "workflow";

const slackHook = defineHook<{ text: string; userId: string }>();

export async function slackBotWorkflow(channelId: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Here's a more complex example showing how you might stream AI chat responses:
```typescript lineNumbers
import { getWritable } from "workflow";
import { generateId, streamText, type UIMessageChunk } from "ai";
import type { ModelMessage } from "ai";

export async function chat(messages: ModelMessage[]) {
"use workflow";
Expand Down
5 changes: 5 additions & 0 deletions docs/content/docs/errors/hook-conflict.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export async function processPayment() {
**Solution:** Use unique tokens that include the run ID or other unique identifiers.

```typescript lineNumbers
import { createHook } from "workflow";

export async function processPayment(orderId: string) {
"use workflow";

Expand All @@ -60,6 +62,8 @@ export async function processPayment(orderId: string) {
The safest approach is to let the Workflow runtime generate a unique token automatically:

```typescript lineNumbers
import { createHook } from "workflow";

export async function processPayment() {
"use workflow";

Expand All @@ -74,6 +78,7 @@ export async function processPayment() {
When a hook conflict occurs, awaiting the hook will throw a `HookConflictError`. You can catch this error to handle the conflict gracefully:

```typescript lineNumbers
import { createHook } from "workflow";
import { HookConflictError } from "@workflow/errors";

export async function processPayment(orderId: string) {
Expand Down
10 changes: 10 additions & 0 deletions docs/content/docs/errors/webhook-invalid-respond-with-value.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export async function webhookWorkflow() {
**Solution:** Use `"manual"` or provide a `Response` object.

```typescript lineNumbers
import { createWebhook } from "workflow";

// Fixed - use "manual"
export async function webhookWorkflow() {
"use workflow";
Expand Down Expand Up @@ -74,6 +76,8 @@ export async function webhookWorkflow() {
**Solution:** Create a proper `Response` object.

```typescript lineNumbers
import { createWebhook } from "workflow";

// Fixed - use Response constructor
export async function webhookWorkflow() {
"use workflow";
Expand All @@ -89,6 +93,8 @@ export async function webhookWorkflow() {
### Default Behavior (202 Response)

```typescript lineNumbers
import { createWebhook } from "workflow";

// Returns 202 Accepted automatically
const webhook = await createWebhook();
const request = await webhook;
Expand All @@ -98,6 +104,8 @@ const request = await webhook;
### Manual Response

```typescript lineNumbers
import { createWebhook } from "workflow";

// Manual response control
const webhook = await createWebhook({
respondWith: "manual",
Expand All @@ -120,6 +128,8 @@ await request.respondWith(
### Pre-defined Response

```typescript lineNumbers
import { createWebhook } from "workflow";

// Immediate response
const webhook = await createWebhook({
respondWith: new Response("Request received", { status: 200 }),
Expand Down
8 changes: 8 additions & 0 deletions docs/content/docs/errors/webhook-response-not-sent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export async function webhookWorkflow() {
**Solution:** Always call `request.respondWith()` when using manual response mode.

```typescript lineNumbers
import { createWebhook } from "workflow";

// Fixed - response sent
export async function webhookWorkflow() {
"use workflow";
Expand Down Expand Up @@ -92,6 +94,8 @@ export async function webhookWorkflow() {
**Solution:** Ensure all code paths send a response.

```typescript lineNumbers
import { createWebhook } from "workflow";

// Fixed - response sent in all branches
export async function webhookWorkflow() {
"use workflow";
Expand Down Expand Up @@ -135,6 +139,8 @@ export async function webhookWorkflow() {
**Solution:** Use try-catch to handle errors and send appropriate responses.

```typescript lineNumbers
import { createWebhook } from "workflow";

// Fixed - error handling with response
export async function webhookWorkflow() {
"use workflow";
Expand Down Expand Up @@ -163,6 +169,8 @@ export async function webhookWorkflow() {
If you don't need custom response control, consider using the default response mode which automatically returns a `202 Accepted` response:

```typescript lineNumbers
import { createWebhook } from "workflow";

// Automatic 202 response - no manual response needed
export async function webhookWorkflow() {
"use workflow";
Expand Down
29 changes: 29 additions & 0 deletions docs/content/docs/foundations/errors-and-retries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,35 @@ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
step can run up to 4 times total (1 initial attempt + 3 retries).
</Callout>

## Error Codes

When a workflow run fails, the error may include a `code` that classifies the failure. You can access it programmatically via the `Run` class:

```typescript lineNumbers
import { WorkflowRunFailedError } from "@workflow/errors";
import { start } from "workflow/api";

const run = await start(myWorkflow, [input]);

try {
const result = await run.returnValue;
} catch (err) {
if (WorkflowRunFailedError.is(err)) {
console.log(err.cause.code); // "USER_ERROR", "RUNTIME_ERROR", or undefined
console.log(err.cause.message); // The error message
}
}
```

| Code | Meaning |
| --- | --- |
| `USER_ERROR` | An error thrown in your workflow or step code (including propagated step failures like `FatalError`) |
| `RUNTIME_ERROR` | An internal runtime error such as a corrupted event log or missing data. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |

<Callout type="info">
The error code is also available on the run entity via the CLI (`npx workflow inspect runs <runId>`) in the `error.code` field, and as an OTEL span attribute (`workflow.error.code`) for observability.
</Callout>

## Rolling Back Failed Steps

When a workflow fails partway through, it can leave the system in an inconsistent state.
Expand Down
6 changes: 6 additions & 0 deletions docs/content/docs/foundations/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ Workflow functions must be deterministic to support replay. Since streams bypass
For more on determinism and replay, see [Workflows and Steps](/docs/foundations/workflows-and-steps).

```typescript title="workflows/bad-example.ts" lineNumbers
import { getWritable } from "workflow";

export async function badWorkflow() {
"use workflow";

Expand All @@ -176,6 +178,8 @@ export async function badWorkflow() {
```

```typescript title="workflows/good-example.ts" lineNumbers
import { getWritable } from "workflow";

export async function goodWorkflow() {
"use workflow";

Expand Down Expand Up @@ -501,6 +505,8 @@ If a lock is not released, the step function's HTTP request cannot terminate. Ev
**Close streams when done:**

```typescript lineNumbers
import { getWritable } from "workflow";

async function finalizeStream() {
"use step";

Expand Down
7 changes: 5 additions & 2 deletions packages/docs-typecheck/src/__tests__/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob';
import { describe, expect, it } from 'vitest';
import { extractCodeSamples } from '../extractor.js';
import { addInferredImports } from '../import-inference.js';
import { formatResult, typeCheckBatch } from '../type-checker.js';
import type {
CodeSample,
Expand Down Expand Up @@ -79,7 +78,11 @@ for (const relativeFile of allDocFiles) {
if (sample.skipTypeCheck) {
skippedSamples.push({ relativeFile, sample });
} else {
const processed = addInferredImports(sample);
const processed: ProcessedCodeSample = {
...sample,
processedSource: sample.source,
addedImports: [],
};
allSamples.push({ relativeFile, sample, processed });
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/docs-typecheck/src/docs-globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,7 @@ declare global {
const MAX_STEPS: number;
const reportId: string;
const userId: string;

// Workflow placeholders used in examples
const myWorkflow: (...args: any[]) => Promise<any>;
}
Loading
Loading