---
title: "Pause for a Sign-off"
description: "Add an approval gate to book_repair so expensive bookings park for a human yes, then resume from the exact step. The durable pause/resume you saw streaming in 1.3."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/pause-for-a-signoff"
md_url: "https://vercel.com/academy/building-agents-with-eve/pause-for-a-signoff.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-30T13:29:19.120Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
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>

# Pause for a Sign-off

# Pause for a Sign-off

In the last lesson, the dispatcher booked a $180 overhaul without so much as a raised eyebrow. The fix is not to make `book_repair` smarter, it's to make it *stop* when the stakes are high enough, and ask a person first. Cheap jobs still sail through. Big ones wait for a yes.

In a lot of frameworks, this is where you'd go build a whole approval system: a queue, a state machine, a way to remember where you were when the human finally clicks "approve" an hour later. In eve, human-in-the-loop isn't a system you build. It's one field on the tool. The pausing, the waiting, the resuming-exactly-where-it-stopped, that's the same durable session machinery you watched stream as `session.waiting` back in 1.3. You just tell the tool when to use it.

## Outcome

`book_repair` runs cheap bookings straight through but parks expensive ones on a human approval, then resumes the booking from the exact step once you say yes.

## Hands-on exercise

Open the `book_repair` you wrote in 3.1. You're adding exactly one field, `approval`, and a threshold constant. Everything else stays.

`approval` decides, before `execute` runs, whether this particular call needs a human first. It can be a blanket helper from `eve/tools/approval`, `always()`, `once()`, `never()`, or, when the decision depends on the *input*, your own predicate. Booking is the second case: a $20 flat repair shouldn't interrupt anyone, but a $180 overhaul should. So we gate on the quote:

```ts
approval: ({ toolInput }) =>
  quoteCents(toolInput?.serviceIds ?? []) > APPROVAL_THRESHOLD_CENTS,
```

The predicate sees the same input `execute` will, so we total the quote the same way `execute` does and compare it to the threshold. Under the line, no approval, the booking just happens. Over it, eve pauses the turn and surfaces an approval request before `execute` ever runs.

\*\*Note: Helper or predicate?\*\*

Reach for `always()`/`once()`/`never()` when the answer is the same every time (a `delete_everything` tool is `always()`). Reach for a predicate when it depends on the arguments, like our cost gate. Same `approval` field, two shapes.

One field, and you're done. You don't write the pause, the prompt, or the resume. eve turns a `true` from `approval` into a parked turn and a structured approval request, and the channel renders it, buttons in the TUI, Block Kit in Slack, a control in your web app.

## Try It

Restart the dev server. That resets the session, so any bike you saved back in 2.2 is gone (`defineState` lives and dies with the session). No problem, just name the bike in the booking itself and the dispatcher works from that. Book under the line first, it behaves exactly like before:

```text
you › I've got a rear flat on my Surly Disc Trucker, 700c. Book a flat repair for Tuesday morning.
  ⚙ book_repair  { serviceIds: ["flat-fix"], slotId: "tue-10", bikeLabel: "Surly Disc Trucker" }   ($20, under $150)
dispatcher › Booked! Flat Repair on your Surly Disc Trucker, Tuesday at 10:00am. $20.00.
```

\*\*Note: A spoken “shall I confirm?” is not the approval gate\*\*

Your dispatcher may still ask "shall I go ahead and book?" before the cheap job. That's the persona being courteous, not eve stopping the turn, and the two are different things. The tell is what you *don't* see: no `⏸ approval required … [approve] [deny]` control. Under the line, `approval` returns `false`, so eve never pauses and `execute` runs the instant the model calls the tool, whatever the model says around it. The hard gate, the structured pause that blocks `execute` until a human approves, is the thing you're about to trigger with the overhaul.

Now push it over the line and watch it stop. Pick a different slot, the Tuesday-morning one is taken now:

```text
you › actually, do the full overhaul on the Surly too, Tuesday afternoon.

  ⚙ book_repair  { serviceIds: ["tune-up-full"], slotId: "tue-14", bikeLabel: "Surly Disc Trucker" }
  ⏸ approval required: Full Overhaul, $180.00. Approve this booking? [approve] [deny]
```

The turn is parked. Nothing was booked yet, `approval` runs *before* `execute`, so the slot is untouched while it waits. Approve it:

```text
you › approve
  ↳ { booked: true, when: "Tue 2:00pm", services: ["Full Overhaul"], total: "$180.00" }
dispatcher › Approved and booked, Full Overhaul on the Surly, Tuesday at 2:00pm. $180.
```

The booking resumed from the exact step it paused on, ran `execute`, and committed. If you'd denied it, `execute` never runs and the model is told the booking was declined.

\*\*Note: This is the durable pause from 1.3, doing real work\*\*

Remember `session.waiting` in the event stream? This is it earning its keep. The parked turn isn't holding a process open, it's durably suspended. The approval can come a minute later or, on a deployed agent, an hour later from a manager's phone. When it arrives, the turn picks up precisely where it stopped.

Cheap bookings prompting for approval, or expensive ones sailing through? Your predicate is probably totaling the wrong thing, confirm it calls `quoteCents(toolInput.serviceIds)` and compares against the same threshold in cents. And make sure `approval` sits on the tool definition object, beside `inputSchema`, not inside `execute`.

## Done-When

- [ ] `book_repair` has an `approval` predicate keyed on the booking's total cost.
- [ ] A booking under the threshold runs with no approval gate (no `⏸ approve/deny` pause, even if the dispatcher confirms conversationally).
- [ ] A booking over the threshold parks on an approval request and books nothing until answered.
- [ ] Approving resumes and commits; denying skips `execute`.

## Solution

```ts title="agent/tools/book_repair.ts" {5,19-20}
import { defineTool } from "eve/tools";
import { z } from "zod";
import { getService, quoteCents, bookSlot, formatUsd } from "../lib/shop.js";

const APPROVAL_THRESHOLD_CENTS = 15000; // anything over $150 needs a human yes

export default defineTool({
  description:
    "Book one or more services into an open slot for a customer's bike. " +
    "Returns the confirmation and the total quote.",
  inputSchema: z.object({
    serviceIds: z.array(z.string()).min(1).describe("Service ids from lookup_service."),
    slotId: z.string().describe("An open slot id from check_availability."),
    bikeLabel: z.string().optional().describe("Which of the customer's bikes this is for."),
  }),
  // Cost-based gate: cheap jobs run straight through, big-ticket bookings park
  // on an approval request. approval runs before execute and sees the tool
  // input, so we re-derive the quote here the same way execute will.
  approval: ({ toolInput }) =>
    quoteCents(toolInput?.serviceIds ?? []) > APPROVAL_THRESHOLD_CENTS,
  async execute({ serviceIds, slotId, bikeLabel }) {
    const names = serviceIds.map((id) => getService(id)?.name ?? id);
    const total = quoteCents(serviceIds);
    const summary = `${names.join(" + ")}${bikeLabel ? ` (${bikeLabel})` : ""}`;
    const slot = bookSlot(slotId, summary);
    return { booked: true, when: slot.label, services: names, total: formatUsd(total) };
  },
});
```

Three lines of change (the threshold and the `approval` predicate, highlighted) turned a reckless tool into a responsible one. That's the payoff of eve treating approval as a property of the tool rather than a system you assemble: the danger and its guardrail live in the same file, and the durable runtime handles the hard part, holding a paused conversation open until a human answers. The dispatcher is now genuinely safe to put in front of people, which is exactly what Section 4 does.


---

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