---
title: "Wrap in a workflow"
description: "Write the processOrder workflow with \"use workflow\", trigger it from the orders Route Handler using start(), and tour the local Workflow dashboard."
canonical_url: "https://vercel.com/academy/workflow-foundations/wrap-it-in-a-workflow"
md_url: "https://vercel.com/academy/workflow-foundations/wrap-it-in-a-workflow.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-06T20:50:40.222Z"
content_type: "lesson"
course: "workflow-foundations"
course_title: "Workflow Foundations"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Wrap in a workflow

# Wrap it in a workflow

Notice what your orders route currently does: it awaits the Resend call. The customer's browser sits there spinning while the SMTP handshake completes. If Resend is slow, the order placement is slow. If Resend is down, the order placement fails.

That's the part workflows fix.

When we wrap the step in a workflow and start it with `start()`, the route returns the moment the workflow is queued. The step runs in the background. The customer gets their redirect immediately. The email arrives whenever Resend is ready to send it. And if Resend has a bad day, the runtime retries without anyone knowing.

`"use workflow"` is the second directive. It marks a function as the orchestrator, the one allowed to call steps. Steps inside a workflow get the full treatment: automatic retries, an event log, observability. Steps outside a workflow are decorative.

Let's wake ours up.

## Outcome

Create `workflows/process-order.ts` with the `processOrder` workflow that calls `sendOrderConfirmation`. Replace the direct step call in `/api/orders` with `start(processOrder, [order])`. Watch the first real workflow run in the local dashboard.

## Fast Track

1. Create `workflows/process-order.ts`. Export an async `processOrder(order)` with `"use workflow"` as the first statement. Call `sendOrderConfirmation(order)` inside it.
2. In `/api/orders`, replace the direct step call and the fake `runId` with `const run = await start(processOrder, [order])`. Use `run.runId`.
3. In another terminal, run `pnpm exec workflow web`. Place an order. Watch the run land.

## Hands-on exercise

**1. Write the workflow.**

Create `workflows/process-order.ts`:

```ts title="workflows/process-order.ts"
import type { Order } from "@/lib/pizza";
import { sendOrderConfirmation } from "./steps/send-order-confirmation";

export async function processOrder(order: Order): Promise<{ orderId: string }> {
  "use workflow";

  await sendOrderConfirmation(order);

  return { orderId: order.id };
}
```

Three things to call out:

`"use workflow"` is the first statement in the body, mirroring `"use step"`. The runtime looks for it at build time and treats the function as a workflow definition.

The workflow imports steps and `await`s them. From the inside it looks like a regular async function calling other async functions. The runtime is doing the heavy lifting: each step runs in its own isolated environment, the result gets persisted, and if the step fails it gets retried automatically.

The return value gets persisted too. When the workflow finishes, anyone holding the `runId` can fetch the result later.

**2. Replace the route.**

Open `app/api/orders/route.ts`. The starter version generates a fake `runId` and calls the step directly. We're going to do neither.

```ts title="app/api/orders/route.ts" {1,4,26}
import { start } from "workflow/api";
import { NextResponse } from "next/server";
import { recordOrder } from "@/lib/orders-store";
import { processOrder } from "@/workflows/process-order";
import type { Order, PizzaName, Size, Crust } from "@/lib/pizza";

type IncomingOrder = {
  customerName: string;
  email: string;
  pizza: PizzaName;
  size: Size;
  crust: Crust;
  address: string;
  cardLast4: string;
};

export async function POST(request: Request) {
  const body = (await request.json()) as IncomingOrder;

  const order: Order = {
    id: crypto.randomUUID(),
    ...body,
    placedAt: new Date().toISOString(),
  };

  const run = await start(processOrder, [order]);
  recordOrder(order, run.runId);

  return NextResponse.json({ runId: run.runId });
}
```

`start(processOrder, [order])` enqueues the workflow with `order` as its first argument. It returns a `Run` object. `run.runId` is available synchronously, with no extra `await` on that property. The workflow itself runs in the background; we don't wait for it.

We also delete the direct `sendOrderConfirmation` import. The step is now called from inside the workflow, which is where it belongs.

**3. Open the dashboard.**

In a second terminal:

```bash
pnpm exec workflow web
```

That boots the local Workflow dashboard at `http://localhost:3700`. Leave it open.

## Try It

Place an order from `http://localhost:3000`. Two things happen:

The browser response is faster than before. The route no longer waits for Resend, so the redirect happens as soon as the workflow is queued.

In the dashboard at `http://localhost:3700`, a new run appears. Click into it. You'll see a timeline:

```
processOrder              completed
  └─ sendOrderConfirmation  completed
       input:  { id: "0a4f…", pizza: "Carbonara", ... }
       output: undefined
       attempts: 1
```

The email still arrives. Same Resend call as before. But now you've got a record of it. Every step input, every step output, every attempt is logged.

\*\*Note: Try a bad email\*\*

Put `nope@invalid.fake` in the email field and place an order. Watch the run go red in the dashboard. Click in: the step shows the `FatalError` we threw, with the original Resend error message. That's observability you didn't have to write.

## Commit

```
feat(workflow): wrap order processing in a workflow
```

## Done-When

- [ ] `workflows/process-order.ts` exists with `"use workflow"` and calls `sendOrderConfirmation`
- [ ] `/api/orders` calls `start(processOrder, [order])` and uses `run.runId`
- [ ] Placing an order from the UI sends an email AND creates a run in the local dashboard
- [ ] A failing email (bad address) shows up as a red run with the FatalError visible

## Solution

`workflows/process-order.ts`:

```ts title="workflows/process-order.ts"
import type { Order } from "@/lib/pizza";
import { sendOrderConfirmation } from "./steps/send-order-confirmation";

export async function processOrder(order: Order): Promise<{ orderId: string }> {
  "use workflow";

  await sendOrderConfirmation(order);

  return { orderId: order.id };
}
```

`app/api/orders/route.ts`:

```ts title="app/api/orders/route.ts"
import { start } from "workflow/api";
import { NextResponse } from "next/server";
import { recordOrder } from "@/lib/orders-store";
import { processOrder } from "@/workflows/process-order";
import type { Order, PizzaName, Size, Crust } from "@/lib/pizza";

type IncomingOrder = {
  customerName: string;
  email: string;
  pizza: PizzaName;
  size: Size;
  crust: Crust;
  address: string;
  cardLast4: string;
};

export async function POST(request: Request) {
  const body = (await request.json()) as IncomingOrder;

  const order: Order = {
    id: crypto.randomUUID(),
    customerName: body.customerName,
    email: body.email,
    pizza: body.pizza,
    size: body.size,
    crust: body.crust,
    address: body.address,
    cardLast4: body.cardLast4,
    placedAt: new Date().toISOString(),
  };

  const run = await start(processOrder, [order]);
  recordOrder(order, run.runId);

  return NextResponse.json({ runId: run.runId });
}
```

One step. One workflow. One `start()` call. That's the entire setup. Everything else this course teaches is variations on this skeleton.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
