Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
240bfce
docs(registry): add Registry tab with installable Workflow patterns
karthikscale3 Apr 29, 2026
a95f4f4
docs(registry): add 14 patterns + multi-category support
karthikscale3 Apr 30, 2026
caa9637
Merge branch 'main' of github.com:vercel/workflow into karthik/shadcn…
karthikscale3 May 1, 2026
fd52308
docs: rename registry to patterns, add shadcn /r routes, enrich insta…
karthikscale3 May 3, 2026
2e2e408
fix: correct installSlug for agent-cancellation stop signal approach
karthikscale3 May 3, 2026
9843a9a
fix: install workflow files into app/workflows/ not root workflows/
karthikscale3 May 3, 2026
768a81a
fix: use registry:file type so shadcn writes inline content instead o…
karthikscale3 May 3, 2026
b5d910d
fix: rename installed workflow file from stoppable-agent.ts to agent-…
karthikscale3 May 3, 2026
b2723cf
fix: all 19 patterns now install correctly with rich comments
karthikscale3 May 3, 2026
b4b3468
fix: remove inline descriptions from snippet captions (ai-sdk support…
karthikscale3 May 3, 2026
207ee27
fix: rename workflow files to match pattern IDs and fix import paths
karthikscale3 May 3, 2026
8c09f40
fix: rename all installed workflow files to end in -workflow.ts
karthikscale3 May 3, 2026
18d83a2
fix: fold ChatTurnPayload into chat-sdk-workflow.ts, remove separate …
karthikscale3 May 3, 2026
5fdf9d0
refactor: rename lib/registry and components/registry to lib/patterns…
karthikscale3 May 3, 2026
5ed0533
refactor: replace hand-written logo SVGs with lucide-react icons, kee…
karthikscale3 May 3, 2026
780684c
fix: use correct whySection field names in copy-page text builder
karthikscale3 May 3, 2026
4558bd2
fix: update all sourceUrls from deleted cookbook MDX files to pattern…
karthikscale3 May 3, 2026
c525ccc
Merge branch 'main' into karthik/shadcn-registry-2
karthikscale3 May 3, 2026
539ca73
fix: split webhooks into two separate installable files (event-listen…
karthikscale3 May 3, 2026
e58afd9
fix: remove invalid createWebhook type parameter in timeouts pattern
karthikscale3 May 3, 2026
be7311f
fix: cast sleep() delay argument to any to satisfy StringValue overlo…
karthikscale3 May 3, 2026
1f3a583
feat: add search bar to patterns grid above cards
karthikscale3 May 3, 2026
8d7c013
fix: update Submit your recipe link to vercel/workflow repo
karthikscale3 May 3, 2026
711cf4d
Merge main into karthik/shadcn-registry-2
karthikscale3 May 4, 2026
62492a6
Merge remote-tracking branch 'origin/main' into karthik/shadcn-regist…
karthikscale3 May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
888 changes: 888 additions & 0 deletions docs/app/[lang]/patterns/[id]/page.tsx

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions docs/app/[lang]/patterns/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { RegistryGrid } from '@/components/patterns/RegistryGrid';
import { registryItems } from '@/lib/patterns/manifest';

export const metadata: Metadata = {
title: 'Patterns | Workflow SDK',
description:
'Installable Workflow patterns for popular providers — durable, cancellable, replay-safe recipes you drop into your app with one shadcn command.',
};

export default function PatternsPage() {
return (
<div className="[&_h1]:tracking-tighter [&_h2]:tracking-tighter [&_h3]:tracking-tighter sm:mt-24">
<div className="mx-auto w-full max-w-[1080px]">
{/* Hero */}
<section className="relative px-4 text-center pt-12 pb-12 sm:pb-16">
<div className="relative z-10 mx-auto w-full max-w-3xl space-y-3 sm:space-y-5">
<h1 className="text-center font-semibold text-4xl leading-[1.1] tracking-tight sm:text-5xl xl:text-6xl text-balance">
Patterns
</h1>
<p className="text-balance text-muted-foreground sm:text-xl leading-relaxed">
Installable Workflow patterns for popular providers. Durable,
cancellable, replay-safe recipes you drop into your app with one{' '}
<code className="font-mono text-base sm:text-lg bg-muted px-1.5 py-0.5 rounded">
shadcn
</code>{' '}
command.
</p>
</div>
</section>

{/* Grid */}
<RegistryGrid items={registryItems} />

{/* CTA */}
<section className="border-t px-4 py-8 sm:pt-24 sm:pb-16 sm:px-12">
<div className="max-w-2xl mx-auto text-center space-y-4">
<h2 className="font-semibold text-3xl tracking-tight sm:text-4xl">
Build your own
</h2>
<p className="text-muted-foreground">
Package any workflow as a shadcn-installable recipe and share it
with the community. Each recipe is just a workflow file plus the
API routes that drive it — anything you can write with the
Workflow SDK qualifies.
</p>
<div className="flex justify-center gap-3 mt-8">
<Button asChild size="lg">
<Link href="/docs">Browse the docs</Link>
</Button>
<Button asChild variant="outline" size="lg">
<a
href="https://github.com/vercel/workflow"
target="_blank"
rel="noopener noreferrer"
>
Submit your recipe
</a>
</Button>
</div>
</div>
</section>
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions docs/app/og/patterns/[id]/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { NextRequest } from 'next/server';
import { getRegistryItem, getRegistryItemIds } from '@/lib/patterns/manifest';
import { createOgImage } from '@/lib/og';

export const GET = async (
_request: NextRequest,
{ params }: RouteContext<'/og/patterns/[id]'>
) => {
const { id } = await params;
const item = getRegistryItem(id);

if (!item) {
return new Response('Not found', { status: 404 });
}

return createOgImage({
title: item.name,
description: item.description,
});
};

export const generateStaticParams = () =>
getRegistryItemIds().map((id) => ({ id }));
124 changes: 124 additions & 0 deletions docs/app/r/[name]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* /r/[name] — shadcn-compatible registry item endpoint.
*
* Returns a single registry item in the shadcn registry-item.json schema so
* the shadcn CLI can install it:
*
* pnpm dlx shadcn@latest add https://workflow-sdk.dev/r/durable-agent
*
* Only workflow source files (captions starting with "workflows/") are
* included in the response. For those files, `installCode` is preferred over
* `code` when present — `installCode` carries the richly-commented version
* with agent-friendly PATTERN / USEFUL WHEN / TO ADAPT sections, while
* `code` is the clean UI display version.
*
* Content negotiation:
* - `Accept: application/json` or `User-Agent: *shadcn*` → JSON response
* - Otherwise → redirect to the human-readable /patterns/[name] page
*/

import { NextResponse } from 'next/server';
import { registryItems } from '@/lib/patterns/manifest';

const WORKFLOW_PATH_PREFIX = 'workflows/';

export const dynamic = 'force-dynamic';

function wantsBrowserRedirect(request: Request): boolean {
const accept = request.headers.get('accept') ?? '';
const userAgent = request.headers.get('user-agent') ?? '';
// shadcn CLI sends Accept: application/json; browsers send text/html first.
if (accept.includes('application/json')) return false;
if (/shadcn/i.test(userAgent)) return false;
if (accept.includes('text/html')) return true;
return false;
}

export async function GET(
request: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params;

const item = registryItems.find((r) => r.id === name);
if (!item) {
// For browser requests, redirect to the patterns index.
if (wantsBrowserRedirect(request)) {
return NextResponse.redirect(new URL('/patterns', request.url), 302);
}
return NextResponse.json(
{ error: `Pattern "${name}" not found` },
{ status: 404 }
);
}

// Browsers visiting /r/durable-agent get the pretty detail page instead.
if (wantsBrowserRedirect(request)) {
return NextResponse.redirect(
new URL(`/patterns/${name}`, request.url),
302
);
}

// Collect workflow files from snippets (installCode > code fallback).
const workflowSnippets = item.snippets.filter((s) =>
s.caption?.startsWith(WORKFLOW_PATH_PREFIX)
);

// Deduplicate by caption path — multiple tabs may point to the same file.
const seenPaths = new Set<string>();
const files: Array<{
path: string;
content: string;
type: 'registry:file';
target: string;
}> = [];

for (const snippet of workflowSnippets) {
const filePath = snippet.caption!;
if (seenPaths.has(filePath)) continue;
seenPaths.add(filePath);

files.push({
path: filePath,
content: snippet.installCode ?? snippet.code,
// registry:file tells the shadcn CLI to write the inline `content` field
// directly to `target` without trying to resolve `path` as a URL.
type: 'registry:file',
// Workflow files live under app/workflows/ in a Next.js app-router project.
target: `app/${filePath}`,
});
}

// If no workflow snippets found, also check conceptSnippets.
if (files.length === 0 && item.conceptSnippets) {
for (const snippet of item.conceptSnippets) {
if (!snippet.caption?.startsWith(WORKFLOW_PATH_PREFIX)) continue;
const filePath = snippet.caption!;
if (seenPaths.has(filePath)) continue;
seenPaths.add(filePath);
files.push({
path: filePath,
content: snippet.installCode ?? snippet.code,
type: 'registry:file',
target: `app/${filePath}`,
});
}
}

const registryItem = {
$schema: 'https://ui.shadcn.com/schema/registry-item.json',
name: item.id,
type: 'registry:file' as const,
title: item.name,
description: item.description,
files,
};

return NextResponse.json(registryItem, {
headers: {
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
'Access-Control-Allow-Origin': '*',
},
});
}
53 changes: 53 additions & 0 deletions docs/app/r/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* /r — shadcn-compatible registry index endpoint.
*
* Returns the full registry in the shadcn registry.json schema so the CLI
* can discover all available patterns:
*
* pnpm dlx shadcn@latest add https://workflow-sdk.dev/r
*
* Each item in the index links to /r/[name] for the full file payload.
*/

import { NextResponse } from 'next/server';
import { registryItems } from '@/lib/patterns/manifest';

export const dynamic = 'force-dynamic';

function wantsBrowserRedirect(request: Request): boolean {
const accept = request.headers.get('accept') ?? '';
const userAgent = request.headers.get('user-agent') ?? '';
if (accept.includes('application/json')) return false;
if (/shadcn/i.test(userAgent)) return false;
if (accept.includes('text/html')) return true;
return false;
}

export async function GET(request: Request) {
if (wantsBrowserRedirect(request)) {
return NextResponse.redirect(new URL('/patterns', request.url), 302);
}
const items = registryItems.map((item) => ({
name: item.id,
type: 'registry:lib' as const,
title: item.name,
description: item.description,
registryDependencies: [],
tags: item.tags,
categories: item.categories,
}));

const registryIndex = {
$schema: 'https://ui.shadcn.com/schema/registry.json',
name: 'workflow-sdk',
homepage: 'https://workflow-sdk.dev',
items,
};

return NextResponse.json(registryIndex, {
headers: {
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
'Access-Control-Allow-Origin': '*',
},
});
}
63 changes: 63 additions & 0 deletions docs/components/patterns/RegistryCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { categoryLabels } from '@/lib/patterns/manifest';
import type { RegistryItem } from '@/lib/patterns/types';
import { getProviderLogo } from './logos';

interface RegistryCardProps {
item: RegistryItem;
}

export function RegistryCard({ item }: RegistryCardProps) {
const Logo = getProviderLogo(item.logo);

return (
<Link href={`/patterns/${item.id}`} className="block group">
<Card className="h-full transition-colors cursor-pointer overflow-hidden py-0! gap-2">
<CardHeader className="px-4 pt-4 pb-0">
<div className="flex items-start gap-3">
{Logo && (
<div
aria-hidden="true"
className="flex h-9 min-w-9 shrink-0 items-center justify-center rounded-md border bg-background text-foreground px-2"
>
<Logo size={18} />
</div>
)}
<div className="space-y-1 min-w-0 flex-1">
<CardTitle className="text-lg flex items-center gap-1.5 flex-wrap">
<span className="truncate">{item.name}</span>
</CardTitle>
<CardDescription className="text-xs font-mono truncate">
{item.shadcnSlug}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="flex-1 px-4 pb-2">
<p className="text-sm text-muted-foreground line-clamp-3">
{item.description}
</p>
</CardContent>
<div className="flex items-center flex-wrap gap-1.5 px-4 pb-4 pt-2">
{item.categories.map((category) => (
<Badge
key={category}
variant="outline"
className="text-xs font-normal py-0.5 px-2"
>
{categoryLabels[category]}
</Badge>
))}
</div>
</Card>
</Link>
);
}
Loading
Loading