---
title: "Hook the driver"
description: "Replace the remaining sleep with two hooks (pickup, delivered) and build the corresponding Route Handlers so the driver UI drives the workflow."
canonical_url: "https://vercel.com/academy/workflow-foundations/hook-the-driver-into-the-flow"
md_url: "https://vercel.com/academy/workflow-foundations/hook-the-driver-into-the-flow.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-10-20T22:57:37.776Z"
content_type: "lesson"
course: "workflow-foundations"
course_title: "Build Durable Workflows"
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>

# Hook the driver

# Hook the driver into the flow

The kitchen pings us when the pizza's ready. The driver should do the same when they pick it up, and again when they hand it to Marge.

Same pattern, twice. Two hooks, two Route Handlers. The shape of the code is now familiar, and that's the point. Hooks are how a workflow talks to anything outside itself.

We also need a step in between so the order goes from "ready-for-pickup" to "out-for-delivery" when the driver actually grabs it. The driver UI in the starter has been listing both states this whole time. Now they'll actually flow through.

## Outcome

Replace the post-dispatch `sleep` with two hooks: one for pickup, one for delivery. Build the two corresponding Route Handlers. The driver ops UI now drives the workflow through pickup and delivery.

## Fast Track

1. Add a `markOutForDelivery` step that sets status to `out-for-delivery`.
2. Update `processOrder` to swap the post-dispatch sleep for a `driver-pickup:{order.id}` hook, then run `markOutForDelivery`, then await a `driver-delivered:{order.id}` hook.
3. Build `/api/driver/pickup` and `/api/driver/delivered` to resume the matching hooks.

## Hands-on exercise

**1. Add the intermediate step.**

Right now `dispatchDelivery` sets status to `out-for-delivery`. That's a lie. The driver hasn't picked the pizza up yet. Let's change `dispatchDelivery` to mean "ready for pickup" and add a separate step for "out for delivery."

Update `workflows/steps/dispatch-delivery.ts`:

```ts title="workflows/steps/dispatch-delivery.ts" {7}
import { updateStatus } from "@/lib/orders-store";
import type { Order } from "@/lib/pizza";

export async function dispatchDelivery(order: Order): Promise<void> {
  "use step";

  await new Promise((resolve) => setTimeout(resolve, 200));

  updateStatus(order.id, "ready-for-pickup");
}
```

Create `workflows/steps/mark-out-for-delivery.ts`:

```ts title="workflows/steps/mark-out-for-delivery.ts"
import { updateStatus } from "@/lib/orders-store";
import type { Order } from "@/lib/pizza";

export async function markOutForDelivery(order: Order): Promise<void> {
  "use step";

  updateStatus(order.id, "out-for-delivery");
}
```

**2. Wire two hooks into the workflow.**

Update `workflows/process-order.ts`:

