Rules and circuit breaker
Control risky agent actions before they run.
The circuit breaker puts your rules in front of an action. Just before your agent does something risky, such as issuing a refund, it asks Guard "may I do this?". Guard checks your organisation's rules and answers one of four things:
| Answer | Meaning |
|---|---|
| Allow | Go ahead |
| Block | Refused, with a reason your agent can show |
| Human approval | Wait until a person approves or rejects it |
| Throttle | Slow down: told how many seconds to wait |
You set the rules in the dashboard under Govern → Rules, or through the API. Your agent only needs the SDK.
Create a rule in the dashboard
Open Govern → Rules. The page has two tabs: Rules, which lists your rules, and Enforcement Activity, which shows what your rules have done (see Enforcement Activity). Only admins can create, change or delete rules. Other roles can see them.
Click New rule and fill in the form:
| Field | What to enter |
|---|---|
| Name | A name for the rule |
| Applies to | All agents, or One specific agent, then choose the agent. An agent appears in the list after it sends its first trace. |
| When | What to measure (see the table below) |
| Operator | greater than, greater or equal, less than, less or equal, equal, or not equal |
| Threshold | The limit, as a number |
| Parameter name or Call or function name | Shown only for the metrics that need it |
| Period (minutes) | Shown for the cost and token period metrics. 1 to 10080. |
| Then | Block, Throttle or Human approval |
| Calls allowed per minute | For Throttle only. 1 to 100000. |
| Approval timeout (seconds) | For Human approval only. 10 to 86400. Default 300. If nobody decides in time, the action does not run. |
| Message to the agent | Optional. Shown to the agent as the reason. |
| Dry run | Log matches but never block, throttle or ask for approval. Off by default. |
Click Save rule. A rule that is not valid is refused, with the reason shown next to the field. An organisation can have up to 100 rules, and when you reach the limit you need to delete unused ones.
To limit every call from an agent, for example with a throttle, use the condition "Tokens in one call is greater or equal to 0". The form has a Use that condition shortcut for it.
Enforcement Activity
The Enforcement Activity tab on Govern → Rules is the history of what the circuit breaker did: every block, throttle and approval request, plus every dry-run match. The newest events are first, 50 to a page, with Previous and Next buttons. The list refreshes on its own.
Each event is one card:
- Outcome badge: Block, Throttle or Human approval.
- What happened, for example "Refund limit blocked support_agent's process_refund".
- Time of the event.
- The measurement, for example "Amount in the action was 50000 (threshold 10000)".
- The reason the agent was given.
- View approval, on approval events, which opens that approval.
Dry-run matches carry a DRY RUN label, are dimmed and use a dashed border. They read "would have blocked", because the agent was not stopped.
Two kinds of event are not caused by one of your rules, so they have no rule or measurement:
- Agent paused: the agent was blocked because it is paused. The card has View agent. If the agent is still paused, an admin sees a Resume button on the card. If not, the card says "This agent is no longer paused."
- Approval limit reached: a new approval request was blocked because too many were already waiting (10 per agent, 50 per organisation). Decide or wait for the pending ones. See Approvals.
Use the filters above the list to narrow it:
| Filter | Choices |
|---|---|
| Agent ID | Type an agent ID |
| Outcome | All outcomes, Block, Throttle or Human approval |
| Rule | All rules, one of your rules, "Agent paused (not a rule)" or "Approval limit reached (not a rule)" |
| Hide dry-run matches | Tick to show only real enforcement |
Rows are not counts of calls
Throttle and pause events are sampled: at most one per minute per rule, however many calls were refused. The number of rows is not the number of refused calls.
Rules
A rule says: when this measurement crosses this limit, do this, for these agents.
| Part | Values |
|---|---|
| Metric | action_amount, single_call_tokens, single_call_cost, daily_cost, hourly_tokens, repeat_call_count, custom_field |
| Operator | gt, gte, lt, lte, eq, neq |
| Threshold | A number, not negative |
| Action | block, human_approval, throttle |
| Scope | all_agents or specific_agent |
| Dry run | On: log the match but never enforce it |
What each metric reads, and its label in the When list:
action_amount("Amount in the action"): the number your code passes in, such as a refund amount. Most rules use this.single_call_tokens("Tokens in one call"),single_call_cost("Cost of one call (USD)"): the size of one call.daily_cost("Cost over a period (USD)"): money the agent spent over a period. The default period is 24 hours (1440 minutes).hourly_tokens("Tokens over a period"): tokens the agent used over a period. The default period is 1 hour (60 minutes).repeat_call_count("Same call repeated"): how many times the same value has been seen, which catches loops. It needs the call or function name.custom_field("A value from the action's parameters"): a value from your own parameters. It needs the parameter name.
Bad rules are refused when you save them (HTTP 422, with the reasons).
Try a rule safely first
Turn on dry run to see what a rule would have done without enforcing it. Dry-run matches are logged, never blocked, throttled or sent for approval. You can see them in Enforcement Activity.
How a decision is made
- Pause first. A paused agent is blocked with rule
agent_paused, whatever the rules say. - All rules are checked, not just the first match. A broken rule is skipped and never breaks the others.
- Most restrictive wins: block, then human approval, then throttle. Equal strength: the smaller rule id wins, so the result never depends on the order rules were created.
- Throttle counts only when nothing stronger applied, so a blocked call spends no rate-limit quota.
- No match means allow.
Protect a function
The simplest way is @guard.tool. It asks Guard before the function runs and handles the answer for you:
from impraint_guard import Guard, ImpraintGuardBlocked
guard = Guard(api_key="...", agent_id="support_agent")
@guard.tool(amount="refund_amount")
def process_refund(order_id: str, refund_amount: float):
... # runs only if the rules allow it
try:
process_refund("A-100", 50000)
except ImpraintGuardBlocked as e:
print(e.decision.rule_name, e.decision.reason)It works on normal and async def functions, bare (@guard.tool) or with options.
| Guard answers | What the decorator does |
|---|---|
| Allow | The function runs |
| Block | The function does not run. Raises ImpraintGuardBlocked, or returns an error dict with on_block="return". |
| Throttle | Waits the time Guard asks for and tries once more. Still over: raises ImpraintGuardThrottled. |
| Human approval | Waits for the person. Rejected or no answer in time: raises ImpraintGuardApprovalRejected. |
| Guard unreachable | fail_open decides (see below) |
All three exceptions share the base class ImpraintGuardDenied, which carries .decision and .as_result() (an error dict you can return to a caller).
| Option | Default | Meaning |
|---|---|---|
name | function name | The action name your rules see |
amount | none | Which argument to send as action_amount |
on_block | "raise" | "raise" or "return" |
wait_on_throttle | True | Wait and retry once when rate limited |
max_wait_seconds | 10 | Longest throttle wait accepted |
approval_timeout | the rule's | Seconds to wait for a person |
Arguments are made safe before they are sent: text is PII-scrubbed and cut to 200 characters, arguments in scrub_fields become [REDACTED], and other objects are sent as just their type name.
To also trace the function itself, stack the two decorators:
@guard.track(action_type=ActionType.TOOL_CALL)
@guard.tool(amount="refund_amount")
def process_refund(order_id, refund_amount): ...Inside a @guard.track function, each decision appears in the trace as one circuit_breaker step.
Ask Guard yourself
Use guard.intercept when you want to handle the answer in your own code:
decision = guard.intercept("process_refund", {"action_amount": amount})
if decision.is_blocked:
return {"error": decision.reason}
if decision.needs_approval:
approved = guard.wait_for_approval(decision.approval_id, decision.timeout_seconds)
if not approved:
return {"error": "Rejected or timed out."}
if decision.is_throttled:
time.sleep(decision.retry_after_seconds)
# otherwise: go aheadIn an async app (FastAPI, async LangGraph), use the non-blocking versions so your event loop is never frozen:
| Sync | Async |
|---|---|
guard.intercept(...) | await guard.intercept_async(...) |
guard.wait_for_approval(...) | await guard.wait_for_approval_async(...) |
guard.shutdown() | await guard.shutdown_async() |
Use one Guard per event loop.
Pause an agent
Pausing is the emergency stop. Every call from that agent is blocked on its next request, whatever the rules say, and no approval request is created. It affects only that agent, and only calls that go through Guard's checks (@guard.tool or intercept). Other calls are not affected.
Only admins can pause or resume. Use the Pause or Resume button on the agent's card in the agents list, or on the agent's own page. Pausing asks you to confirm first. A paused agent stays paused until someone resumes it, and an admin can also resume it from an "Agent paused" event in Enforcement Activity.
Throttle
A throttle rule lets an agent call at most N times per minute (Calls allowed per minute in the form, throttle_rpm in the API). Over the limit, Guard does not refuse for good. It tells the agent how many seconds to wait:
decision = guard.intercept(
"lookup", {"action_amount": 1},
wait_on_throttle=True, max_wait_seconds=10,
)
if decision.is_throttled: # still over the limit after waiting
return {"error": "Rate limited, try again later."}By default intercept only reports the throttle. With wait_on_throttle=True the SDK waits and asks once more. Each agent has its own count.
Human approval
A Human approval rule holds the call until a person approves or rejects it in the dashboard (Govern → Approvals) or Slack. Only an admin of the same organisation can decide, and the first decision is final. The agent waits 300 seconds by default, or the time you set in Approval timeout on the rule (approval_timeout_seconds in the API). If nobody answers in time, the approval becomes timed_out and the call is not allowed.
See Approvals for how to link Slack, who can decide, flood limits and what is masked.
If Guard cannot be reached
If rules cannot be loaded from the usual place, Guard falls back to its stored copy, so a budget rule is not switched off by an outage. If Guard cannot be reached at all, your fail_open setting decides: True allows the action (the default, so the agent keeps working) and False blocks it. Teams that prefer safety over uptime should set fail_open=False.
Audit log
Every rule change, API key change, login, approval decision, and pause or resume is written to a tamper-evident audit log. Each entry is chained to the one before it, so editing or deleting an entry is detectable.
Known limits in beta
- Only checked calls are protected. A tool that forgets
@guard.toolorinterceptruns with no checks, and pausing the agent does not stop it. - Rules match on numbers only. You cannot yet write "block the tool
delete_database" or "block if the text contains X". - A missing or non-numeric amount means
action_amountis not sent, so a rule on it cannot match. A warning is logged and the call goes through unless another rule applies. - Throttle windows reset every minute, so an agent can make up to twice the limit across a minute boundary.
- Budget rules can trip a few seconds late, because spend is counted after the trace is processed.
- Waiting for a throttle in sync code blocks the thread for up to 10 seconds. Use
wait_on_throttle=Falseto fail at once. - Enforcement Activity is sampled for throttle and pause events, so its row count is not a count of refused calls.
check_contenthas no async version yet.