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
87 changes: 86 additions & 1 deletion docs/content/docs/api-reference/workflow-ai/durable-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export default OutputSpecification;`}
- Tools can use core library features like `sleep()` and Hooks within their `execute` functions
- The agent processes tool calls iteratively until completion or `maxSteps` is reached
- **Default `maxSteps` is unlimited** - set a value to limit the number of LLM calls
- The `stream()` method returns `{ messages, steps, experimental_output, uiMessages }` containing the full conversation history, step details, optional structured output, and optionally accumulated UI messages
- The `stream()` method returns `{ messages, steps, toolCalls, toolResults, experimental_output, uiMessages }` containing the full conversation history, step details, tool call details, optional structured output, and optionally accumulated UI messages
- Use `collectUIMessages: true` to accumulate `UIMessage[]` during streaming, useful for persisting conversation state without re-reading the stream
- The `prepareStep` callback runs before each step and can modify model, messages, generation settings, tool choice, and context
- Generation settings (temperature, maxOutputTokens, etc.) can be set on the constructor and overridden per-stream call
Expand Down Expand Up @@ -842,6 +842,91 @@ async function saveConversation(messages: UIMessage[]) {
The `uiMessages` property is only available when `collectUIMessages` is set to `true`. When disabled, `uiMessages` is `undefined`.
</Callout>

### Machine-Readable Tool Results

`stream()` returns tool call information you can inspect programmatically. Compare `toolCalls` with `toolResults` to find unresolved tool calls that need client-side handling:

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

async function checkOrderStatus({ orderId }: { orderId: string }) {
"use step";
return `Order ${orderId}: shipped`;
}

async function agentWithToolInspection(userMessage: string) {
"use workflow";

const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
checkOrderStatus: {
description: "Check order status",
inputSchema: z.object({ orderId: z.string() }),
execute: checkOrderStatus,
},
},
});

const result = await agent.stream({
messages: [{ role: "user", content: userMessage }],
writable: getWritable<UIMessageChunk>(),
});

const unresolved = result.toolCalls.filter( // [!code highlight]
(tc) => !result.toolResults.some((tr) => tr.toolCallId === tc.toolCallId) // [!code highlight]
); // [!code highlight]

if (unresolved.length > 0) {
return {
status: "needs-client-tools",
unresolved,
};
}

return {
status: "complete",
messages: result.messages,
toolResults: result.toolResults,
};
}
```

<Callout type="info">
`toolCalls` and `toolResults` reflect the *last step* of the agent loop. Tools without an `execute` function will appear in `toolCalls` but not in `toolResults`, which is how you detect calls that need client-side handling.
</Callout>

### Aborting Long-Running Streams

Use `timeout` to abort a stream automatically after a fixed duration:

<Callout type="warn">
`abortSignal` is not yet supported and will be available in a future release. Use `timeout` for now.
</Callout>

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

async function agentWithTimeout(userMessage: string) {
"use workflow";

const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
});

await agent.stream({
messages: [{ role: "user", content: userMessage }],
writable: getWritable<UIMessageChunk>(),
timeout: 30_000, // [!code highlight]
});
}
```

## See Also

- [Building Durable AI Agents](/docs/ai) - Complete guide to creating durable agents
Expand Down
51 changes: 51 additions & 0 deletions docs/content/docs/api-reference/workflow-api/get-world.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,57 @@ const hydrated = hydrateResourceIO(step, observabilityRevivers); // [!code highl

See [Observability Utilities](/docs/api-reference/workflow-api/world/observability) for the full hydration, parsing, and encryption API.

### List Workflow Runs (Display Names)

List workflow runs and derive human-readable names from the `workflowName` field:

```typescript lineNumbers
import { getWorld } from "workflow/runtime";
import { parseWorkflowName } from "@workflow/utils/parse-name"; // [!code highlight]

