---
title: "Send a confirmation"
description: "Write the sendOrderConfirmation step with the \"use step\" directive, learn what the directive promises, and wire it into the orders Route Handler."
canonical_url: "https://vercel.com/academy/workflow-foundations/send-a-confirmation-email"
md_url: "https://vercel.com/academy/workflow-foundations/send-a-confirmation-email.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-06T05:06:55.184Z"
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>

# Send a confirmation

# Send a confirmation email

Here's the trick of the Workflow SDK: directives don't change your code. They change what the runtime does with your code.

Your first step today is `sendOrderConfirmation`. It calls Resend. It sends an email. We could write it as a plain async function and it would work fine, until Resend hiccups. Then the request fails, the function returns, and Marge Pepperoni never gets her confirmation. The usual fix is the usual ceremony: `try`, `catch`, `setTimeout`, retry counter, hope.

Or we can write three letters.

Add `"use step"` to the top of the function. The function body is identical. Same Resend call, same params. But now the runtime has a contract with that function: this is a unit of work, track it, retry it on failure, log the inputs and outputs.

The directive doesn't change what your function does. It changes what happens around it.

## Outcome

Create `workflows/steps/send-order-confirmation.ts` as your first step, then wire it into `/api/orders` so placing an order sends a real Resend email.

## Fast Track

1. Create `workflows/steps/send-order-confirmation.ts`. Export an async function `sendOrderConfirmation(order: Order)` that calls Resend and throws on failure. Put `"use step"` as the first statement in the body.
2. In `app/api/orders/route.ts`, import the step and `await sendOrderConfirmation(order)` before the redirect.
3. Place an order. Check your inbox.

## Hands-on exercise

Steps live in `workflows/steps/` by convention. One step per file keeps things tidy when you get into the dashboard later.

**1. Write the step.**

Create `workflows/steps/send-order-confirmation.ts`:

```ts title="workflows/steps/send-order-confirmation.ts"
import { FatalError } from "workflow";
import { resend, FROM } from "@/lib/resend";
import { updateStatus } from "@/lib/orders-store";
import type { Order } from "@/lib/pizza";

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

  const resp = await resend.emails.send({
    from: FROM,
    to: [order.email],
    subject: `Order confirmed: ${order.size} ${order.pizza}`,
    html: `
      <p>Hi ${order.customerName},</p>
      <p>We got your order for a ${order.size} ${order.pizza} on ${order.crust} crust.</p>
      <p>We'll let you know when it's out for delivery to ${order.address}.</p>
      <p>Sal</p>
    `,
  });

  if (resp.error) {
    throw new FatalError(`Resend failed: ${resp.error.message}`);
  }

  updateStatus(order.id, "confirmed");
}
```

A few things worth noting.

`"use step"` is the first statement in the function body, like `"use strict"`. Anywhere else it does nothing.

We use `FatalError` from `workflow` when Resend reports a real failure (like a malformed email address). That class tells the runtime: don't retry, this won't work. We'll come back to this in 4.2. For now: if the call returns an error, we want it to be final.

Everything else is a regular Resend call. No new APIs. No special handling. The directive is the only thing that makes this a step.

**2. Wire it into the route.**

The starter's `/api/orders` stores the order and returns a fake `runId`. Add a call to our new step right before the response:

```ts title="app/api/orders/route.ts" {3,29}
import { NextResponse } from "next/server";
import { recordOrder } from "@/lib/orders-store";
import { sendOrderConfirmation } from "@/workflows/steps/send-order-confirmation";
import type { Order, PizzaName, Size, Crust } from "@/lib/pizza";

// ... type IncomingOrder unchanged ...

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(),
  };

  // TODO (Lesson 1.3): Replace this stub with start(processOrder, [order]).
  const fakeRunId = crypto.randomUUID();
  recordOrder(order, fakeRunId);

  await sendOrderConfirmation(order);

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

\*\*Note: The directive isn't active yet\*\*

Calling a `"use step"` function directly, like we just did, runs it as a regular function. No retries. No event log. The directive becomes meaningful in the next lesson when we call this from inside a workflow. Right now we're laying the wiring.

## Try It

Open `http://localhost:3000`. Put your real email in the form. Click **Place order**.

Two things should happen:

1. The browser redirects to `/orders/<some-uuid>` and shows the order details.
2. A confirmation email lands in your inbox from `onboarding@resend.dev`.

If you don't see the email, check the dev server output. Resend logs go through the standard console, and a missing or invalid `RESEND_API_KEY` shows up as a 401 from their API.

Try ordering a few times. Different pizzas. Different sizes. The email subject should reflect what you ordered. Marge would want her Carbonara confirmation to say "Carbonara," not "Margherita."

## Commit

```
feat(workflow): add sendOrderConfirmation step
```

## Done-When

- [ ] `workflows/steps/send-order-confirmation.ts` exists with `"use step"` at the top of the function body
- [ ] The step throws a `FatalError` when Resend returns an error
- [ ] `app/api/orders/route.ts` imports and awaits `sendOrderConfirmation(order)`
- [ ] Placing an order from the UI delivers an actual email to the address you entered

## Solution

`workflows/steps/send-order-confirmation.ts`:

```ts title="workflows/steps/send-order-confirmation.ts"
import { FatalError } from "workflow";
import { resend, FROM } from "@/lib/resend";
import { updateStatus } from "@/lib/orders-store";
import type { Order } from "@/lib/pizza";

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

  const resp = await resend.emails.send({
    from: FROM,
    to: [order.email],
    subject: `Order confirmed: ${order.size} ${order.pizza}`,
    html: `
      <p>Hi ${order.customerName},</p>
      <p>We got your order for a ${order.size} ${order.pizza} on ${order.crust} crust.</p>
      <p>We'll let you know when it's out for delivery to ${order.address}.</p>
      <p>Sal</p>
    `,
  });

  if (resp.error) {
    throw new FatalError(`Resend failed: ${resp.error.message}`);
  }

  updateStatus(order.id, "confirmed");
}
```

The function works. The email sends. The directive is sitting there, dormant, waiting for a workflow to bring it to life. That's next.


---

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