Skip to content

About guardrails

Guardrails put governance around AI usage. They set a hard ceiling on what a user, an agent, or an app can spend, and on how often it can run. UC AI enforces this ceiling around every agent run.

Guardrails are part of the paid UC AI Pro layer. They install next to core UC AI and start automatically. Your agents and your generate_text calls need no change.

FeatureCapsAnswers
BudgetMonetary cost (priced from token usage)“This app can spend at most $100 this month”
Rate limitingCounts — runs, tool calls, concurrency”This user can start at most 100 runs per hour”

Both features share the same scope and audience vocabulary. Either one can express “anonymous visitors get a little, logged-in users get more”. See the recipe.

Use them together. Budgets stop runaway cost. Rate limits stop runaway frequency: abuse, retry storms, and infinite tool loops.

Core UC AI gives a generic execution hook: a package named UC_AI_HOOK. If this package is present and valid, UC AI calls it around every top-level execute_agent run, and before every tool call. In Pro, UC_AI_HOOK is a dispatcher. It sends each callback to every enabled feature in uc_ai_pro_hook_subscribers.

uc_ai_agents_api.execute_agent
│ before_execution / after_execution
UC_AI_HOOK (Pro dispatcher)
├──► UC_AI_RATELIMIT_HOOK (sort_order 50)
└──► UC_AI_BUDGET_HOOK (sort_order 100)
uc_ai_tools_api (inside the tool-calling loop)
│ before_tool_call
UC_AI_HOOK ──► subscribers flagged tool_hook = 'Y' (rate limiting)

Each feature reads the token counts, the identity, and the timestamps directly from the public uc_ai_agent_executions table. The Pro layer keeps only its own configuration, and a cost ledger for budgets. At the start of a run, core writes the class of the caller to uc_ai_agent_executions.audience as public, authenticated, or db. This column lets the guardrails separate anonymous traffic from signed-in traffic.

  • before_execution runs before a run starts. A guardrail raises an error here to veto the run. This is a hard cap, and the run spends no tokens.
  • before_tool_call runs before each tool call. When a run reaches a tool-call cap, rate limiting raises an error here and stops the run in mid-flight.
  • after_execution runs when a run finishes. Budgets use it to price the finished execution tree into the cost ledger. This step is best-effort. It never turns a completed run into a failure.

Both features share the same governance vocabulary.

Every limit targets a scope — what it counts, and in which bucket:

scope_typescope_valueCounts
global(null)Every execution in the schema
userthe created_by valueRuns by one named user (APEX user, else DB user)
agentan agent codeRuns of one agent
appan APEX application idRuns from one APEX app
apex_session(must be null)Runs in the APEX session of the calling visitor. One bucket for each browser session
each_user(must be null)Runs by the calling user. One row gives each user a separate bucket

The first four are static. You name the bucket in scope_value. The last two are per-caller. One configuration row then creates a separate bucket for each visitor or user. For these two, scope_value must be null. The database rejects any other value.

UC AI evaluates every limit that matches a run. The strictest outcome wins.

Every limit also targets an audience — which callers it governs:

audienceGoverns
any (default)Every caller
publicAn anonymous APEX visitor (a session that is not authenticated)
authenticatedA logged-in APEX user
dbA database session (SQL client, scheduler job, or trigger), or the internal session of uc_ai

audience selects which callers a limit governs. For every value except any, it also restricts what the limit counts to that same class of caller. A public limit therefore counts only anonymous usage. Signed-in traffic can never consume an anonymous allowance, and anonymous traffic can never consume a signed-in allowance.

For each run, UC AI reads the class of the caller from the live APEX session. It asks APEX whether the session is authenticated. A username cannot answer this question, because every anonymous visitor of an application shares one APEX public user name, nobody by default. With scope_type = 'user', all these visitors share one bucket.

enforcement_modeBehavior when the limit is reached
hardDeny — raise an exception and abort the run / tool call
softWarn — log a warning, allow the run to proceed
advisoryTrack only — never warn or deny

warn_threshold_pct (default 80) sets when a limit starts to warn. At the default, the limit warns at 80% of the cap and denies at 100%.

Recipe: public visitors a little, logged-in users more

Section titled “Recipe: public visitors a little, logged-in users more”

The standard setup for an AI feature on a page that anonymous visitors can reach takes three rows:

-- 1. Each anonymous visitor: 3 runs per browser session per day
insert into uc_ai_rate_limits (
code, description, audience, scope_type,
limit_type, limit_count, window_kind, period, enforcement_mode
) values (
'PUBLIC_SESSION_DAY', 'Anonymous: 3 runs/session/day', 'public', 'apex_session',
'AGENT_EXEC', 3, 'calendar', 'day', 'hard'
);
-- 2. Backstop, NOT optional: a visitor can clear cookies for a fresh session and
-- a fresh quota, so also cap anonymous traffic in aggregate.
insert into uc_ai_rate_limits (
code, description, audience, scope_type, scope_value,
limit_type, limit_count, window_kind, period, enforcement_mode
) values (
'PUBLIC_APP_DAY', 'Anonymous: 500 runs/day across the app', 'public', 'app', '512',
'AGENT_EXEC', 500, 'calendar', 'day', 'hard'
);
-- 3. Logged-in users: 100 runs EACH per day — one row covers everybody
insert into uc_ai_rate_limits (
code, description, audience, scope_type,
limit_type, limit_count, window_kind, period, enforcement_mode
) values (
'AUTH_USER_DAY', 'Logged in: 100 runs/user/day', 'authenticated', 'each_user',
'AGENT_EXEC', 100, 'calendar', 'day', 'hard'
);
commit;

Row 2 has audience = 'public', so its 500 counts only anonymous runs. Your employees in the same app cannot exhaust the allowance of the visitors. The visitors cannot exhaust the allowance of the company.

The same three shapes work for budgets with limit_amount in place of limit_count. Examples are $0.10 for each anonymous session per day, and $5 for each logged-in user per day.

  • Do not use each_user for anonymous traffic. All anonymous visitors share one created_by. A per-user bucket therefore becomes a shared cap, and the first visitor of the day locks out everybody else. The database rejects each_user together with audience = 'public'. Use apex_session instead.
  • apex_session cannot use period = 'total'. APEX recycles its session ids. An all-time bucket therefore gives a new visitor an exhausted quota, and the cause is hard to find. The database rejects this combination also.
  • Database and job callers resolve to db. An audience = 'any' limit governs them. An each_user limit governs them also, in a bucket for their database username. An apex_session limit never applies to them.
  • UC AI evaluates the audience again before every tool call. A tool that opens its own APEX session can therefore change the tier inside a run.
  • The check-then-act race still applies. Two runs that start at the same moment can both pass, because neither row exists yet. A hard cap of N can therefore allow N+1 for a short time. Small per-visitor caps show this effect more than coarse limits. If simultaneous requests matter, add a CONCURRENCY limit.

Core UC AI must already be installed in the schema. Then run the Pro installer:

@ext/uc-ai-pro/install_uc_ai_pro.sql

The script installs the UC_AI_HOOK dispatcher and the subscriber registry. It then installs each feature: budget and rate limiting. A new database session activates the hook automatically. No per-session registration is necessary.