export async function GET(req: Request) {
const url = new URL(req.url);
const cursor = url.searchParams.get("cursor") ?? undefined;

try {
const world = getWorld(); // [!code highlight]
const runs = await world.runs.list({
pagination: { cursor },
resolveData: "none",
});

return Response.json({
data: runs.data.map((run) => {
const parsed = parseWorkflowName(run.workflowName); // [!code highlight]

return {
runId: run.runId,
// Use shortName for UI display (e.g., "processOrder") // [!code highlight]
displayName: parsed?.shortName ?? run.workflowName, // [!code highlight]
// Module info available for debugging // [!code highlight]
module: parsed?.moduleSpecifier, // [!code highlight]
status: run.status,
startedAt: run.startedAt,
completedAt: run.completedAt,
};
}),
cursor: runs.cursor,
});
} catch (error) {
return Response.json(
{ error: "Failed to list workflow runs" },
{ status: 500 }
);
}
}
```

<Callout type="info">
The `workflowName` field contains a machine-readable identifier like `workflow//./src/workflows/order//processOrder`.
Use `parseWorkflowName()` from `@workflow/utils/parse-name` to extract the `shortName` (e.g., `"processOrder"`)
and `moduleSpecifier` for display in your UI.
</Callout>

## Related Functions

- [`getRun()`](/docs/api-reference/workflow-api/get-run) - Higher-level API for working with individual runs by ID.
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/api-reference/workflow-api/start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-
* All arguments must be [serializable](/docs/foundations/serialization).
* When `deploymentId` is provided, the argument types and return type become `unknown` since there is no guarantee the workflow function's types will be consistent across different deployments.

<Callout type="info">
If `start()` throws `'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.`, the passed function was not transformed as a workflow. The two most common causes are a missing `"use workflow"` directive or missing framework integration. See [start-invalid-workflow-function](/docs/errors/start-invalid-workflow-function).
</Callout>

## Examples

### With Arguments
Expand Down
51 changes: 51 additions & 0 deletions docs/content/docs/api-reference/workflow-next/with-workflow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,57 @@ const workflowConfig = {}
export default withWorkflow(nextConfig, workflowConfig); // [!code highlight]
```

### Monorepos and Workspace Imports

By default, Next.js detects the correct workspace root automatically. If your Next.js app lives in a subdirectory such as `apps/web` and workspace resolution is not working correctly, you can set `outputFileTracingRoot` as a workaround:

```typescript title="apps/web/next.config.ts" lineNumbers
import { resolve } from "node:path";
import type { NextConfig } from "next";
import { withWorkflow } from "workflow/next";

const nextConfig: NextConfig = {
outputFileTracingRoot: resolve(process.cwd(), "../.."),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijjk can you validate is this is right? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by default Next.js should detect the right value we should only mention this as a workaround if things are detecting correctly

};

export default withWorkflow(nextConfig);
```

<Callout type="info">
Use the smallest directory that contains every workspace package imported by your workflows. If your app already lives at the repository root, you do not need to set `outputFileTracingRoot`.
</Callout>

## Options

`withWorkflow` accepts an optional second argument to configure the Next.js integration.

```typescript title="next.config.ts" lineNumbers
import type { NextConfig } from "next";
import { withWorkflow } from "workflow/next";

const nextConfig: NextConfig = {};

export default withWorkflow(nextConfig, {
workflows: {
lazyDiscovery: true,
local: {
port: 4000,
},
},
});
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `workflows.lazyDiscovery` | `boolean` | `false` | When `true`, defers workflow discovery until files are requested instead of scanning eagerly at startup. Useful for large projects where startup time matters. |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lazyDiscovery is actually something that @ijjk wants to ship as the default for GA

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed with team — lazyDiscovery is not shipping as default for GA. Docs are accurate as opt-in.

| `workflows.local.port` | `number` | — | Overrides the `PORT` environment variable for local development. Has no effect when deployed to Vercel. |

<Callout type="info">
The `workflows.local` options only affect local development. When deployed to Vercel, the runtime ignores `local` settings and uses the Vercel world automatically.
</Callout>

## Exporting a Function

If you are exporting a function in your `next.config` you will need to ensure you call the function returned from `withWorkflow`.

