Inline authorize() gate
The universal pattern for gating agent actions. Works with any framework, including ones Aira labels audit-only. Call aira.authorize() before the side effect, check auth.status, act only when authorized.
The pattern in one sentence. Call aira.authorize() at the top of your tool function, before any side effect. If auth.status != "authorized", bail out. If it is, do the work, then call aira.notarize() with the outcome. Framework-agnostic. Works everywhere.
Why this page exists
Aira ships first-class integrations for LangChain, Vercel AI, OpenAI Agents, Bedrock, Google ADK, CrewAI, and MCP. Five of those integrations are real pre-execution gates — they wrap the framework's own hook and block denied calls automatically. You install the extra, attach the handler, and every tool is gated.
But one integration — CrewAI — is audit-only, because CrewAI exposes no upstream pre-execution hook to intercept. For CrewAI agents, the task_callback and step_callback fire after the work already ran.
Also, sometimes you want to gate a specific branch inside a tool, or a non-tool side effect, or a custom runner that isn't one of the integrations we ship. In any of those cases, you write the gate yourself. It's ten lines of code and works with any framework.
This page is the canonical pattern.
The pattern
from aira import Aira, AiraError
aira = Aira(api_key="aira_live_...")
def send_wire_transfer(amount: float, to: str) -> str:
"""
Real-world side effect, gated inline.
The gate is three parts:
1. authorize() BEFORE any side effect
2. check auth.status — authorized / pending_approval / implicit denial
via POLICY_DENIED exception
3. notarize() AFTER with outcome="completed" or "failed"
"""
# 1. Authorize before acting
try:
auth = aira.authorize(
action_type="wire_transfer",
details=f"Send €{amount} to {to}",
agent_id="payments-agent",
# optional: everything below strengthens the audit trail
model_id="claude-sonnet-4-6",
instruction_hash="sha256:abc123...",
)
except AiraError as e:
if e.code == "POLICY_DENIED":
# Policy engine explicitly denied. Surface a clean error
# to whatever caller is asking — agent, chain, UI.
return f"Denied by Aira policy: {e.message}"
# Network / 5xx — fail closed or open depending on your taste
raise
# 2. Check the non-denial outcomes
if auth.status == "pending_approval":
return (
f"Held for human approval. Action id: {auth.action_uuid}. "
"The agent should pause or retry once approved."
)
# Anything else is "authorized" — we're good to act
# 3. Do the side effect, with a try/finally so we always notarize
try:
tx_id = stripe.transfers.create(amount=int(amount * 100), destination=to)
except Exception as e:
aira.notarize(
action_uuid=auth.action_uuid,
outcome="failed",
outcome_details=f"{type(e).__name__}: {str(e)[:200]}",
)
raise
aira.notarize(
action_uuid=auth.action_uuid,
outcome="completed",
outcome_details=f"stripe_tx={tx_id}",
)
return f"Wire sent: €{amount} to {to}"That's it. Every call to send_wire_transfer now:
- Runs your policies first (rules / AI / consensus / content scan)
- Blocks denied calls so the wire never happens
- Holds for human review when a policy says so
- Emits a signed receipt committing both the intent and the outcome
- Fails closed on execution errors so your audit trail reflects reality
TypeScript version
import { Aira, AiraError } from "aira-sdk";
const aira = new Aira({ apiKey: "aira_live_..." });
export async function sendWireTransfer(amount: number, to: string): Promise<string> {
// 1. Authorize before acting
let auth;
try {
auth = await aira.authorize({
actionType: "wire_transfer",
details: `Send €${amount} to ${to}`,
agentId: "payments-agent",
modelId: "claude-sonnet-4-6",
});
} catch (e) {
if (e instanceof AiraError && e.code === "POLICY_DENIED") {
return `Denied by Aira policy: ${e.message}`;
}
throw e;
}
// 2. Check the non-denial outcomes
if (auth.status === "pending_approval") {
return `Held for human approval. Action id: ${auth.actionId}.`;
}
// 3. Do the side effect, notarize either outcome
try {
const tx = await stripe.transfers.create({ amount, destination: to });
await aira.notarize({
actionId: auth.actionId,
outcome: "completed",
outcomeDetails: `stripe_tx=${tx.id}`,
});
return `Wire sent: €${amount} to ${to}`;
} catch (e) {
const err = e as Error;
await aira.notarize({
actionId: auth.actionId,
outcome: "failed",
outcomeDetails: `${err.name}: ${err.message.slice(0, 200)}`,
});
throw e;
}
}When to use this vs the framework integrations
| Use a framework integration when… | Use inline authorize() when… |
|---|---|
| You're using LangChain, Vercel AI, OpenAI Agents, Bedrock, or Google ADK — all real pre-execution gates | You're using CrewAI (audit-only — inline is the only way to get real gating) |
| You want blanket gating on every tool with no code changes in the tool bodies | You need to gate a specific branch inside a tool, or a non-tool side effect |
Your tool details string can be generic ("Tool 'X' called with arg keys [a, b]") | You need the details string to include actual argument values |
| You want the framework integration to own the action_uuid lifecycle | You want the action_uuid in your tool body for logging, correlation ids, etc. |
The two patterns compose. You can use the LangChain callback handler for blanket gating on most tools AND drop inline aira.authorize() calls inside specific high-stakes tools for extra business-logic gates. The receipts chain correctly via parent_action_uuid.
Anti-patterns to avoid
Calling notarize() without authorize() first
Notarize is a free bookkeeping call — it can't mint a receipt for an action_uuid that doesn't exist. Every successful notarize must be backed by a prior authorize. The endpoint returns 404 for unknown action ids and 409 for actions that aren't in the authorized or approved state.
Putting the side effect before the authorize call
# WRONG — receipt committed but the action already happened
tx_id = stripe.transfers.create(...)
auth = aira.authorize(action_type="wire_transfer", ...) # too lateThe whole point is that denied actions never run. If you call the side effect first, you've lost the gate property — you just have an audit log, not authorization.
Swallowing POLICY_DENIED silently
# WRONG — agent thinks the call succeeded
try:
auth = aira.authorize(...)
except AiraError:
pass # don't do this
tx_id = stripe.transfers.create(...)If authorize() raised, you don't have a valid action_uuid and the side effect should not run. Propagate the error (or return a clean denial string if you're in a tool function that needs to return a message to the agent).
Forgetting to notarize on failure
# INCOMPLETE — authorized ops that failed leave the action in "authorized" state forever
auth = aira.authorize(...)
tx_id = stripe.transfers.create(...) # raises
aira.notarize(action_uuid=auth.action_uuid, outcome="completed", ...) # never runsUse a try/finally so both success and failure paths notarize. The action transitions to notarized (success) or failed (error) and the audit trail matches reality.
Composing with CrewAI's audit hook
On CrewAI you can use both patterns at once — the audit hook records step/task completions for the whole crew, and inline gates run on the specific tools that need to be blocked:
from aira import Aira
from aira.extras.crewai import AiraCrewHook
from crewai import Agent, Crew, Task, tool
aira = Aira(api_key="aira_live_...")
# Audit hook — records every step + task completion
audit_kwargs = AiraCrewHook.for_crew(aira, "research-crew")
# Inline gate on the high-stakes tool
@tool("send_wire_transfer")
def send_wire_transfer(amount: float, to: str) -> str:
# paste the pattern from above
...
crew = Crew(
agents=[Agent(role="payer", tools=[send_wire_transfer])],
tasks=[Task(description="Pay vendor-x €75K", agent=...)],
**audit_kwargs,
)
crew.kickoff()You get:
- A post-hoc audit record for every step (from the callback hook)
- A real pre-execution gate specifically on
send_wire_transfer(from the inline pattern)
Related
- CrewAI integration — why it's audit-only + more context on the workaround
- LangChain integration — real pre-execution gate, comparison example
- Policies — what the policy engine checks
- Human approval — what happens when
authorize()returnspending_approval - Error handling — complete list of AiraError codes
Policy Engine
Aira evaluates every agent action against your policies at authorize() time, before the agent executes. Three modes: rules, AI, and consensus.
Human Approval
Hold high-stakes actions for human review at authorize() time, before the agent acts. The receipt is only minted after approval and execution.