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
- Add a
markOutForDeliverystep that sets status toout-for-delivery. - Update
processOrderto swap the post-dispatch sleep for adriver-pickup:{order.id}hook, then runmarkOutForDelivery, then await adriver-delivered:{order.id}hook. - Build
/api/driver/pickupand/api/driver/deliveredto 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:
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:
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:
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:
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:
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:
/kitchen→ click Mark ready. Status moves to "Ready for pickup."/driver→ first list shows the order. Click Pick up. Status moves to "Out for delivery."/driver→ second list shows the order. Click Mark delivered. Status moves to "Delivered."- 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.
Commit
feat(workflow): drive pickup and delivery with hooks
Done-When
dispatchDeliverynow sets status toready-for-pickup- A new
markOutForDeliverystep sets status toout-for-delivery processOrderawaits the pickup hook, runsmarkOutForDelivery, then awaits the delivery hook/api/driver/pickupand/api/driver/deliveredexist and useresumeHook- 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:
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.
Was this helpful?