```typescript title="next.config.ts" lineNumbers
Expand Down
22 changes: 16 additions & 6 deletions docs/content/docs/deploying/building-a-world.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ A World connects workflows to the infrastructure that powers them. The World int
```typescript
interface World extends Storage, Queue, Streamer {
start?(): Promise<void>;
close?(): Promise<void>;
getEncryptionKeyForRun?(run: WorkflowRun): Promise<Uint8Array | undefined>;
getEncryptionKeyForRun?(runId: string, context?: Record<string, unknown>): Promise<Uint8Array | undefined>;
}
```

The optional `start()` method initializes any background tasks needed by your World (e.g., queue polling).
The optional `start()` method initializes background tasks (for example, queue polling). The optional `close()` method releases resources like connection pools and listeners. The optional `getEncryptionKeyForRun()` method returns the AES-256 key used to encrypt data for a run; if it is not implemented, encryption is disabled.

## The Event Log Model

Expand All @@ -63,8 +66,8 @@ interface Storage {
};

events: {
// Create a new workflow run (runId must be null - server generates it)
create(runId: null, data: RunCreatedEventRequest, params?: CreateEventParams): Promise<EventResult>;
// Create a new workflow run (runId may be client-provided or null for server generation)
create(runId: string | null, data: RunCreatedEventRequest, params?: CreateEventParams): Promise<EventResult>;

// Create an event for an existing run
create(runId: string, data: CreateEventRequest, params?: CreateEventParams): Promise<EventResult>;
Expand All @@ -88,7 +91,7 @@ interface Storage {
2. Atomically update the affected entity (run, step, or hook)
3. Return both the created event and the updated entity

**Run Creation:** For `run_created` events, the `runId` parameter is `null`. Your World generates and returns a new `runId`.
**Run Creation:** For `run_created` events, the `runId` parameter may be a client-provided string or `null`. When `null`, your World generates and returns a new `runId`.

**Hook Tokens:** Hook tokens must be unique. If a `hook_created` event conflicts with an existing token, return a `hook_conflict` event instead.

Expand Down Expand Up @@ -165,13 +168,19 @@ The Streamer interface enables real-time data streaming:
interface Streamer {
writeToStream(
name: string,
runId: string | Promise<string>,
runId: string,
chunk: string | Uint8Array
): Promise<void>;

writeToStreamMulti?(
name: string,
runId: string,
chunks: (string | Uint8Array)[]
): Promise<void>;

closeStream(
name: string,
runId: string | Promise<string>
runId: string
): Promise<void>;

readFromStream(
Expand Down Expand Up @@ -202,6 +211,7 @@ interface Streamer {
```

Streams are identified by a combination of `runId` and `name`. Each workflow run can have multiple named streams.
`writeToStreamMulti()` is an optional optimization for batching multiple writes.

`getStreamChunks` returns a paginated snapshot of currently available chunks (unlike `readFromStream` which returns a live `ReadableStream` that waits for new chunks). `getStreamInfo` returns the tail index (last chunk index, 0-based, or `-1` when empty) and whether the stream is complete — useful for resolving negative `startIndex` values into absolute positions.

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/deploying/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ For self-hosting or deploying to other cloud providers, you can use community-ma
To use a different World implementation, set the `WORKFLOW_TARGET_WORLD` environment variable:

```bash
export WORKFLOW_TARGET_WORLD=@workflow-worlds/postgres
export WORKFLOW_TARGET_WORLD=@workflow/world-postgres
# Plus any world-specific configuration
export DATABASE_URL=postgres://...
```
Expand All @@ -89,7 +89,7 @@ The [Observability tools](/docs/observability) work with any World backend. By d
npx workflow inspect runs

# Inspect remote workflows
npx workflow inspect runs --backend @workflow-worlds/postgres
npx workflow inspect runs --backend @workflow/world-postgres
```

Learn more about [Observability](/docs/observability) tools.
2 changes: 1 addition & 1 deletion docs/content/docs/errors/hook-conflict.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This error occurs when you try to create a hook with a token that is already in
## Error Message

```
Hook token conflict: Hook with token <token> already exists for this project
Hook token "<token>" is already in use by another workflow
```

## Why This Happens
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/errors/node-js-module-in-workflow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async function read(filePath: string) {
These common Node.js core modules cannot be used in workflow functions:

- File system: `fs`, `path`
- Network: `http`, `https`, `net`, `dns`, `fetch`
- Network: `http`, `https`, `net`, `dns`
- Process: `child_process`, `cluster`
- Crypto: `crypto` (use Web Crypto API instead)
- Operating system: `os`
Expand Down
Loading
Loading