---
title: "Wait on a hook"
description: "Replace the fake cook-time sleep with createHook(). Build a Route Handler that uses resumeHook() to wake the workflow when the kitchen marks an order ready."
canonical_url: "https://vercel.com/academy/workflow-foundations/pause-until-the-kitchen-pings-us"
md_url: "https://vercel.com/academy/workflow-foundations/pause-until-the-kitchen-pings-us.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-10-20T22:57:37.757Z"
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>

# Wait on a hook

# Pause until the kitchen pings us

Pizzas don't cook on a timer. They're ready when the kitchen says they're ready.

The `sleep("30s")` we added in 2.2 is a lie we agreed to until we had something better. A real pizza shop has no idea when a pizza will be ready. It might be three minutes. It might be twenty. The workflow needs to wait until someone in the kitchen actually says, "this one's done."

That's what hooks are for. A hook is a suspension point with a unique token. The workflow awaits the hook and goes to sleep. Some external request, like a button in the kitchen ops UI, calls `resumeHook(token, payload)` and the workflow wakes up with that payload as the value of the await.

Three pieces: the hook in the workflow, the token that ties them together, and the Route Handler that fires the wakeup.

## Outcome

Replace the post-kitchen `sleep("30s")` with a hook the workflow waits on. Build `/api/kitchen/ready` so the existing kitchen ops UI can wake up the workflow when a pizza is ready.

## Fast Track

1. In `processOrder`, replace `await sleep("30s")` after `acknowledgeKitchen` with a `createHook` waiting on `kitchen:{order.id}`.
2. Create `app/api/kitchen/ready/route.ts` that calls `resumeHook(\`kitchen:$\`, )\`.
3. Place an order. Open `/kitchen`. Click **Mark ready**. Watch the workflow continue.

## Hands-on exercise

**1. Add the hook to the workflow.**

`workflows/process-order.ts`:

```ts title="workflows/process-order.ts" {1,12-15}
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 { 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);
  await sleep("30s");
  await confirmDelivery(order);
  await sendReviewRequest(order);

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

Wait. There's something new here.

`using` is a TC39 syntax for automatic resource cleanup. When the workflow scope ends, the hook gets disposed automatically, so you don't have stale tokens floating around. You'll see it again every time we create a hook.

`createHook<T>({ token })` returns a hook object. The generic `T` is the payload type. We're saying the kitchen will send us a `{ readyAt: string }` when it wakes us up.

`{ token: \`kitchen:$\` }`is a custom token. Default tokens are random, but we want a deterministic one so the kitchen ops UI can construct it from data it already has (the order ID). Namespacing it with`kitchen:\` keeps tokens from colliding with the driver hooks we add in 3.2.

`await kitchenHook` suspends the workflow. Compute stops. The workflow waits, possibly for hours, until someone resumes the hook with that token.

**2. Build the resume endpoint.**

Create `app/api/kitchen/ready/route.ts`:

```ts title="app/api/kitchen/ready/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(`kitchen:${orderId}`, {
    readyAt: new Date().toISOString(),
  });

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

`resumeHook` takes the same token the workflow used and the payload to deliver. The runtime looks up the suspended workflow waiting on that token and wakes it up with the payload. The `await` on the workflow side resolves with what we passed here.

The kitchen ops UI in the starter already POSTs to this exact endpoint. We're making the endpoint exist.

## Try It

Place an order. The status page should tick to "In the kitchen" and stay there.

In a new tab, open `/kitchen`. Your order is sitting in the oven with a **Mark ready** button.

Click it.

Two things happen in quick succession. The kitchen ops UI removes the order from the list. The customer's order page advances to "Out for delivery."

In the dashboard, look at the run timeline:

```
processOrder                running
  ├─ sendOrderConfirmation    completed   389ms
  ├─ acknowledgeKitchen       completed   208ms
  ├─ kitchen hook (kitchen:f8c2…)
  │      suspended  3m 22s
  │      resumed with { readyAt: "2026-..." }
  ├─ dispatchDelivery         completed   213ms
  └─ (sleep 30s)
```

The hook line shows how long the workflow was suspended (minutes, hours, doesn't matter) and the payload it woke up with.

\*\*Note: Why use a custom token\*\*

We could have let `createHook` generate a random token, but then the kitchen ops UI would need to know that token to call `resumeHook`. With a deterministic token like `kitchen:{order.id}`, the UI just needs the order ID, which it already has from the order list. No extra round-trip to fetch a token.

## Commit

```
feat(workflow): wait on a kitchen hook instead of fake sleep
```

## Done-When

- [ ] `processOrder` creates a hook with token `kitchen:{order.id}` after `acknowledgeKitchen`
- [ ] The workflow awaits the hook before `dispatchDelivery`
- [ ] `app/api/kitchen/ready/route.ts` calls `resumeHook` with the same token
- [ ] Clicking **Mark ready** in `/kitchen` wakes the workflow and the order moves to "Out for delivery"

## Solution

`workflows/process-order.ts`:

```ts title="workflows/process-order.ts"
import { createHook, sleep } 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 { 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);
  await sleep("30s");
  await confirmDelivery(order);
  await sendReviewRequest(order);

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

`app/api/kitchen/ready/route.ts`:

```ts title="app/api/kitchen/ready/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(`kitchen:${orderId}`, {
    readyAt: new Date().toISOString(),
  });

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

One hook, one Route Handler. The workflow can now wait for arbitrarily long without a fake timer. We do the same trick for the driver next.


---

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