diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cda39152e..a73fc32c9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,8 +14,8 @@ concurrency: cancel-in-progress: true jobs: - docs-links: - name: Docs Links + docs: + name: Docs runs-on: ubuntu-latest env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} @@ -41,6 +41,14 @@ jobs: - name: Install Dependencies run: pnpm install --frozen-lockfile - - name: Validate docs links - run: bun ./scripts/lint.ts + - name: Check docs links + run: pnpm run lint:links + working-directory: docs + + - name: Check single quotes in code blocks + run: pnpm run lint:quotes + working-directory: docs + + - name: Check TypeScript syntax in examples + run: pnpm run lint:ts-examples working-directory: docs diff --git a/docs/app/og/[...slug]/background.png b/docs/app/og/[...slug]/background.png new file mode 100644 index 000000000..2539505b9 Binary files /dev/null and b/docs/app/og/[...slug]/background.png differ diff --git a/docs/app/og/[...slug]/geist-sans-regular.ttf b/docs/app/og/[...slug]/geist-sans-regular.ttf new file mode 100644 index 000000000..a10d58ae2 Binary files /dev/null and b/docs/app/og/[...slug]/geist-sans-regular.ttf differ diff --git a/docs/app/og/[...slug]/geist-sans-semibold.ttf b/docs/app/og/[...slug]/geist-sans-semibold.ttf new file mode 100644 index 000000000..5732da7a6 Binary files /dev/null and b/docs/app/og/[...slug]/geist-sans-semibold.ttf differ diff --git a/docs/app/og/[...slug]/route.tsx b/docs/app/og/[...slug]/route.tsx new file mode 100644 index 000000000..3a9d71130 --- /dev/null +++ b/docs/app/og/[...slug]/route.tsx @@ -0,0 +1,89 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { ImageResponse } from 'next/og'; +import type { NextRequest } from 'next/server'; +import { getPageImage, source } from '@/lib/geistdocs/source'; + +export const GET = async ( + _request: NextRequest, + { params }: RouteContext<'/og/[...slug]'> +) => { + const { slug } = await params; + const page = source.getPage(slug.slice(0, -1)); + + if (!page) { + return new Response('Not found', { status: 404 }); + } + + const { title, description } = page.data; + + const regularFont = await readFile( + join(process.cwd(), 'app/og/[...slug]/geist-sans-regular.ttf') + ); + + const semiboldFont = await readFile( + join(process.cwd(), 'app/og/[...slug]/geist-sans-semibold.ttf') + ); + + const backgroundImage = await readFile( + join(process.cwd(), 'app/og/[...slug]/background.png') + ); + + const backgroundImageData = backgroundImage.buffer.slice( + backgroundImage.byteOffset, + backgroundImage.byteOffset + backgroundImage.byteLength + ); + + return new ImageResponse( +
+ {/** biome-ignore lint/performance/noImgElement: "Required for Satori" */} + Vercel OpenGraph Background +
+
+ {title} +
+
+ {description} +
+
+
, + { + width: 1200, + height: 628, + fonts: [ + { + name: 'Geist', + data: regularFont, + weight: 400, + }, + { + name: 'Geist', + data: semiboldFont, + weight: 500, + }, + ], + } + ); +}; + +export const generateStaticParams = () => + source.getPages().map((page) => ({ + slug: getPageImage(page).segments, + })); diff --git a/docs/components/docs/getting-started/intro.tsx b/docs/components/docs/getting-started/intro.tsx new file mode 100644 index 000000000..2f4ab8b8e --- /dev/null +++ b/docs/components/docs/getting-started/intro.tsx @@ -0,0 +1,9 @@ +export function GettingStartedIntro({ framework }: { framework: string }) { + return ( +

+ This guide will walk through setting up your first workflow in a{' '} + {framework} app. Along the way, you'll learn more about the concepts that + are fundamental to using the development kit in your own projects. +

+ ); +} diff --git a/docs/components/geistdocs/docs-page.tsx b/docs/components/geistdocs/docs-page.tsx index 405ee2253..4373b9530 100644 --- a/docs/components/geistdocs/docs-page.tsx +++ b/docs/components/geistdocs/docs-page.tsx @@ -7,7 +7,7 @@ import { import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import type { ComponentProps, CSSProperties } from 'react'; -import { source } from '@/lib/geistdocs/source'; +import { getPageImage, source } from '@/lib/geistdocs/source'; import { cn } from '@/lib/utils'; const containerStyle = { @@ -75,6 +75,9 @@ export const generatePageMetadata = (slug: PageProps['slug']) => { const metadata: Metadata = { title: page.data.title, description: page.data.description, + openGraph: { + images: getPageImage(page).url, + }, }; return metadata; diff --git a/docs/components/geistdocs/mdx-components.tsx b/docs/components/geistdocs/mdx-components.tsx index 616e144ef..9ba257306 100644 --- a/docs/components/geistdocs/mdx-components.tsx +++ b/docs/components/geistdocs/mdx-components.tsx @@ -14,6 +14,7 @@ import { CodeBlockTabsList, CodeBlockTabsTrigger, } from './code-block-tabs'; +import { GettingStartedIntro } from '../docs/getting-started/intro'; import { Mermaid } from './mermaid'; import { Video } from './video'; @@ -44,4 +45,6 @@ export const getMDXComponents = ( Mermaid, Video, + + GettingStartedIntro, }); diff --git a/docs/content/docs/ai/chat-session-modeling.mdx b/docs/content/docs/ai/chat-session-modeling.mdx index a947e144b..270801d09 100644 --- a/docs/content/docs/ai/chat-session-modeling.mdx +++ b/docs/content/docs/ai/chat-session-modeling.mdx @@ -14,7 +14,7 @@ Each user message triggers a new workflow run. The client or API route owns the -```typescript title="workflows/chat/workflow.ts" lineNumbers +```ts title="workflows/chat/workflow.ts" lineNumbers import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools"; @@ -44,7 +44,7 @@ export async function chatWorkflow(messages: ModelMessage[]) { -```typescript title="app/api/chat/route.ts" lineNumbers +```ts title="app/api/chat/route.ts" lineNumbers import type { UIMessage } from "ai"; import { createUIMessageStreamResponse, convertToModelMessages } from "ai"; import { start } from "workflow/api"; @@ -68,7 +68,7 @@ export async function POST(req: Request) { 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 +```ts title="app/chats/[id]/page.tsx" lineNumbers "use client"; import { useChat } from "@ai-sdk/react"; @@ -131,7 +131,7 @@ A single workflow handles the entire conversation session across multiple turns, -```typescript title="workflows/chat/workflow.ts" lineNumbers +```ts title="workflows/chat/workflow.ts" lineNumbers import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import { chatMessageHook } from "./hooks/chat-message"; @@ -179,7 +179,7 @@ export async function chatWorkflow(threadId: string, initialMessage: string) { Two endpoints: one to start the session, one to send follow-up messages. -```typescript title="app/api/chat/route.ts" lineNumbers +```ts title="app/api/chat/route.ts" lineNumbers import { createUIMessageStreamResponse } from "ai"; import { start } from "workflow/api"; import { chatWorkflow } from "@/workflows/chat/workflow"; @@ -195,7 +195,7 @@ export async function POST(req: Request) { } ``` -```typescript title="app/api/chat/[id]/route.ts" lineNumbers +```ts title="app/api/chat/[id]/route.ts" lineNumbers import { chatMessageHook } from "@/workflows/chat/hooks/chat-message"; export async function POST( @@ -215,7 +215,7 @@ export async function POST( -```typescript title="workflows/chat/hooks/chat-message.ts" lineNumbers +```ts title="workflows/chat/hooks/chat-message.ts" lineNumbers import { defineHook } from "workflow"; import { z } from "zod"; @@ -232,7 +232,7 @@ export const chatMessageHook = defineHook({ We can replace our `useChat` react hook with a custom hook that manages the chat session. This hook will handle switching between the API endpoints for creating a new thread and sending follow-up messages. -```typescript title="hooks/use-multi-turn-chat.ts" lineNumbers +```ts title="hooks/use-multi-turn-chat.ts" lineNumbers "use client"; import type { UIMessage } from "ai"; @@ -324,7 +324,7 @@ The multi-turn pattern also easily enables multi-player chat sessions. New messa Internal system events like scheduled tasks, background jobs, or database triggers can inject updates into an active conversation. -```typescript title="app/api/internal/flight-update/route.ts" lineNumbers +```ts title="app/api/internal/flight-update/route.ts" lineNumbers import { chatMessageHook } from "@/workflows/chat/hooks/chat-message"; // Called by your flight status monitoring system @@ -345,7 +345,7 @@ export async function POST(req: Request) { External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events. -```typescript title="app/api/webhooks/payment/route.ts" lineNumbers +```ts title="app/api/webhooks/payment/route.ts" lineNumbers import { chatMessageHook } from "@/workflows/chat/hooks/chat-message"; export async function POST(req: Request) { @@ -367,7 +367,7 @@ export async function POST(req: Request) { Multiple human users can participate in the same conversation. Each user's client connects to the same workflow stream. -```typescript title="app/api/chat/[id]/route.ts" lineNumbers +```ts title="app/api/chat/[id]/route.ts" lineNumbers import { chatMessageHook } from "@/workflows/chat/hooks/chat-message"; import { getUser } from "@/lib/auth"; diff --git a/docs/content/docs/ai/defining-tools.mdx b/docs/content/docs/ai/defining-tools.mdx index fc17ca381..7464d53df 100644 --- a/docs/content/docs/ai/defining-tools.mdx +++ b/docs/content/docs/ai/defining-tools.mdx @@ -12,7 +12,7 @@ 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 +```ts title="tools.ts" lineNumbers async function getWeather( { city }: { city: string }, { messages, toolCallId }: { messages: LanguageModelV2Prompt, toolCallId: string }) { // [!code highlight] @@ -27,7 +27,7 @@ As discussed in [Streaming Updates from Tools](/docs/ai/streaming-updates-from-t This can be made generic, by creating a helper step function to write arbitrary data to the stream: -```typescript title="tools.ts" lineNumbers +```ts title="tools.ts" lineNumbers import { getWritable } from "workflow"; async function writeToStream(data: any) { @@ -54,7 +54,7 @@ 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 +```ts title="tools.ts" lineNumbers // Step: handles I/O with retries async function performFetch(url: string) { "use step"; diff --git a/docs/content/docs/ai/human-in-the-loop.mdx b/docs/content/docs/ai/human-in-the-loop.mdx index 1b4d421d6..4c6dcd965 100644 --- a/docs/content/docs/ai/human-in-the-loop.mdx +++ b/docs/content/docs/ai/human-in-the-loop.mdx @@ -48,7 +48,7 @@ Add a tool that allows the agent to deliberately pause execution until a human a Create a typed hook with a Zod schema for validation: -```typescript title="workflow/steps/tools.ts" lineNumbers +```ts title="workflow/steps/tools.ts" lineNumbers import { defineHook } from "workflow"; import { z } from "zod"; // ... existing imports ... @@ -71,7 +71,7 @@ export const bookingApprovalHook = defineHook({ 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. -```typescript title="workflows/chat/steps/tools.ts" lineNumbers +```ts title="workflows/chat/steps/tools.ts" lineNumbers import { z } from "zod"; // ... @@ -122,7 +122,7 @@ Note that the `defineHook().create()` function must be called from within a work Create a new API endpoint that the UI will call to submit the approval decision: -```typescript title="app/api/approve-booking/route.ts" lineNumbers +```ts title="app/api/approve-booking/route.ts" lineNumbers import { bookingApprovalHook } from "@/workflow/steps/tools"; export async function POST(request: Request) { @@ -147,7 +147,7 @@ export async function POST(request: Request) { Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking: -```typescript title="components/booking-approval.tsx" lineNumbers +```ts title="components/booking-approval.tsx" lineNumbers "use client"; import { useState } from "react"; @@ -236,7 +236,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr Use the component we just created to render the tool call and approval controls in your chat interface: -```typescript title="app/page.tsx" lineNumbers +```ts title="app/page.tsx" lineNumbers // ... existing imports ... import { BookingApproval } from "@/components/booking-approval"; @@ -313,7 +313,7 @@ export default function ChatPage() { 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: -```typescript title="workflows/chat/steps/tools.ts" lineNumbers +```ts title="workflows/chat/steps/tools.ts" lineNumbers import { createWebhook } from "workflow"; import { z } from "zod"; diff --git a/docs/content/docs/ai/index.mdx b/docs/content/docs/ai/index.mdx index 3ac30e7d5..98bedcb50 100644 --- a/docs/content/docs/ai/index.mdx +++ b/docs/content/docs/ai/index.mdx @@ -78,7 +78,7 @@ OPENAI_API_KEY=... Then modify your API endpoint to use the OpenAI provider: -```typescript title="app/api/chat/route.ts" lineNumbers +```ts title="app/api/chat/route.ts" lineNumbers // ... import { openai } from "@workflow/ai/openai"; // [!code highlight] @@ -90,6 +90,8 @@ export async function POST(req: Request) { model: openai("gpt-5.1"), // [!code highlight] // ... }); + // ... +} ``` @@ -110,7 +112,7 @@ 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 +```ts title="app/api/chat/route.ts" lineNumbers export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const agent = new Agent({ // [!code highlight] @@ -132,7 +134,7 @@ export async function POST(req: Request) { Our tools are mostly mocked out for the sake of the example. We use AI SDK's `tool` function to define the tool, and pass it to the agent. In your own app, this might be any kind of tool call, like database queries, calls to external services, etc. -```typescript title="workflows/chat/steps/tools.ts" lineNumbers +```ts title="workflows/chat/steps/tools.ts" lineNumbers import { tool } from "ai"; import { z } from "zod"; @@ -155,7 +157,7 @@ async function searchFlights({ from, to, date }: { from: string; to: string; dat Our `ChatPage` component has a lot of logic for nicely displaying the chat messages, but at it's core, it's simply managing input/output for the [`useChat` hook](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat#usechat) from AI SDK. -```typescript title="app/chat.tsx" lineNumbers +```ts title="app/chat.tsx" lineNumbers "use client"; import { useChat } from "@ai-sdk/react"; @@ -221,7 +223,7 @@ npm i workflow @workflow/ai and extend the Next.js config to transform your workflow code (see [Getting Started](/docs/getting-started/next) for more details). -```typescript title="next.config.ts" lineNumbers +```ts title="next.config.ts" lineNumbers import { withWorkflow } from "workflow/next"; import type { NextConfig } from "next"; @@ -240,7 +242,7 @@ export default withWorkflow(nextConfig); Move the agent logic into a separate function, which will serve as our workflow definition. -```typescript title="workflows/chat/workflow.ts" lineNumbers +```ts title="workflows/chat/workflow.ts" lineNumbers import { DurableAgent } from "@workflow/ai/agent"; // [!code highlight] import { getWritable } from "workflow"; // [!code highlight] import { tools } from "@/ai/tools"; @@ -283,7 +285,7 @@ Key changes: Remove the agent call that we just extracted, and replace it with a call to `start()` to run the workflow: -```typescript title="app/api/chat/route.ts" lineNumbers +```ts title="app/api/chat/route.ts" lineNumbers import type { UIMessage } from "ai"; import { convertToModelMessages, createUIMessageStreamResponse } from "ai"; import { start } from "workflow/api"; @@ -313,7 +315,7 @@ Key changes: Mark all tool definitions with `"use step"` to make them durable. This enables automatic retries and observability for each tool call: -```typescript title="workflows/chat/steps/tools.ts​" lineNumbers +```ts title="workflows/chat/steps/tools.ts​" lineNumbers lint-nocheck // ... export async function searchFlights( diff --git a/docs/content/docs/ai/message-queueing.mdx b/docs/content/docs/ai/message-queueing.mdx index 255a80d6b..afa31d2bb 100644 --- a/docs/content/docs/ai/message-queueing.mdx +++ b/docs/content/docs/ai/message-queueing.mdx @@ -22,7 +22,7 @@ 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 +```ts lineNumbers interface PrepareStepInfo { model: string | (() => Promise); // Current model stepNumber: number; // 0-indexed step count @@ -40,7 +40,7 @@ interface PrepareStepResult { Once you have a [multi-turn workflow](/docs/ai/chat-session-modeling#multi-turn-workflows), you can combine a message queue with `prepareStep` to inject messages that arrive during processing: -```typescript title="workflows/chat/workflow.ts" lineNumbers +```ts title="workflows/chat/workflow.ts" lineNumbers import { DurableAgent } from "@workflow/ai/agent"; import type { UIMessageChunk } from "ai"; import { getWritable } from "workflow"; diff --git a/docs/content/docs/ai/resumable-streams.mdx b/docs/content/docs/ai/resumable-streams.mdx index a2cf3f9a4..905e3c079 100644 --- a/docs/content/docs/ai/resumable-streams.mdx +++ b/docs/content/docs/ai/resumable-streams.mdx @@ -20,7 +20,7 @@ Let's add stream resumption to our Flight Booking Agent that we build in the [Bu Modify your chat endpoint to include the workflow run ID in a response header. The Run ID uniquely identifies the run's stream, so it allows the client to know which stream to reconnect to. -```typescript title="app/api/chat/route.ts" lineNumbers +```ts title="app/api/chat/route.ts" lineNumbers // ... imports ... export async function POST(req: Request) { @@ -46,7 +46,7 @@ export async function POST(req: Request) { Currently we only have one API endpoint that always creates a new run, so we need to create a new API route that returns the stream for an existing run: -```typescript title="app/api/chat/[id]/stream/route.ts" lineNumbers +```ts title="app/api/chat/[id]/stream/route.ts" lineNumbers import { createUIMessageStreamResponse } from "ai"; import { getRun } from "workflow/api"; // [!code highlight] @@ -83,7 +83,7 @@ Replace the default transport in AI-SDK's `useChat` with [`WorkflowChatTransport /docs/api-reference/workflow-ai/workflow-chat-transport ), and update the callbacks to store and use the latest run ID. For now, we'll store the run ID in localStorage. For your own app, this would be stored wherever you store session information. -```typescript title="app/page.tsx" lineNumbers +```ts title="app/page.tsx" lineNumbers "use client"; import { useChat } from "@ai-sdk/react"; diff --git a/docs/content/docs/ai/sleep-and-delays.mdx b/docs/content/docs/ai/sleep-and-delays.mdx index b2bd82eef..928ddcbc4 100644 --- a/docs/content/docs/ai/sleep-and-delays.mdx +++ b/docs/content/docs/ai/sleep-and-delays.mdx @@ -22,7 +22,7 @@ Sleep is a built-in function in Workflow DevKit, so exposing it as a tool is as Add a new "sleep" tool to the `tools` defined in `workflows/chat/steps/tools.ts`: -```typescript title="workflows/chat/steps/tools.ts" lineNumbers +```ts title="workflows/chat/steps/tools.ts" lineNumbers import { getWritable, sleep } from "workflow"; // [!code highlight] // ... existing imports ... @@ -63,7 +63,7 @@ export const flightBookingTools = { To round it off, extend the UI to display the tool call status. This can be done either by displaying the tool call information directly, or by emitting custom data parts to the stream (see [Streaming Updates from Tools](/docs/ai/streaming-updates-from-tools) for more details). In this case, since there aren't any fine-grained progress updates to show, we'll just display the tool call information directly: -```typescript title="app/page.tsx" lineNumbers +```tsx title="app/page.tsx" lineNumbers export default function ChatPage() { // ... @@ -77,8 +77,7 @@ export default function ChatPage() { return (
- // ... - + {/* ... */} {messages.map((message, index) => { @@ -86,7 +85,7 @@ export default function ChatPage() { return (
- // ... + {/* ... */} {message.parts.map((part, partIndex) => { @@ -98,7 +97,7 @@ export default function ChatPage() { part.type === "tool-checkFlightStatus" || part.type === "tool-getAirportInfo" || part.type === "tool-bookFlight" || - part.type === "tool-checkBaggageAllowance" + part.type === "tool-checkBaggageAllowance" || part.type === "tool-sleep" // [!code highlight] ) { // ... @@ -113,8 +112,7 @@ export default function ChatPage() { - - // ... + {/* ... */}
); } @@ -131,6 +129,7 @@ function renderToolOutput(part: any) { ); // [!code highlight] } // ... + } } ``` @@ -149,7 +148,7 @@ 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 +```ts lineNumbers async function callRateLimitedAPI(endpoint: string) { "use step"; @@ -170,7 +169,7 @@ async function callRateLimitedAPI(endpoint: string) { Poll for a result with increasing delays: -```typescript lineNumbers +```ts lineNumbers export async function pollForResult(jobId: string) { "use workflow"; diff --git a/docs/content/docs/ai/streaming-updates-from-tools.mdx b/docs/content/docs/ai/streaming-updates-from-tools.mdx index addbe73b2..7f4b1935d 100644 --- a/docs/content/docs/ai/streaming-updates-from-tools.mdx +++ b/docs/content/docs/ai/streaming-updates-from-tools.mdx @@ -16,7 +16,7 @@ As an example, we'll extend out Flight Booking Agent to use emit more granular p First, define a TypeScript type for your custom data part. This ensures type safety across your tool and client code: -```typescript title="schemas/chat.ts" lineNumbers +```ts title="schemas/chat.ts" lineNumbers export interface FoundFlightDataPart { type: "data-found-flight"; // [!code highlight] id: string; @@ -38,7 +38,7 @@ The `type` field must be a string starting with `data-` followed by your custom Use [`getWritable()`](/docs/api-reference/workflow/get-writable) inside a step function to get a handle to the stream. This is the same stream that the LLM and other tools calls are writing to, so we can inject out own data packets directly. -```typescript title="workflows/chat/steps/tools.ts" lineNumbers +```ts title="workflows/chat/steps/tools.ts" lineNumbers import { getWritable } from "workflow"; // [!code highlight] import type { UIMessageChunk } from "ai"; @@ -88,7 +88,7 @@ Key points: Update your chat component to detect and render the custom data parts. Data parts are stored in the message's `parts` array alongside text and tool invocation parts: -```typescript title="app/page.tsx" lineNumbers +```ts title="app/page.tsx" lineNumbers {message.parts.map((part, partIndex) => { // Render text parts if (part.type === "text") { diff --git a/docs/content/docs/api-reference/workflow-ai/durable-agent.mdx b/docs/content/docs/api-reference/workflow-ai/durable-agent.mdx index 9b8fc7df5..a6afe6aa5 100644 --- a/docs/content/docs/api-reference/workflow-ai/durable-agent.mdx +++ b/docs/content/docs/api-reference/workflow-ai/durable-agent.mdx @@ -10,7 +10,7 @@ The `DurableAgent` class enables you to create AI-powered agents that can mainta Tool calls can be implemented as workflow steps for automatic retries, or as regular workflow-level logic utilizing core library features such as [`sleep()`](/docs/api-reference/workflow/sleep) and [Hooks](/docs/foundations/hooks). -```typescript lineNumbers +```ts lineNumbers import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import { z } from "zod"; @@ -113,7 +113,7 @@ export default PrepareStepResult;`} ### Basic Agent with Tools -```typescript +```ts import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import { z } from "zod"; @@ -155,7 +155,7 @@ async function weatherAgentWorkflow(userQuery: string) { ### Multiple Tools -```typescript +```ts import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import { z } from "zod"; @@ -204,7 +204,7 @@ async function multiToolAgentWorkflow(userQuery: string) { ### Multi-turn Conversation -```typescript +```ts import { DurableAgent } from "@workflow/ai/agent"; import { z } from "zod"; @@ -256,7 +256,7 @@ async function multiTurnAgentWorkflow() { ### Tools with Workflow Library Features -```typescript +```ts import { DurableAgent } from "@workflow/ai/agent"; import { sleep, defineHook, getWritable } from "workflow"; import { z } from "zod"; @@ -325,7 +325,7 @@ async function agentWithLibraryFeaturesWorkflow(userRequest: string) { Use `prepareStep` to modify settings before each step in the agent loop: -```typescript +```ts import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import type { UIMessageChunk } from "ai"; @@ -369,7 +369,7 @@ async function agentWithPrepareStep(userMessage: string) { Inject messages from external sources (like hooks) before each LLM call: -```typescript +```ts import { DurableAgent } from "@workflow/ai/agent"; import { getWritable, defineHook } from "workflow"; import type { UIMessageChunk } from "ai"; diff --git a/docs/content/docs/api-reference/workflow-ai/workflow-chat-transport.mdx b/docs/content/docs/api-reference/workflow-ai/workflow-chat-transport.mdx index 54c3f2629..23eefb6fc 100644 --- a/docs/content/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +++ b/docs/content/docs/api-reference/workflow-ai/workflow-chat-transport.mdx @@ -12,7 +12,7 @@ A chat transport implementation for the AI SDK that provides reliable message st `WorkflowChatTransport` implements the [`ChatTransport`](https://ai-sdk.dev/docs/ai-sdk-ui/transport) interface from the AI SDK and is designed to work with workflow-based chat applications. It requires endpoints that return the `x-workflow-run-id` header to enable stream resumption. -```typescript lineNumbers +```ts lineNumbers import { useChat } from "@ai-sdk/react"; import { WorkflowChatTransport } from "@workflow/ai"; @@ -69,7 +69,7 @@ export default WorkflowChatTransportOptions;`} ### Basic Chat Setup -```typescript +```ts "use client"; import { useChat } from "@ai-sdk/react"; @@ -112,7 +112,7 @@ export default function BasicChat() { ### With Session Persistence and Resumption -```typescript +```ts "use client"; import { useChat } from "@ai-sdk/react"; @@ -180,7 +180,7 @@ export default function ChatWithResumption() { ### With Custom Request Configuration -```typescript +```ts "use client"; import { useChat } from "@ai-sdk/react"; diff --git a/docs/content/docs/api-reference/workflow-api/get-run.mdx b/docs/content/docs/api-reference/workflow-api/get-run.mdx index 05862bd02..0bedaccb8 100644 --- a/docs/content/docs/api-reference/workflow-api/get-run.mdx +++ b/docs/content/docs/api-reference/workflow-api/get-run.mdx @@ -6,7 +6,7 @@ Retrieves the workflow run metadata and status information for a given run ID. T Use this function when you need to check workflow status, get timing information, or access workflow metadata without blocking on workflow completion. -```typescript lineNumbers +```ts lineNumbers import { getRun } from "workflow/api"; const run = getRun("my-run-id"); @@ -48,7 +48,7 @@ export default WorkflowReadableStreamOptions;`} Check the current status of a workflow run: -```typescript lineNumbers +```ts lineNumbers import { getRun } from "workflow/api"; export async function GET(req: Request) { diff --git a/docs/content/docs/api-reference/workflow-api/resume-hook.mdx b/docs/content/docs/api-reference/workflow-api/resume-hook.mdx index 938f406d6..06968cfe5 100644 --- a/docs/content/docs/api-reference/workflow-api/resume-hook.mdx +++ b/docs/content/docs/api-reference/workflow-api/resume-hook.mdx @@ -10,7 +10,7 @@ It creates a `hook_received` event and re-triggers the workflow to continue exec `resumeHook` is a runtime function that must be called from outside a workflow function. -```typescript lineNumbers +```ts lineNumbers import { resumeHook } from "workflow/api"; export async function POST(request: Request) { @@ -55,7 +55,7 @@ showSections={["returns"]} Using `resumeHook` in a basic API route to resume a hook: -```typescript lineNumbers +```ts lineNumbers import { resumeHook } from "workflow/api"; export async function POST(request: Request) { @@ -78,7 +78,7 @@ export async function POST(request: Request) { Defining a payload type and using `resumeHook` to resume a hook with type safety: -```typescript lineNumbers +```ts lineNumbers import { resumeHook } from "workflow/api"; type ApprovalPayload = { @@ -106,7 +106,7 @@ export async function POST(request: Request) { Using `resumeHook` in Next.js server actions to resume a hook: -```typescript lineNumbers +```ts lineNumbers "use server"; import { resumeHook } from "workflow/api"; @@ -125,7 +125,7 @@ export async function approveRequest(token: string, approved: boolean) { Using `resumeHook` in a generic webhook handler to resume a hook: -```typescript lineNumbers +```ts lineNumbers import { resumeHook } from "workflow/api"; // Generic webhook handler that forwards data to a hook diff --git a/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx b/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx index 902124d43..81287f3c9 100644 --- a/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx +++ b/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx @@ -10,7 +10,7 @@ This function creates a `hook_received` event and re-triggers the workflow to co `resumeWebhook` is a runtime function that must be called from outside a workflow function. -```typescript lineNumbers +```ts lineNumbers import { resumeWebhook } from "workflow/api"; export async function POST(request: Request) { @@ -55,7 +55,7 @@ Throws an error if the webhook token is not found or invalid. Forward incoming HTTP requests to a webhook by token: -```typescript lineNumbers +```ts lineNumbers import { resumeWebhook } from "workflow/api"; export async function POST(request: Request) { @@ -79,7 +79,7 @@ export async function POST(request: Request) { Handle GitHub webhook events and forward them to workflows: -```typescript lineNumbers +```ts lineNumbers import { resumeWebhook } from "workflow/api"; import { verifyGitHubSignature } from "@/lib/github"; @@ -112,7 +112,7 @@ export async function POST(request: Request) { Process Slack slash commands and route them to workflow webhooks: -```typescript lineNumbers +```ts lineNumbers import { resumeWebhook } from "workflow/api"; export async function POST(request: Request) { @@ -151,7 +151,7 @@ export async function POST(request: Request) { Route webhooks to different workflows based on tenant/organization: -```typescript lineNumbers +```ts lineNumbers import { resumeWebhook } from "workflow/api"; export async function POST(request: Request) { @@ -194,7 +194,7 @@ async function verifyTenantApiKey(tenantId: string, apiKey: string | null) { Use `resumeWebhook` in a Next.js server action: -```typescript lineNumbers +```ts lineNumbers "use server"; import { resumeWebhook } from "workflow/api"; diff --git a/docs/content/docs/api-reference/workflow-api/start.mdx b/docs/content/docs/api-reference/workflow-api/start.mdx index fd2f63383..856562bfa 100644 --- a/docs/content/docs/api-reference/workflow-api/start.mdx +++ b/docs/content/docs/api-reference/workflow-api/start.mdx @@ -4,7 +4,7 @@ title: start Start/enqueue a new workflow run. -```typescript lineNumbers +```ts lineNumbers import { start } from "workflow/api"; import { myWorkflow } from "./workflows/my-workflow"; @@ -54,7 +54,7 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow- ### With Arguments -```typescript +```ts import { start } from "workflow/api"; import { userSignupWorkflow } from "./workflows/user-signup"; @@ -63,7 +63,7 @@ const run = await start(userSignupWorkflow, ["user@example.com"]); // [!code hig ### With `StartOptions` -```typescript +```ts import { start } from "workflow/api"; import { myWorkflow } from "./workflows/my-workflow"; diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index 5c6428457..ea827c6a7 100644 --- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx +++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx @@ -8,7 +8,7 @@ Configures webpack/turbopack loaders to transform workflow code (`"use step"`/`" To enable `"use step"` and `"use workflow"` directives while developing locally or deploying to production, wrap your `nextConfig` with `withWorkflow`. -```typescript title="next.config.ts" lineNumbers +```ts title="next.config.ts" lineNumbers import { withWorkflow } from "workflow/next"; // [!code highlight] import type { NextConfig } from "next"; @@ -24,7 +24,7 @@ export default withWorkflow(nextConfig, workflowConfig); // [!code highlight] 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 +```ts title="next.config.ts" lineNumbers import { NextConfig } from "next"; import { withWorkflow } from "workflow/next"; import createNextIntlPlugin from "next-intl/plugin"; diff --git a/docs/content/docs/api-reference/workflow/create-hook.mdx b/docs/content/docs/api-reference/workflow/create-hook.mdx index ab1281966..9be88e078 100644 --- a/docs/content/docs/api-reference/workflow/create-hook.mdx +++ b/docs/content/docs/api-reference/workflow/create-hook.mdx @@ -62,7 +62,7 @@ The returned `Hook` object also implements `AsyncIterable`, which allows you When creating a hook, you can specify a payload type to be used for automatic type safety. -```typescript lineNumbers +```ts lineNumbers import { createHook } from "workflow" export async function approvalWorkflow() { @@ -83,7 +83,7 @@ export async function approvalWorkflow() { Tokens are used to identify a specific hook. You can customize the token to be more specific to a use case. -```typescript lineNumbers +```ts lineNumbers import { createHook } from "workflow"; export async function slackBotWorkflow(channelId: string) { @@ -107,7 +107,7 @@ export async function slackBotWorkflow(channelId: string) { You can also wait for multiple payloads by using the `for await...of` syntax. -```typescript lineNumbers +```ts lineNumbers import { createHook } from "workflow" export async function collectHookWorkflow() { diff --git a/docs/content/docs/api-reference/workflow/create-webhook.mdx b/docs/content/docs/api-reference/workflow/create-webhook.mdx index 600bda62c..e07615eb7 100644 --- a/docs/content/docs/api-reference/workflow/create-webhook.mdx +++ b/docs/content/docs/api-reference/workflow/create-webhook.mdx @@ -53,7 +53,7 @@ The `RequestWithResponse` type extends the standard `Request` interface with a ` Create a webhook that receives HTTP requests and logs the request details: -```typescript lineNumbers +```ts lineNumbers import { createWebhook } from "workflow" export async function basicWebhookWorkflow() { @@ -76,7 +76,7 @@ export async function basicWebhookWorkflow() { Use the `respondWith()` method to send custom HTTP responses. Note that `respondWith()` must be called from within a step function: -```typescript lineNumbers +```ts lineNumbers import { createWebhook, type RequestWithResponse } from "workflow" async function sendResponse(request: RequestWithResponse) { // [!code highlight] @@ -116,7 +116,7 @@ async function processData(data: any) { Tokens are used to identify a specific webhook. You can customize the token to be more specific to a use case. -```typescript lineNumbers +```ts lineNumbers import { type RequestWithResponse } from "workflow" async function sendAck(request: RequestWithResponse) { @@ -156,7 +156,7 @@ async function deployCommit(event: any) { You can also wait for multiple requests by using the `for await...of` syntax. -```typescript lineNumbers +```ts lineNumbers import { createWebhook, type RequestWithResponse } from "workflow" async function sendSlackResponse(request: RequestWithResponse, message: string) { diff --git a/docs/content/docs/api-reference/workflow/define-hook.mdx b/docs/content/docs/api-reference/workflow/define-hook.mdx index d9af601da..79baecbbd 100644 --- a/docs/content/docs/api-reference/workflow/define-hook.mdx +++ b/docs/content/docs/api-reference/workflow/define-hook.mdx @@ -63,7 +63,7 @@ export default DefineHook;`} By defining the hook once with a specific payload type, you can reuse it in multiple workflows and API routes with automatic type safety. -```typescript lineNumbers +```ts lineNumbers import { defineHook } from "workflow"; // Define once with a specific payload type @@ -88,7 +88,7 @@ export async function workflowWithApproval() { Hooks can be resumed using the same defined hook and a token. By using the same hook, you can ensure that the payload matches the defined type when resuming a hook. -```typescript lineNumbers +```ts lineNumbers // Use the same defined hook to resume export async function POST(request: Request) { const { token, approved, comment } = await request.json(); @@ -119,7 +119,7 @@ Standard Schema is a standardized specification for schema validation libraries. Here's an example using [Zod](https://zod.dev) to validate and transform hook payloads: -```typescript lineNumbers +```ts lineNumbers import { defineHook } from "workflow"; import { z } from "zod"; @@ -146,7 +146,7 @@ export async function approvalWorkflow(approvalId: string) { When resuming the hook from an API route, the schema validates and transforms the incoming payload before the workflow resumes: -```typescript lineNumbers +```ts lineNumbers export async function POST(request: Request) { // Incoming payload: { token: "...", approved: true, comment: " Ready! " } const { token, approved, comment } = await request.json(); @@ -169,7 +169,7 @@ export async function POST(request: Request) { The same pattern works with any Standard Schema v1 compliant library. Here's an example with [Valibot](https://valibot.dev): -```typescript lineNumbers +```ts lineNumbers import { defineHook } from "workflow"; import * as v from "valibot"; @@ -185,7 +185,7 @@ 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 +```ts lineNumbers const slackHook = defineHook<{ text: string; userId: string }>(); export async function slackBotWorkflow(channelId: string) { diff --git a/docs/content/docs/api-reference/workflow/fatal-error.mdx b/docs/content/docs/api-reference/workflow/fatal-error.mdx index a83f91177..064000c59 100644 --- a/docs/content/docs/api-reference/workflow/fatal-error.mdx +++ b/docs/content/docs/api-reference/workflow/fatal-error.mdx @@ -6,7 +6,7 @@ When a `FatalError` is thrown in a step, it indicates that the workflow should n You should use this when you don't want a specific step to retry. -```typescript lineNumbers +```ts lineNumbers import { FatalError } from "workflow" async function fallibleWorkflow() { @@ -35,3 +35,8 @@ interface Error { } export default Error;`} /> + +## Related Functions + +- [`RetryableError`](/docs/api-reference/workflow/retryable-error) - Signal that a step should be retried with custom options +- [`fetch()`](/docs/api-reference/workflow/fetch) - Make HTTP requests with automatic retry handling diff --git a/docs/content/docs/api-reference/workflow/fetch.mdx b/docs/content/docs/api-reference/workflow/fetch.mdx index 754dae12c..56715d6aa 100644 --- a/docs/content/docs/api-reference/workflow/fetch.mdx +++ b/docs/content/docs/api-reference/workflow/fetch.mdx @@ -10,7 +10,7 @@ This is useful when you need to call external APIs or services from within your `fetch` is a *special* type of step function provided and should be called directly inside workflow functions. -```typescript lineNumbers +```ts lineNumbers import { fetch } from "workflow" async function apiWorkflow() { @@ -52,7 +52,7 @@ showSections={['returns']} Here's a simple example of how you can use `fetch` inside your workflow. -```typescript lineNumbers +```ts lineNumbers import { fetch } from "workflow" async function apiWorkflow() { @@ -83,7 +83,7 @@ This API is provided as a convenience to easily use `fetch` in workflow, but oft Here's an example of a custom fetch wrapper that provides more sophisticated error handling with custom retry logic: -```typescript lineNumbers +```ts lineNumbers import { FatalError, RetryableError } from "workflow" export async function customFetch( @@ -137,3 +137,9 @@ This example demonstrates: - Throwing [`FatalError`](/docs/api-reference/workflow/fatal-error) for client errors (400-499) to prevent retries. - Handling 429 rate limiting by reading the `Retry-After` header and using [`RetryableError`](/docs/api-reference/workflow/retryable-error). - Allowing automatic retries for server errors (5xx). + +## Related Functions + +- [`FatalError`](/docs/api-reference/workflow/fatal-error) - Prevent a step from retrying +- [`RetryableError`](/docs/api-reference/workflow/retryable-error) - Customize retry behavior with delay options +- [`sleep()`](/docs/api-reference/workflow/sleep) - Suspend a workflow for a duration diff --git a/docs/content/docs/api-reference/workflow/get-step-metadata.mdx b/docs/content/docs/api-reference/workflow/get-step-metadata.mdx index c8d97cdfd..7ac458181 100644 --- a/docs/content/docs/api-reference/workflow/get-step-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-step-metadata.mdx @@ -14,7 +14,7 @@ You may want to use this function when you need to: This function can only be called inside a step function. -```typescript lineNumbers +```ts lineNumbers import { getStepMetadata } from "workflow"; async function testWorkflow() { @@ -31,7 +31,7 @@ async function logStepId() { ### Example: Use `stepId` as an idempotency key -```typescript lineNumbers +```ts lineNumbers import { getStepMetadata } from "workflow"; async function chargeUser(userId: string, amount: number) { @@ -74,3 +74,7 @@ export default getStepMetadata;`} import type { StepMetadata } from "workflow"; export default StepMetadata;`} /> + +## Related Functions + +- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) - Get metadata about the current workflow run diff --git a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx index 2e1749d35..504e2fd4c 100644 --- a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx @@ -6,14 +6,14 @@ Returns additional metadata available in the current workflow function. You may want to use this function when you need to: -* Log workflow run IDs -* Access timing information of a workflow +- Log workflow run IDs +- Access timing information of a workflow If you need to access step context, take a look at [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata). -```typescript lineNumbers +```ts lineNumbers import { getWorkflowMetadata } from "workflow" async function testWorkflow() { @@ -42,3 +42,7 @@ definition={` import type { WorkflowMetadata } from "workflow"; export default WorkflowMetadata;`} /> + +## Related Functions + +- [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) - Get metadata about the current step execution diff --git a/docs/content/docs/api-reference/workflow/get-writable.mdx b/docs/content/docs/api-reference/workflow/get-writable.mdx index 5881befe4..2bb0e8197 100644 --- a/docs/content/docs/api-reference/workflow/get-writable.mdx +++ b/docs/content/docs/api-reference/workflow/get-writable.mdx @@ -20,7 +20,7 @@ Use this function in your workflows and steps to produce streaming output that c `getWritable()` directly themselves. -```typescript lineNumbers +```ts lineNumbers import { getWritable } from "workflow"; export async function myWorkflow() { @@ -81,7 +81,7 @@ Returns a `WritableStream` where `W` is the type of data you plan to write to Here's a simple example streaming text data: -```typescript lineNumbers +```ts lineNumbers import { sleep, getWritable } from "workflow"; export async function outputStreamWorkflow() { @@ -118,7 +118,7 @@ async function stepCloseOutputStream(writable: WritableStream) { You can also call `getWritable()` directly inside step functions without passing it as a parameter: -```typescript lineNumbers +```ts lineNumbers import { sleep, getWritable } from "workflow"; export async function outputStreamFromStepWorkflow() { @@ -157,7 +157,7 @@ async function stepCloseOutputStreamInside() { You can also use namespaced streams when calling `getWritable()` from steps: -```typescript lineNumbers +```ts lineNumbers import { getWritable } from "workflow"; export async function multiStreamWorkflow() { @@ -201,7 +201,7 @@ async function closeStreams() { Here's a more complex example showing how you might stream AI chat responses: -```typescript lineNumbers +```ts lineNumbers import { getWritable } from "workflow"; import { generateId, streamText, type UIMessageChunk } from "ai"; @@ -287,3 +287,11 @@ async function endStream(writable: WritableStream) { await writable.close(); } ``` + +## Related Functions + +- [`getRun()`](/docs/api-reference/workflow-api/get-run) - Get a workflow run object including the `readable` stream + +## Learn More + +- [Streaming](/docs/foundations/streaming) - Learn about streaming patterns in workflows diff --git a/docs/content/docs/api-reference/workflow/retryable-error.mdx b/docs/content/docs/api-reference/workflow/retryable-error.mdx index f97dc5968..3be60cedc 100644 --- a/docs/content/docs/api-reference/workflow/retryable-error.mdx +++ b/docs/content/docs/api-reference/workflow/retryable-error.mdx @@ -6,7 +6,7 @@ When a `RetryableError` is thrown in a step, it indicates that the workflow shou You should use this when you want to retry a step or retry after a certain duration. -```typescript lineNumbers +```ts lineNumbers import { RetryableError } from "workflow" async function retryableWorkflow() { @@ -53,7 +53,7 @@ export default RetryableErrorOptions;`} `RetryableError` can be configured with a `retryAfter` parameter to specify when the step should be retried after. -```typescript lineNumbers +```ts lineNumbers import { RetryableError } from "workflow" async function retryableWorkflow() { @@ -71,7 +71,7 @@ async function retryStep() { You can also specify the retry delay in milliseconds: -```typescript lineNumbers +```ts lineNumbers import { RetryableError } from "workflow" async function retryableWorkflow() { @@ -89,7 +89,7 @@ async function retryStep() { Or retry at a specific date and time: -```typescript lineNumbers +```ts lineNumbers import { RetryableError } from "workflow" async function retryableWorkflow() { @@ -104,3 +104,8 @@ async function retryStep() { }) } ``` + +## Related Functions + +- [`FatalError`](/docs/api-reference/workflow/fatal-error) - Prevent a step from retrying +- [`fetch()`](/docs/api-reference/workflow/fetch) - Make HTTP requests with automatic retry handling diff --git a/docs/content/docs/api-reference/workflow/sleep.mdx b/docs/content/docs/api-reference/workflow/sleep.mdx index d47e185bf..9b727285c 100644 --- a/docs/content/docs/api-reference/workflow/sleep.mdx +++ b/docs/content/docs/api-reference/workflow/sleep.mdx @@ -10,7 +10,7 @@ This is useful when you want to resume a workflow after some duration or date. `sleep` is a *special* type of step function and should be called directly inside workflow functions. -```typescript lineNumbers +```ts lineNumbers import { sleep } from "workflow" async function testWorkflow() { @@ -36,7 +36,7 @@ showSections={['parameters']} You can specify a duration for `sleep` to suspend the workflow for a fixed amount of time. -```typescript lineNumbers +```ts lineNumbers import { sleep } from "workflow" async function testWorkflow() { @@ -49,7 +49,7 @@ async function testWorkflow() { You can specify a future `Date` object for `sleep` to suspend the workflow until a specific date. -```typescript lineNumbers +```ts lineNumbers import { sleep } from "workflow" async function testWorkflow() { @@ -57,3 +57,8 @@ async function testWorkflow() { await sleep(new Date(Date.now() + 10_000)) // [!code highlight] } ``` + +## Related Functions + +- [`fetch()`](/docs/api-reference/workflow/fetch) - Make HTTP requests from within a workflow +- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Suspend a workflow until an HTTP request is received diff --git a/docs/content/docs/deploying/world/local-world.mdx b/docs/content/docs/deploying/world/local-world.mdx index 0ce73609b..ce1787ffb 100644 --- a/docs/content/docs/deploying/world/local-world.mdx +++ b/docs/content/docs/deploying/world/local-world.mdx @@ -47,7 +47,7 @@ The queue automatically detects your development server's port and adjusts the q The local world provides a simple authentication implementation since no authentication is required or enforced in local development. -```typescript +```ts lint-nocheck getAuthHeaders(): Promise> { return Promise.resolve({}); } @@ -67,7 +67,7 @@ export WORKFLOW_LOCAL_DATA_DIR=./custom-workflow-data **Programmatically:** -```typescript +```ts import { createLocalWorld } from "@workflow/world-local"; const world = createLocalWorld({ dataDir: "./custom-workflow-data" }); @@ -79,7 +79,7 @@ By default, the local world **automatically detects** which port your applicatio **Auto-detection example** (recommended): -```typescript +```ts import { createLocalWorld } from "@workflow/world-local"; // Port is automatically detected - no configuration needed! @@ -92,7 +92,7 @@ If auto-detection fails, the world will fall back to the `PORT` environment vari You can override the auto-detected port by specifying it explicitly: -```typescript +```ts import { createLocalWorld } from "@workflow/world-local"; const world = createLocalWorld({ port: 3000 }); @@ -116,7 +116,7 @@ export WORKFLOW_LOCAL_BASE_URL=https://local.example.com:3000 **Programmatically:** -```typescript +```ts import { createLocalWorld } from "@workflow/world-local"; // HTTPS @@ -207,7 +207,7 @@ For production deployments, use the [Vercel World](/docs/deploying/world/vercel- Creates a local world instance: -```typescript +```ts lint-nocheck function createLocalWorld( args?: Partial<{ dataDir: string; @@ -230,7 +230,7 @@ function createLocalWorld( **Examples:** -```typescript +```ts import { createLocalWorld } from "@workflow/world-local"; // Use all defaults (recommended - auto-detects port) diff --git a/docs/content/docs/deploying/world/vercel-world.mdx b/docs/content/docs/deploying/world/vercel-world.mdx index e489cd15a..1fac16301 100644 --- a/docs/content/docs/deploying/world/vercel-world.mdx +++ b/docs/content/docs/deploying/world/vercel-world.mdx @@ -135,7 +135,7 @@ The Vercel world implements security best practices: Creates a Vercel world instance: -```typescript +```ts lint-nocheck function createVercelWorld( config?: APIConfig ): World @@ -156,7 +156,7 @@ function createVercelWorld( **Example:** -```typescript +```ts import { createVercelWorld } from "@workflow/world-vercel"; const world = createVercelWorld({ diff --git a/docs/content/docs/errors/fetch-in-workflow.mdx b/docs/content/docs/errors/fetch-in-workflow.mdx index e8760ff5d..28e792fa8 100644 --- a/docs/content/docs/errors/fetch-in-workflow.mdx +++ b/docs/content/docs/errors/fetch-in-workflow.mdx @@ -22,7 +22,7 @@ Import the `fetch` step function from the `workflow` package and assign it to `g **Before:** -```typescript lineNumbers title="workflows/ai.ts" +```ts lineNumbers title="workflows/ai.ts" import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; @@ -41,7 +41,7 @@ export async function chatWorkflow(prompt: string) { **After:** -```typescript lineNumbers title="workflows/ai.ts" +```ts lineNumbers title="workflows/ai.ts" import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { fetch } from "workflow"; // [!code highlight] @@ -67,7 +67,7 @@ export async function chatWorkflow(prompt: string) { This is the most common scenario - using AI SDK functions that make HTTP requests: -```typescript lineNumbers +```ts lineNumbers import { generateText, streamText } from "ai"; import { openai } from "@ai-sdk/openai"; import { fetch } from "workflow"; // [!code highlight] @@ -91,7 +91,7 @@ export async function aiWorkflow(userMessage: string) { You can also use the fetch step function directly for your own HTTP requests: -```typescript lineNumbers +```ts lineNumbers import { fetch } from "workflow"; export async function dataWorkflow() { diff --git a/docs/content/docs/errors/node-js-module-in-workflow.mdx b/docs/content/docs/errors/node-js-module-in-workflow.mdx index 180723b9c..ad1f519dc 100644 --- a/docs/content/docs/errors/node-js-module-in-workflow.mdx +++ b/docs/content/docs/errors/node-js-module-in-workflow.mdx @@ -24,7 +24,7 @@ For example, when trying to read a file in a workflow function, you should move **Before:** -```typescript lineNumbers +```ts lineNumbers import * as fs from "fs"; export async function processFileWorkflow(filePath: string) { @@ -38,7 +38,7 @@ export async function processFileWorkflow(filePath: string) { **After:** -```typescript lineNumbers +```ts lineNumbers import * as fs from "fs"; export async function processFileWorkflow(filePath: string) { diff --git a/docs/content/docs/errors/serialization-failed.mdx b/docs/content/docs/errors/serialization-failed.mdx index 2c8582201..be20f98e6 100644 --- a/docs/content/docs/errors/serialization-failed.mdx +++ b/docs/content/docs/errors/serialization-failed.mdx @@ -31,7 +31,7 @@ Functions, class instances, symbols, and other non-serializable types cannot be ### Passing Functions -```typescript lineNumbers +```ts lineNumbers // Error - functions cannot be serialized export async function processWorkflow() { "use workflow"; @@ -43,7 +43,7 @@ export async function processWorkflow() { **Solution:** Pass data instead, then define the function logic in the step. -```typescript lineNumbers +```ts lineNumbers // Fixed - pass configuration data instead export async function processWorkflow() { "use workflow"; @@ -62,7 +62,7 @@ async function processStep(config: { shouldLog: boolean }) { ### Class Instances -```typescript lineNumbers +```ts lineNumbers class User { constructor(public name: string) {} greet() { return `Hello ${this.name}`; } @@ -78,7 +78,7 @@ export async function greetWorkflow() { **Solution:** Pass plain objects and reconstruct the class in the step. -```typescript lineNumbers +```ts lineNumbers class User { constructor(public name: string) {} greet() { return `Hello ${this.name}`; } diff --git a/docs/content/docs/errors/start-invalid-workflow-function.mdx b/docs/content/docs/errors/start-invalid-workflow-function.mdx index 761df34a8..0cb4c0220 100644 --- a/docs/content/docs/errors/start-invalid-workflow-function.mdx +++ b/docs/content/docs/errors/start-invalid-workflow-function.mdx @@ -26,7 +26,7 @@ This error typically happens when: ### Missing `"use workflow"` Directive -```typescript lineNumbers title="workflows/order.ts" +```ts lineNumbers title="workflows/order.ts" // Error - missing directive export async function processOrder(orderId: string) { // [!code highlight] // workflow logic @@ -36,7 +36,7 @@ export async function processOrder(orderId: string) { // [!code highlight] **Solution:** Add the `"use workflow"` directive. -```typescript lineNumbers title="workflows/order.ts" +```ts lineNumbers title="workflows/order.ts" // Fixed - includes directive export async function processOrder(orderId: string) { "use workflow"; // [!code highlight] @@ -48,7 +48,7 @@ export async function processOrder(orderId: string) { ### Incorrect Import -```typescript lineNumbers title="app/api/route.ts" +```ts lineNumbers title="app/api/route.ts" import { start } from "workflow/api"; // Error - importing step function instead of workflow import { processStep } from "@/workflows/order"; // [!code highlight] @@ -61,7 +61,7 @@ export async function POST(request: Request) { **Solution:** Import the correct workflow function. -```typescript lineNumbers title="app/api/route.ts" +```ts lineNumbers title="app/api/route.ts" import { start } from "workflow/api"; // Fixed - import workflow function import { processOrder } from "@/workflows/order"; // [!code highlight] @@ -74,7 +74,7 @@ export async function POST(request: Request) { ### Next.js Configuration Missing -```typescript lineNumbers title="next.config.ts" +```ts lineNumbers title="next.config.ts" // Error - missing withWorkflow wrapper import type { NextConfig } from "next"; @@ -87,7 +87,7 @@ export default nextConfig; **Solution:** Wrap with `withWorkflow()`. -```typescript lineNumbers title="next.config.ts" +```ts lineNumbers title="next.config.ts" // Fixed - includes withWorkflow import { withWorkflow } from "workflow/next"; // [!code highlight} import type { NextConfig } from "next"; diff --git a/docs/content/docs/errors/webhook-invalid-respond-with-value.mdx b/docs/content/docs/errors/webhook-invalid-respond-with-value.mdx index e269ea3b6..33573061b 100644 --- a/docs/content/docs/errors/webhook-invalid-respond-with-value.mdx +++ b/docs/content/docs/errors/webhook-invalid-respond-with-value.mdx @@ -22,7 +22,7 @@ When creating a webhook with `createWebhook()`, you can specify how the webhook ### Using an Invalid String Value -```typescript lineNumbers +```ts lineNumbers // Error - invalid string value export async function webhookWorkflow() { "use workflow"; @@ -35,7 +35,7 @@ export async function webhookWorkflow() { **Solution:** Use `"manual"` or provide a `Response` object. -```typescript lineNumbers +```ts lineNumbers // Fixed - use "manual" export async function webhookWorkflow() { "use workflow"; @@ -53,7 +53,7 @@ export async function webhookWorkflow() { ### Using a Non-Response Object -```typescript lineNumbers +```ts lineNumbers // Error - plain object instead of Response export async function webhookWorkflow() { "use workflow"; @@ -66,7 +66,7 @@ export async function webhookWorkflow() { **Solution:** Create a proper `Response` object. -```typescript lineNumbers +```ts lineNumbers // Fixed - use Response constructor export async function webhookWorkflow() { "use workflow"; @@ -81,7 +81,7 @@ export async function webhookWorkflow() { ### Default Behavior (202 Response) -```typescript lineNumbers +```ts lineNumbers // Returns 202 Accepted automatically const webhook = await createWebhook(); const request = await webhook; @@ -90,7 +90,7 @@ const request = await webhook; ### Manual Response -```typescript lineNumbers +```ts lineNumbers // Manual response control const webhook = await createWebhook({ respondWith: "manual", @@ -112,7 +112,7 @@ await request.respondWith( ### Pre-defined Response -```typescript lineNumbers +```ts lineNumbers // Immediate response const webhook = await createWebhook({ respondWith: new Response("Request received", { status: 200 }), diff --git a/docs/content/docs/errors/webhook-response-not-sent.mdx b/docs/content/docs/errors/webhook-response-not-sent.mdx index 3f9df807d..9ae96328e 100644 --- a/docs/content/docs/errors/webhook-response-not-sent.mdx +++ b/docs/content/docs/errors/webhook-response-not-sent.mdx @@ -20,7 +20,7 @@ The webhook infrastructure waits for a response to be sent, and if none is provi ### Forgetting to Call `request.respondWith()` -```typescript lineNumbers +```ts lineNumbers // Error - no response sent export async function webhookWorkflow() { "use workflow"; @@ -41,7 +41,7 @@ export async function webhookWorkflow() { **Solution:** Always call `request.respondWith()` when using manual response mode. -```typescript lineNumbers +```ts lineNumbers // Fixed - response sent export async function webhookWorkflow() { "use workflow"; @@ -63,7 +63,7 @@ export async function webhookWorkflow() { ### Conditional Response Logic -```typescript lineNumbers +```ts lineNumbers // Error - response only sent in some branches export async function webhookWorkflow() { "use workflow"; @@ -84,7 +84,7 @@ export async function webhookWorkflow() { **Solution:** Ensure all code paths send a response. -```typescript lineNumbers +```ts lineNumbers // Fixed - response sent in all branches export async function webhookWorkflow() { "use workflow"; @@ -106,7 +106,7 @@ export async function webhookWorkflow() { ### Exception Before Response -```typescript lineNumbers +```ts lineNumbers // Error - exception thrown before response export async function webhookWorkflow() { "use workflow"; @@ -127,7 +127,7 @@ export async function webhookWorkflow() { **Solution:** Use try-catch to handle errors and send appropriate responses. -```typescript lineNumbers +```ts lineNumbers // Fixed - error handling with response export async function webhookWorkflow() { "use workflow"; @@ -155,7 +155,7 @@ 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 +```ts lineNumbers // Automatic 202 response - no manual response needed export async function webhookWorkflow() { "use workflow"; diff --git a/docs/content/docs/foundations/control-flow-patterns.mdx b/docs/content/docs/foundations/control-flow-patterns.mdx index c731573c0..5adcd0272 100644 --- a/docs/content/docs/foundations/control-flow-patterns.mdx +++ b/docs/content/docs/foundations/control-flow-patterns.mdx @@ -8,7 +8,7 @@ Common distributed control flow patterns are simple to implement in workflows an The simplest way to orchestrate steps is to execute them one after another, where each step can be dependent on the previous step. -```typescript lineNumbers +```ts lineNumbers export async function dataPipelineWorkflow(data: any) { "use workflow"; @@ -24,7 +24,7 @@ export async function dataPipelineWorkflow(data: any) { When you need to execute multiple steps in parallel, you can use `Promise.all` to run them all at the same time. -```typescript lineNumbers +```ts lineNumbers export async function fetchUserData(userId: string) { "use workflow"; @@ -41,7 +41,7 @@ export async function fetchUserData(userId: string) { This not only applies to steps—since [`sleep()`](/docs/api-reference/workflow/sleep) and [`webhook`](/docs/api-reference/workflow/create-webhook) are also just promises, we can await those in parallel too. We can also use `Promise.race` instead of `Promise.all` to stop executing promises after the first one completes. -```typescript lineNumbers +```ts lineNumbers import { sleep, createWebhook } from "workflow"; @@ -66,7 +66,7 @@ export async function runExternalTask(userId: string) { Here's a simplified example taken from the [birthday card generator demo](https://github.com/vercel/workflow-examples/tree/main/birthday-card-generator), to illustrate how more complex orchestration can be modelled in promises. -```typescript lineNumbers +```ts lineNumbers import { createWebhook, sleep } from "workflow" async function birthdayWorkflow( diff --git a/docs/content/docs/foundations/errors-and-retries.mdx b/docs/content/docs/foundations/errors-and-retries.mdx index 7d56f0bdb..09124579a 100644 --- a/docs/content/docs/foundations/errors-and-retries.mdx +++ b/docs/content/docs/foundations/errors-and-retries.mdx @@ -8,7 +8,7 @@ By default, errors thrown inside steps are retried. Additionally, Workflow DevKi By default, steps retry up to 3 times on arbitrary errors. You can customize the number of retries by adding a `maxRetries` property to the step function. -```typescript lineNumbers +```ts lineNumbers async function callApi(endpoint: string) { "use step"; @@ -38,7 +38,7 @@ Steps get enqueued immediately after a failure. Read on to see how this can be c When your step needs to intentionally throw an error and skip retrying, simply throw a [`FatalError`](/docs/api-reference/workflow/fatal-error). -```typescript lineNumbers +```ts lineNumbers import { FatalError } from "workflow"; async function callApi(endpoint: string) { @@ -63,7 +63,7 @@ async function callApi(endpoint: string) { When you need to customize the delay on a retry, use [`RetryableError`](/docs/api-reference/workflow/retryable-error) and set the `retryAfter` property. -```typescript lineNumbers +```ts lineNumbers import { FatalError, RetryableError } from "workflow"; async function callApi(endpoint: string) { @@ -93,7 +93,7 @@ async function callApi(endpoint: string) { This final example combines everything we've learned, along with [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata). -```typescript lineNumbers +```ts lineNumbers import { FatalError, RetryableError, getStepMetadata } from "workflow"; async function callApi(endpoint: string) { @@ -137,7 +137,7 @@ Key guidelines: - Ensure rollbacks are [idempotent](/docs/foundations/idempotency); they may run more than once. - Only enqueue a compensation after its forward step succeeds. -```typescript lineNumbers +```ts lineNumbers // Forward steps async function reserveInventory(orderId: string) { "use step"; diff --git a/docs/content/docs/foundations/hooks.mdx b/docs/content/docs/foundations/hooks.mdx index 5dc294ff0..d0d21303a 100644 --- a/docs/content/docs/foundations/hooks.mdx +++ b/docs/content/docs/foundations/hooks.mdx @@ -18,7 +18,7 @@ When you create a hook, it generates a unique token that external systems can us Let's start with a simple example. Here's a workflow that creates a hook and waits for external data: -```typescript lineNumbers +```ts lineNumbers import { createHook } from "workflow"; export async function approvalWorkflow() { @@ -53,7 +53,7 @@ See the full API reference for [`createHook()`](/docs/api-reference/workflow/cre To send data to a waiting workflow, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) from an API route, server action, or any other external context: -```typescript lineNumbers +```ts lineNumbers import { resumeHook } from "workflow/api"; // In an API route or external handler @@ -81,7 +81,7 @@ By default, hooks generate a random token. However, you often want to use a **cu For example, imagine a Slack bot where each channel should have its own workflow instance: -```typescript lineNumbers +```ts lineNumbers import { createHook } from "workflow"; export async function slackChannelBot(channelId: string) { @@ -111,7 +111,7 @@ async function processMessage(message: SlackMessage) { Now your Slack webhook handler can deterministically resume the correct workflow: -```typescript lineNumbers +```ts lineNumbers import { resumeHook } from "workflow/api"; export async function POST(request: Request) { @@ -133,7 +133,7 @@ export async function POST(request: Request) { Hooks are _reusable_ - they implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time: -```typescript lineNumbers +```ts lineNumbers import { createHook } from "workflow"; export async function dataCollectionWorkflow() { @@ -177,7 +177,7 @@ See the full API reference for [`createWebhook()`](/docs/api-reference/workflow/ Here's a simple webhook that receives HTTP requests: -```typescript lineNumbers +```ts lineNumbers import { createWebhook } from "workflow"; export async function webhookWorkflow() { @@ -210,7 +210,7 @@ Webhooks provide two ways to send custom HTTP responses: **static responses** an Use the `respondWith` option to provide a static response that will be sent automatically for every request: -```typescript lineNumbers +```ts lineNumbers import { createWebhook } from "workflow"; export async function webhookWithStaticResponse() { @@ -240,7 +240,7 @@ async function processData(data: any) { For dynamic responses based on the request content, set `respondWith: "manual"` and call the `respondWith()` method on the request: -```typescript lineNumbers +```ts lineNumbers import { createWebhook, type RequestWithResponse } from "workflow"; async function sendCustomResponse(request: RequestWithResponse, message: string) { @@ -286,7 +286,7 @@ When using `respondWith: "manual"`, the `respondWith()` method **must** be calle Like hooks, webhooks support iteration: -```typescript lineNumbers +```ts lineNumbers import { createWebhook, type RequestWithResponse } from "workflow"; async function respondToSlack(request: RequestWithResponse, text: string) { @@ -365,7 +365,7 @@ async function postToSlack(channelId: string, message: string) { The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot: -```typescript lineNumbers +```ts lineNumbers import { defineHook } from "workflow"; import { z } from "zod"; @@ -430,7 +430,7 @@ When using custom tokens: Both hooks and webhooks support iteration, making them perfect for long-running event loops: -```typescript +```ts const hook = createHook(); for await (const event of hook) { diff --git a/docs/content/docs/foundations/idempotency.mdx b/docs/content/docs/foundations/idempotency.mdx index c8fc3bd97..6719bd5c3 100644 --- a/docs/content/docs/foundations/idempotency.mdx +++ b/docs/content/docs/foundations/idempotency.mdx @@ -14,7 +14,7 @@ To prevent this, many external APIs support idempotency keys. An idempotency key Every step invocation has a stable `stepId` that stays the same across retries. Use it as the idempotency key when calling third-party APIs. -```typescript lineNumbers +```ts lineNumbers import { getStepMetadata } from "workflow"; async function chargeUser(userId: string, amount: number) { diff --git a/docs/content/docs/foundations/serialization.mdx b/docs/content/docs/foundations/serialization.mdx index 1d053fbbd..6182efc37 100644 --- a/docs/content/docs/foundations/serialization.mdx +++ b/docs/content/docs/foundations/serialization.mdx @@ -69,7 +69,7 @@ For example, consider how receiving a webhook request provides the entire `Reque instance into the workflow context. You may consume the body of that request directly in the workflow, which will be cached as a step result for future resumptions of the workflow: -```typescript title="workflows/webhook.ts" lineNumbers +```ts title="workflows/webhook.ts" lineNumbers import { createWebhook } from "workflow"; export async function handleWebhookWorkflow() { @@ -89,7 +89,7 @@ export async function handleWebhookWorkflow() { Because `Request` and `Response` are serializable, Workflow DevKit provides a `fetch` function that can be used directly in workflow functions: -```typescript title="workflows/api-call.ts" lineNumbers +```ts title="workflows/api-call.ts" lineNumbers import { fetch } from "workflow"; // [!code highlight] export async function apiWorkflow() { @@ -105,7 +105,7 @@ export async function apiWorkflow() { The implementation is straightforward - `fetch` from workflow is a step function that wraps the standard `fetch`: -```typescript title="Implementation" lineNumbers +```ts title="Implementation" lineNumbers export async function fetch(...args: Parameters) { "use step"; return globalThis.fetch(...args); @@ -120,7 +120,7 @@ This allows you to make HTTP requests directly in workflow functions while maint **Incorrect:** -```typescript title="workflows/incorrect-mutation.ts" lineNumbers +```ts title="workflows/incorrect-mutation.ts" lineNumbers export async function updateUserWorkflow(userId: string) { "use workflow"; @@ -139,7 +139,7 @@ async function updateUserStep(user: { id: string; name: string; email: string }) **Correct - return the modified data:** -```typescript title="workflows/correct-mutation.ts" lineNumbers +```ts title="workflows/correct-mutation.ts" lineNumbers export async function updateUserWorkflow(userId: string) { "use workflow"; diff --git a/docs/content/docs/foundations/starting-workflows.mdx b/docs/content/docs/foundations/starting-workflows.mdx index e346f9bd9..611a98eba 100644 --- a/docs/content/docs/foundations/starting-workflows.mdx +++ b/docs/content/docs/foundations/starting-workflows.mdx @@ -8,7 +8,7 @@ Once you've defined your workflow functions, you need to trigger them to begin e The [`start()`](/docs/api-reference/workflow-api/start) function is used to programmatically trigger workflow executions from runtime contexts like API routes, Server Actions, or any server-side code. -```typescript lineNumbers +```ts lineNumbers import { start } from "workflow/api"; import { handleUserSignup } from "./workflows/user-signup"; @@ -38,7 +38,7 @@ export async function POST(request: Request) { When you call `start()`, it returns a [`Run`](/docs/api-reference/workflow-api/start#returns) object that provides access to the workflow's status and results. -```typescript lineNumbers +```ts lineNumbers import { start } from "workflow/api"; import { processOrder } from "./workflows/process-order"; @@ -73,7 +73,7 @@ Most `Run` properties are async getters that return promises. You need to `await The most common pattern is to start a workflow and immediately return, letting it execute in the background: -```typescript lineNumbers +```ts lineNumbers import { start } from "workflow/api"; import { sendNotifications } from "./workflows/notifications"; @@ -93,7 +93,7 @@ export async function POST(request: Request) { If you need to wait for the workflow to complete before responding: -```typescript lineNumbers +```ts lineNumbers import { start } from "workflow/api"; import { generateReport } from "./workflows/reports"; @@ -115,7 +115,7 @@ Be cautious when waiting for `returnValue` - if your workflow takes a long time, Stream real-time updates from your workflow as it executes, without waiting for completion: -```typescript lineNumbers +```ts lineNumbers import { start } from "workflow/api"; import { generateAIContent } from "./workflows/ai-generation"; @@ -139,7 +139,7 @@ export async function POST(request: Request) { Your workflow can write to the stream using [`getWritable()`](/docs/api-reference/workflow/get-writable): -```typescript lineNumbers +```ts lineNumbers import { getWritable } from "workflow"; export async function generateAIContent(prompt: string) { @@ -180,7 +180,7 @@ Streams are particularly useful for AI workflows where you want to show progress You can retrieve a workflow run later using its `runId` with [`getRun()`](/docs/api-reference/workflow-api/get-run): -```typescript lineNumbers +```ts lineNumbers import { getRun } from "workflow/api"; export async function GET(request: Request) { diff --git a/docs/content/docs/foundations/streaming.mdx b/docs/content/docs/foundations/streaming.mdx index fdb684708..b0f4fb824 100644 --- a/docs/content/docs/foundations/streaming.mdx +++ b/docs/content/docs/foundations/streaming.mdx @@ -8,7 +8,7 @@ Workflows can stream data in real-time to clients without waiting for the entire Every workflow run has a default writable stream that steps can write to using [`getWritable()`](/docs/api-reference/workflow/get-writable). Data written to this stream becomes immediately available to clients consuming the workflow's output. -```typescript title="workflows/simple-streaming.ts" lineNumbers +```ts title="workflows/simple-streaming.ts" lineNumbers import { getWritable } from "workflow"; async function writeProgress(message: string) { @@ -34,7 +34,7 @@ export async function simpleStreamingWorkflow() { Use the `Run` object's `readable` property to consume the stream from your API route: -```typescript title="app/api/stream/route.ts" lineNumbers +```ts title="app/api/stream/route.ts" lineNumbers import { start } from "workflow/api"; import { simpleStreamingWorkflow } from "./workflows/simple"; @@ -54,7 +54,7 @@ When a client makes a request to this endpoint, they'll receive each message as Use `run.getReadable({ startIndex })` to resume a stream from a specific position. This is useful for reconnecting after timeouts or network interruptions: -```typescript title="app/api/resume-stream/[runId]/route.ts" lineNumbers +```ts title="app/api/resume-stream/[runId]/route.ts" lineNumbers import { getRun } from "workflow/api"; export async function GET( @@ -105,7 +105,7 @@ Since streams are serializable data types, you don't need to use the special [`g Here's an example of passing a request body stream through a workflow to a step that processes it: -```typescript title="app/api/upload/route.ts" lineNumbers +```ts title="app/api/upload/route.ts" lineNumbers import { start } from "workflow/api"; import { streamProcessingWorkflow } from "./workflows/streaming"; @@ -118,7 +118,7 @@ export async function POST(request: Request) { } ``` -```typescript title="workflows/streaming.ts" lineNumbers +```ts title="workflows/streaming.ts" lineNumbers export async function streamProcessingWorkflow( inputStream: ReadableStream // [!code highlight] ) { @@ -155,7 +155,7 @@ 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 +```ts title="workflows/bad-example.ts" lineNumbers export async function badWorkflow() { "use workflow"; @@ -167,7 +167,7 @@ export async function badWorkflow() { } ``` -```typescript title="workflows/good-example.ts" lineNumbers +```ts title="workflows/good-example.ts" lineNumbers export async function goodWorkflow() { "use workflow"; @@ -190,7 +190,7 @@ async function writeToStream(data: string) { Use `getWritable({ namespace: 'name' })` to create multiple independent streams for different types of data. This is useful when you want to separate logs, metrics, data outputs, or other distinct channels. -```typescript title="workflows/multi-stream.ts" lineNumbers +```ts title="workflows/multi-stream.ts" lineNumbers import { getWritable } from "workflow"; type LogEntry = { level: string; message: string }; @@ -240,7 +240,7 @@ export async function multiStreamWorkflow() { Use `run.getReadable({ namespace: 'name' })` to access specific streams: -```typescript title="app/api/multi-stream/route.ts" lineNumbers +```ts title="app/api/multi-stream/route.ts" lineNumbers import { start } from "workflow/api"; import { multiStreamWorkflow } from "./workflows/multi"; @@ -267,7 +267,7 @@ export async function POST(request: Request) { Send incremental progress updates to keep users informed during lengthy workflows: -```typescript title="workflows/batch-processing.ts" lineNumbers +```ts title="workflows/batch-processing.ts" lineNumbers import { getWritable, sleep } from "workflow"; type ProgressUpdate = { @@ -321,7 +321,7 @@ export async function batchProcessingWorkflow(items: string[]) { Stream AI-generated content using [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) from `@workflow/ai`. Tools can also emit progress updates to the same stream using [data chunks](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data#streaming-custom-data) with the [`UIMessageChunk`](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol) type from the AI SDK: -```typescript title="workflows/ai-assistant.ts" lineNumbers +```ts title="workflows/ai-assistant.ts" lineNumbers import { DurableAgent } from "@workflow/ai/agent"; import { getWritable } from "workflow"; import { z } from "zod"; @@ -367,7 +367,7 @@ export async function aiAssistantWorkflow(userMessage: string) { } ``` -```typescript title="app/api/ai-assistant/route.ts" lineNumbers +```ts title="app/api/ai-assistant/route.ts" lineNumbers import { createUIMessageStreamResponse } from "ai"; import { start } from "workflow/api"; import { aiAssistantWorkflow } from "./workflows/ai"; @@ -391,7 +391,7 @@ For a complete implementation, see the [flight booking example](https://github.c One step produces a stream and another step consumes it: -```typescript title="workflows/stream-pipeline.ts" lineNumbers +```ts title="workflows/stream-pipeline.ts" lineNumbers export async function streamPipelineWorkflow() { "use workflow"; @@ -432,7 +432,7 @@ async function consumeData(readable: ReadableStream) { Process large files by streaming chunks through transformation steps: -```typescript title="workflows/file-processing.ts" lineNumbers +```ts title="workflows/file-processing.ts" lineNumbers export async function fileProcessingWorkflow(fileUrl: string) { "use workflow"; @@ -473,7 +473,7 @@ async function uploadResult(stream: ReadableStream) { **Release locks properly:** -```typescript lineNumbers +```ts lineNumbers const writer = writable.getWriter(); try { await writer.write(data); @@ -492,7 +492,7 @@ If a lock is not released, the step process cannot terminate. Even though the st **Close streams when done:** -```typescript lineNumbers +```ts lineNumbers async function finalizeStream() { "use step"; @@ -504,7 +504,7 @@ Streams are automatically closed when the workflow run completes, but explicitly **Use typed streams for type safety:** -```typescript lineNumbers +```ts lineNumbers const writable = getWritable(); const writer = writable.getWriter(); await writer.write({ /* typed data */ }); @@ -514,7 +514,7 @@ await writer.write({ /* typed data */ }); When a step returns a stream, the step is considered successful once it returns, even if the stream later encounters an error. The workflow won't automatically retry the step. The consumer of the stream must handle errors gracefully. For more on retry behavior, see [Errors and Retries](/docs/foundations/errors-and-retries). -```typescript title="workflows/stream-error-handling.ts" lineNumbers +```ts title="workflows/stream-error-handling.ts" lineNumbers import { FatalError } from "workflow"; async function produceStream(): Promise> { diff --git a/docs/content/docs/foundations/workflows-and-steps.mdx b/docs/content/docs/foundations/workflows-and-steps.mdx index 15537d13e..00f9c13ff 100644 --- a/docs/content/docs/foundations/workflows-and-steps.mdx +++ b/docs/content/docs/foundations/workflows-and-steps.mdx @@ -21,7 +21,7 @@ Although this may seem limiting initially, this feature is important in order to It helps to think of the workflow function less like a full JavaScript runtime and more like "stitching together" various steps using conditionals, loops, try/catch handlers, `Promise.all`, and other language primitives. -```typescript lineNumbers +```ts lineNumbers export async function processOrderWorkflow(orderId: string) { "use workflow"; // [!code highlight] @@ -49,7 +49,7 @@ The sandboxed environment that workflows run in already ensures determinism. For Step functions perform the actual work in a workflow and have full runtime access. -```typescript lineNumbers +```ts lineNumbers async function chargePayment(order: Order) { "use step"; // [!code highlight] @@ -85,10 +85,10 @@ Step functions are primarily meant to be used inside a workflow. Calling a step from outside a workflow or from another step will essentially run the step in the same process like a normal function (in other words, the `use step` directive is a no-op). This means you can reuse step functions in other parts of your codebase without needing to duplicate business logic. -```typescript lineNumbers +```ts lineNumbers async function updateUser(userId: string) { "use step"; - await db.insert(...); + await db.insert(userId); } // Used inside a workflow @@ -119,7 +119,7 @@ There are multiple ways a workflow can suspend: - Using `sleep()` to pause for some fixed duration. - Awaiting on a promise returned by [`createWebhook()`](/docs/api-reference/workflow/create-webhook), which resumes the workflow when an external system passes data into the workflow. -```typescript lineNumbers +```ts lineNumbers import { sleep, createWebhook } from "workflow"; export async function documentReviewProcess(userId: string) { @@ -145,7 +145,7 @@ export async function documentReviewProcess(userId: string) { The simplest workflow consists of a workflow function and one or more step functions. -```typescript lineNumbers +```ts lineNumbers // Workflow function (orchestrates the steps) export async function greetingWorkflow(name: string) { "use workflow"; diff --git a/docs/content/docs/getting-started/astro.mdx b/docs/content/docs/getting-started/astro.mdx index 459d2b84a..19b293596 100644 --- a/docs/content/docs/getting-started/astro.mdx +++ b/docs/content/docs/getting-started/astro.mdx @@ -1,17 +1,16 @@ --- title: Astro +description: Set up Workflow DevKit with Astro for durable, long-running functions. --- -This guide will walk through setting up your first workflow in an Astro app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. - ---- + ## Create Your Astro Project -Start by creating a new Astro project. This command will create a new directory named `my-workflow-app` and setup a minimal Astro project inside it. +Start by creating a new Astro project. This command will create a new directory named `my-workflow-app` and set up a minimal Astro project inside it. ```bash npm create astro@latest my-workflow-app -- --template minimal --install --yes @@ -33,7 +32,7 @@ npm i workflow Add `workflow()` to your Astro config. This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="astro.config.mjs" lineNumbers +```ts title="astro.config.mjs" lineNumbers // @ts-check import { defineConfig } from "astro/config"; import { workflow } from "workflow/astro"; @@ -46,12 +45,12 @@ export default defineConfig({ - - ### Setup IntelliSense for TypeScript (Optional) + + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -78,7 +77,7 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json Create a new file for our first workflow: -```typescript title="src/workflows/user-signup.ts" lineNumbers +```ts title="src/workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -90,6 +89,8 @@ export async function handleUserSignup(email: string) { await sleep("5s"); // Pause for 5s - doesn't consume any resources await sendOnboardingEmail(user); + console.log("Workflow is complete! Run 'npx workflow web' to inspect your run") + return { userId: user.id, status: "onboarded" }; } @@ -97,14 +98,14 @@ export async function handleUserSignup(email: string) { We'll fill in those functions next, but let's take a look at this code: -* We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. -* The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. +- We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. +- The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. ## Create Your Workflow Steps Let's now define those missing functions. -```typescript title="src/workflows/user-signup.ts" lineNumbers +```ts title="src/workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow" // Our workflow function defined earlier @@ -124,8 +125,8 @@ async function sendWelcomeEmail(user: { id: string; email: string; }) { console.log(`Sending welcome email to user: ${user.id}`); if (Math.random() < 0.3) { - // By default, steps will be retried for unhandled errors - throw new Error("Retryable!"); + // By default, steps will be retried for unhandled errors + throw new Error("Retryable!"); } } @@ -143,9 +144,9 @@ async function sendOnboardingEmail(user: { id: string; email: string}) { Taking a look at this code: -* Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. -* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). -* Steps can throw a `FatalError` if an error is intentional and should not be retried. +- Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. +- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). +- Steps can throw a `FatalError` if an error is intentional and should not be retried. We'll dive deeper into workflows, steps, and other ways to suspend or handle events in [Foundations](/docs/foundations). @@ -159,7 +160,7 @@ We'll dive deeper into workflows, steps, and other ways to suspend or handle eve To invoke your new workflow, we'll have to add your workflow to a `POST` API route handler, `src/pages/api/signup.ts` with the following code: -```typescript title="src/pages/api/signup.ts" +```ts title="src/pages/api/signup.ts" lineNumbers import type { APIRoute } from "astro"; import { start } from "workflow/api"; import { handleUserSignup } from "../../workflows/user-signup"; @@ -189,7 +190,7 @@ Workflows can be triggered from API routes or any server-side code. ## Run in Development -To start your development server, run the following command in your terminal in the Vite root directory: +To start your development server, run the following command in your terminal in the Astro root directory: ```bash npm run dev @@ -206,8 +207,10 @@ Check the Astro development server logs to see your workflow execute as well as Additionally, you can use the [Workflow DevKit CLI or Web UI](/docs/observability) to inspect your workflow runs and steps in detail. ```bash +# Open the observability Web UI +npx workflow web +# or if you prefer a terminal interface, use the CLI inspect command npx workflow inspect runs -# or add '--web' for an interactive Web based UI ``` Workflow DevKit Web UI @@ -216,7 +219,7 @@ npx workflow inspect runs ## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. To deploy your Astro project to Vercel, ensure that the [Astro Vercel adapter](https://docs.astro.build/en/guides/integrations-guide/vercel) is configured: @@ -228,6 +231,6 @@ Additionally, check the [Deploying](/docs/deploying) section to learn how your w ## Next Steps -* Learn more about the [Foundations](/docs/foundations). -* Check [Errors](/docs/errors) if you encounter issues. -* Explore the [API Reference](/docs/api-reference). +- Learn more about the [Foundations](/docs/foundations). +- Check [Errors](/docs/errors) if you encounter issues. +- Explore the [API Reference](/docs/api-reference). diff --git a/docs/content/docs/getting-started/express.mdx b/docs/content/docs/getting-started/express.mdx index 27fdc159e..4b418988d 100644 --- a/docs/content/docs/getting-started/express.mdx +++ b/docs/content/docs/getting-started/express.mdx @@ -1,10 +1,9 @@ --- title: Express +description: Set up Workflow DevKit with Express for durable, long-running functions. --- -This guide will walk through setting up your first workflow in a Express app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. - ---- + @@ -49,7 +48,7 @@ npm i -D @types/express Create a new file `nitro.config.ts` for your Nitro configuration with module `workflow/nitro`. This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="nitro.config.ts" lineNumbers +```ts title="nitro.config.ts" lineNumbers import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ @@ -64,10 +63,10 @@ export default defineNitroConfig({ - Setup IntelliSense for TypeScript (Optional) + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -110,7 +109,7 @@ To use the Nitro builder, update your `package.json` to include the following sc Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -122,6 +121,8 @@ export async function handleUserSignup(email: string) { await sleep("5s"); // Pause for 5s - doesn't consume any resources await sendOnboardingEmail(user); + console.log("Workflow is complete! Run 'npx workflow web' to inspect your run") + return { userId: user.id, status: "onboarded" }; } ``` @@ -135,7 +136,7 @@ We'll fill in those functions next, but let's take a look at this code: Let's now define those missing functions. -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow"; // Our workflow function defined earlier @@ -191,7 +192,7 @@ Taking a look at this code: To invoke your new workflow, we'll create both the Express app and a new API route handler at `src/index.ts` with the following code: -```typescript title="src/index.ts" +```ts title="src/index.ts" lineNumbers import express from "express"; import { start } from "workflow/api"; import { handleUserSignup } from "../workflows/user-signup.js"; @@ -247,9 +248,9 @@ npx workflow inspect runs --- -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. diff --git a/docs/content/docs/getting-started/fastify.mdx b/docs/content/docs/getting-started/fastify.mdx index 53a7f5c6e..dcd96c1c6 100644 --- a/docs/content/docs/getting-started/fastify.mdx +++ b/docs/content/docs/getting-started/fastify.mdx @@ -1,10 +1,9 @@ --- title: Fastify +description: Set up Workflow DevKit with Fastify for durable, long-running functions. --- -This guide will walk through setting up your first workflow in a Fastify app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. - ---- + @@ -36,7 +35,7 @@ npm i workflow fastify nitro rollup ``` -By default, Fastify doesn't include a build system. Nitro adds one which enables compiling workflows, runs, and deploys for development and production. Learn more about [Nitro](https://v3.nitro.build). +By default, Fastify doesn't include a build system. Nitro adds one which enables compiling workflows, runs, and deploys for development and production. Learn more about Nitro [here](https://v3.nitro.build). If using TypeScript, you need to install the `@types/node` and `typescript` packages @@ -49,7 +48,7 @@ npm i -D @types/node typescript Create a new file `nitro.config.ts` for your Nitro configuration with module `workflow/nitro`. This enables usage of the `"use workflow"` and `"use step"` directives -```typescript title="nitro.config.ts" lineNumbers +```ts title="nitro.config.ts" lineNumbers import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ @@ -64,7 +63,7 @@ export default defineNitroConfig({ - Setup IntelliSense for TypeScript (Optional) + Set Up IntelliSense for TypeScript (Optional) To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: @@ -109,7 +108,7 @@ To use the Nitro builder, update your `package.json` to include the following sc Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -121,6 +120,8 @@ export async function handleUserSignup(email: string) { await sleep("5s"); // Pause for 5s - doesn't consume any resources await sendOnboardingEmail(user); + console.log("Workflow is complete! Run 'npx workflow web' to inspect your run") + return { userId: user.id, status: "onboarded" }; } ``` @@ -132,32 +133,39 @@ We'll fill in those functions next, but let's take a look at this code: ## Create Your Workflow Steps Let's now define those missing functions: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow"; // Our workflow function defined earlier async function createUser(email: string) { "use step"; // [!code highlight] + console.log(`Creating user with email: ${email}`); + + // Full Node.js access - database calls, APIs, etc. return { id: crypto.randomUUID(), email }; } async function sendWelcomeEmail(user: { id: string; email: string }) { "use step"; // [!code highlight] + console.log(`Sending welcome email to user: ${user.id}`); + if (Math.random() < 0.3) { - // Steps retry on unhandled errors + // By default, steps will be retried for unhandled errors throw new Error("Retryable!"); } } async function sendOnboardingEmail(user: { id: string; email: string }) { "use step"; // [!code highlight] + if (!user.email.includes("@")) { - // FatalError skips retries + // To skip retrying, throw a FatalError instead throw new FatalError("Invalid Email"); } + console.log(`Sending onboarding email to user: ${user.id}`); } ``` @@ -179,7 +187,7 @@ Taking a look at this code: To invoke your new workflow, we'll create both the Fastify app and a new API route handler at `src/index.ts` with the following code: -```typescript title="src/index.ts" +```ts title="src/index.ts" lineNumbers import Fastify from "fastify"; import { start } from "workflow/api"; import { handleUserSignup } from "../workflows/user-signup.js"; @@ -223,7 +231,10 @@ Check the Fastify development server logs to see your workflow execute as well a Additionally, you can use the [Workflow DevKit CLI or Web UI](/docs/observability) to inspect your workflow runs and steps in detail. ```bash -npx workflow inspect runs # add '--web' for an interactive Web based UI +# Open the observability Web UI +npx workflow web +# or if you prefer a terminal interface, use the CLI inspect command +npx workflow inspect runs ``` Workflow DevKit Web UI @@ -234,9 +245,9 @@ npx workflow inspect runs # add '--web' for an interactive Web based UI --- -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. diff --git a/docs/content/docs/getting-started/hono.mdx b/docs/content/docs/getting-started/hono.mdx index df7cb3ded..d499bc30f 100644 --- a/docs/content/docs/getting-started/hono.mdx +++ b/docs/content/docs/getting-started/hono.mdx @@ -1,8 +1,10 @@ --- title: Hono -description: This guide will walk through setting up your first workflow in a Hono app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. +description: Set up Workflow DevKit with Hono for durable, long-running functions. --- + + @@ -34,7 +36,7 @@ By default, Hono doesn't include a build system. Nitro adds one which enables co Create a new file `nitro.config.ts` for your Nitro configuration with module `workflow/nitro`. This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="nitro.config.ts" lineNumbers +```ts title="nitro.config.ts" lineNumbers import { defineConfig } from "nitro"; export default defineConfig({ @@ -48,10 +50,10 @@ export default defineConfig({ - Setup IntelliSense for TypeScript (Optional) + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -94,7 +96,7 @@ To use the Nitro builder, update your `package.json` to include the following sc Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -121,7 +123,7 @@ We'll fill in those functions next, but let's take a look at this code: Let's now define those missing functions. -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow"; // Our workflow function defined earlier @@ -177,7 +179,7 @@ Taking a look at this code: To invoke your new workflow, we'll create a new API route handler at `src/index.ts` with the following code: -```typescript title="src/index.ts" +```ts title="src/index.ts" lineNumbers import { Hono } from "hono"; import { start } from "workflow/api"; import { handleUserSignup } from "../workflows/user-signup.js"; @@ -230,9 +232,9 @@ npx workflow inspect runs -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. diff --git a/docs/content/docs/getting-started/next.mdx b/docs/content/docs/getting-started/next.mdx index ebe7df98a..5abdd6ba7 100644 --- a/docs/content/docs/getting-started/next.mdx +++ b/docs/content/docs/getting-started/next.mdx @@ -1,8 +1,10 @@ --- title: Next.js -description: This guide will walk through setting up your first workflow in a Next.js app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. +description: Set up Workflow DevKit with Next.js for durable, long-running functions. --- + + @@ -30,7 +32,7 @@ npm i workflow Wrap your `next.config.ts` with `withWorkflow()`. This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="next.config.ts" lineNumbers +```ts title="next.config.ts" lineNumbers import { withWorkflow } from "workflow/next"; // [!code highlight] import type { NextConfig } from "next"; @@ -43,12 +45,12 @@ export default withWorkflow(nextConfig); // [!code highlight] - - ### Setup IntelliSense for TypeScript (Optional) + + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -69,8 +71,8 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json - - ### Configure Proxy Handler (if applicable) + + Configure Proxy Handler (if applicable) @@ -80,7 +82,7 @@ internal paths to prevent the proxy handler from running on them. Add `.well-known/workflow/*` to your middleware's exclusion list: -```typescript title="proxy.ts" lineNumbers +```ts title="proxy.ts" lineNumbers import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -112,35 +114,35 @@ This ensures that internal Workflow paths are not intercepted by your middleware Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { - "use workflow"; // [!code highlight] + "use workflow"; // [!code highlight] - const user = await createUser(email); - await sendWelcomeEmail(user); + const user = await createUser(email); + await sendWelcomeEmail(user); - await sleep("5s"); // Pause for 5s - doesn't consume any resources - await sendOnboardingEmail(user); + await sleep("5s"); // Pause for 5s - doesn't consume any resources + await sendOnboardingEmail(user); - console.log("Workflow is complete! Run 'npx workflow web' to inspect your run") + console.log("Workflow is complete! Run 'npx workflow web' to inspect your run") - return { userId: user.id, status: "onboarded" }; + return { userId: user.id, status: "onboarded" }; } ``` We'll fill in those functions next, but let's take a look at this code: -* We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. -* The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. +- We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. +- The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. ## Create Your Workflow Steps Let's now define those missing functions. -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow" // Our workflow function defined earlier @@ -160,28 +162,28 @@ async function sendWelcomeEmail(user: { id: string; email: string; }) { console.log(`Sending welcome email to user: ${user.id}`); if (Math.random() < 0.3) { - // By default, steps will be retried for unhandled errors - throw new Error("Retryable!"); + // By default, steps will be retried for unhandled errors + throw new Error("Retryable!"); } } async function sendOnboardingEmail(user: { id: string; email: string}) { - "use step"; // [!code highlight] + "use step"; // [!code highlight] if (!user.email.includes("@")) { // To skip retrying, throw a FatalError instead throw new FatalError("Invalid Email"); } - console.log(`Sending onboarding email to user: ${user.id}`); + console.log(`Sending onboarding email to user: ${user.id}`); } ``` Taking a look at this code: -* Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. -* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). -* Steps can throw a `FatalError` if an error is intentional and should not be retried. +- Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. +- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). +- Steps can throw a `FatalError` if an error is intentional and should not be retried. We'll dive deeper into workflows, steps, and other ways to suspend or handle events in [Foundations](/docs/foundations). @@ -195,20 +197,20 @@ We'll dive deeper into workflows, steps, and other ways to suspend or handle eve To invoke your new workflow, we'll need to add your workflow to a `POST` API Route Handler, `app/api/signup/route.ts`, with the following code: -```typescript title="app/api/signup/route.ts" +```ts title="app/api/signup/route.ts" lineNumbers import { start } from "workflow/api"; import { handleUserSignup } from "@/workflows/user-signup"; import { NextResponse } from "next/server"; export async function POST(request: Request) { - const { email } = await request.json(); + const { email } = await request.json(); - // Executes asynchronously and doesn't block your app - await start(handleUserSignup, [email]); + // Executes asynchronously and doesn't block your app + await start(handleUserSignup, [email]); - return NextResponse.json({ - message: "User signup workflow started", - }); + return NextResponse.json({ + message: "User signup workflow started", + }); } ``` @@ -249,7 +251,7 @@ npx workflow inspect runs Workflow DevKit Web UI -## Deploying to production +## Deploying to Production Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. @@ -257,6 +259,6 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b ## Next Steps -* Learn more about the [Foundations](/docs/foundations). -* Check [Errors](/docs/errors) if you encounter issues. -* Explore the [API Reference](/docs/api-reference). +- Learn more about the [Foundations](/docs/foundations). +- Check [Errors](/docs/errors) if you encounter issues. +- Explore the [API Reference](/docs/api-reference). diff --git a/docs/content/docs/getting-started/nitro.mdx b/docs/content/docs/getting-started/nitro.mdx index 9e77ced47..867e2d767 100644 --- a/docs/content/docs/getting-started/nitro.mdx +++ b/docs/content/docs/getting-started/nitro.mdx @@ -1,14 +1,16 @@ --- title: Nitro -description: This guide will walk through setting up your first workflow in a Nitro v3 project. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. +description: Set up Workflow DevKit with Nitro for durable, long-running functions. --- + + ## Create Your Nitro Project -Start by creating a new [Nitro v3](https://v3.nitro.build/) project. This command will create a new directory named `nitro-app` and setup a Nitro project inside it. +Start by creating a new [Nitro v3](https://v3.nitro.build/) project. This command will create a new directory named `nitro-app` and set up a Nitro project inside it. ```bash npx create-nitro-app @@ -30,7 +32,7 @@ npm i workflow Add `workflow/nitro` module to your `nitro.config.ts` This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="nitro.config.ts" lineNumbers +```ts title="nitro.config.ts" lineNumbers import { defineConfig } from "nitro"; export default defineConfig({ @@ -43,10 +45,10 @@ export default defineConfig({ - Setup IntelliSense for TypeScript (Optional) + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -74,7 +76,7 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -101,7 +103,7 @@ We'll fill in those functions next, but let's take a look at this code: Let's now define those missing functions. -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow"; // Our workflow function defined earlier @@ -157,7 +159,7 @@ Taking a look at this code: To invoke your new workflow, we'll create a new API route handler at `server/api/signup.post.ts` with the following code: -```typescript title="server/api/signup.post.ts" +```ts title="server/api/signup.post.ts" lineNumbers import { start } from "workflow/api"; import { defineEventHandler } from "nitro/h3"; import { handleUserSignup } from "../../workflows/user-signup"; @@ -214,9 +216,9 @@ npx workflow inspect runs -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. diff --git a/docs/content/docs/getting-started/nuxt.mdx b/docs/content/docs/getting-started/nuxt.mdx index 35d948ce8..a513d1f15 100644 --- a/docs/content/docs/getting-started/nuxt.mdx +++ b/docs/content/docs/getting-started/nuxt.mdx @@ -1,14 +1,16 @@ --- title: Nuxt -description: This guide will walk through setting up your first workflow in a Nuxt app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. +description: Set up Workflow DevKit with Nuxt for durable, long-running functions. --- + + ## Create Your Nuxt Project -Start by creating a new Nuxt project. This command will create a new directory named `nuxt-app` and setup a Nuxt project inside it. +Start by creating a new Nuxt project. This command will create a new directory named `nuxt-app` and set up a Nuxt project inside it. ```bash npm create nuxt@latest nuxt-app @@ -30,7 +32,7 @@ npm i workflow Add `workflow` to your `nuxt.config.ts`. This automatically configures the Nitro integration and enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="nuxt.config.ts" lineNumbers +```ts title="nuxt.config.ts" lineNumbers import { defineNuxtConfig } from "nuxt/config"; export default defineNuxtConfig({ @@ -49,7 +51,7 @@ This will also automatically enable the TypeScript plugin, which provides helpfu The TypeScript plugin is enabled by default. If you need to disable it, you can configure it in your `nuxt.config.ts`: -```typescript title="nuxt.config.ts" lineNumbers +```ts title="nuxt.config.ts" lineNumbers export default defineNuxtConfig({ modules: ["workflow/nuxt"], workflow: { @@ -72,7 +74,7 @@ export default defineNuxtConfig({ Create a new file for our first workflow: -```typescript title="server/workflows/user-signup.ts" lineNumbers +```ts title="server/workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -99,7 +101,7 @@ We'll fill in those functions next, but let's take a look at this code: Let's now define those missing functions. -```typescript title="server/workflows/user-signup.ts" lineNumbers +```ts title="server/workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow"; // Our workflow function defined earlier @@ -151,11 +153,11 @@ Taking a look at this code: -## Create Your API Route +## Create Your Route Handler To invoke your new workflow, we'll create a new API route handler at `server/api/signup.post.ts` with the following code: -```typescript title="server/api/signup.post.ts" +```ts title="server/api/signup.post.ts" lineNumbers import { start } from "workflow/api"; import { defineEventHandler, readBody } from "h3"; import { handleUserSignup } from "../workflows/user-signup"; @@ -214,9 +216,9 @@ npx workflow inspect runs -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. diff --git a/docs/content/docs/getting-started/sveltekit.mdx b/docs/content/docs/getting-started/sveltekit.mdx index 0cd85534e..05f194c00 100644 --- a/docs/content/docs/getting-started/sveltekit.mdx +++ b/docs/content/docs/getting-started/sveltekit.mdx @@ -1,14 +1,16 @@ --- title: SvelteKit -description: This guide will walk through setting up your first workflow in a SvelteKit app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. +description: Set up Workflow DevKit with SvelteKit for durable, long-running functions. --- + + ## Create Your SvelteKit Project -Start by creating a new SvelteKit project. This command will create a new directory named `my-workflow-app` with a minimal setup and setup a SvelteKit project inside it. +Start by creating a new SvelteKit project. This command will create a new directory named `my-workflow-app` with a minimal setup and set up a SvelteKit project inside it. ```bash npx sv create my-workflow-app --template=minimal --types=ts --no-add-ons @@ -30,7 +32,7 @@ npm i workflow Add `workflowPlugin()` to your Vite config. This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="vite.config.ts" lineNumbers +```ts title="vite.config.ts" lineNumbers import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; import { workflowPlugin } from "workflow/sveltekit"; // [!code highlight] @@ -42,12 +44,12 @@ export default defineConfig({ - - ### Setup IntelliSense for TypeScript (Optional) + + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -74,7 +76,7 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -95,14 +97,14 @@ export async function handleUserSignup(email: string) { We'll fill in those functions next, but let's take a look at this code: -* We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. -* The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. +- We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. +- The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. ## Create Your Workflow Steps Let's now define those missing functions. -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow" // Our workflow function defined earlier @@ -122,8 +124,8 @@ async function sendWelcomeEmail(user: { id: string; email: string; }) { console.log(`Sending welcome email to user: ${user.id}`); if (Math.random() < 0.3) { - // By default, steps will be retried for unhandled errors - throw new Error("Retryable!"); + // By default, steps will be retried for unhandled errors + throw new Error("Retryable!"); } } @@ -141,9 +143,9 @@ async function sendOnboardingEmail(user: { id: string; email: string}) { Taking a look at this code: -* Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. -* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). -* Steps can throw a `FatalError` if an error is intentional and should not be retried. +- Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. +- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). +- Steps can throw a `FatalError` if an error is intentional and should not be retried. We'll dive deeper into workflows, steps, and other ways to suspend or handle events in [Foundations](/docs/foundations). @@ -157,7 +159,7 @@ We'll dive deeper into workflows, steps, and other ways to suspend or handle eve To invoke your new workflow, we'll have to add your workflow to a `POST` API route handler, `src/routes/api/signup/+server.ts` with the following code: -```typescript title="src/routes/api/signup/+server.ts" +```ts title="src/routes/api/signup/+server.ts" lineNumbers import { start } from "workflow/api"; import { handleUserSignup } from "../../../../workflows/user-signup"; import { json, type RequestHandler } from "@sveltejs/kit"; @@ -214,14 +216,14 @@ npx workflow inspect runs Workflow DevKit Web UI -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. ## Next Steps -* Learn more about the [Foundations](/docs/foundations). -* Check [Errors](/docs/errors) if you encounter issues. -* Explore the [API Reference](/docs/api-reference). +- Learn more about the [Foundations](/docs/foundations). +- Check [Errors](/docs/errors) if you encounter issues. +- Explore the [API Reference](/docs/api-reference). diff --git a/docs/content/docs/getting-started/vite.mdx b/docs/content/docs/getting-started/vite.mdx index 5bb7711b1..67e5e1142 100644 --- a/docs/content/docs/getting-started/vite.mdx +++ b/docs/content/docs/getting-started/vite.mdx @@ -1,17 +1,16 @@ --- title: Vite +description: Set up Workflow DevKit with Vite for durable, long-running functions. --- -This guide will walk through setting up your first workflow in a Vite app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. - ---- + ## Create Your Vite Project -Start by creating a new Vite project. This command will create a new directory named `my-workflow-app` with a minimal setup and setup a Vite project inside it. +Start by creating a new Vite project. This command will create a new directory named `my-workflow-app` with a minimal setup and set up a Vite project inside it. ```bash npm create vite@latest my-workflow-app -- --template react-ts @@ -37,7 +36,7 @@ While Vite provides the build tooling and development server, Nitro adds the ser Add `workflow()` to your Vite config. This enables usage of the `"use workflow"` and `"use step"` directives. -```typescript title="vite.config.ts" lineNumbers +```ts title="vite.config.ts" lineNumbers import { nitro } from "nitro/vite"; import { defineConfig } from "vite"; import { workflow } from "workflow/vite"; @@ -52,12 +51,12 @@ export default defineConfig({ - - ### Setup IntelliSense for TypeScript (Optional) + + Set Up IntelliSense for TypeScript (Optional) -To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`: +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: ```json title="tsconfig.json" lineNumbers { @@ -84,7 +83,7 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json Create a new file for our first workflow: -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { sleep } from "workflow"; export async function handleUserSignup(email: string) { @@ -96,6 +95,8 @@ export async function handleUserSignup(email: string) { await sleep("5s"); // Pause for 5s - doesn't consume any resources await sendOnboardingEmail(user); + console.log("Workflow is complete! Run 'npx workflow web' to inspect your run") + return { userId: user.id, status: "onboarded" }; } @@ -103,14 +104,14 @@ export async function handleUserSignup(email: string) { We'll fill in those functions next, but let's take a look at this code: -* We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. -* The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. +- We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. +- The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. ## Create Your Workflow Steps Let's now define those missing functions. -```typescript title="workflows/user-signup.ts" lineNumbers +```ts title="workflows/user-signup.ts" lineNumbers import { FatalError } from "workflow" // Our workflow function defined earlier @@ -130,8 +131,8 @@ async function sendWelcomeEmail(user: { id: string; email: string; }) { console.log(`Sending welcome email to user: ${user.id}`); if (Math.random() < 0.3) { - // By default, steps will be retried for unhandled errors - throw new Error("Retryable!"); + // By default, steps will be retried for unhandled errors + throw new Error("Retryable!"); } } @@ -149,9 +150,9 @@ async function sendOnboardingEmail(user: { id: string; email: string}) { Taking a look at this code: -* Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. -* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). -* Steps can throw a `FatalError` if an error is intentional and should not be retried. +- Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. +- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). +- Steps can throw a `FatalError` if an error is intentional and should not be retried. We'll dive deeper into workflows, steps, and other ways to suspend or handle events in [Foundations](/docs/foundations). @@ -165,7 +166,7 @@ We'll dive deeper into workflows, steps, and other ways to suspend or handle eve To invoke your new workflow, we'll have to add your workflow to a `POST` API route handler, `api/signup.post.ts` with the following code: -```typescript title="api/signup.post.ts" +```ts title="api/signup.post.ts" lineNumbers import { start } from "workflow/api"; import { defineEventHandler } from "nitro/h3"; import { handleUserSignup } from "../workflows/user-signup"; @@ -219,14 +220,14 @@ npx workflow inspect runs --- -## Deploying to production +## Deploying to Production -Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration. Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. ## Next Steps -* Learn more about the [Foundations](/docs/foundations). -* Check [Errors](/docs/errors) if you encounter issues. -* Explore the [API Reference](/docs/api-reference). +- Learn more about the [Foundations](/docs/foundations). +- Check [Errors](/docs/errors) if you encounter issues. +- Explore the [API Reference](/docs/api-reference). diff --git a/docs/content/docs/how-it-works/code-transform.mdx b/docs/content/docs/how-it-works/code-transform.mdx index 1b13797c5..135ae413f 100644 --- a/docs/content/docs/how-it-works/code-transform.mdx +++ b/docs/content/docs/how-it-works/code-transform.mdx @@ -12,7 +12,7 @@ Workflows use special directives to mark code for transformation by the Workflow Workflows use two directives to mark functions for special handling: -```typescript +```ts export async function handleUserSignup(email: string) { "use workflow"; // [!code highlight] @@ -69,7 +69,7 @@ flowchart LR **Input:** -```typescript +```ts export async function createUser(email: string) { "use step"; return { id: crypto.randomUUID(), email }; @@ -78,7 +78,7 @@ export async function createUser(email: string) { **Output:** -```typescript +```ts import { registerStepFunction } from "workflow/internal/private"; // [!code highlight] export async function createUser(email: string) { @@ -106,7 +106,7 @@ registerStepFunction("step//workflows/user.js//createUser", createUser); // [!co **Input:** -```typescript +```ts export async function createUser(email: string) { "use step"; return { id: crypto.randomUUID(), email }; @@ -121,7 +121,7 @@ export async function handleUserSignup(email: string) { **Output:** -```typescript +```ts export async function createUser(email: string) { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//workflows/user.js//createUser")(email); // [!code highlight] } @@ -155,7 +155,7 @@ handleUserSignup.workflowId = "workflow//workflows/user.js//handleUserSignup"; / **Input:** -```typescript +```ts export async function handleUserSignup(email: string) { "use workflow"; const user = await createUser(email); @@ -165,7 +165,7 @@ export async function handleUserSignup(email: string) { **Output:** -```typescript +```ts export async function handleUserSignup(email: string) { throw new Error("You attempted to execute ..."); // [!code highlight] } diff --git a/docs/content/docs/how-it-works/framework-integrations.mdx b/docs/content/docs/how-it-works/framework-integrations.mdx index 6a18107ba..ea033e3f1 100644 --- a/docs/content/docs/how-it-works/framework-integrations.mdx +++ b/docs/content/docs/how-it-works/framework-integrations.mdx @@ -77,7 +77,7 @@ Each file exports a `POST` function that accepts Web standard `Request` objects. Client mode transforms your application code to provide better DX. Add a Bun plugin to apply this transformation at runtime: -```typescript title="workflow-plugin.ts" lineNumbers +```ts title="workflow-plugin.ts" lineNumbers import { plugin } from "bun"; import { transform } from "@swc/core"; @@ -127,7 +127,7 @@ preload = ["./workflow-plugin.ts"] Wire up the generated handlers to HTTP endpoints using `Bun.serve()`: -```typescript title="server.ts" lineNumbers +```ts title="server.ts" lineNumbers import flow from "./.well-known/workflow/v1/flow.js"; import step from "./.well-known/workflow/v1/step.js"; import * as webhook from "./.well-known/workflow/v1/webhook.js"; @@ -215,7 +215,7 @@ This will default to scanning the `./workflows` top-level directory for workflow **Option 2: Extend `BaseBuilder`** (recommended) -```typescript lineNumbers +```ts lineNumbers import { BaseBuilder } from "@workflow/cli/dist/lib/builders/base-builder"; class MyFrameworkBuilder extends BaseBuilder { @@ -253,7 +253,7 @@ If your framework supports virtual server routes and dev mode watching, make sur Hook into your framework's build: -```typescript title="pseudocode.ts" lineNumbers +```ts title="pseudocode.ts" lineNumbers framework.hooks.hook("build:before", async () => { await new MyFrameworkBuilder(framework).build(); }); @@ -265,7 +265,7 @@ Add a loader/plugin for your bundler: **Rollup/Vite:** -```typescript lineNumbers +```ts lineNumbers export function workflowPlugin() { return { name: "workflow-client-transform", @@ -308,7 +308,7 @@ Route the three endpoints to the generated handlers. The exact implementation de In the bun example above, we left routing to the user. Essentially, the user has to serve routes like this: -```typescript title="server.ts" lineNumbers +```ts title="server.ts" lineNumbers import flow from "./.well-known/workflow/v1/flow.js"; import step from "./.well-known/workflow/v1/step.js"; import * as webhook from "./.well-known/workflow/v1/webhook.js"; @@ -359,7 +359,7 @@ Learn more about [world abstractions](/docs/deploying/world). Create a test workflow: -```typescript title="workflows/test.ts" lineNumbers +```ts title="workflows/test.ts" lineNumbers import { sleep, createWebhook } from "workflow"; export async function handleUserSignup(email: string) { @@ -423,7 +423,7 @@ curl -X POST http://localhost:3000/.well-known/workflow/v1/webhook/test ### 3. Run a Workflow End-to-End -```typescript +```ts import { start } from "workflow/api"; import { handleUserSignup } from "./workflows/test"; diff --git a/docs/content/docs/how-it-works/understanding-directives.mdx b/docs/content/docs/how-it-works/understanding-directives.mdx index 5f2787459..18b3b1185 100644 --- a/docs/content/docs/how-it-works/understanding-directives.mdx +++ b/docs/content/docs/how-it-works/understanding-directives.mdx @@ -20,7 +20,7 @@ The Workflow DevKit has two types of functions: **Step functions** are side-effecting operations with full Node.js runtime access. Think of them like named RPC calls - they run once, their result is persisted, and they can be [retried on failure](/docs/foundations/errors-and-retries): -```typescript lineNumbers +```ts lineNumbers async function fetchUserData(userId: string) { "use step"; @@ -32,7 +32,7 @@ async function fetchUserData(userId: string) { **Workflow functions** are deterministic orchestrators that coordinate steps. They must be pure functions - during replay, the same step results always produce the same output. This is necessary because workflows resume by replaying their code from the beginning using cached step results; non-deterministic logic would break resumption. They run in a sandboxed environment without direct Node.js access: -```typescript lineNumbers +```ts lineNumbers export async function onboardUser(userId: string) { "use workflow"; @@ -104,7 +104,7 @@ Before settling on directives, we prototyped several other approaches. Each had Our first proof of concept used a wrapper-based API without a build step: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(() => { const message = run(async () => step()); return `${message}!`; @@ -119,7 +119,7 @@ This implementation used "throwing promises" (similar to early React Suspense) t Any operation that could produce non-deterministic results had to be wrapped in `run()`: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(async () => { // These would be non-deterministic without wrapping const now = await run(() => Date.now()); // [!code highlight] @@ -134,7 +134,7 @@ This was verbose and easy to forget. Moreover, if a developer forgot to wrap som For example: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(async () => { // Nothing stops you from doing this: const now = Date.now(); // Non-deterministic, untracked! // [!code highlight] @@ -149,7 +149,7 @@ export const myWorkflow = workflow(async () => { Variables captured in closures would behave unexpectedly when steps mutated them: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(async () => { let counter = 0; @@ -171,7 +171,7 @@ The workflow function would replay multiple times, but mutations inside `run()` Since we used thrown promises for control flow, `try/catch` blocks became unreliable: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(async () => { try { const result = await run(() => step()); @@ -191,7 +191,7 @@ export const myWorkflow = workflow(async () => { We explored using generators for explicit suspension points, inspired by libraries like Effect.ts: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(function*() { const message = yield* run(() => step()); return `${message}!`; @@ -208,7 +208,7 @@ We're big fans of [Effect.ts](https://effect.website/) and the power of generato Generators require a custom mental model that differs significantly from familiar async/await patterns. The `yield*` syntax and generator delegation were unfamiliar to many developers: -```typescript lineNumbers +```ts lineNumbers // Standard async/await (familiar) const result = await fetchData(); @@ -218,7 +218,7 @@ const result = yield* run(() => fetchData()); // [!code highlight] Complex workflows became particularly verbose and difficult to read: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(function*() { const user = yield* run(() => fetchUser()); @@ -240,7 +240,7 @@ export const myWorkflow = workflow(function*() { Like the runtime-only approach, generators couldn't prevent non-deterministic code: -```typescript lineNumbers +```ts lineNumbers export const myWorkflow = workflow(function*() { const now = Date.now(); // Still possible, still problematic // [!code highlight] const user = yield* run(() => fetchUser()); @@ -297,7 +297,7 @@ We considered decorators, but they presented significant challenges both technic Decorators are not yet a standard syntax ([TC39 proposal](https://github.com/tc39/proposal-decorators)) and they currently only work with classes. A class decorator approach could look like this: -```typescript lineNumbers +```ts lineNumbers import {workflow, step} from "workflow"; class MyWorkflow { @@ -335,7 +335,7 @@ See the [Macro Wrapper](#macro-wrapper-approach) section below for a deeper dive We also explored compile-time macro approaches - using a compiler to transform wrapper functions or decorators into directive-based code: -```typescript lineNumbers +```ts lineNumbers // Function wrapper approach import { useWorkflow } from "workflow" @@ -358,7 +358,7 @@ class MyWorkflow { The compiler could transform both to be equivalent to WDK's directive approach: -```typescript lineNumbers +```ts lineNumbers export const processOrder = async (orderId: string) => { "use workflow"; // [!code highlight] const order = await fetchOrder(orderId); @@ -374,7 +374,7 @@ The fundamental issue is that both wrappers and decorators make workflows appear **Concrete examples of how this breaks:** -```typescript lineNumbers +```ts lineNumbers // Someone writes a "helpful" utility function withRetry(fn: Function) { return useWorkflow(async (...args) => { // Works with useWorkflow // [!code highlight] @@ -419,7 +419,7 @@ Directives address all the issues we encountered with previous approaches: The `"use workflow"` directive tells the compiler to treat this code differently: -```typescript lineNumbers +```ts lineNumbers export async function processOrder(orderId: string) { "use workflow"; // Compiler knows: transform this for sandbox execution // [!code highlight] @@ -432,7 +432,7 @@ export async function processOrder(orderId: string) { The compiler can enforce restrictions before deployment: -```typescript lineNumbers +```ts lineNumbers export async function badWorkflow() { "use workflow"; @@ -447,7 +447,7 @@ In fact, Workflow DevKit will throw an error that links to this error page: [Nod Steps are transformed into function calls that communicate with the runtime: -```typescript lineNumbers +```ts lineNumbers export async function processOrder(orderId: string) { "use workflow"; @@ -463,7 +463,7 @@ export async function processOrder(orderId: string) { Callbacks, however, run inside the workflow sandbox and work as expected: -```typescript lineNumbers +```ts lineNumbers export async function processOrders(orderIds: string[]) { "use workflow"; @@ -488,7 +488,7 @@ The callback runs in the workflow sandbox, so closure reads and mutations behave Looks and feels like regular JavaScript: -```typescript lineNumbers +```ts lineNumbers export async function processOrder(orderId: string) { "use workflow"; @@ -508,7 +508,7 @@ The `"use step"` directive maintains consistency. While steps run in the full No We could have used a function wrapper just for steps: -```typescript lineNumbers +```ts lineNumbers // Mixed approach (inconsistent) export async function processOrder(orderId: string) { "use workflow"; // Directive for workflow // [!code highlight] @@ -526,7 +526,7 @@ Mixing syntaxes felt inconsistent. An alternative approach we considered was to treat *all* async function calls as steps by default: -```typescript lineNumbers +```ts lineNumbers export async function processOrder(orderId: string) { "use workflow"; @@ -542,7 +542,7 @@ export async function processOrder(orderId: string) { This breaks down because many valid async operations inside workflows aren't steps: -```typescript lineNumbers +```ts lineNumbers lint-nocheck export async function processOrder(orderId: string) { "use workflow"; diff --git a/docs/package.json b/docs/package.json index 8fd00027a..5c58b1e34 100644 --- a/docs/package.json +++ b/docs/package.json @@ -10,7 +10,10 @@ "postinstall": "fumadocs-mdx", "postbuild": "node scripts/pack.ts", "lint": "biome check", - "lint:links": "bun ./scripts/lint.ts", + "lint:links": "bun ./scripts/lint-links.ts", + "lint:quotes": "bun ./scripts/lint-quotes.ts", + "lint:ts-examples": "bun ./scripts/lint-ts-examples.ts", + "lint:docs": "bun ./scripts/lint-links.ts && bun ./scripts/lint-quotes.ts && bun ./scripts/lint-ts-examples.ts", "format": "biome format --write" }, "dependencies": { diff --git a/docs/scripts/lint.ts b/docs/scripts/lint-links.ts similarity index 100% rename from docs/scripts/lint.ts rename to docs/scripts/lint-links.ts diff --git a/docs/scripts/lint-quotes.ts b/docs/scripts/lint-quotes.ts new file mode 100644 index 000000000..84ec02d6c --- /dev/null +++ b/docs/scripts/lint-quotes.ts @@ -0,0 +1,296 @@ +import * as fs from 'node:fs'; +import ts from 'typescript'; +import { source } from '../lib/geistdocs/source'; + +interface QuoteIssue { + file: string; + codeBlockLine: number; + lineInBlock: number; + text: string; + suggestion: string; +} + +interface SingleQuoteLocation { + start: number; + end: number; + replacement: string; +} + +// Match code blocks - meta must be on same line as language (space, not newline) +const CODE_BLOCK_REGEX = + /```(ts|tsx|js|jsx|typescript|javascript)(?: +([^\n]*))?\n([\s\S]*?)```/g; + +function extractCodeBlocks(content: string): Array<{ + code: string; + language: string; + startLine: number; + meta: string; + codeStartIndex: number; +}> { + const blocks: Array<{ + code: string; + language: string; + startLine: number; + meta: string; + codeStartIndex: number; + }> = []; + let match: RegExpExecArray | null; + + while ((match = CODE_BLOCK_REGEX.exec(content)) !== null) { + const language = match[1]; + const meta = match[2] || ''; + const code = match[3]; + // Calculate line number where this code block starts + const beforeMatch = content.slice(0, match.index); + const startLine = beforeMatch.split('\n').length; + // Calculate the index where the code content starts (after ```lang meta\n) + const codeStartIndex = match.index + match[0].length - code.length - 3; // -3 for closing ``` + + blocks.push({ code, language, startLine, meta, codeStartIndex }); + } + + // Reset regex state + CODE_BLOCK_REGEX.lastIndex = 0; + + return blocks; +} + +function cleanCode(code: string): string { + // Remove fumadocs annotations - remove the entire comment including any text after + return code + .replace(/\s*\/\/\s*\[!code[^\]]*\].*$/gm, '') + .replace(/\s*\/\*\s*\[!code[^\]]*\]\s*\*\//g, ''); +} + +// Detect if code contains JSX syntax +function containsJsx(code: string): boolean { + return ( + /<[A-Z][a-zA-Z]*[\s/>]/.test(code) || + /<\/[a-zA-Z]+>/.test(code) || + /return\s*\(?\s* { + const issues: Array<{ line: number; text: string; suggestion: string }> = []; + + // Determine script kind based on language, title metadata, OR JSX content + const isTsx = + language === 'tsx' || + language === 'jsx' || + meta.includes('.tsx') || + meta.includes('.jsx') || + containsJsx(code); + + const scriptKind = isTsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS; + + const sourceFile = ts.createSourceFile( + `example.${isTsx ? 'tsx' : 'ts'}`, + code, + ts.ScriptTarget.Latest, + true, + scriptKind + ); + + function visit(node: ts.Node) { + if (ts.isStringLiteral(node)) { + const start = node.getStart(sourceFile); + const rawChar = code[start]; + + if (rawChar === "'") { + const pos = sourceFile.getLineAndCharacterOfPosition(start); + issues.push({ + line: pos.line + 1, + text: node.getText(sourceFile), + suggestion: `"${node.text}"`, + }); + } + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return issues; +} + +function findSingleQuoteLocations( + code: string, + language: string, + meta: string +): SingleQuoteLocation[] { + const locations: SingleQuoteLocation[] = []; + + const isTsx = + language === 'tsx' || + language === 'jsx' || + meta.includes('.tsx') || + meta.includes('.jsx') || + containsJsx(code); + + const scriptKind = isTsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS; + + const sourceFile = ts.createSourceFile( + `example.${isTsx ? 'tsx' : 'ts'}`, + code, + ts.ScriptTarget.Latest, + true, + scriptKind + ); + + function visit(node: ts.Node) { + if (ts.isStringLiteral(node)) { + const start = node.getStart(sourceFile); + const rawChar = code[start]; + + if (rawChar === "'") { + const end = node.getEnd(); + // Escape any double quotes inside the string and create replacement + const escapedText = node.text.replace(/"/g, '\\"'); + locations.push({ + start, + end, + replacement: `"${escapedText}"`, + }); + } + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); + // Sort by start position descending so we can replace from end to start + return locations.sort((a, b) => b.start - a.start); +} + +function fixCodeBlock(code: string, language: string, meta: string): string { + const locations = findSingleQuoteLocations(code, language, meta); + let fixedCode = code; + + // Replace from end to start to maintain correct positions + for (const loc of locations) { + fixedCode = + fixedCode.slice(0, loc.start) + + loc.replacement + + fixedCode.slice(loc.end); + } + + return fixedCode; +} + +async function checkQuotes() { + const fixMode = process.argv.includes('--fix'); + const allIssues: QuoteIssue[] = []; + const filesToFix = new Map(); + + const pages = source.getPages(); + + for (const page of pages) { + const content = await page.data.getText('raw'); + const blocks = extractCodeBlocks(content); + let fixedContent = content; + let hasIssues = false; + + // Process blocks in reverse order for fixing (to maintain positions) + const blocksReversed = [...blocks].reverse(); + + for (const block of blocksReversed) { + // Skip blocks marked with lint-nocheck + if (block.meta.includes('lint-nocheck')) { + continue; + } + + const cleanedCode = cleanCode(block.code); + const stringIssues = findSingleQuotedStrings( + cleanedCode, + block.language, + block.meta + ); + + if (stringIssues.length > 0) { + hasIssues = true; + + for (const issue of stringIssues) { + allIssues.push({ + file: page.absolutePath, + codeBlockLine: block.startLine, + lineInBlock: issue.line, + text: issue.text, + suggestion: issue.suggestion, + }); + } + + if (fixMode) { + // Fix the original code (not cleaned), then replace in content + const fixedCode = fixCodeBlock( + block.code, + block.language, + block.meta + ); + const codeStart = block.codeStartIndex; + const codeEnd = codeStart + block.code.length; + fixedContent = + fixedContent.slice(0, codeStart) + + fixedCode + + fixedContent.slice(codeEnd); + } + } + } + + if (hasIssues && fixMode) { + filesToFix.set(page.absolutePath, fixedContent); + } + } + + // Write fixed files + if (fixMode && filesToFix.size > 0) { + for (const [filePath, content] of filesToFix) { + fs.writeFileSync(filePath, content); + } + const green = (s: string) => `\x1b[32m\x1b[1m${s}\x1b[0m`; + console.log( + green( + `Fixed ${allIssues.length} single-quoted strings in ${filesToFix.size} files` + ) + ); + return; + } + + // Group issues by file + const issuesByFile = new Map(); + for (const issue of allIssues) { + const existing = issuesByFile.get(issue.file) || []; + existing.push(issue); + issuesByFile.set(issue.file, existing); + } + + const green = (s: string) => `\x1b[32m\x1b[1m${s}\x1b[0m`; + const red = (s: string) => `\x1b[31m${s}\x1b[0m`; + const redBold = (s: string) => `\x1b[31m\x1b[1m${s}\x1b[0m`; + + if (allIssues.length > 0) { + console.error(); + for (const [file, issues] of issuesByFile) { + console.error(`Single-quoted strings in ${file}:`); + for (const issue of issues) { + console.error( + ` ${red(issue.text)} → ${issue.suggestion}: at line ${issue.codeBlockLine + issue.lineInBlock}` + ); + } + } + console.log('------'); + console.error( + redBold( + `${issuesByFile.size} errored file${issuesByFile.size === 1 ? '' : 's'}, ${allIssues.length} error${allIssues.length === 1 ? '' : 's'}` + ) + ); + console.error(); + process.exit(1); + } else { + console.log(green('0 errored files, 0 errors')); + } +} + +void checkQuotes(); diff --git a/docs/scripts/lint-ts-examples.ts b/docs/scripts/lint-ts-examples.ts new file mode 100644 index 000000000..7023c3823 --- /dev/null +++ b/docs/scripts/lint-ts-examples.ts @@ -0,0 +1,186 @@ +import ts from 'typescript'; +import { source } from '../lib/geistdocs/source'; + +interface SyntaxError { + file: string; + codeBlockLine: number; + lineInBlock: number; + message: string; + code: string; +} + +// Match code blocks - meta must be on same line as language (space, not newline) +const CODE_BLOCK_REGEX = + /```(ts|tsx|js|jsx|typescript|javascript)(?: +([^\n]*))?\n([\s\S]*?)```/g; + +function extractCodeBlocks( + content: string +): Array<{ code: string; language: string; startLine: number; meta: string }> { + const blocks: Array<{ + code: string; + language: string; + startLine: number; + meta: string; + }> = []; + let match: RegExpExecArray | null; + + while ((match = CODE_BLOCK_REGEX.exec(content)) !== null) { + const language = match[1]; + const meta = match[2] || ''; + const code = match[3]; + // Calculate line number where this code block starts + const beforeMatch = content.slice(0, match.index); + const startLine = beforeMatch.split('\n').length; + + blocks.push({ code, language, startLine, meta }); + } + + // Reset regex state + CODE_BLOCK_REGEX.lastIndex = 0; + + return blocks; +} + +function cleanCode(code: string): string { + // Remove fumadocs annotations - remove the entire comment including any text after the annotation + // e.g., "// [!code highlight] Full history from client" -> "" + return code + .replace(/\s*\/\/\s*\[!code[^\]]*\].*$/gm, '') + .replace(/\s*\/\*\s*\[!code[^\]]*\]\s*\*\//g, ''); +} + +// Detect if code contains JSX syntax +function containsJsx(code: string): boolean { + // Look for JSX patterns: , or JSX expressions like {value} + // within what looks like JSX context + return ( + /<[A-Z][a-zA-Z]*[\s/>]/.test(code) || // + /<\/[a-zA-Z]+>/.test(code) || // + /return\s*\(?\s* { + // Determine script kind based on language, title metadata, OR JSX content detection + // e.g., title="app/page.tsx" should be parsed as TSX even if language is "ts" + const isTsx = + language === 'tsx' || + language === 'jsx' || + meta.includes('.tsx') || + meta.includes('.jsx') || + containsJsx(code); + const isJs = language === 'js' || language === 'javascript'; + + const scriptKind = isTsx + ? ts.ScriptKind.TSX + : isJs + ? ts.ScriptKind.JS + : ts.ScriptKind.TS; + + const sourceFile = ts.createSourceFile( + `example.${isTsx ? 'tsx' : language}`, + code, + ts.ScriptTarget.Latest, + true, + scriptKind + ); + + // parseDiagnostics contains only syntax errors + return sourceFile.parseDiagnostics.map((d) => { + const pos = + d.start !== undefined + ? sourceFile.getLineAndCharacterOfPosition(d.start) + : { line: 0 }; + + return { + line: pos.line + 1, + message: ts.flattenDiagnosticMessageText(d.messageText, '\n'), + }; + }); +} + +async function checkTsExamples() { + const allErrors: SyntaxError[] = []; + + const pages = source.getPages(); + + for (const page of pages) { + const content = await page.data.getText('raw'); + const blocks = extractCodeBlocks(content); + + for (const block of blocks) { + // Skip blocks marked with lint-nocheck + if (block.meta.includes('lint-nocheck')) { + continue; + } + + const cleanedCode = cleanCode(block.code); + const syntaxErrors = checkSyntax(cleanedCode, block.language, block.meta); + + for (const error of syntaxErrors) { + allErrors.push({ + file: page.absolutePath, + codeBlockLine: block.startLine, + lineInBlock: error.line, + message: error.message, + code: getCodeContext(cleanedCode, error.line), + }); + } + } + } + + // Deduplicate errors by file + block + line (TS can report multiple diagnostics per issue) + const seen = new Set(); + const uniqueErrors = allErrors.filter((error) => { + const key = `${error.file}:${error.codeBlockLine}:${error.lineInBlock}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + // Group errors by file + const errorsByFile = new Map(); + for (const error of uniqueErrors) { + const existing = errorsByFile.get(error.file) || []; + existing.push(error); + errorsByFile.set(error.file, existing); + } + + const green = (s: string) => `\x1b[32m\x1b[1m${s}\x1b[0m`; + const red = (s: string) => `\x1b[31m${s}\x1b[0m`; + const redBold = (s: string) => `\x1b[31m\x1b[1m${s}\x1b[0m`; + + if (uniqueErrors.length > 0) { + console.error(); + for (const [file, errors] of errorsByFile) { + console.error(`Syntax errors in ${file}:`); + for (const error of errors) { + console.error( + ` ${red(error.message)}: at code block line ${error.codeBlockLine}, error line ${error.lineInBlock}` + ); + } + } + console.log('------'); + console.error( + redBold( + `${errorsByFile.size} errored file${errorsByFile.size === 1 ? '' : 's'}, ${uniqueErrors.length} error${uniqueErrors.length === 1 ? '' : 's'}` + ) + ); + console.error(); + process.exit(1); + } else { + console.log(green('0 errored files, 0 errors')); + } +} + +function getCodeContext(code: string, line: number): string { + const lines = code.split('\n'); + const targetLine = lines[line - 1]; + return targetLine ? targetLine.trim() : ''; +} + +void checkTsExamples();