Approvals
Human-in-the-loop as an API primitive — the agent proposes, a person taps Approve / Edit / Reject on their phone, the agent acts only after action.approved.
An agent that can do consequential things needs a person in the loop, and "reply yes in chat" is not a safe primitive: anyone who can type in the channel can say yes, and a prompt-injected agent can fake the question. nmbr's approvals are bound to one person and one conversation, render as a native card, and fail closed.
The flow
- Propose.
POST /agent/v1/actionswithto(the person's nmbr), a developer-definedkind, a one-linetitle, optionaldescription, and anypayloadyour agent needs back. - Card. The person sees an approval card in the 1:1 chat with your agent — push, badge, preview, like any message. They can Approve, edit the payload and approve, or Reject.
- Decision event.
action.approved(withpayload.action.editedPayloadset if they edited — use it instead ofpayload),action.rejected, oraction.expired, over long-poll or your webhook. - Act — on your side. For a plain proposal nmbr never executes anything: only after
action.approveddoes your agent do the thing. (For a platform skill, nmbr does it for you — see below.)
Binding rules
- Only the person named on the proposal can decide, and only from the conversation it was proposed in. A reply from anyone else, from a group, or from another chat is refused — the API answers as if the action didn't exist.
- Decisions are final: a resolved action cannot be re-opened.
- Expiry is fail-closed. Default 24 h (60 s – 7 days via
expiresAt). Nobody decided in time ⇒action.expired. Treat it exactly like a rejection. Never act on a proposal you have not seen approved. - At most 10 pending proposals per conversation (
too_many_pending). A looping agent cannot flood someone's phone.
In five lines (bash)
curl -s -X POST https://nmbr.ai/api/agent/v1/actions -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to":"123-456-789","kind":"deploy","title":"Deploy v2 to prod?","payload":{"ref":"abc123"}}'
curl -s "https://nmbr.ai/api/agent/v1/updates?afterSeq=$SEQ&wait=25" -H "Authorization: Bearer $TOKEN"
# → { "events": [ { "type": "action.approved", "payload": { "action": { "id": "…", "editedPayload": null, … } } } ] }
Loop the second call until an action.* event names your action id. GET /agent/v1/actions/{id} returns the current state at any time (useful after a missed webhook).
With the SDK
const decision = await agent.proposeAndWait({ to: "123-456-789", kind: "send_email", title: "Send the Q3 summary to Dana?", payload: { threadId: "t1" } });
switch (decision.state) {
case "approved": await sendEmail(decision.editedPayload ?? decision.payload); break;
default: /* rejected or expired: do nothing */
}
If you already run agent.updates() elsewhere, use isActionEvent(event) in that loop instead of proposeAndWait (one consumer per event stream).
Let nmbr do it: platform skills
Some things your agent wants done live inside the person's nmbr — a task on their list, a reminder, a calendar event, a note. For those, add a skill to the proposal and nmbr executes it on their account the moment they approve, exactly where their own assistant would put it:
skill |
Needs their grant of | payload |
|---|---|---|
create_task |
tasks:write |
{ title, description?, dueDate?, priority? } — priority low | normal | high |
create_reminder |
reminders:write |
{ title, reminderTime } |
create_event |
events:write |
{ title, description?, startTime, endTime?, location? } |
create_note |
notes:write |
{ title?, content } |
update_task |
tasks:write |
{ id, title?, description?, dueDate?, priority? } — at least one field besides id; null clears an optional one |
complete_task · delete_task |
tasks:write |
{ id } |
update_reminder |
reminders:write |
{ id, title?, reminderTime? } |
complete_reminder · delete_reminder |
reminders:write |
{ id } |
update_event |
events:write |
{ id, title?, description?, startTime?, endTime?, location? } |
delete_event |
events:write |
{ id } |
update_note |
notes:write |
{ id, title?, content? } |
delete_note |
notes:write |
{ id } |
id is a record from the person's GET /conversations/:id/{tasks,reminders,events,notes} list (needs their <surface>:read) or an earlier execution.recordId; nmbr re-checks at execution that it is theirs, and a stranger's id fails with execution.status: "failed". Dates are ISO-8601 with an offset. The payload is validated when you propose (400 invalid_payload names the field) and again when the person approves an edited version, so what the card shows is what runs. Unknown keys are refused. kind is optional for a skill proposal and defaults to the skill id.
curl -s -X POST https://nmbr.ai/api/agent/v1/actions -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to":"123-456-789","skill":"create_reminder","title":"Remind you to call Dana at 3?","payload":{"title":"Call Dana","reminderTime":"2026-09-03T15:00:00-07:00"}}'
The decision comes back as usual; on action.approved, payload.action.execution tells you what happened:
{ "status": "succeeded", "recordType": "ai_reminder", "recordId": "…", "executedAt": "…", "auto": false }
status: "failed" carries a one-line error — the approval stands, but nothing was created; propose again if it still makes sense. Your agent has nothing to execute either way.
Scopes. A skill proposal needs the person's grant of the matching write scope on top of messages:write (the card is a message). Ask for those scopes in your agent's requestedScopes and they appear on the consent card when someone adds it; without the grant the proposal is 403 scope_not_granted. See Scopes and consent.
Without asking. On your agent's profile, a person can flip any granted write scope from asks first to runs without asking for its create skill. A proposal for such a skill is approved and executed on the spot: the POST /actions response already has state: "approved" and execution.auto: true, and no action.* event follows (the SDK's proposeAndWait returns immediately). The card is still posted in the chat, already resolved, so nothing ever happens invisibly. It is their setting: your agent cannot ask for it, and removing the contact clears it with the grant.
Always a card. Only the four create_* skills can be set to run without asking; an update, completion or deletion always shows the card, whatever the person's settings. Sending messages or email as the person is not a skill and never will be — an agent that wants to say something says it as itself with POST /messages or POST /emails.
Writing good proposals
titleis what they approve — make it the whole decision in one line ("Deploy v2 to prod?" not "Deploy?").descriptionis where the consequences go. Keep secrets out of both; they render on a phone.kindis yours (^[a-z][a-z0-9_.-]{0,63}$), shown as a small label and echoed in events — a stable vocabulary makes your logs readable.- Put in
payloadexactly what you need to execute, so an edited approval is executable as returned.