Approval-required actions
An agent does not have to guess at a consequential action or missing context. It stops at a durable interaction, your application brings a person in, and the same run continues after the answer. This guide builds that application-side flow against the Platform API: permission decisions and questions, disconnects, competing reviewers, expiry, and a runtime that suspends while it waits.
Availability: Durable approvals and questions are on in hosted Tanomis and operator-enabled when self-hosted. The examples below are the supported wire contract. See What is available where.
Declare the moments
Section titled “Declare the moments”The deployed version opts into the interactions it can emit:
policy: approval_required_tools: - Bash enable_ask_user: trueapproval_required_tools makes each listed harness tool wait for a permission decision;
enable_ask_user lets the harness ask a question and wait for a string answer. These are runtime
interactions, separate from approval_thresholds in the held scoped-token contract.
Use an explicit session and run so your application holds stable identifiers before an interaction appears; see Sessions and runs. The API enforces tenant access but does not assign an interaction to one approver: enforce your own reviewer roles before exposing Approve or Deny.
Two commits to watch
Section titled “Two commits to watch”run_id- 01Agent runrunningThe action needs a person
- 02Durable rowpendingStore the stable interaction_key
- 03Your applicationreviewShow context; submit once
- 04202 AcceptedansweredDurable, not delivered yet
- 05Answer handoffdeliveredBounded restore attempts if suspended
- 06Same agent runterminalRead succeeded, failed, or cancelled
- expiredexpires_at passed before a human answer
- cancelledthe run or session can no longer consume the answer
The run remains running while it waits. answered means the control plane durably accepted one
answer; delivered means it handed that answer to the agent service or its durable replay journal.
Only the run’s later terminal status tells you how the work ended.
Content-identical waits can share this review unit; the diagram shows the ordinary single-run path.
- Person → control plane. The first valid answer moves the row from
pendingtoansweredand returns202 Accepted. - Control plane → agent. Tanomis restores a suspended session if needed, delivers the answer
idempotently by
interaction_key, and moves the row todeliveredafter a successful handoff.
A 202 Accepted response moves the durable row to answered; it does not collapse these two
commits or prove that the agent received the answer. Neither commit approves the final action: a
delivered approve lets the agent continue toward a tool call, and the run’s terminal status says
whether the work succeeded.
1. Discover the pending interaction
Section titled “1. Discover the pending interaction”Keep the run’s event stream open, and independently list durable pending rows. The row is the recovery source after a disconnect or reload:
curl --silent --show-error --fail-with-body \ "$TANOMIS_API/v1/sessions/$SESSION_ID/interactions?status=pending" \ --header "Authorization: Bearer $TANOMIS_TOKEN" \ --header "X-Organization-ID: $TANOMIS_ORG" \ --header "X-Workspace-Tenant-ID: $TANOMIS_WORKSPACE"{ "interaction_key": "ihk_4f72", "run_id": "run_a12bc3", "kind": "permission", "status": "pending", "payload": { "tool_name": "Bash", "tool_input": { "command": "kubectl rollout restart deployment/api -n production" } }, "created_at": "2026-09-04T08:31:12Z", "expires_at": "2026-09-04T09:31:12Z"}{ "question": "Which region should receive the canary?", "header": "Choose a region", "options": [ { "label": "eu", "description": "EU production" }, { "label": "us", "description": "US production" } ], "multi_select": false}interaction_key is an opaque, session-scoped review address. Store it with the session and run
IDs exactly as returned and URL-encode it as one path segment; never derive it, use an event’s
request ID, or reuse it in another session. The key identifies interaction content, so
content-identical asks in one session can share a row and one answer settles every matching ask
parked at that moment. A row therefore proves one review unit, not one tool execution. If policy needs a separate
decision for every repeated identical action, give each its own session and run.
2. Validate before showing a decision
Section titled “2. Validate before showing a decision”| Check | Safe behaviour |
|---|---|
| Session and tenant | The session URL and selected tenant are the ones you intended. |
run_id |
Matches the run whose context you are showing. |
interaction_key |
The exact opaque key from the row. |
kind |
permission or question; reject anything else. |
status |
Still pending; a terminal row is not actionable. |
expires_at |
Still in the future; use the row’s value, not an assumed TTL. |
payload |
Permission: show the tool and input. Question: show the prompt and options. |
An oversized payload is abridged to tool_name plus "payload_omitted": true. Do not turn missing
context into a blanket approval; recover context through your application or deny. Escape payload
text when rendering it: it is agent-produced review material, not trusted HTML.
3. Submit exactly one kind-specific answer
Section titled “3. Submit exactly one kind-specific answer”POST /v1/sessions/{session_id}/interactions/{interaction_key}/answer{ "kind": "permission", "answer": { "decision": "approve", "reason": "Reviewed change request CR-1842." }}{ "kind": "question", "answer": "eu"}To refuse a permission, send "decision": "deny" with a useful reason. For a question, answer is the string
itself—not an object containing a second answer field. A multi-select answer is the chosen labels
as one comma-separated string. Send
kind so a mismatch fails before the answer is stored, and keep the encoded answer at or below
65,536 bytes. The API stores answer as opaque JSON and validates the row’s kind, not the inner
shape, so validate that shape in your backend. The first pending → answered transition wins;
disable the controls after submitting, but expect another tab or reviewer to win the race.
4. Reconcile acceptance and ambiguity
Section titled “4. Reconcile acceptance and ambiguity”On 202 Accepted, show Answer accepted, not Action approved. Then list the session’s
interactions and follow the same key until it settles. Use the same read-after-write when the POST
times out; never send a different answer because the client missed the response.
curl --silent --show-error --fail-with-body \ "$TANOMIS_API/v1/sessions/$SESSION_ID/interactions" \ --header "Authorization: Bearer $TANOMIS_TOKEN" \ --header "X-Organization-ID: $TANOMIS_ORG" \ --header "X-Workspace-Tenant-ID: $TANOMIS_WORKSPACE"| POST or row result | Meaning | Next action |
|---|---|---|
202, row answered |
One answer is durably accepted; delivery is pending. | Poll with backoff and keep observing the same run. |
row delivered |
Tanomis handed the answer to the agent runtime or its replay journal. | Continue to the run’s terminal status. |
409 interaction_already_answered |
Another caller won the first-answer race. | Read the row and use the recorded answer. |
409 interaction_terminal |
The row is expired or cancelled. |
Retire the UI action and read the session and run. |
404 |
The key or session is absent in the selected tenant; cross-tenant existence is hidden. | Recheck tenant context and stored identifiers. |
400 |
The filter or top-level kind is invalid, or the answer is absent, malformed, or oversized. | Correct the request. |
The row records the answer and timestamps, but it does not expose an approver identity. If your audit needs the person and business reason, authenticate them in your application and keep that record beside the session, run, and interaction keys.
5. Let Tanomis attempt to restore the waiting runtime
Section titled “5. Let Tanomis attempt to restore the waiting runtime”Do not call POST /resume for an interaction wait. If Tanomis suspended the runtime while the
person was thinking, the delivery worker makes a bounded number of restore attempts; after that,
the row stays retryable and delivery waits until another supported request makes the session live.
answered can therefore persist; only delivered proves the handoff.
Keep the original event stream, reconnecting with its stored Last-Event-ID and bounded backoff,
and read the original run until it is succeeded, failed, or cancelled. A pending interaction
adds no run status: the run stays running, and a session read may show awaiting_input: true.
Do not start a replacement run. If a row stays answered beyond your threshold, keep the session,
run, and interaction keys and contact your platform operator.
Your completion record should pair the interaction key, kind, accepted answer, and
answered_at/delivered_at; the session and run IDs; the run’s terminal status; and the terminal
event or application outcome you rely on.
6. Close expired and cancelled requests safely
Section titled “6. Close expired and cancelled requests safely”After expires_at passes, retire the controls and refresh the row. Expiry and answer race safely:
whichever claims the pending row first stands. An expired row is no longer answerable; Tanomis
winds the waiting agent down with a synthetic denial or an empty answer, and the row stays
expired. Never present that synthetic value as a human decision.
A row becomes cancelled when its run or session ends before it is consumed, including an accepted
row whose session became terminal while delivery was queued: the human answer was stored but the agent did not receive it. For either terminal state, disable the controls, preserve the record, and read
the session and run before offering a new operation. A new operation is a new run; a fresh decision
on content-identical work belongs in a new session.
Application checklist
Section titled “Application checklist”- Create an explicit session and run; store both IDs before processing events.
- List
pendingrows independently of the event stream; deduplicate by session plusinteraction_key. - Validate tenant, run, kind, status, expiry, and payload before rendering the decision.
- Enforce your approver roles and escape agent-produced content.
- Send the exact kind-specific answer once.
- Treat
202as acceptance; reconcile the row throughdelivered,expired, orcancelled. - On timeout or
409, read the winning row; never replace its answer. - Let Tanomis restore a suspended wait; require
delivered, then follow the same run to an authoritative terminal state.
Where to go next
Section titled “Where to go next”- Human interactions in the Platform API: the exact fields.
- Answer approvals in the Console: the same decision without writing code.
- Sessions and runs: the surrounding lifecycle.