[workbench] Add TanStack Start workbench and tests (#1875)

This commit is contained in:
Peter Wielander
2026-05-04 09:42:44 +09:00
committed by GitHub
parent 92dc82608a
commit 8202663857
28 changed files with 1608 additions and 1223 deletions
+5
View File
@@ -0,0 +1,5 @@
---
---
Add TanStack Start workbench app and getting-started guide. The existing
`workflow/vite` plugin already supports TanStack Start since it runs on Vite.
+6
View File
@@ -251,6 +251,12 @@ jobs:
- name: "astro"
project-id: "prj_YDAXj3K8LM0hgejuIMhioz2yLgTI"
project-slug: "workbench-astro-workflow"
# TODO: re-enable once a Vercel project is connected for this branch.
# The local-dev/local-prod/local-postgres matrices already cover
# tanstack-start via scripts/create-test-matrix.mjs.
# - name: "tanstack-start"
# project-id: "prj_643jeVugTMq5ivsOFQHcbLG1qcnu"
# project-slug: "workbench-tanstack-start-workflow"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+3 -1
View File
@@ -25,7 +25,9 @@
"!**/.workflow-data",
"!**/.nuxt",
"!**/.nitro",
"!**/.output"
"!**/.output",
"!**/.tanstack",
"!**/routeTree.gen.ts"
],
"ignoreUnknown": true
},
File diff suppressed because one or more lines are too long
@@ -66,7 +66,10 @@ function CopyButton({ text }: { text: string }) {
);
}
export function PreviewBadge({ deploymentUrl, tarballsUrl }: PreviewBadgeProps) {
export function PreviewBadge({
deploymentUrl,
tarballsUrl,
}: PreviewBadgeProps) {
const baseUrl = (tarballsUrl || deploymentUrl).replace(/\/$/, '');
const installCmd = `pnpm i ${baseUrl}/workflow.tgz`;
const npxCmd = `npx workflow@${baseUrl}/workflow.tgz web`;
+4 -1
View File
@@ -41,7 +41,10 @@ const Home = () => (
<Hero title={title} description={description} />
{isPreview && deploymentUrl && (
<div className="fixed bottom-4 right-4 z-50">
<PreviewBadge deploymentUrl={deploymentUrl} tarballsUrl={tarballsUrl} />
<PreviewBadge
deploymentUrl={deploymentUrl}
tarballsUrl={tarballsUrl}
/>
</div>
)}
<div className="grid divide-y border-y sm:border-x">
+6 -7
View File
@@ -63,6 +63,12 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
<span className="font-medium">SvelteKit</span>
</div>
</Card>
<Card href="/docs/getting-started/tanstack-start" >
<div className="flex flex-col items-center justify-center gap-2">
<TanStack className="size-16 dark:invert" />
<span className="font-medium">TanStack Start</span>
</div>
</Card>
<Card href="/docs/getting-started/python">
<div className="flex flex-col items-center justify-center gap-2">
<Python className="size-16" />
@@ -77,11 +83,4 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
<Badge variant="secondary">Coming soon</Badge>
</div>
</Card>
<Card className="opacity-50">
<div className="flex flex-col items-center justify-center gap-2">
<TanStack className="size-16 dark:invert grayscale" />
<span className="font-medium">TanStack Start</span>
<Badge variant="secondary">Coming soon</Badge>
</div>
</Card>
</Cards>
@@ -9,6 +9,7 @@
"nitro",
"nuxt",
"sveltekit",
"tanstack-start",
"vite",
"python"
],
@@ -0,0 +1,241 @@
---
title: TanStack Start
description: Set up your first durable workflow in a TanStack Start application.
type: guide
summary: Set up Workflow SDK in a TanStack Start app.
prerequisites:
- /docs/getting-started
related:
- /docs/foundations/workflows-and-steps
---
This guide will walk through setting up your first workflow in a TanStack Start app. Along the way, you'll learn more about the concepts that are fundamental to using the Workflow SDK in your own projects.
---
<Steps>
<Step>
## Create Your TanStack Start Project
Start by creating a new TanStack Start project:
```bash
npm create @tanstack/start@latest my-workflow-app
```
Enter the newly made directory:
```bash
cd my-workflow-app
```
### Install `workflow`
```package-install
npm i workflow
```
### Configure TanStack Start
TanStack Start runs on Vite, so the Workflow SDK is wired in via the same `workflow/vite` plugin. Add `workflow()` to your Vite config to enable usage of the `"use workflow"` and `"use step"` directives.
```typescript title="vite.config.ts" lineNumbers
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { defineConfig } from "vite";
import { workflow } from "workflow/vite";
export default defineConfig({
plugins: [
workflow(), // [!code highlight]
tanstackStart(),
],
});
```
<Accordion type="single" collapsible>
<AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
<AccordionTrigger className="text-sm">
### Setup IntelliSense for TypeScript (Optional)
</AccordionTrigger>
<AccordionContent className="[&_p]:my-2">
To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`:
```json title="tsconfig.json" lineNumbers
{
"compilerOptions": {
// ... rest of your TypeScript config
"plugins": [
{
"name": "workflow" // [!code highlight]
}
]
}
}
```
</AccordionContent>
</AccordionItem>
</Accordion>
</Step>
<Step>
## Create Your First Workflow
Create a new file for our first workflow:
```typescript title="src/workflows/user-signup.ts" lineNumbers
import { sleep } from "workflow";
export async function handleUserSignup(email: string) {
"use workflow"; // [!code highlight]
const user = await createUser(email);
await sendWelcomeEmail(user);
await sleep("5s"); // Pause for 5s - doesn't consume any resources
await sendOnboardingEmail(user);
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 SDK'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
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) {
// 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("@")) {
// To skip retrying, throw a FatalError instead
throw new FatalError("Invalid Email");
}
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.
<Callout>
We'll dive deeper into workflows, steps, and other ways to suspend or handle events in [Foundations](/docs/foundations).
</Callout>
</Step>
<Step>
## Create Your Route Handler
To invoke your new workflow, add a server handler at `src/routes/api/signup.ts`:
```typescript title="src/routes/api/signup.ts"
import { createFileRoute } from "@tanstack/react-router";
import { json } from "@tanstack/react-start";
import { start } from "workflow/api";
import { handleUserSignup } from "../../workflows/user-signup";
export const Route = createFileRoute("/api/signup")({
server: {
handlers: {
POST: async ({ request }) => {
const { email } = await request.json();
// Executes asynchronously and doesn't block your app
await start(handleUserSignup, [email]);
return json({ message: "User signup workflow started" });
},
},
},
});
```
This route handler creates a `POST` request endpoint at `/api/signup` that will trigger your workflow.
<Callout>
Workflows can be triggered from API routes or any server-side code.
</Callout>
</Step>
</Steps>
## Run in development
To start your development server, run the following command in your terminal in the TanStack Start root directory:
```bash
npm run dev
```
Once your development server is running, you can trigger your workflow by running this command in the terminal:
```bash
curl -X POST --json '{"email":"hello@example.com"}' http://localhost:3000/api/signup
```
Check the dev server logs to see your workflow execute as well as the steps that are being processed.
Additionally, you can use the [Workflow SDK 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
```
![Workflow SDK Web UI](/o11y-ui.png)
---
## Deploying to production
Workflow SDK apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
<FluidComputeCallout />
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).
+1
View File
@@ -109,6 +109,7 @@ describe.each([
'fastify',
'nest',
'astro',
'tanstack-start',
])('e2e', (project) => {
test('builds without errors', { timeout: 180_000 }, async () => {
// skip if we're targeting specific app to test
+1 -1
View File
@@ -112,7 +112,7 @@ export function hasWorkflowSourceMaps(): boolean {
// TODO: figure out how to get sourcemaps working in these frameworks too
if (
process.env.DEV_TEST_CONFIG &&
['vite', 'astro', 'sveltekit'].includes(appName)
['vite', 'astro', 'sveltekit', 'tanstack-start'].includes(appName)
) {
return false;
}
+1107 -1177
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -69,6 +69,12 @@ const DEV_TEST_CONFIGS = {
apiFileImportPath: '../..',
workflowsDir: 'src/workflows',
},
'tanstack-start': {
generatedStepPath: 'node_modules/.nitro/workflow/steps.mjs',
generatedWorkflowPath: 'node_modules/.nitro/workflow/workflows.mjs',
apiFilePath: 'src/routes/api/chat.ts',
apiFileImportPath: '../../..',
},
};
const matrix = {
@@ -148,4 +154,10 @@ matrix.app.push({
...DEV_TEST_CONFIGS.astro,
});
matrix.app.push({
name: 'tanstack-start',
project: 'workbench-tanstack-start-workflow',
...DEV_TEST_CONFIGS['tanstack-start'],
});
console.log(JSON.stringify(matrix));
+12
View File
@@ -0,0 +1,12 @@
node_modules
.DS_Store
dist
.nitro
.output
.tanstack
.vinxi
.workflow-data
.well-known/
.vercel
_workflows.ts
src/routeTree.gen.ts
+1
View File
@@ -0,0 +1 @@
../../LICENSE.md
+10
View File
@@ -0,0 +1,10 @@
# Workflow + TanStack Start
Workbench app for testing the Workflow SDK with TanStack Start. TanStack Start
runs on Vite + Nitro, so the workflow integration is the standard `workflow/vite`
plugin.
```sh
pnpm dev
pnpm build && pnpm start
```
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@workflow/example-tanstack-start",
"private": true,
"type": "module",
"version": "0.0.0",
"license": "Apache-2.0",
"scripts": {
"generate:workflows": "node ../scripts/generate-workflows-registry.js",
"predev": "pnpm generate:workflows",
"prebuild": "pnpm generate:workflows",
"dev": "vite dev",
"build": "vite build",
"start": "node .output/server/index.mjs"
},
"dependencies": {
"@node-rs/xxhash": "1.7.6",
"@tanstack/react-router": "^1.140.1",
"@tanstack/react-start": "^1.140.1",
"react": "^19.2.1",
"react-dom": "^19.2.1"
},
"devDependencies": {
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"@workflow/ai": "workspace:*",
"@workflow/world-postgres": "workspace:*",
"ai": "catalog:",
"lodash.chunk": "^4.2.0",
"nitro": "catalog:",
"openai": "^6.1.0",
"vite": "^7.3.2",
"workflow": "workspace:*",
"zod": "catalog:"
}
}
+1
View File
@@ -0,0 +1 @@
../nitro-v3/plugins
+10
View File
@@ -0,0 +1,10 @@
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export const getRouter = () => {
return createRouter({
routeTree,
scrollRestoration: true,
defaultPreloadStaleTime: 0,
});
};
@@ -0,0 +1,26 @@
import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'Workflow + TanStack Start' },
],
}),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
@@ -0,0 +1,20 @@
// HMR sentinel route. The dev test suite watches that touching the imported
// workflow file rebuilds this handler. We use a dynamic import here (rather than
// the static `import * as workflows from '...'` used by other workbench apps)
// so the workflow module isn't pulled into this route's chunk in production —
// see the comment in test-direct-step-call.ts for why that matters here.
import { createFileRoute } from '@tanstack/react-router';
import { json } from '@tanstack/react-start';
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async () => {
const workflows = await import('../../../workflows/3_streams.js');
console.log(workflows);
return json('hello world');
},
},
},
});
@@ -0,0 +1,38 @@
// This route tests calling step functions directly outside of any workflow context.
// After the SWC compiler changes, step functions in client mode have their directive
// removed and keep their original implementation, allowing them to be called as
// regular async functions.
//
// The step is defined inline rather than imported from workflows/99_e2e.ts because
// TanStack Start bundles all `src/routes/**` files together. Statically importing a
// workflow file pulls its class definitions (Counter, etc.) into this chunk, where
// the SWC plugin re-emits class-registration IIFEs under this app's host. Combined
// with the canonical registrations under the source-file host, the second
// `Object.defineProperty(cls, "classId", { configurable: false })` then throws at
// module-load time.
import { createFileRoute } from '@tanstack/react-router';
import { json } from '@tanstack/react-start';
async function add(a: number, b: number) {
'use step';
return a + b;
}
export const Route = createFileRoute('/api/test-direct-step-call')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json();
const { x, y } = body;
console.log(`Calling step function directly with x=${x}, y=${y}`);
const result = await add(x, y);
console.log(`add(${x}, ${y}) = ${result}`);
return json({ result });
},
},
},
});
@@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
component: () => <h1>Workflow + TanStack Start workbench</h1>,
});
+26
View File
@@ -0,0 +1,26 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@repo/*": ["../../*"]
},
"plugins": [{ "name": "workflow" }]
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://turborepo.org/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"outputs": [".output/**", ".nitro/**", ".tanstack/**"]
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"env": {
"WORKFLOW_PUBLIC_MANIFEST": "1"
}
}
+12
View File
@@ -0,0 +1,12 @@
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react';
import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';
import { workflow } from 'workflow/vite';
export default defineConfig({
plugins: [workflow(), tanstackStart(), nitro(), viteReact()],
nitro: {
plugins: ['./plugins/start-pg-world.ts'],
},
});
+1
View File
@@ -0,0 +1 @@
../nitro-v3/workflows