# Human-in-the-loop for AI agents — approve, confirm and ask are language primitives

> A gate that waits for a real person — in the terminal, or queued behind one-time links when the program runs as a server — and denies when nobody is there. An agent cannot fake an approval.

Published 2026-09-02 · https://synsema.org/blog/human-in-the-loop-approve-confirm-ask


Every agent framework has a human-in-the-loop story, usually a callback you wire yourself and hope
every path goes through. In Synsema the gate is a statement of the language, so the runtime — not
your discipline — decides where the answer can come from.

## Four words

```synsema
approve "Deploy to production?"                         -- yes/no gate
confirm "Send this email to 500 customers?"             -- confirmation
let env be ask "Which environment?" with ["staging", "prod"]
show plan as "What the agent is about to do"            -- preview for the human

let ok be approve "Refund " + text(amount) + " to " + customer + "?" within 1h
when not ok
    give nothing
```

`within` bounds the wait (`s`, `m`, `h`, `d`). On expiry the gate returns `false` and says so once.
It never hangs forever.

## Where the answer comes from

- **A terminal.** The prompt appears and the program waits for `y`/`n`.
- **No terminal** — a pipe, CI, an agent driving the program: the gate **denies instantly**, with a
  notice that tells an AI agent a human must approve. There is no environment variable that
  auto-approves.
- **Under `serve`.** The gate is queued; the request blocks until a person answers or the deadline
  denies. The console prints a one-time token and the ready-to-use command; `GET /approvals` lists
  what is pending, `POST /approvals/{id}` answers. With `SYNSEMA_HUMAN_WEBHOOK` every gate also
  posts to your channel with signed yes/no links you can tap from a phone.

## Why a primitive and not a callback

Because the property you want is negative: *no path exists* where the model's own output counts as
consent. With a callback, that is true until someone forgets it. With a statement the interpreter
implements, it is true by construction — the same way a `fetch` to an undeclared host is refused
before any byte leaves.

This is how Lampson approves a scheduled task's dangerous step from your phone, and how the
support-ticket agent on the [home page](/) refunds nothing without a person.

**See it run:** the [human interaction page](https://synsema.dev/en/0.6.x/62-human) in the
docs has every option, and [try.synsema.org](https://try.synsema.org) lets you write the gate and
watch it deny you in the browser.