```ts title="workflows/process-order.ts" {7,21-31}
import { createHook } from "workflow";
import type { Order } from "@/lib/pizza";
import { sendOrderConfirmation } from "./steps/send-order-confirmation";
import { acknowledgeKitchen } from "./steps/acknowledge-kitchen";
import { dispatchDelivery } from "./steps/dispatch-delivery";
import { markOutForDelivery } from "./steps/mark-out-for-delivery";
import { confirmDelivery } from "./steps/confirm-delivery";
import { sendReviewRequest } from "./steps/send-review-request";

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

  await sendOrderConfirmation(order);
  await acknowledgeKitchen(order);

  using kitchenHook = createHook<{ readyAt: string }>({
    token: `kitchen:${order.id}`,
  });
  await kitchenHook;

  await dispatchDelivery(order);

  using pickupHook = createHook<{ pickedUpAt: string }>({
    token: `driver-pickup:${order.id}`,
  });
  await pickupHook;
  await markOutForDelivery(order);

  using deliveryHook = createHook<{ deliveredAt: string }>({
    token: `driver-delivered:${order.id}`,
  });
  await deliveryHook;

  await confirmDelivery(order);
  await sendReviewRequest(order);

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

Two new hooks. Notice the token namespacing: `driver-pickup:` and `driver-delivered:` so they don't collide with the kitchen's hook or each other. We don't import `sleep` anymore; nothing in the workflow is on a fake timer.

**3. Build the resume endpoints.**

`app/api/driver/pickup/route.ts`:

```ts title="app/api/driver/pickup/route.ts"
import { resumeHook } from "workflow/api";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { orderId } = (await request.json()) as { orderId: string };

  await resumeHook(`driver-pickup:${orderId}`, {
    pickedUpAt: new Date().toISOString(),
  });

  return NextResponse.json({ ok: true });
}
```

`app/api/driver/delivered/route.ts`:

```ts title="app/api/driver/delivered/route.ts"
import { resumeHook } from "workflow/api";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { orderId } = (await request.json()) as { orderId: string };

  await resumeHook(`driver-delivered:${orderId}`, {
    deliveredAt: new Date().toISOString(),
  });

  return NextResponse.json({ ok: true });
}
```

If you're squinting and thinking "these are almost identical," you're right. We could DRY them up. We won't. Two endpoints with three lines each that say what they do is clearer than one clever endpoint with a `state` param.

## Try It

Place an order. Walk the order through all four humans now:

1. `/kitchen` → click **Mark ready**. Status moves to "Ready for pickup."
2. `/driver` → first list shows the order. Click **Pick up**. Status moves to "Out for delivery."
3. `/driver` → second list shows the order. Click **Mark delivered**. Status moves to "Delivered."
4. Inbox → the review email arrives a moment later.

The customer's `/orders/[runId]` page polls and updates as you click. So does the kitchen and driver ops UIs.

In the dashboard timeline:

```
processOrder                completed
  ├─ sendOrderConfirmation    completed
  ├─ acknowledgeKitchen       completed
  ├─ kitchen hook             resumed
  ├─ dispatchDelivery         completed
  ├─ driver-pickup hook       resumed
  ├─ markOutForDelivery       completed
  ├─ driver-delivered hook    resumed
  ├─ confirmDelivery          completed
  └─ sendReviewRequest        completed
```

Every step. Every hook. Every resume. Every payload. All of it persisted by Vercel.

\*\*Note: A workflow is glue\*\*

We've now written six steps and three hooks. The workflow itself is almost entirely `await` statements. That's the right amount of code for a workflow to have. The actual work lives in steps. The actual events live outside. The workflow's job is to sequence them.

## Commit

```
feat(workflow): drive pickup and delivery with hooks
```

## Done-When

- [ ] `dispatchDelivery` now sets status to `ready-for-pickup`
- [ ] A new `markOutForDelivery` step sets status to `out-for-delivery`
- [ ] `processOrder` awaits the pickup hook, runs `markOutForDelivery`, then awaits the delivery hook
- [ ] `/api/driver/pickup` and `/api/driver/delivered` exist and use `resumeHook`
- [ ] The driver ops UI walks an order from ready-for-pickup to delivered

## Solution

The new step files appear in the hands-on section above. The complete workflow:

```ts title="workflows/process-order.ts"
import { createHook } from "workflow";
import type { Order } from "@/lib/pizza";
import { sendOrderConfirmation } from "./steps/send-order-confirmation";
import { acknowledgeKitchen } from "./steps/acknowledge-kitchen";
import { dispatchDelivery } from "./steps/dispatch-delivery";
import { markOutForDelivery } from "./steps/mark-out-for-delivery";
import { confirmDelivery } from "./steps/confirm-delivery";
import { sendReviewRequest } from "./steps/send-review-request";

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

  await sendOrderConfirmation(order);
  await acknowledgeKitchen(order);

  using kitchenHook = createHook<{ readyAt: string }>({
    token: `kitchen:${order.id}`,
  });
  await kitchenHook;

  await dispatchDelivery(order);

  using pickupHook = createHook<{ pickedUpAt: string }>({
    token: `driver-pickup:${order.id}`,
  });
  await pickupHook;
  await markOutForDelivery(order);

  using deliveryHook = createHook<{ deliveredAt: string }>({
    token: `driver-delivered:${order.id}`,
  });
  await deliveryHook;

  await confirmDelivery(order);
  await sendReviewRequest(order);

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

Everything in this workflow is now event-driven. There's just one problem: if the kitchen ghosts us, the workflow waits forever. That's 3.3.


---

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