Skip to main content

Questions and approvals API

Kheish exposes approvals and structured user questions as first-class suspended-run states. These endpoints do not create unrelated work. They resume runs that are waiting.

Endpoint inventory

List pending user questions:
  • GET /v1/questions
  • GET /v1/sessions/{session_id}/questions
Resume from a session context:
  • POST /v1/sessions/{session_id}/approvals
  • POST /v1/sessions/{session_id}/questions
  • POST /v1/sessions/{session_id}/approval-runs
Resume from a run handle:
  • POST /v1/runs/{run_id}/approvals
  • POST /v1/runs/{run_id}/questions
  • POST /v1/runs/{run_id}/questions/{request_id}/cancel

Which endpoint should you use?

Use the session-scoped endpoints when you already operate in a session workflow and want the result projected back onto the session surface. Use the run-scoped endpoints when you already have a concrete suspended run_id and want a detached resume handle. Behavior difference:
  • POST /v1/sessions/{session_id}/approvals
    • returns an updated SessionView
  • POST /v1/sessions/{session_id}/questions
    • returns an updated SessionView
  • POST /v1/sessions/{session_id}/approval-runs
    • returns 202 Accepted plus a detached RunView
  • POST /v1/runs/{run_id}/approvals
    • returns 202 Accepted plus a detached RunView
  • POST /v1/runs/{run_id}/questions
    • returns 202 Accepted plus a detached RunView
  • POST /v1/runs/{run_id}/questions/{request_id}/cancel
    • validates that request_id is pending on run_id, then returns the cancelled or declined RunView

List pending user questions

GET /v1/questions supports:
  • session_id
GET /v1/sessions/{session_id}/questions is the session-scoped equivalent. The response is a list of PendingQuestionView objects:
  • session_id
  • agent_id
  • run_id
  • run_kind
  • requester_agent_id
  • requester_session_id
  • requester_run_id
  • requester_tool_call_id
  • requester_project_ids
  • requester_channel_ids
  • parent_project_ids
  • parent_channel_ids
  • request
The requester and project/channel fields are populated for parent_clarification runs. They let operators see which child run is blocked, where the question came from, and which project or channel contexts were active when the child escalated the clarification. request is a UserQuestionRequest:
  • id
  • tool_call_id
  • questions
  • created_at_ms
  • expires_at_ms
Each structured question contains:
  • id
  • header
  • question
  • options
  • multi_select

Resolve approvals

Approval endpoints accept:
Each ApprovalResolution supports:
  • request_id
  • behavior
    • allow
    • deny
  • updated_input
  • justification
  • reason
Use updated_input when the approver wants to modify the pending tool input before allowing it. Clients can also send the idempotency key through the Idempotency-Key header. Reusing the same key with an identical payload returns the same resumed run; reusing it with a different payload is rejected.

Resolve structured user questions

Question endpoints accept:
UserQuestionResolution fields:
  • request_id
  • answers
  • declined
  • justification
Each answer supports:
  • question_id
  • selected_option_ids
  • freeform_answer
For declined questions, set declined to true and leave answers empty. Clients can also send the idempotency key through the Idempotency-Key header. This is recommended for CLI and UI retries because question answers resume the original suspended run.

Cancel structured user questions

Cancel a specific pending question through the run-scoped cancel endpoint:
For normal user questions, POST /v1/runs/{run_id}/questions/{request_id}/cancel cancels the waiting run only when the request id still matches a pending question on that run. A stale or mistyped request_id is rejected and the run remains waiting. For parent-clarification questions, the same endpoint records a declined clarification and delivers that declined answer back to the requesting child. The optional justification is included in the declined clarification payload. questions cancel uses this request-scoped endpoint. The older run-level POST /v1/runs/{run_id}/cancel still cancels a run directly, but clients that are acting on a question should prefer the request-scoped question cancel endpoint. Clients can also send the cancel idempotency key through the Idempotency-Key header. This is recommended when retrying a request-scoped cancel after network loss or double-submit. CLI retries with questions cancel --idempotency-key require --run-id, because a completed cancel removes the pending question from session-scoped discovery.

Question errors

Question endpoints return application/problem+json with stable domain and code fields. Common codes:

Run states

These endpoints are relevant when a run enters one of these states:
  • waiting_for_approval
  • waiting_for_user_question
You can inspect those states through Sessions and runs API using:
  • GET /v1/sessions/{session_id}
  • GET /v1/runs/{run_id}
Relevant fields on RunView:
  • pending_approval_ids
  • pending_approvals
  • pending_question_ids
  • pending_questions
Question-producing tools may set:
  • expires_at_ms for an absolute Unix timestamp in milliseconds
  • expires_after_ms for a relative timeout from question creation
Expired normal user questions cancel the waiting run. Expired parent-clarification questions are delivered back to the requesting child as a declined clarification. Parent-clarification resolution is durable and idempotent. If the daemon restarts after an answer is captured but before every side effect finishes, startup replays the missing mailbox/output/audit work once. Parent interrupts and parent question cancellation are delivered to the child as declined clarifications. If the child has already been closed, the parent run still completes and the answer is moved to the child’s mailbox dead-letter queue. GET /v1/runs/{run_id}/events and GET /v1/sessions/{session_id}/events include a structured parent_clarification_resolved event for parent-clarification answers.

Operational notes

  • Approval resolution is batch-based: send every decision you want to resolve in the current request.
  • Approval batches are atomic per run: invalid, duplicate, or stale request IDs reject the batch without resolving any approval in that run.
  • approvals allow-all and approvals deny-all group work by run and submit one atomic batch per run; they are not one global transaction across multiple sessions or runs.
  • Session-scoped resume endpoints operate on the active waiting state for that session.
  • Run-scoped resume endpoints are safer when multiple suspended runs could exist over time and the client wants to target one exact waiting run.
  • CLI operators can answer with questions answer --interactive, decline with questions answer --declined, and cancel with questions cancel.