Approval Workflows
Approval workflows let a person decide on an agent's riskiest actions before they run. Not every governance decision should be automated. Some actions need a human to review and approve them first, such as privilege escalation, bulk data operations and financial transactions.
In MITRITY, you define hold policies for those actions. A hold policy pauses the agent's action and sends it to a human approval queue.
This guide covers the whole 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's action and the matching rule has hold: true, this happens:
- The gateway pauses the action. It 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: 0, it gets the answerheldat once, with the pending approval's ID. - The control plane creates an approval record with status
pending, and starts the timeout clock. - 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.
- An authorized user reviews the pending action in the dashboard or through the API.
- 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.
- If no human responds before the timeout expires, the
timeout_actiondecides 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:
openlets the action through;closedrefuses it.
Agent ──action──► Gateway ──hold──► Control Plane ──notify──► Slack / Email
│ │
│◄────decision─────────┤◄────approve/deny──── Human
│ │
▼ ▼
forward or block log audit event
Defining a Hold Policy
You set a hold 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, because 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 the timeout action applies. If you leave it out, or set it to 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). If you leave it 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: the rule matches only while the argument stays at or below the limit, which is 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 changes 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 you cannot limit a hold to business hours. The closest control is hold_timeout_minutes plus timeout_action: 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: the agent that triggered the action.
- Action: the action the agent attempted.
- Risk: the action's risk score.
- 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.
Select an approval to see its details: the agent, the action, its risk score and when it was requested. Once someone has decided it, you also see 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
List 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.
The API answers 404 Not Found, and changes nothing, when the approval:
- does not exist;
- is already decided;
- belongs to an agent you cannot see.
A Member or Viewer gets 403 Forbidden.
Count Pending Approvals
Get the number of pending approvals you can see. This is 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
If no human responds before hold_timeout_minutes runs out, the timeout_action decides 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 mainly for visibility;
- blocking the agent on timeout would cause unacceptable downstream failures;
- you want human review when someone is available, but default to letting the action through.
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
A hold can lead to three notices. Each is a notification type that 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 at it.
Slack and email deliver only when the channel itself is set up (see 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. You turn email on and list the recipients in Settings > Notifications > Email Notifications.
- 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 level is read from the action's drift score:
- Low: below 0.4;
- Medium: from 0.4 to below 0.7;
- High: from 0.7.
Review in Dashboard opens the approval queue.
Webhook Notifications
Webhook notices go to the destinations listed on the SIEM Integrations page (Settings → Notifications, then Manage integrations). These are 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_cef destinations are the exception: they never receive notification events.
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-Eventheader, and its ID inX-Mitrity-Event-ID; - 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
scopedsees, 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 answers404 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); - the note, when one was left (
resolution_note).
An approval that 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
Start with hold policies on a small set of high-risk actions, such as IAM changes and bulk exports. As your team gets used to the approval workflow, extend holds to more 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. Take your team's response time into account when you set timeouts.
Always Provide Reasons
Encourage approvers, or require them, to leave a note with every decision. The note stays on the approval and helps with post-incident analysis.
Use Environment Scoping
If you run several environments, consider different timeout and action settings for each:
- 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 share of approvals time out, your timeouts may be too short, 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 the priority system
- Enforcement Modes: how Monitor, Alert and Enforce modes interact with hold policies
- RBAC: role-based access control and approval permissions
- API Overview: the full API reference for approval endpoints