Paidwen

A person approves the risky actions before they run.

A rule that returns require_approval sends the action to the people who follow the agent. A rule that returns escalate sends it up to a manager. Both follow one circuit: the agent waits, the first approver who answers decides, and the action runs or stops.

An approval group says who approves, where, and for how long.

  • members: 1 to 100 email addresses.
  • slack_channel: a channel of the Slack workspace connected to your organization, optional.
  • teams_webhook_url: the webhook address of a Teams workflow, optional. Paidwen keeps it encrypted and shows only its host.
  • expires_in: from 60 to 86,400 seconds. Without it, the group takes the approval_expires_in of the organization.
  • on_expiry: what happens without an answer, deny or allow. Without it, the group takes the approval_on_expiry of the organization.

A rule names its group in approval_group_id. A rule without a group uses the default group of its verdict, set by default_for on the group. Without any group, the request appears only in the dashboard, and it follows the timeout of the organization.

HTTP
POST /v1/approval-groups
Content-Type: application/json

{
  "name": "Support leads",
  "members": ["[email protected]", "[email protected]"],
  "expires_in": 600,
  "on_expiry": "deny",
  "default_for": ["require_approval"]
}

The organization sets the timeout of a request without a group.

  • approval_expires_in: from 60 to 86,400 seconds, 300 by default.
  • approval_on_expiry: what happens without an answer, deny by default or allow.

An admin sets them in Settings, then Organization, under Approval timeout and On timeout. A script sets them with an admin key:

HTTP
PATCH /v1/organization
Content-Type: application/merge-patch+json

{
  "approval_expires_in": 3600,
  "approval_on_expiry": "deny"
}

A new group without expires_in or on_expiry takes these values when it is created. A change reaches the decision points in under a second. A request that is already open keeps its deadline.

Each channel shows the same request.

The request names the agent, the tool, its parameters when the agent keeps its content, the estimated cost, the rule, the deadline and what happens without an answer. A parameter such as reason therefore appears in the request.

  • In Slack, the request arrives in the channel of the group with two buttons, Approve and Deny. Only a member of the group can answer, and the buttons then give way to the answer, its author and its time.
  • In Teams, the request arrives as a card with two buttons, Approve and Deny. Each button opens the request in the dashboard with that answer selected, and the approver confirms with a click or Enter.
  • By email, each member gets a message from [email protected] with a personal link. The link opens a page with two buttons, so a link scanner never answers in your place.
  • In the dashboard, the Approvals screen lists the open requests, and Approve and Deny work for the members of the group.

The first answer wins, even when two approvers answer at the same moment. A channel that fails does not stop the others.

A Teams workflow brings the requests into a channel.

  1. In Teams, select More options (...) next to the channel, then Workflows.
  2. Pick the template Send webhook alerts to a channel, check the team and the channel, then select Save.
  3. Copy the webhook link that Teams shows.
  4. In Paidwen, open Approvers, open the group, paste the link in Teams webhook, then select Save.

The templates that take alerts only from specific people or from people in your organization ask for a token that Paidwen does not send. A workflow belongs to the person who created it: add a co-owner in the Workflows app, so the workflow keeps running when that person leaves.

The agent waits, or hands back control.

By default, the call waits for the answer. The SDK calls onWait once, so the agent can tell its user that the action waits.

const paidwen = new Paidwen({
  onWait: (wait) => notifyUser(wait.approvalId, wait.expiresAt),
});
paidwen = Paidwen(on_wait=lambda wait: notify_user(wait.approval_id, wait.expires_at))

Approved, the action runs. Denied, the call throws DeniedError. Expired, the SDK applies on_expiry: deny throws DeniedError, and allow runs the action.

A serverless function cannot wait for minutes. With onPending: "throw" in TypeScript or on_pending="raise" in Python, the call throws PendingError as soon as the request is open, and the action does not run. The agent retries later with the same action id, and Paidwen gives back the same request:

const paidwen = new Paidwen({ onPending: "throw" });

await paidwen.context({ actionId }, () => refund({ order: "ord_4821", amount: 2500 }));
paidwen = Paidwen(on_pending="raise")

with paidwen.context(action_id=action_id):
    refund("ord_4821", 2500)

When the answer has arrived, the action runs at once, and no second request opens.

The SDK polls every 5 seconds.

The SDK asks for the answer once at the decision, then every 5 seconds, following the poll mode of the OpenID CIBA standard. The agent opens no address on the internet, so it works behind a firewall and in a serverless function. The action starts at most 5 seconds after the answer, plus the network time.

decide() never waits. waitForApproval(decision) in TypeScript, or wait_for_approval(decision) in Python, waits for the answer to a decision that decide() returned.

Approvals have seven limits.

  • An agent that waits keeps its process open for the whole wait, unless it hands back control.
  • A request reaches the dashboard, Slack, Teams and the email up to 100 ms after the decision, plus the sending time.
  • In Teams, the answer is given in the dashboard, and the card keeps its buttons after the answer.
  • Microsoft lets a Teams workflow post 25 cards in 5 minutes, and about 28 KB per message. Paidwen shortens long parameters in the card. Beyond that pace, the dashboard and the email still carry every request.
  • Teams workflows do not post in private channels yet.
  • An approved action sent again with the same action id runs again. Your tool keeps its own idempotency, for example with the action id as its idempotency key.
  • CIBA in poll mode does not push the answer to the agent: the action starts at the next poll.