Imagine writing a function that can stop mid-execution and resume hours, days, or even weeks later without losing its place or any of its variables. No checkpoints. No state machines. No external storage. Just a function that time-travels.
Vercel Workflow is a fully managed implementation of the open source Workflow Development Kit (WDK), a TypeScript framework for building apps and AI Agents that can resume, suspend, and maintain state with ease.
With Vercel Workflow, a function can suspend itself until a desired state (e.g. a webhook event arriving), then pick up exactly where it left off with full state and stack continuity.
Here's an example of what this kind of function might look like:
import { defineHook } from "workflow";
export const messageHook = defineHook<{text: string}>();
export async function conversation(conversationId: string) { "use workflow"; let history = []; // This survives across executions const messages = messageHook.create({ token: `chat-${conversationId}` }); // This loop pauses and resumes as events arrive for await (const message of messages) { history.push(message.text); console.log(`History: ${history.join(', ')}`); if (history.length >= 5) break; } console.log("Conversation complete!");}
// Webhook receives events and resumes the workflowexport async function POST(req: Request) { const { chatId, text } = await req.json(); await messageHook.resume(`chat-${conversationId}`, { text }); return new Response("OK");}We'll get into how this works in the guide below.
Let’s dive into a Slack bot that invites users to write a story together with AI.
When a user runs /storytime in Slack, the bot begins a collaborative story in a thread. Users add messages one at a time, and the AI continues the story after each contribution. When the story reaches a natural ending, the bot generates an image based on the full story.
The key is that the workflow powering this bot pauses after each message and resumes when a new one arrives, using a workflow hook. This allows the function to maintain the entire story state over time without a database or any manual state management.
Now let's go through the code.
import { defineHook } from "workflow";import { generateStoryPiece, generateStoryboardImage } from "./steps";import { postToSlack } from "./lib/slack";
const slackMessageHook = defineHook<{text: string}>();
export async function storytime(slashCommand: URLSearchParams) { "use workflow"; const channelId = slashCommand.get("channel_id"); let messages = []; let finalStory = ""; // Generate AI story introduction const intro = await generateStoryPiece(messages); const { ts } = await postToSlack(`Story started: ${intro.story}`); // Create hook to listen for Slack messages in this thread const slackMessages = slackMessageHook.create({ token: `story-${channelId}-${ts}` }); // Wait for users to contribute - this is where the magic happens for await (const userMessage of slackMessages) { messages.push({ role: "user", content: userMessage.text }); const aiResponse = await generateStoryPiece(messages); await postToSlack(aiResponse.encouragement); if (aiResponse.done) { finalStory = aiResponse.story; break; } } // Story complete - generate final image await generateStoryboardImage(finalStory);}
// Webhook receives Slack messages and resumes the workflowexport async function POST(req: Request) { const slackEvent = await req.json(); const { channel, thread_ts, text } = slackEvent.event; const token = `story-${channel}-${thread_ts}`; await slackMessageHook.resume(token, { text }); return new Response("OK");}- User types
/storytime→ Workflow starts, creates hook with unique token - AI generates introduction → Posts to Slack
- Function pauses → Waits at
for awaitloop for user messages - User adds to story → Slack webhook receives event
- Webhook resumes workflow → Uses token to find paused workflow
- AI continues story → Processes with full conversation history
- Repeat until complete → Each message resumes the workflow
- Generate final image → Workflow completes naturally
The messages array grows with each interaction, the AI always has full context, and you never think about databases or state management. This complete message history can then be used to generate the final image.

The key insight is that webhooks don't just receive events, they resume paused workflows with perfect state preservation.
Workflows are stateful functions that can pause and resume:
export async function myWorkflow(items: string[]) { "use workflow"; // This makes it stateful let processedCount = 0; let results = []; // Workflow is suspended for processing of each item for (const item of items) { const result = await processData(item); results.push(result); processedCount++; // Send progress updates await sendNotification(`Processed ${processedCount}/${items.length} items`); // State survives across async operations and potential pauses if (processedCount % 5 === 0) { await sendSummary(results.slice(-5)); // Last 5 results } } return { processedCount, results };}Steps are reliable, retryable operations within workflows:
export async function processData(data: string) { "use step";
// If the external api request were to fail, the entire step would be retried const result = await externalAPI.process(data); return result;}
export async function sendNotification(message: string) { "use step"; await emailService.send(message);}Hooks let you pass data from external events and resume workflow execution:
- Define what an event looks like
import { defineHook } from "workflow";
const messageHook = defineHook<{text: string, userId: string}>();2. Create a listener in your workflow
export async function myWorkflow() { "use workflow"; const events = messageHook.create({ token: "unique-workflow-identifier" });
for await (const event of events) { // Process event with full workflow state console.log("Received event:", event); }}3. Resume the hook when events arrive
export async function POST(req: Request) { const data = await req.json(); await messageHook.resume("unique-workflow-identifier", { text: data.message, userId: data.user }); return new Response("OK");}The token is the key. It's given to the hook with the corresponding token which identifies the correct workflow to resume and run.
In addition to hooks, a mechanism to orchestrate workflow pausing includes using a timer.
In traditional serverless development, functions are stateless by design. To maintain continuity, you have to scatter state across databases, message queues, and more. With workflows, your functions are stateful. They listen. They pause. They resume. They remember.
Before: "How do I coordinate these distributed systems?"
After: "What should happen when the user does X?"
Before: "Where do I store this state? How do I handle failures?"
After: "This variable holds the state. It just works."
Vercel Workflows let's you write simple functions that just work, even for complex stateful processes.
You can now build sophisticated applications by focusing on what should happen, not how to coordinate infrastructure.
The Storytime bot shows how complex, interactive systems become simple when you have the right abstractions.
Try it yourself by deploying the complete Storytime Slack bot example to see workflows in action.

