Human-in-the-Loop Approvals
How `action: ask` maps to the shipped approval records, HTTP endpoints, and Telegram webhook callback route in edictum-api.
Right page if: you need the shipped approval lifecycle in edictum-api -- create, poll, decide, list, and timeout behavior. Wrong page if: you want to author a ruleset with `action: ask` -- see https://docs.edictum.ai/docs/rulesets/yaml-reference. Gotcha: the current edictum-api server stores approvals, exposes HTTP endpoints, and ships a Telegram webhook callback route. Do not claim Slack or Discord interaction routes unless you verify they exist.
When a rule uses action: ask, the SDK pauses the tool call and hands off to an approval backend. In the shipped edictum-api server, that means storing an approval record and exposing it over HTTP.
This repo gives you the approval state machine and persistence layer:
POST /v1/approvalscreates a pending approvalGET /v1/approvals/{id}returns the current statePOST /v1/approvals/{id}/decideresolves it asapprovedorrejectedGET /v1/approvalslists approvals with basic filters
The API surface is the source of truth here. A dashboard or operator tool can sit on top of these endpoints, but the server behavior comes from edictum-api.
Lifecycle
SDK hits POST /v1/approvals
|
v
Server stores approval with status=pending
|
+-- Agent or operator polls GET /v1/approvals/{id}
|
+-- Human or automation resolves via POST /v1/approvals/{id}/decide
|
+-- Background sweeper marks stale approvals timed_outThe approvals service also publishes best-effort bus messages:
approvals.newapprovals.decided
The store is still authoritative. If bus delivery fails, the approval record remains correct in the database.
States
Approvals use four persisted states:
| State | Meaning |
|---|---|
pending | Waiting for a decision |
approved | Resolved by a human or operator |
rejected | Explicitly rejected by a human or operator |
timed_out | Expired without a human decision |
Transitions are one-way. Once an approval is resolved, another decide call returns a conflict instead of reopening it.
Timeout Behavior
Each approval stores:
timeouttimeout_action
timeout_action is either:
blockallow
The important detail is that the stored approval status still becomes timed_out. timeout_action controls what the calling guard does with the tool call after the timeout, not the terminal approval state itself.
The sweeper runs on APPROVAL_SWEEP_EVERY, which defaults to 1m in the current server config.
API Flow
Create a pending approval:
POST /v1/approvals
Authorization: Bearer edk_...
Content-Type: application/json{
"agent_id": "mimi",
"session_id": "sess-1",
"tool_name": "bash",
"tool_args": { "cmd": "rm -rf /tmp/nope" },
"message": "Need approval before running this command",
"rule_name": "dangerous-command",
"timeout": 300,
"timeout_action": "block"
}Response:
{
"id": "1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7",
"status": "pending"
}Poll the record:
GET /v1/approvals/1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7
Authorization: Bearer edk_...Resolve it:
POST /v1/approvals/1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7/decide
Authorization: Bearer edk_...
Content-Type: application/json{
"decision": "approved",
"decided_by": "arnold",
"decided_via": "api",
"reason": "looks safe"
}Telegram approval callbacks are handled by the shipped webhook route:
POST /v1/telegram/webhook/{id}That route is the current interactive callback path for Telegram-based approvals. Do not generalize it to Slack or Discord unless those routes are actually shipped.
Current shipped decision values for POST /decide are:
approvedrejected
timed_out is reserved for the sweeper and is not a valid human decision payload.
Stored Fields
These are the approval fields the server persists and returns today:
| Field | Notes |
|---|---|
id | Public UUID generated by the server |
agent_id | Agent identifier |
session_id | Optional session lineage identifier |
tool_name | Tool name |
tool_args | Tool arguments |
message | Human approval prompt |
rule_name | Rule that triggered the approval |
status | pending, approved, rejected, or timed_out |
timeout | Timeout in seconds |
timeout_action | block or allow |
decided_by | Resolver identity |
decided_at | Decision timestamp |
decided_via | Resolution channel label supplied by the caller, for example api |
decision_reason | Optional decision reason |
created_at | Creation timestamp |
Listing Approvals
GET /v1/approvals supports these filters:
statusagent_idsession_idlimitoffset
Example:
GET /v1/approvals?status=pending&agent_id=mimi&session_id=sess-1
Authorization: Bearer edk_...The server returns:
{
"approvals": [
{
"id": "1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7",
"agent_id": "mimi",
"session_id": "sess-1",
"tool_name": "bash",
"tool_args": { "cmd": "rm -rf /tmp/nope" },
"message": "Need approval before running this command",
"rule_name": "dangerous-command",
"status": "pending",
"timeout": 300,
"timeout_action": "block",
"created_at": "2026-04-06T12:00:00Z"
}
]
}What This Repo Does Not Ship
This repo does not currently expose:
- Slack interaction endpoints
- Discord interaction endpoints
- approval-specific SSE endpoints
- approval creation rate limiting
If you need those, document them only after the routes and service behavior exist in edictum-api.
Next Steps
- API Reference -- full request and response shapes
- Rulesets -- where
action: askcomes from - Control Plane Setup -- boot the server and call the approval endpoints
Last updated on