Skip to content

Instantly share code, notes, and snippets.

@snarktank
Created December 2, 2025 17:09
Show Gist options
  • Select an option

  • Save snarktank/1e3664de0eb42f96afd4962ac95092e1 to your computer and use it in GitHub Desktop.

Select an option

Save snarktank/1e3664de0eb42f96afd4962ac95092e1 to your computer and use it in GitHub Desktop.
Vercel Workflows Development Guide

Vercel Workflows Development Guide

Critical Configuration Requirements

1. Public Routes for Workflow Runtime

The workflow runtime uses /.well-known/workflow/* endpoints internally. These MUST be public (not protected by auth middleware).

In proxy.ts, ensure this route is in the public routes list:

const isPublicRoute = createRouteMatcher([
  // ... other routes
  "/.well-known/(.*)", // Workflow runtime endpoints - must be public
]);

Symptom if missing: [embedded world] Queue operation failed: TypeError: fetch failed with workflow runs stuck in "pending" status.

2. React Strict Mode Handling

React Strict Mode double-invokes effects, which can abort the first stream request. Use a ref guard instead of disabling Strict Mode globally.

Pattern to prevent double-send:

const sendingRef = useRef(false);

const safeSendMessage = useCallback((message) => {
  if (sendingRef.current) {
    console.log('Skipping duplicate send (Strict Mode)');
    return;
  }
  sendingRef.current = true;
  sendMessage(message);
}, [sendMessage]);

// In onFinish callback:
onFinish(data) {
  sendingRef.current = false; // Reset for next message
  
  if (data.isAbort) {
    console.log('Stream aborted, skipping save');
    return; // Don't save on aborted requests
  }
  // ... save chat history
}

Symptom without guard: isAbort: true in onFinish callback, messages array is empty, ResponseAborted errors.

3. Package Version Compatibility

These versions are tested and working together:

{
  "dependencies": {
    "@ai-sdk/react": "2.0.60",
    "@workflow/ai": "4.0.1-beta.19",
    "workflow": "4.0.1-beta.19",
    "ai": "5.0.104"
  },
  "overrides": {
    "ai": "5.0.104",
    "@ai-sdk/react": "2.0.60"
  }
}

Symptom if mismatched: Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer errors.

4. Conflicting Packages

Mastra packages (@mastra/*) pull in conflicting ai SDK versions. If using Vercel Workflows, remove Mastra:

npm uninstall @mastra/ai-sdk @mastra/client-js @mastra/core @mastra/evals @mastra/libsql @mastra/memory @mastra/observability @mastra/pg @mastra/react mastra

5. OpenTelemetry Instrumentation

The instrumentation.ts file with @vercel/otel can interfere with the workflow's embedded world runtime. If experiencing issues, try renaming/removing it temporarily.

6. Turbopack (Recommended)

The reference implementation uses turbopack for development:

{
  "scripts": {
    "dev": "next dev --turbopack"
  }
}

Workflow File Structure

workflows/
├── AGENTS.md                    # This file
├── tools/                       # Shared tool definitions
│   ├── index.ts                 # Re-exports all tools
│   ├── legal-research.ts        # Step function + tool definition
│   ├── child-support-guidelines.ts
│   └── ...                      # Other tools
├── legal-chat/
│   ├── index.ts                 # Main workflow function with "use workflow"
│   └── prompts.ts               # System prompts
├── onboarding/
│   ├── index.ts                 # Main workflow function
│   ├── types.ts                 # Type definitions
│   └── steps/
│       └── tools.ts             # Workflow-specific tools
└── [other-workflows]/

Workflow Pattern

Main Workflow (index.ts)

import { DurableAgent } from '@workflow/ai/agent';
import { convertToModelMessages, type UIMessage, type UIMessageChunk } from 'ai';
import { getWritable } from 'workflow';
import { SYSTEM_PROMPT, myTools } from './steps/tools';

export async function myWorkflow(messages: UIMessage[]) {
  'use workflow';

  const writable = getWritable<UIMessageChunk>();

  const agent = new DurableAgent({
    model: 'google/gemini-2.5-flash',
    system: SYSTEM_PROMPT,
    tools: myTools,
  });

  await agent.stream({
    messages: convertToModelMessages(messages),
    writable,
  });
}

Tool Definitions (steps/tools.ts)

import { z } from 'zod';

// Step function - runs in Node.js, auto-retries on failure
export async function myTool({ param }: { param: string }) {
  'use step';
  
  // Full Node.js access here - API calls, database, etc.
  return { result: 'success' };
}

export const myTools = {
  myTool: {
    description: 'Description for the LLM',
    inputSchema: z.object({
      param: z.string().describe('Parameter description'),
    }),
    execute: myTool,
  },
};

export const SYSTEM_PROMPT = `Your system prompt here`;

API Routes

POST Route (Start Workflow)

// app/api/[name]/route.ts
import { createUIMessageStreamResponse, type UIMessage } from 'ai';
import { start } from 'workflow/api';
import { myWorkflow } from '@/workflows/[name]';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const run = await start(myWorkflow, [messages]);

  return createUIMessageStreamResponse({
    stream: run.readable,
    headers: {
      'x-workflow-run-id': run.runId,
    },
  });
}

GET Route (Reconnect to Stream)

// app/api/[name]/[id]/stream/route.ts
import { createUIMessageStreamResponse } from 'ai';
import { getRun } from 'workflow/api';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const { searchParams } = new URL(request.url);
  const startIndex = searchParams.get('startIndex');
  
  const run = getRun(id);
  const stream = run.getReadable({ 
    startIndex: startIndex ? parseInt(startIndex, 10) : undefined 
  });

  return createUIMessageStreamResponse({ stream });
}

Debugging Commands

# List recent workflow runs
npx workflow inspect runs --limit 5 --json

# Inspect steps for a specific run
npx workflow inspect steps --run [runId] --json

# Get details for a specific step
npx workflow inspect step [stepId]

Common Issues Checklist

When workflows aren't working, check:

  1. Is /.well-known/(.*) in public routes?
  2. Is Strict Mode handled with ref guard pattern? (see section 2)
  3. Are package versions correct with overrides?
  4. Are there conflicting packages (Mastra)?
  5. Is instrumentation.ts interfering?
  6. Is the API route public in middleware?
  7. Check workflow run status: npx workflow inspect runs --limit 1 --json
  8. Are there stale generated files? (see below)

Stale Generated Files

The workflow runtime generates route files in app/.well-known/workflow/. After refactoring (e.g., moving code from src/mastra/ to workflows/), these generated files can contain stale imports that break the workflow runtime.

Symptom: [embedded world] Queue operation failed: TypeError: fetch failed with ECONNREFUSED, even though the workflow runs show as "completed".

Fix: Delete the generated directories and restart the dev server:

rm -rf app/.well-known
rm -rf .next
# Then restart dev server

The workflow runtime will regenerate fresh routes based on the current codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment