Skip to content
All posts
Agents2 min read

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.

Keep reading

Agents

The Full Potential of the Question Protocol

One primitive, a question a human answers with a tap, covers deploy gates, environment routing, team decisions, and typed replies. Here is everything it unlocks, and how to reach it from the SDK, the CLI, and MCP.

5 min read
Product

PingRoom v1.2: Questions Are Here

Our biggest update yet. Connected agents and integrations can now ask questions in your rooms, answer from the lock screen with one tap, first response wins. Plus an interactive Watch widget, room folders, free templates, every sound free, link Pings, and more.

2 min read