Approval Workflows
Not every governance decision should be automated. Some actions — privilege escalation, bulk data operations, financial transactions — require a human to review and approve before they proceed. MITRITY's approval workflow system lets you define hold policies that pause agent actions and route them to a human approval queue.
This guide covers the end-to-end approval workflow: defining hold policies, managing the approval queue, configuring timeouts, and setting up notifications.
How Hold Policies Work
When the gateway evaluates an agent action and the matching rule has hold: true, the following sequence occurs:
- Action paused — The gateway asks the control plane to open an approval, and keeps the agent's call open while it waits for the outcome. A caller of the admission API sets its own budget for the wait: with
hold_timeout_seconds: 0it is answeredheldat once, with the pending approval's ID. - Approval created — The control plane creates an approval record with status
pendingand starts the timeout clock. - Notifications sent — MITRITY announces the held action: in the dashboard's notifications, and on Slack when your organization has connected it. Email and webhook destinations receive it when you turn those channels on for it; see Notification Channels.
- Human reviews — An authorized user reviews the pending action in the dashboard or via API.
- Decision applied — The user approves or denies the action. The gateway receives the decision and either forwards the original request or returns an error to the agent.
- Timeout fallback — If no human responds before the timeout expires, the
timeout_actiondetermines the outcome (auto-deny or auto-allow).
If the gateway cannot reach the control plane to open or follow the approval, the policy's fail mode decides: open lets the action through, closed refuses it.
Agent ──action──► Gateway ──hold──► Control Plane ──notify──► Slack / Email
│ │
│◄────decision─────────┤◄────approve/deny──── Human
│ │
▼ ▼
forward or block log audit event
Defining a Hold Policy
A hold is set on a policy rule, not on the policy itself. A rule whose action is deny and whose hold is true pauses the actions it matches for a human decision instead of blocking them. On a rule whose action is allow, hold has no effect. The rule's hold_timeout_minutes and timeout_action say how long to wait, and what happens when nobody decides.
Rule Structure
{
"tool_pattern": "regex:^mcp:iam\\.(create_role|assign_role|escalate_permissions)$",
"action": "deny",
"hold": true,
"hold_timeout_minutes": 15,
"timeout_action": "deny",
"priority": 400,
"enabled": true
}
To scope a hold to particular agents, assign the policy to those agents. An agent_id constraint does not do it: rule constraints match the arguments of an action, not the caller — see Writing Policies.
Hold-Specific Fields
| Field | Type | Required | Description |
|---|---|---|---|
hold | boolean | No | true pauses the matched action for a human decision. Takes effect on a deny rule only. |
hold_timeout_minutes | integer | No | Minutes to wait for a human decision before applying the timeout action. Left out, or zero, the wait is 30 minutes. |
timeout_action | enum | No | What happens when the timeout expires: "deny" (block the action) or "allow" (permit the action). Left out, it is "deny". |
Adding a Hold Rule via API
Add the rule to an existing policy:
curl -X POST https://api.mitrity.com/api/v1/policies/pol_8f3k2m1n/rules \
-H "Authorization: Bearer mk_your-api-key" \
-H "Content-Type: application/json" \
-d '{
"tool_pattern": "mcp:finance.transfer_*",
"action": "deny",
"hold": true,
"priority": 350,
"hold_timeout_minutes": 30,
"timeout_action": "deny"
}'
Response (201 Created): the rule.
{
"id": "rul_3k9m2x",
"policy_id": "pol_8f3k2m1n",
"tool_pattern": "mcp:finance.transfer_*",
"action": "deny",
"alert": false,
"hold": true,
"hold_timeout_minutes": 30,
"timeout_action": "deny",
"priority": 350,
"enabled": true,
"created_at": "2026-03-01T10:00:00Z"
}
A new policy can carry hold rules from the start in the rules array of POST /api/v1/policies.
There is no numeric threshold constraint. A rule cannot say "hold transfers above $1000". Numeric constraints are integer upper bounds, and the rule matches only while the argument stays at or below the limit — the opposite of a threshold.
resource_patternis a tool-permission field, not a rule constraint, and glob matching has no>operator. See Writing Policies.
Common Hold Rule Examples
Privilege Escalation
Hold any action that modifies IAM roles or grants elevated permissions:
{
"action": "deny",
"hold": true,
"tool_pattern": "regex:^mcp:(iam|rbac)\\.(create_role|assign_role|grant_permission|escalate)$",
"priority": 500,
"hold_timeout_minutes": 15,
"timeout_action": "deny"
}
Bulk Data Operations
Hold bulk export, delete, or migration operations:
{
"action": "deny",
"hold": true,
"tool_pattern": "regex:^mcp:bulk_(export|delete|migrate)_.*$",
"priority": 450,
"hold_timeout_minutes": 60,
"timeout_action": "deny"
}
Infrastructure Changes
Hold infrastructure modifications for a human decision:
{
"action": "deny",
"hold": true,
"tool_pattern": "mcp:infra.*",
"priority": 400,
"hold_timeout_minutes": 30,
"timeout_action": "deny"
}
There is no time-window constraint, so a hold cannot be scoped to business hours. hold_timeout_minutes plus timeout_action is the closest control: outside working hours nobody approves, and the request resolves to the timeout action.
Database Schema Changes
Hold any DDL operation with a long timeout for DBA review:
{
"action": "deny",
"hold": true,
"tool_pattern": "regex:^mcp:db\\.(alter_table|create_table|drop_table|create_index|drop_index)$",
"priority": 480,
"hold_timeout_minutes": 1440,
"timeout_action": "deny"
}
The Approval Queue
Dashboard View
The approval queue is the Approvals page in the dashboard sidebar. It lists the approvals you can see, newest first, under the All, Pending and Resolved tabs. Each row shows:
- Agent that triggered the action
- Action the agent attempted
- Risk score of the action
- Timeout: for a pending approval, the time left before the timeout action applies; for an approved or denied one, when it was decided
- Status: pending, approved, denied or timed out
Selecting an approval shows its details: the agent, the action, its risk score, when it was requested and, once someone has decided it, the decision, who made it, when, and the note left with it.
Filtering the Queue
The API lists approvals with these query parameters:
| Parameter | Description | Example |
|---|---|---|
status | Only approvals in this status: pending, approved, denied or timed_out | status=pending |
sort_by | created_at (default), action_type, drift_score or status | sort_by=drift_score |
sort_order | desc (default) or asc | sort_order=asc |
limit | Results per page, at most 100 (default 50) | limit=20 |
offset | Number of approvals to skip | offset=50 |
Which approvals a user sees depends on their access scope, not on a filter; see Approval Permissions.
Approve or Deny via Dashboard
Owners and Managers decide approvals in the dashboard:
- Open Approvals in the sidebar.
- Select an approval to see its details.
- Choose Approve or Deny on a pending approval.
- Optionally add a note; it is kept with the decision.
- The decision is applied immediately.
After a decision:
- Approved: the gateway lets the held action through.
- Denied: the gateway refuses the held action.
When the governance gate enforces agent approval, the held action of an agent that is not approved is refused, whatever the decision.
Approve or Deny via API
List Pending Approvals
Retrieve the pending approvals you can see:
curl "https://api.mitrity.com/api/v1/approvals?status=pending" \
-H "Authorization: Bearer mk_your-api-key"
Response:
{
"approvals": [
{
"id": "apr_9x2k4m",
"tenant_id": "tnt_4f9a",
"event_id": "evt_7h2k9d",
"agent_id": "agt_sales-bot",
"agent_name": "sales-bot",
"action_type": "bulk_export_contacts",
"resource_pattern": "crm/contacts/*",
"drift_score": 0.42,
"status": "pending",
"timeout_at": "2026-03-01T11:00:00Z",
"timeout_action": "deny",
"created_at": "2026-03-01T10:00:00Z"
}
]
}
An approved or denied approval also carries resolved_by, resolved_at and, when a note was left, resolution_note.
Approve an Action
curl -X POST https://api.mitrity.com/api/v1/approvals/apr_9x2k4m/approve \
-H "Authorization: Bearer mk_your-api-key" \
-H "Content-Type: application/json" \
-d '{
"note": "Approved: EMEA contact export requested by sales director for quarterly review"
}'
The body is optional. Response: the approval, now with status approved:
{
"id": "apr_9x2k4m",
"tenant_id": "tnt_4f9a",
"event_id": "evt_7h2k9d",
"agent_id": "agt_sales-bot",
"agent_name": "sales-bot",
"action_type": "bulk_export_contacts",
"resource_pattern": "crm/contacts/*",
"drift_score": 0.42,
"status": "approved",
"timeout_at": "2026-03-01T11:00:00Z",
"timeout_action": "deny",
"resolved_by": "user_jsmith",
"resolved_at": "2026-03-01T10:06:00Z",
"resolution_note": "Approved: EMEA contact export requested by sales director for quarterly review",
"created_at": "2026-03-01T10:00:00Z"
}
Deny an Action
curl -X POST https://api.mitrity.com/api/v1/approvals/apr_9x2k4m/deny \
-H "Authorization: Bearer mk_your-api-key" \
-H "Content-Type: application/json" \
-d '{
"note": "Denied: No business justification provided for bulk export of 15K contacts"
}'
Response: the approval, now with status denied, in the same shape.
An approval that does not exist, is already decided, or belongs to an agent you cannot see answers 404 Not Found, and nothing changes. A Member or Viewer gets 403 Forbidden.
Count Pending Approvals
Get the number of pending approvals you can see (useful for badge counters):
curl https://api.mitrity.com/api/v1/approvals/count \
-H "Authorization: Bearer mk_your-api-key"
Response:
{
"count": 3
}
Timeout Behavior
When no human responds before the hold_timeout_minutes expires, the timeout_action determines the outcome.
Auto-Deny (Recommended)
{
"timeout_action": "deny"
}
When the timeout expires, the action is blocked: the gateway refuses it, and the approval's status becomes timed_out.
Use auto-deny when:
- The action is high-risk (privilege escalation, data deletion, financial transactions)
- It is better to err on the side of caution
- The agent can retry the action later after explicit approval
Auto-Allow
{
"timeout_action": "allow"
}
When the timeout expires, the action is permitted: the gateway lets it through, and the approval's status becomes timed_out.
Use auto-allow when:
- The action is low-to-medium risk and the hold is primarily for visibility
- Blocking the agent on timeout would cause unacceptable downstream failures
- You want human review when available, but default to permissive behavior
Timeout Recommendations
| Scenario | Recommended Timeout | Recommended Action |
|---|---|---|
| Privilege escalation | 15 minutes | deny |
| Bulk data export | 60 minutes | deny |
| Infrastructure changes | 30 minutes | deny |
| Schema changes (DBA review) | 24 hours (1440 min) | deny |
| Non-critical alerts | 30 minutes | allow |
| After-hours monitoring | 60 minutes | allow |
Notification Channels
Three notices follow a hold, each a notification type you route in Settings > Notifications > Alert routing:
| Notice | When | Channels by default |
|---|---|---|
| Action held for approval | A hold opens an approval | The dashboard's notifications and Slack |
| Held action resolved | Someone approves or denies it | The dashboard's notifications |
| Held action timed out | Nobody decided in time and the timeout_action applied | The dashboard's notifications, Slack and email |
Watch the timed-out notice in particular: with timeout_action: "allow" it means an action went through without anyone looking.
Slack and email deliver only when the channel itself is set up (below). Webhook delivery is off for every notice until you turn it on under Alert routing.
Email Notifications
Emails go to the organization's recipient list: Settings > Notifications > Email Notifications, where you enable email and list the recipients. A held action is emailed when email is turned on for Action held for approval under Alert routing; a timed-out hold is emailed by default.
The email for a held action names the agent, the action type and the resource, and says how long remains before the timeout.
Slack Notifications
Slack messages go to the incoming webhook set in Settings > Notifications > Slack Integration. A held action posts a message like this:
Action Held for Approval
Agent: sales-bot Action: bulk_export_contacts
Resource: crm/contacts/* Risk: Medium (0.42)
Timeout: 60 minutes | Approval ID: apr_9x2k4m
[Review in Dashboard]
The risk is read from the action's drift score: Low below 0.4, Medium below 0.7, and High from 0.7. Review in Dashboard opens the approval queue.
Webhook Notifications
Webhook notices go to the destinations set up in Settings > Integrations, the same destinations that receive SIEM forwarding. When the webhook channel is on for a notice, every enabled destination receives a POST for it. Syslog destinations are the exception: they are not HTTP endpoints and are skipped. The body is the notification envelope:
{
"type": "approval.requested",
"version": "1.0",
"event_id": "5f0c2d1e-9a7b-4c3d-8e6f-1a2b3c4d5e6f",
"tenant": { "id": "tnt_4f9a" },
"severity": "warning",
"title": "Action held for approval",
"body": "sales-bot was held for approval: bulk_export_contacts on crm/contacts/*. It times out in 60 minutes.",
"link_url": "/app/approvals",
"generated_at": "2026-03-01T10:00:00Z",
"payload": {
"approval_id": "apr_9x2k4m",
"agent_id": "agt_sales-bot",
"agent_name": "sales-bot",
"action_type": "bulk_export_contacts",
"resource_pattern": "crm/contacts/*",
"drift_score": 0.42,
"timeout_minutes": 60,
"dashboard_url": "https://mitrity.com/app/approvals",
"dedup_key": "apr_9x2k4m"
}
}
The other two notices arrive as approval.resolved and approval.timed_out. Each request carries the notice's type in the X-Mitrity-Event header and its ID in X-Mitrity-Event-ID, and sends the destination's Authorization header when one is set. A Splunk HEC destination receives the same envelope as the HEC event, with the sourcetype mitrity:notification.
Approval Permissions
Every role can view approvals. Owners and Managers approve and deny them:
| Role | Can Approve | Can Deny | Can View Queue |
|---|---|---|---|
| Owner | Yes | Yes | Yes |
| Manager | Yes | Yes | Yes |
| Member | No | No | Yes |
| Viewer | No | No | Yes |
Approvals follow agent grants. A user whose access scope is scoped sees, counts and decides only the approvals of the agents granted to them; their environment access does not change which approvals they see. An approval of any other agent answers 404 Not Found, and a decision on it changes nothing. An Owner, or a user whose access scope is all, sees every approval.
Audit Trail
Every decision stays on the approval. An approved or denied approval keeps its status, who decided (resolved_by), when (resolved_at) and, when one was left, the note (resolution_note); an approval nobody decided in time has the status timed_out. Decided approvals stay in the list, on the Resolved tab of the Approvals page and in GET /api/v1/approvals.
Best Practices
Start Narrow, Expand Gradually
Begin with hold policies on a small set of high-risk actions (IAM changes, bulk exports). As your team builds comfort with the approval workflow, expand to cover additional action types.
Set Reasonable Timeouts
Choose timeouts that balance security with operational needs. A 5-minute timeout on a database migration during off-hours will almost certainly auto-deny. Consider your team's response time when setting timeouts.
Always Provide Reasons
Encourage (or require) approvers to leave a note with every decision. The note stays on the approval and helps with post-incident analysis.
Use Environment Scoping
In multi-environment setups, consider different timeout and action configurations:
- Production: Strict holds, short timeouts, auto-deny
- Staging: Looser holds, longer timeouts, auto-allow
- Development: Minimal holds, or monitor mode instead
Monitor Timeout Rates
If a large percentage of approvals are timing out, your timeouts may be too aggressive or the wrong people are being notified. Review timed-out approvals on the Resolved tab of the Approvals page.
Related Documentation
- Writing Policies — Policy structure, pattern matching, and priority system
- Enforcement Modes — How Monitor, Alert, and Enforce modes interact with hold policies
- RBAC — Role-based access control and approval permissions
- API Overview — Full API reference for approval endpoints