
Workflow Devkit
- 40 installs
- 24 repo stars
- Updated December 19, 2025
- johnlindquist/claude
Develop and test Claude Code workflows locally with debugging, hot-reload, and type checking capabilities.
About
Local development toolkit for building Claude Code workflows. Provides debugging, testing, and hot-reload for fast iteration.
- Local debugging and hot-reload for development
- Type checking and format validation
Workflow Devkit by the numbers
- 40 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #369 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/johnlindquist/claude --skill workflow-devkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 24 |
| Last updated | December 19, 2025 |
| Repository | johnlindquist/claude ↗ |
What it does
Develop and test Claude Code workflows locally with debugging, hot-reload, and type checking capabilities.
Files
Workflow DevKit
Build reliable, long-running processes with automatic retries, state persistence, and observability.
Quick Reference
| Pattern | Use Case | Key API |
|---|---|---|
| Workflows | Orchestrate durable operations | "use workflow" directive |
| Steps | Atomic, retriable units | "use step" directive |
| Webhooks | Human-in-the-loop, callbacks | createWebhook() |
| Actors | Event-driven state machines | defineHook() + for await |
| Streaming | Real-time frontend updates | getWritable() / run.readable |
| AI Agents | Durable LLM workflows | DurableAgent + globalThis.fetch = fetch |
| AI Gateway | Multi-provider model switching | "provider/model" strings, @ai-sdk/gateway |
Prerequisites
pnpm add workflow @workflow/ai ai @ai-sdk/gateway zodCore Concepts
Workflows ("use workflow")
import { sleep } from "workflow";
export async function myWorkflow(input: string) {
"use workflow";
const result = await step1(input);
await sleep("5s");
return result;
}Steps ("use step") - MUST be in SAME FILE as workflow
async function step1(input: string) {
"use step";
return await fetch(`/api/data?q=${input}`).then(r => r.json());
}Error Types
import { FatalError, RetryableError } from "workflow";
// Auto-retried
throw new Error("Transient failure");
// No retry - stops workflow
throw new FatalError("Invalid credentials");
// Custom retry timing
throw new RetryableError("Rate limited", { retryAfter: "60s" });Detailed Documentation
- patterns.md - Workflow patterns (sequential, parallel, routing, actors)
- api-reference.md - Complete API reference
- ai-integration.md - AI SDK and DurableAgent patterns
Imports Cheat Sheet
// Core workflow
import {
sleep, fetch, FatalError, RetryableError,
createWebhook, createHook, defineHook,
getWritable, getWorkflowMetadata, getStepMetadata,
} from "workflow";
// API routes
import { start, getRun } from "workflow/api";
// AI integration
import { DurableAgent } from "@workflow/ai/agent";
import { generateText, generateObject } from "ai";
import { createUIMessageStreamResponse } from "ai";Examples Directory
Reference implementations: ~/dev/workflow-examples/
| Example | Pattern |
|---|---|
nextjs/ | Basic user signup workflow |
kitchen-sink/ | All patterns reference |
actors/ | Event-driven actor pattern |
ai-sdk-workflow-patterns/ | AI agent patterns |
flight-booking-app/ | DurableAgent with tools |
rag-agent/ | RAG with PostgreSQL + embeddings |
birthday-card-generator/ | Webhooks + scheduling |
AI SDK Integration
Critical: Override fetch
The AI SDK uses globalThis.fetch. Override it with workflow's durable fetch.
import { fetch } from "workflow";
import { generateText, generateObject } from "ai";
export async function aiWorkflow(prompt: string) {
"use workflow";
// CRITICAL: Must override for durability
globalThis.fetch = fetch;
const { text } = await generateText({
model: "openai/gpt-4o",
prompt,
});
return text;
}Structured Output with generateObject
import { fetch } from "workflow";
import { generateObject } from "ai";
import { z } from "zod";
export async function analyzeWorkflow(content: string) {
"use workflow";
globalThis.fetch = fetch;
const { object } = await generateObject({
model: "openai/gpt-4o",
schema: z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
keywords: z.array(z.string()),
summary: z.string(),
}),
prompt: `Analyze: ${content}`,
});
return object;
}DurableAgent with Tools
Full agentic workflow with tool calling.
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable, FatalError } from "workflow";
import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
import { z } from "zod";
export async function agentWorkflow(messages: UIMessage[]) {
"use workflow";
const writable = getWritable<UIMessageChunk>();
const agent = new DurableAgent({
model: "anthropic/claude-4-opus",
system: "You are a helpful assistant with access to weather data.",
tools: {
getWeather: {
description: "Get current weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: getWeather,
},
searchDatabase: {
description: "Search the knowledge base",
inputSchema: z.object({ query: z.string() }),
execute: searchDatabase,
},
},
});
await agent.stream({
messages: convertToModelMessages(messages),
writable,
});
}
// Tool implementations as steps
async function getWeather({ city }: { city: string }) {
"use step";
if (Math.random() < 0.3) {
throw new Error("Weather service unavailable"); // Will retry
}
const response = await fetch(`https://api.weather.com/v1/${city}`);
if (!response.ok) throw new FatalError("City not found");
return response.json();
}
async function searchDatabase({ query }: { query: string }) {
"use step";
// Query your database
return await db.query(`SELECT * FROM knowledge WHERE content LIKE '%${query}%'`);
}RAG Agent (Knowledge Base)
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
export async function ragAgent(messages: UIMessage[]) {
"use workflow";
const writable = getWritable<UIMessageChunk>();
const agent = new DurableAgent({
model: "openai/gpt-4o",
system: `You are a helpful assistant. Check your knowledge base before answering.
If information isn't in the knowledge base, tell the user.`,
tools: {
addResource: {
description: "Add information to the knowledge base",
inputSchema: z.object({ content: z.string() }),
execute: async ({ content }) => {
await createResource({ content });
return `Added "${content}" to knowledge base`;
},
},
getInformation: {
description: "Search the knowledge base for relevant information",
inputSchema: z.object({ question: z.string() }),
execute: async ({ question }) => {
return await findRelevant(question);
},
},
},
});
await agent.stream({
messages: convertToModelMessages(messages),
writable,
});
}
// Nested steps for RAG operations
async function createResource(input: { content: string }) {
"use step";
const resource = await insertResource(input.content);
const embeddings = await generateEmbeddings(input.content);
await insertEmbeddings(resource.id, embeddings);
}
async function findRelevant(query: string) {
"use step";
const embedding = await generateEmbedding(query);
return await vectorSearch(embedding);
}Streaming API Route for AI Chat
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { start, getRun } from "workflow/api";
import { agentWorkflow } from "@/workflows/agent";
// Start new chat
export async function POST(request: Request) {
const { messages }: { messages: UIMessage[] } = await request.json();
const run = await start(agentWorkflow, [messages]);
return createUIMessageStreamResponse({
stream: run.readable,
headers: { "x-workflow-run-id": run.runId },
});
}
// Reconnect to existing chat
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const runId = searchParams.get("runId")!;
const startIndex = parseInt(searchParams.get("startIndex") ?? "0");
const run = getRun(runId);
const stream = run.getReadable({ startIndex });
return createUIMessageStreamResponse({ stream });
}Vercel AI Gateway
The AI Gateway provides a unified API for switching between models and providers without code changes. Models are specified as provider/model-name strings.
Basic Usage (Plain Strings)
import { fetch } from "workflow";
import { generateText } from "ai";
export async function gatewayWorkflow(prompt: string) {
"use workflow";
globalThis.fetch = fetch;
// Switch models by changing the string
const { text } = await generateText({
model: "openai/gpt-4o", // or "anthropic/claude-sonnet-4", "xai/grok-3", etc.
prompt,
});
return text;
}Custom Gateway Provider Instance
Create a custom provider for different API keys or base URLs (e.g., corporate proxy):
import { createGateway } from "@ai-sdk/gateway";
const gateway = createGateway({
apiKey: process.env.CUSTOM_GATEWAY_KEY, // Different env var
baseURL: "https://proxy.company.com/ai", // Custom base URL
});
export async function customGatewayWorkflow(prompt: string) {
"use workflow";
globalThis.fetch = fetch;
const { text } = await generateText({
model: gateway("anthropic/claude-sonnet-4"),
prompt,
});
return text;
}Set Default Provider Globally
In instrumentation.ts (runs before AI SDK calls):
import { gateway } from "@ai-sdk/gateway";
import { experimental_setDefaultProvider } from "ai";
export function register() {
experimental_setDefaultProvider(gateway);
}Then use models without specifying provider each time:
const { text } = await generateText({
model: "anthropic/claude-sonnet-4", // Uses default gateway
prompt,
});Model Providers
// OpenAI
model: "openai/gpt-5"
model: "openai/gpt-4o"
model: "openai/gpt-4o-mini"
model: "openai/o4-mini"
// Anthropic
model: "anthropic/claude-opus-4"
model: "anthropic/claude-sonnet-4"
// Google
model: "google/gemini-2.5-flash"
// xAI
model: "xai/grok-3"
// Same model, different providers (format: hosted-provider@creator/model)
model: "openai@xai/grok-3" // Grok hosted on OpenAI
model: "xai/grok-3" // Grok on xAI directlyGateway Embeddings
import { embed, embedMany } from "ai";
import { gateway } from "@ai-sdk/gateway";
// Single embedding via Gateway
const { embedding } = await embed({
model: gateway.textEmbeddingModel("openai/text-embedding-3-small"),
value: "Hello world",
});
// Batch embeddings
const { embeddings } = await embedMany({
model: gateway.textEmbeddingModel("openai/text-embedding-3-small"),
values: ["Hello", "World"],
});List Available Models
import { gateway } from "@ai-sdk/gateway";
const models = await gateway.listModels();
// Filter by type
const languageModels = models.filter(m => m.type === "language");
const embeddingModels = models.filter(m => m.type === "embedding");API Reference
Starting Workflows
Fire-and-Forget
import { start } from "workflow/api";
import { myWorkflow } from "@/workflows/my-workflow";
export async function POST(request: Request) {
const { input } = await request.json();
const run = await start(myWorkflow, [input]);
return Response.json({ runId: run.runId, message: "Started" });
}Wait for Result (Blocking)
import { start } from "workflow/api";
import { FatalError } from "workflow";
export async function POST(request: Request) {
try {
const run = await start(myWorkflow, [input]);
const result = await run.returnValue; // Blocks until complete
return Response.json(result);
} catch (error) {
const isFatal = error instanceof FatalError;
return Response.json(
{ error: error.message, fatal: isFatal },
{ status: isFatal ? 400 : 500 }
);
}
}Streaming
Initial Stream
import { createUIMessageStreamResponse } from "ai";
import { start } from "workflow/api";
export async function POST(request: Request) {
const run = await start(streamingWorkflow, [input]);
return createUIMessageStreamResponse({
stream: run.readable,
headers: { "x-workflow-run-id": run.runId },
});
}Reconnect to Existing Stream
import { getRun } from "workflow/api";
import { createUIMessageStreamResponse } from "ai";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const runId = searchParams.get("runId")!;
const startIndex = parseInt(searchParams.get("startIndex") ?? "0");
const run = getRun(runId);
const stream = run.getReadable({ startIndex });
return createUIMessageStreamResponse({ stream });
}Binary Streaming (Files)
import { Readable } from "node:stream";
import { start } from "workflow/api";
export async function POST(request: Request) {
const run = await start(fileWorkflow, [input]);
const webReadable = run.readable;
const nodeReadable = Readable.fromWeb(webReadable);
return new Response(nodeReadable, {
headers: {
"Content-Type": "audio/mp4",
"Content-Disposition": 'attachment; filename="output.mp4"',
},
});
}Utility Functions
sleep
import { sleep } from "workflow";
await sleep("5s"); // Duration string
await sleep(5000); // Milliseconds
await sleep(new Date(...)); // Until specific dateMetadata
import { getWorkflowMetadata, getStepMetadata } from "workflow";
// In workflow
const { workflowRunId } = getWorkflowMetadata();
// In step
const { attempt } = getStepMetadata(); // Current retry attempt (1, 2, 3...)Durable Fetch
import { fetch } from "workflow";
// Override globalThis for AI SDK compatibility
globalThis.fetch = fetch;
// Or use directly
const response = await fetch("https://api.example.com/data");Hooks (Custom Tokens)
createHook (One-Time)
import { createHook } from "workflow";
export async function myWorkflow() {
"use workflow";
const hook = createHook<{ status: string }>({ token: `callback:${id}` });
await triggerExternalService(hook.token);
const payload = await hook; // Waits for hook.resume()
return payload.status;
}defineHook (Reusable, Multi-Event)
import { defineHook } from "workflow";
export type MyEvent = { type: "start" } | { type: "stop" };
export const myHook = defineHook<MyEvent>();
export async function myActor() {
"use workflow";
const receive = myHook.create({ token: `actor:${id}` });
for await (const event of receive) {
// Process multiple events
}
}Resume Hook (from API Route)
import { myHook, type MyEvent } from "@/workflows/my-actor";
export async function POST(request: Request) {
const event: MyEvent = await request.json();
const result = await myHook.resume(`actor:${id}`, event);
return Response.json({ success: !!result });
}State Management
In-Memory Map
// lib/store.ts
export const store = new Map<string, any>();
// workflow.ts
store.set(id, { status: "processing" });
const result = await processData();
store.set(id, { status: "complete", result });
// api/status/route.ts
return Response.json(store.get(id) ?? { status: "not_found" });PostgreSQL with Drizzle
import { pgTable, text, jsonb, timestamp, vector, index } from "drizzle-orm/pg-core";
export const workflowRuns = pgTable("workflow_runs", {
id: text("id").primaryKey(),
status: text("status").notNull(),
result: jsonb("result"),
createdAt: timestamp("created_at").defaultNow(),
});
// With embeddings (RAG)
export const embeddings = pgTable("embeddings", {
id: text("id").primaryKey(),
content: text("content").notNull(),
embedding: vector("embedding", { dimensions: 1536 }).notNull(),
}, (table) => ({
idx: index("idx_embedding").using("hnsw", table.embedding.op("vector_cosine_ops")),
}));Error Types
| Type | Behavior | When to Use |
|---|---|---|
Error | Auto-retried with exponential backoff | Transient failures (network, timeout) |
FatalError | Stops immediately, no retry | Unrecoverable (bad credentials, invalid data) |
RetryableError | Custom retry timing | Rate limits, quota exceeded |
import { FatalError, RetryableError, getStepMetadata } from "workflow";
async function apiCall() {
"use step";
const { attempt } = getStepMetadata();
const res = await fetch("https://api.example.com");
if (res.status === 401) throw new FatalError("Invalid credentials");
if (res.status === 429) throw new RetryableError("Rate limited", { retryAfter: "60s" });
if (!res.ok) throw new Error(`HTTP ${res.status}`); // Will retry
return res.json();
}Workflow Patterns
Sequential Pipeline
Multi-step processing with quality checks.
export async function sequentialWorkflow(input: string) {
"use workflow";
globalThis.fetch = fetch;
const { text } = await generateText({ model: "openai/gpt-4o", prompt: input });
const { object } = await generateObject({
model: "openai/gpt-4o",
schema: z.object({ score: z.number(), feedback: z.string() }),
prompt: `Evaluate: ${text}`,
});
return { output: text, quality: object };
}Parallel Operations
Concurrent execution with Promise.all.
export async function parallelWorkflow(code: string) {
"use workflow";
globalThis.fetch = fetch;
const [security, performance, style] = await Promise.all([
reviewSecurity(code),
reviewPerformance(code),
reviewStyle(code),
]);
return { security, performance, style };
}
async function reviewSecurity(code: string) {
"use step";
const { object } = await generateObject({
model: "openai/gpt-4o",
schema: z.object({ vulnerabilities: z.array(z.string()), risk: z.enum(["low", "medium", "high"]) }),
prompt: `Security review: ${code}`,
});
return object;
}Dynamic Routing
Route to different handlers based on classification.
export async function routingWorkflow(query: string) {
"use workflow";
globalThis.fetch = fetch;
const { object } = await generateObject({
model: "openai/gpt-4o",
schema: z.object({ type: z.enum(["general", "technical", "billing"]) }),
prompt: `Classify this query: ${query}`,
});
const handlers = { general: handleGeneral, technical: handleTechnical, billing: handleBilling };
return handlers[object.type](query);
}Evaluator Loop
Iterative refinement until quality threshold.
export async function evaluatorWorkflow(input: string) {
"use workflow";
globalThis.fetch = fetch;
let result = await generateInitial(input);
for (let i = 0; i < 3; i++) {
const { object: evaluation } = await generateObject({
model: "openai/gpt-4o",
schema: z.object({ score: z.number(), improvements: z.array(z.string()) }),
prompt: `Evaluate: ${result}`,
});
if (evaluation.score >= 8) break;
result = await improve(result, evaluation.improvements);
}
return result;
}Webhooks (Human-in-the-Loop)
Pause workflow until external callback.
import { createWebhook } from "workflow";
export async function approvalWorkflow(userId: string) {
"use workflow";
const webhook = createWebhook();
await sendEmail({
to: "manager@example.com",
approveUrl: `${webhook.url}?action=approve`,
rejectUrl: `${webhook.url}?action=reject`,
});
// PAUSES until webhook called
const response = await webhook;
const action = new URL(response.url).searchParams.get("action");
return { approved: action === "approve" };
}Multiple Concurrent Webhooks
export async function rsvpWorkflow(emails: string[]) {
"use workflow";
const webhooks = emails.map(() => createWebhook());
await Promise.all(emails.map((email, i) => sendRsvpEmail(email, webhooks[i].url)));
const responses = await Promise.all(
webhooks.map(wh => wh.then(req => new URL(req.url).searchParams.get("reply")))
);
return responses;
}Actor Pattern (Event-Driven)
Long-running workflow processing events sequentially.
import { defineHook, getWorkflowMetadata } from "workflow";
export type Event = { type: "increment" } | { type: "decrement" } | { type: "reset" };
export const counterHook = defineHook<Event>();
export async function counterActor(initialCount: number) {
"use workflow";
const { workflowRunId } = getWorkflowMetadata();
let count = initialCount;
const receive = counterHook.create({ token: `counter:${workflowRunId}` });
for await (const event of receive) {
switch (event.type) {
case "increment": count++; break;
case "decrement": count--; break;
case "reset": count = 0; break;
}
}
}Send Events to Actor (API Route)
import { counterHook, type Event } from "@/workflows/counter-actor";
export async function POST(req: Request, { params }: { params: { id: string } }) {
const event: Event = await req.json();
const result = await counterHook.resume(`counter:${params.id}`, event);
return Response.json({ success: !!result });
}Batching
Process items in chunks.
export async function batchWorkflow(items: string[]) {
"use workflow";
const chunks = chunk(items, 10);
for (const batch of chunks) {
await Promise.all(batch.map(processItem));
}
}
async function processItem(item: string) {
"use step";
return await doWork(item);
}
function chunk<T>(arr: T[], size: number): T[][] {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
}Streaming Progress
Real-time updates to frontend.
import { getWritable } from "workflow";
export async function progressWorkflow(taskId: string) {
"use workflow";
const writable = getWritable<{ step: number; message: string }>();
const writer = writable.getWriter();
await writer.write({ step: 1, message: "Starting..." });
await step1();
await writer.write({ step: 2, message: "Processing..." });
await step2();
await writer.close();
return { success: true };
}/**
* Actor Workflow Template
*
* A long-running workflow that processes events sequentially.
* Perfect for chat sessions, game loops, or stateful agents.
*/
import { defineHook, getWorkflowMetadata } from "workflow";
// Define the actor's state
export interface ActorState {
count: number;
lastUpdated: string;
history: Array<{ action: string; timestamp: string }>;
}
// Define events the actor can receive
export type ActorEvent =
| { type: "increment"; amount?: number }
| { type: "decrement"; amount?: number }
| { type: "reset" }
| { type: "shutdown" };
// Export hook for use in API routes
export const actorHook = defineHook<ActorEvent>();
// In-memory state store (use Redis/database in production)
const stateStore = new Map<string, ActorState>();
// Main actor workflow
export async function actorWorkflow(initialState?: ActorState) {
"use workflow";
const { workflowRunId } = getWorkflowMetadata();
const actorId = workflowRunId;
// Initialize state
let state: ActorState = initialState ?? {
count: 0,
lastUpdated: new Date().toISOString(),
history: [],
};
stateStore.set(actorId, state);
// Create event receiver
const receive = actorHook.create({
token: `actor:${actorId}`,
});
console.log(`Actor ${actorId} started`);
// Event loop - processes events sequentially forever
for await (const event of receive) {
console.log(`Actor ${actorId} received:`, event);
if (event.type === "shutdown") {
console.log(`Actor ${actorId} shutting down`);
break;
}
// Process event and update state
state = await processEvent(state, event);
stateStore.set(actorId, state);
console.log(`Actor ${actorId} state:`, state);
}
return { actorId, finalState: state };
}
// Step function for processing events
async function processEvent(state: ActorState, event: ActorEvent): Promise<ActorState> {
"use step";
const timestamp = new Date().toISOString();
let newCount = state.count;
let action = "";
switch (event.type) {
case "increment":
newCount += event.amount ?? 1;
action = `increment by ${event.amount ?? 1}`;
break;
case "decrement":
newCount -= event.amount ?? 1;
action = `decrement by ${event.amount ?? 1}`;
break;
case "reset":
newCount = 0;
action = "reset";
break;
}
return {
count: newCount,
lastUpdated: timestamp,
history: [...state.history, { action, timestamp }].slice(-10),
};
}
// Helper to get actor state (for API routes)
export function getActorState(actorId: string): ActorState | undefined {
return stateStore.get(actorId);
}
/**
* AI Agent Workflow Template
*
* A durable AI agent with tool calling capabilities.
* Uses DurableAgent from @workflow/ai/agent.
*/
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable, FatalError } from "workflow";
import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
import { z } from "zod";
// Main workflow function
export async function aiAgentWorkflow(messages: UIMessage[]) {
"use workflow";
const writable = getWritable<UIMessageChunk>();
const agent = new DurableAgent({
model: "openai/gpt-4o",
system: "You are a helpful assistant with access to various tools.",
tools: {
getWeather: {
description: "Get current weather for a city",
inputSchema: z.object({
city: z.string().describe("The city name"),
}),
execute: getWeather,
},
searchKnowledge: {
description: "Search the knowledge base for information",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
execute: searchKnowledge,
},
},
});
await agent.stream({
messages: convertToModelMessages(messages),
writable,
});
}
// Tool implementations as durable steps
async function getWeather({ city }: { city: string }) {
"use step";
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?q=${encodeURIComponent(city)}`
);
if (!response.ok) {
if (response.status === 404) {
throw new FatalError(`City "${city}" not found`);
}
throw new Error(`Weather API error: ${response.status}`);
}
const data = await response.json();
return `Weather in ${city}: ${data.current.condition.text}, ${data.current.temp_c}°C`;
}
async function searchKnowledge({ query }: { query: string }) {
"use step";
// Replace with your actual database query
console.log(`Searching for: ${query}`);
return `Found relevant information about "${query}"`;
}
/**
* API Route Templates for Workflow DevKit
*
* Copy these patterns into your Next.js/Hono/etc routes.
*/
// ============================================================
// 1. START WORKFLOW (Fire-and-Forget)
// ============================================================
import { start } from "workflow/api";
import { basicWorkflow } from "@/workflows/basic-workflow";
export async function POST_start(request: Request) {
const { input } = await request.json();
const run = await start(basicWorkflow, [input]);
return Response.json({
runId: run.runId,
message: "Workflow started",
});
}
// ============================================================
// 2. START WORKFLOW (Wait for Result)
// ============================================================
import { FatalError } from "workflow";
export async function POST_blocking(request: Request) {
try {
const { input } = await request.json();
const run = await start(basicWorkflow, [input]);
// Blocks until workflow completes
const result = await run.returnValue;
return Response.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
const isFatal = error instanceof FatalError;
return Response.json(
{ error: message, fatal: isFatal },
{ status: isFatal ? 400 : 500 }
);
}
}
// ============================================================
// 3. STREAMING (AI Chat)
// ============================================================
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { aiAgentWorkflow } from "@/workflows/ai-agent-workflow";
export async function POST_stream(request: Request) {
const { messages }: { messages: UIMessage[] } = await request.json();
const run = await start(aiAgentWorkflow, [messages]);
return createUIMessageStreamResponse({
stream: run.readable,
headers: {
"x-workflow-run-id": run.runId,
},
});
}
// ============================================================
// 4. RECONNECT TO STREAM
// ============================================================
import { getRun } from "workflow/api";
export async function GET_reconnect(request: Request) {
const { searchParams } = new URL(request.url);
const runId = searchParams.get("runId");
const startIndex = parseInt(searchParams.get("startIndex") ?? "0");
if (!runId) {
return Response.json({ error: "runId required" }, { status: 400 });
}
const run = getRun(runId);
const stream = run.getReadable({ startIndex });
return createUIMessageStreamResponse({ stream });
}
// ============================================================
// 5. ACTOR - START
// ============================================================
import { actorWorkflow } from "@/workflows/actor-workflow";
export async function POST_actor_start(request: Request) {
const body = await request.json();
const initialState = body.initialState;
const run = await start(actorWorkflow, [initialState]);
// Don't await returnValue - actors run indefinitely
return Response.json({
actorId: run.runId,
message: "Actor started",
});
}
// ============================================================
// 6. ACTOR - SEND EVENT
// ============================================================
import { actorHook, type ActorEvent } from "@/workflows/actor-workflow";
export async function POST_actor_event(
request: Request,
actorId: string
) {
const event: ActorEvent = await request.json();
const result = await actorHook.resume(`actor:${actorId}`, event);
if (result) {
return Response.json({ success: true, runId: result.runId });
}
return Response.json(
{ error: "Actor not found" },
{ status: 404 }
);
}
// ============================================================
// 7. ACTOR - GET STATE
// ============================================================
import { getActorState } from "@/workflows/actor-workflow";
export async function GET_actor_state(actorId: string) {
const state = getActorState(actorId);
if (!state) {
return Response.json(
{ error: "Actor not found" },
{ status: 404 }
);
}
return Response.json({ actorId, state });
}
/**
* Basic Workflow Template
*
* A simple durable workflow with steps.
* Steps are automatically retried on failure.
*/
import { sleep, FatalError } from "workflow";
// Main workflow function
export async function basicWorkflow(input: string) {
"use workflow";
// Step 1: Validate input
const validated = await validateInput(input);
// Step 2: Process data
const result = await processData(validated);
// Wait 5 seconds (doesn't consume resources)
await sleep("5s");
// Step 3: Finalize
await finalizeResult(result);
return { success: true, result };
}
// Steps must be in the SAME FILE as the workflow
async function validateInput(input: string) {
"use step";
if (!input || input.trim().length === 0) {
// FatalError stops immediately, no retry
throw new FatalError("Input cannot be empty");
}
return input.trim().toLowerCase();
}
async function processData(data: string) {
"use step";
// Simulate processing
const response = await fetch(`https://api.example.com/process?data=${data}`);
if (!response.ok) {
// Regular errors are automatically retried
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
async function finalizeResult(result: unknown) {
"use step";
console.log("Finalizing result:", result);
// Save to database, send notification, etc.
}