Skip to main content
All posts
Agents2 min read

Agent Approvals In Practice

A technical walkthrough of using PingRoom approvals for human-in-the-loop agent workflows.

Editorial illustration for Agent Approvals In Practice

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.

Keep reading

Product

Share a Point, Not a Trail

A custom Ping or Question can now carry one deliberately chosen place, with a quiet feed preview, a useful map sheet, and no background location trail.

3 min read
Product

Urgent Is Not Confirmation

Urgent now controls time-sensitive delivery; confirmation asks for a recorded human response. Either can stand alone, and a Ping can carry both when the moment needs both.

3 min read