Concepts
Decisions
Every action request gets one of three answers, with reasons a person can read and the signals that produced them. This page is the whole of how an answer is reached.
An agent describes what it is about to do. Immiscible answers with one of three words:
| Decision | Meaning | What the agent should do |
|---|---|---|
allow | The action is within a mandate, the agent’s tier and every floor below them | Proceed. Pass the receipt to whoever needs proof |
approval_required | A person must decide this one | Tell its user it is waiting, then poll |
deny | Refused, with reasons | Stop, and tell its user why |
A refusal is a decision, not an error: it comes back as HTTP 200 with "decision": "deny". HTTP errors are for requests Immiscible could not evaluate at all (see errors). When Immiscible cannot reach a decision, the answer is deny with a reason, never allow.
#The action request
{
"type": "payment",
"summary": "Pay the March invoice from Northwind Supplies",
"payment": { "amount": 182000, "currency": "GBP", "merchant": { "name": "Northwind", "domain": "northwind.example" } },
"provenance": [{ "source": "email", "detail": "invoice attached to an inbound email" }],
"volume": { "records": 1 },
"session": { "client": "claude-code", "id": "8f0c2d1e" },
"idempotencyKey": "inv-2026-03-northwind"
}| Field | Required | Meaning |
|---|---|---|
type | yes | payment, data.release, email.send, calendar.write, account.change, tool.call or your own dotted type |
summary | yes | One sentence a person can read: what and why. Shown to whoever approves |
payment | for payments | amount in whole minor units, currency (ISO 4217), merchant with a domain |
data | for data releases | fields from the vault, the recipient domain, a purpose |
target | for other actions | the domain or recipient the action reaches |
provenance | no, but see below | what influenced the request: user, agent, web, email, document or tool |
volume.records | for data that moves | how many records; an export that will not say is asked about |
session | no | the inference session, so declared provenance can be compared with what the gateway saw |
idempotencyKey | recommended | retries with the same key get the same decision; a different body is 409 |
#How the answer is reached
Every request is scored against the signals below. Signals combine but never override one another: any deny wins, then any approval, then allow. After the engine decides, the agent’s autonomy tier may turn an allow into a question, never the other way round.
- The kill switch. A frozen agent is denied everything (
agent_frozen). - Authority. Some active mandate must cover this action type for this agent (
no_mandateotherwise), within its limits, currency and recipients. - The floors. Checks no mandate can switch off: the Rule of Two, lookalike domains, injection language, velocity, sensitive fields.
- Standing. The agent’s tier and the volume of data it moves.
#Signals
| Signal | Fires when | Effect |
|---|---|---|
agent_frozen | the agent is frozen | deny |
no_mandate | nothing authorises this action type for this agent | deny |
mandate_pending_confirmation | a rule for this action type exists but waits for a second owner to confirm it; the reason names the rule | deny |
over_transaction, over_period | the amount breaches the mandate | deny |
currency_mismatch | the payment currency differs from the mandate’s | deny |
blocked_merchant | the merchant is on the mandate’s block list | deny |
recipient_not_allowed | data or a message is going to a domain the mandate does not cover | deny |
lookalike_domain | the domain is within two edits of a known one (arnazon.com) or uses a confusable character | deny |
approve_above | the amount is above the mandate’s approval line | approval |
new_merchant | a merchant not on the allow list and not seen before | as the mandate says: ask, allow or deny |
rule_of_two | untrusted provenance, plus a payment or data release, plus an external effect | approval, whatever the mandate says |
provenance_mismatch | the agent declared only trusted sources, but the gateway saw untrusted content enter the session | approval; deny for a payment to a merchant the agent has never paid and the mandate does not list |
injection_language | the summary or provenance carries instruction-override or pressure language | approval |
velocity | too many requests in a short window: 10 actions in 10 minutes, or 200 tool calls, counted apart (both set in the workspace settings) | approval |
sensitive_field | a release includes passport, national id, bank account, card or health data | approval, unless the mandate names that field and recipient |
tier_intern, tier_amount, tier_release, tier_action | the agent’s tier does not cover this alone | approval |
volume_over_tier, volume_undeclared | more records than the tier moves alone, or an export that will not say | approval |
volume_hard_limit | more records than the tier may ever move in one action | deny |
#The decision
{
"id": "act_Vd1x0a9e",
"decision": "approval_required",
"reasons": ["this payment was prompted by content from an email, and it would spend money"],
"risk": {
"score": 72,
"signals": [{ "id": "rule_of_two", "severity": "high", "detail": "untrusted provenance (email) + payment + external effect" }]
},
"mandateId": "mdt_91c3e0b2",
"approval": { "id": "apr_3k9d02aa", "url": "https://immiscible.fly.dev/app/approvals/apr_3k9d02aa", "expiresAt": "2026-10-04T10:30:00Z" },
"expiresAt": "2026-10-04T10:30:00Z"
}reasons is for people; risk.signals is for code. An allow carries a receipt. Every decision records the access profile version and hash it was made under, and lands in the evidence ledger with the trace it belongs to.
#Asking a person
When the answer is approval_required, nothing happens until someone decides. The person sees the agent, the mandate it acts under, what it wants to do, every signal explained in plain English, and what influenced the request. They can approve, deny, or freeze the agent.
- Who decides. The person the agent acts for, owners and admins, or only the workspace’s named approvers when it has them. Above the workspace’s line, neither the person the agent acts for nor whoever wrote its mandate may approve: separation of duties.
- Where. The console, an email link, or Slack and Teams. A decision in chat is the console’s decision, with the same checks.
- Deadlines. Each action type has a wait. Half way to it the request escalates to named contacts; at the deadline a background sweep refuses it, whether or not anyone is looking.
- Fresh look. At the moment of approval the request is checked against the mandate again. A frozen agent’s request cannot be approved.
The agent polls GET /v1/actions/:id (or the MCP tool check_action_status): every five seconds for the first minute, every thirty after. Do not retry with a fresh idempotency key to get a different answer: each attempt is a new request, and a burst of them trips velocity.
#Next
- Mandates: the authority a decision is checked against.
- Autonomy tiers: how an agent earns fewer questions.
POST /v1/actions/authorize: the endpoint, with examples.