Skip to content
Open Console

Approval-required actions

6 min readUpdated

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.

The deployed version opts into the interactions it can emit:

agent.yaml
policy:
approval_required_tools:
- Bash
enable_ask_user: true

approval_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.

One durable review unit · one runAcceptance is not delivery
same run_id
  1. 01Agent runrunningThe action needs a person
  2. 02Durable rowpendingStore the stable interaction_key
  3. 03Your applicationreviewShow context; submit once
  4. 04202 AcceptedansweredDurable, not delivered yet
  5. 05Answer handoffdeliveredBounded restore attempts if suspended
  6. 06Same agent runterminalRead succeeded, failed, or cancelled
Pending can close without a human answer
  • 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.

  1. Person → control plane. The first valid answer moves the row from pending to answered and returns 202 Accepted.
  2. Control plane → agent. Tanomis restores a suspended session if needed, delivers the answer idempotently by interaction_key, and moves the row to delivered after 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.

Keep the run’s event stream open, and independently list durable pending rows. The row is the recovery source after a disconnect or reload:

Terminal
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"
A permission row
{
"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"
}
A question payload
{
"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.

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
Permission
{
"kind": "permission",
"answer": {
"decision": "approve",
"reason": "Reviewed change request CR-1842."
}
}
Question
{
"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.

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.

Terminal
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.

  • Create an explicit session and run; store both IDs before processing events.
  • List pending rows independently of the event stream; deduplicate by session plus interaction_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 202 as acceptance; reconcile the row through delivered, expired, or cancelled.
  • 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.