Agent Approvals In Practice
A technical walkthrough of using PingRoom approvals for human-in-the-loop agent workflows.
Approvals are the first concrete human-in-the-loop primitive in PingRoom.
An agent asks a named human for a decision. The human receives a real push through a room context, decides from the app, and the agent reads the result. That loop is simple enough to explain and powerful enough to gate real work.
This post walks through the shape.
Create The Approval
The agent creates an approval request against a private room:
POST /api/agent/rooms/{inviteCode}/approvals
Authorization: Bearer AGENT_TOKEN
Content-Type: application/json
{
"title": "Deploy production?",
"question": "Ship commit abc123 to production now?",
"options": ["approve", "deny"],
"ttl": 900,
"correlation_id": "deploy-abc123",
"data": {
"environment": "production",
"commit": "abc123",
"pipeline": "release"
}
}
The approval is persisted, the room notification is enqueued, and the human sees a PingRoom approval prompt.

Wait For The Result
The agent then waits:
GET /api/agent/approvals/{approvalId}/wait
Authorization: Bearer AGENT_TOKEN
If the human decides while the request is held, the wait returns immediately. If the wait times out but the approval is still pending, the agent re-issues the wait. If the approval expires, the result should be terminal.
The agent can also fetch directly:
GET /api/agent/approvals/{approvalId}
Authorization: Bearer AGENT_TOKEN
That direct fetch is useful for recovery after a process restart.
Human Decision
The human side has an approvals inbox:
GET /api/approvals/pending
POST /api/approvals/{approvalId}/decide
Only the eligible approver should be able to decide. A different user should get a permission error. A second decision after the approval is already resolved should get a clean already-closed result, not create a second outcome.
Result Handling
The agent should treat outcomes distinctly:
if (approval.status === "decided" && approval.decision === "approve") {
await deploy();
} else if (approval.status === "decided") {
await stop("Human denied deploy");
} else if (approval.status === "expired") {
await stop("No human approval before expiry");
}
"Denied" and "expired" are different. One is a human answer. The other is no answer. The product should preserve that distinction everywhere.
Custom Options
Approvals already point toward Questions because they can carry custom options.
For many workflows, binary is enough:
"options": ["approve", "deny"]
But the shape can support choices like:
"options": ["deploy_prod", "deploy_staging", "cancel"]
The general Question primitive formalizes that broader shape, including responder scope, answer envelope, cancellation, and first-answer-wins room questions.
Best Practices
Keep the title short. Put the exact decision in the question. Use ttl so requests do not hang. Always include correlation_id. Keep data compact and free of secrets. Fetch the final state after any client interruption. Treat every terminal state as immutable.
Most importantly, make the human decision meaningful. If the agent can safely proceed without a person, do not ask. PingRoom approvals should be used for moments where a real human answer changes the workflow.
Mahdi Salmanzade
The Ping that cuts through.


