Skip to main content

Policy & Governance

Waxell Observe provides a policy enforcement layer that checks whether an agent is allowed to execute before it runs. Policies are configured on the control plane and evaluated server-side, giving administrators centralized control over agent behavior without modifying agent code.

Getting Started with Governance

New to policies? Start with Policy Categories & Templates to see what's available, then use the Platform Assistant to create your first policy through chat.

How It Works​

  1. Before an agent executes, the client sends a policy check request to the control plane
  2. The control plane evaluates all applicable policies (sorted by priority)
  3. The response contains an action that determines what happens next

Policies are defined across 26 categories -- from cost budgets and content scanning to data access boundaries, cognitive governance, and regulatory compliance. The platform also generates automated recommendations based on your agents' actual behavior.

Policy Actions​

Every policy check returns one of these actions:

ActionMeaningClient Behavior
allowExecution is permittedAgent runs normally
warnExecution is permitted with a warningAgent runs; warning recorded in trace
redactSensitive content maskedContent replaced with ##TYPE## placeholders
throttleRate limitedPolicyViolationError raised
blockExecution deniedPolicyViolationError raised
skipExecution skipped silentlyRun not started, no error raised
retryExecution retriedAutomatic retry with backoff

The PolicyCheckResult object provides convenience properties:

result.action    # "allow", "block", "warn", "throttle", "redact", "skip", "retry"
result.reason # Human-readable explanation
result.metadata # Additional data from the policy engine

result.allowed # True if action is "allow" or "warn"
result.blocked # True if action is "block", "throttle", or "skip"

Pre-Execution Checks​

The recommended pattern: call waxell.init() before importing your LLM SDK, then wrap your agent with @observe(enforce_policy=True). Policies are checked server-side before your function runs, and every LLM call inside is auto-captured.

import waxell_observe as waxell
from waxell_observe.errors import PolicyViolationError

waxell.init() # BEFORE importing the LLM SDK

import openai
client = openai.OpenAI()

@waxell.observe(agent_name="support-bot", enforce_policy=True)
async def handle_query(query: str) -> str:
# Policies checked automatically before this runs
response = client.chat.completions.create( # auto-captured
model="gpt-4o",
messages=[{"role": "user", "content": query}],
)
return response.choices[0].message.content

try:
result = await handle_query("test")
except PolicyViolationError as e:
print(f"Blocked: {e.policy_result.action}")
print(f"Reason: {e.policy_result.reason}")
print(f"Details: {e.policy_result.metadata}")

With LangChain​

from waxell_observe.integrations.langchain import WaxellLangChainHandler
from waxell_observe.errors import PolicyViolationError

handler = WaxellLangChainHandler(
agent_name="langchain-bot",
enforce_policy=True,
)

try:
result = chain.invoke(input, config={"callbacks": [handler]})
handler.flush_sync(result={"output": result})
except PolicyViolationError as e:
print(f"Blocked: {e}")

Mid-Execution Policy Checks​

For long-running agents, check policies between steps to catch budget exhaustion or policy changes during execution:

import waxell_observe as waxell

waxell.init()

@waxell.observe(
agent_name="pipeline-agent",
enforce_policy=True,
mid_execution_governance=True, # Check on each step
)
async def run_pipeline(query: str) -> str:
# Step 1
data = await retrieve(query)
waxell.step("retrieve", output={"sources": len(data)})

# Step 2 -- mid-execution check happens automatically
summary = await summarize(data)
waxell.step("summarize", output={"length": len(summary)})

return summary

Advanced: Manual Mid-Execution Checks with WaxellContext​

For full control over when policy checks happen (e.g., conditional checks based on intermediate state), use WaxellContext directly:

from waxell_observe import WaxellContext

async with WaxellContext(agent_name="pipeline-agent") as ctx:
await step_one()
ctx.record_step("step_one")

# Manual mid-execution check
policy = await ctx.check_policy()
if policy.blocked:
ctx.set_result({"stopped_at": "step_one", "reason": policy.reason})
return

await step_two()
ctx.record_step("step_two")

For most agents, prefer @observe(mid_execution_governance=True) above -- the decorator runs the check automatically on each waxell.step() call.

Approval Workflows​

When a policy blocks execution, you can handle it with human approval instead of failing. Pass on_policy_block to define what happens:

@waxell.observe(
agent_name="data-manager",
workflow_name="delete",
enforce_policy=True,
on_policy_block=waxell.prompt_approval, # terminal Y/N prompt
)
async def delete_records(table: str) -> dict:
return {"deleted": 500, "table": table}

If the human approves, the function executes normally. If denied or timed out, PolicyViolationError propagates to the caller.

Built-in handlers: prompt_approval (terminal), auto_approve (testing), auto_deny (testing). You can also write custom handlers for Slack, webhooks, or any approval channel.

See Approval Workflows for the full guide — custom handlers, ApprovalDecision, and what gets traced.

Disabling Policy Checks​

Skip enforcement in development or testing:

# Decorator
@waxell.observe(agent_name="my-agent", enforce_policy=False)
async def my_function():
...

# Or disable all observability
# export WAXELL_OBSERVE=false

Configuring Policies​

Policies are configured on the Waxell control plane, not in agent code. This separation means:

  • Administrators define what agents are allowed to do
  • Developers write agents that respect those policies automatically
  • Policy changes take effect immediately without redeploying agents

Three ways to configure policies:

  1. Dashboard -- Governance > Policies in the UI
  2. API -- POST /waxell/v1/policies/ (see API Reference)
  3. Platform Assistant -- Ask the AI to create policies through chat (see Platform Assistant)

See Policy Categories & Templates for the full list of available policy types and pre-built templates.

When the control plane is unreachable​

The SDK is fail-open by default: if a policy check cannot complete, the operation proceeds and the run records that governance was skipped. An agent does not stop working because telemetry is down.

Set WAXELL_SURFACE_GATE=closed (or WAXELL_GATE_FAIL_CLOSED=1) to invert that. In fail-closed mode the preventive surfaces — run.start, llm.before, tool.before, network.before, mcp.before, spawn.before, memory.write, memory.delete, code.exec — block instead of allowing, so governance cannot be bypassed by taking the control plane offline. Surfaces that only observe are unaffected: there is nothing to prevent once the call has happened.

export WAXELL_SURFACE_GATE=closed   # compliance-critical deployments

Choose per environment. Fail-closed means a control-plane outage stops your agents; fail-open means an outage leaves them ungoverned. Both are defensible and neither is a safe default for everyone, which is why it is explicit.

Runs Waxell executes for you​

Experiments, the model arena, the prompt playground and version replay run on Waxell's servers rather than in your process, so there is no SDK in the path. They are governed too: the tenant's Content policies are evaluated on the prompt before it reaches the provider and on the response before it reaches you, with the same block / redact / warn actions.

Two differences from the SDK path, both deliberate:

  • Identity. These runs have no agent of yours to attribute to, so they carry a synthetic one — system:model-arena, system:experiment-runner, system:prompt-playground, system:prompt-replay. A policy scoped to a named agent will not apply to them; a policy with no agent scope is global and will. If you want a policy to cover hosted runs specifically, scope it to one of those names.
  • Failure posture. Hosted evaluation runs in the same process as the policy engine, with no network between them, so there is nothing to partition. An evaluation error there is a defect rather than an outage and fails closed — unlike the SDK, which fails open by default for the reason described above.

Evaluators and LLM-as-judge passes are exempt. A judge reads the output it is scoring, which may contain exactly the content a policy targets; redacting underneath it would score text the judge never saw and report a pass.

What an outage costs​

Governance is never the reason an agent is slow. Each in-flight policy check is a single attempt with a short timeout (WAXELL_GATE_TIMEOUT, default 2s). Telemetry calls bound the connect stage separately (WAXELL_CONNECT_TIMEOUT, default 2s) and a refused connection is not retried — nothing is listening, so the ladder would only cost the caller time. After two consecutive connection failures against a host, further calls to it short-circuit for 30 seconds and the first request after that window is allowed through as a probe.

Scoping a policy to sub-agents​

Policies resolve by agent name, and a sub-agent runs under its own name — so a policy scoped to the agent that spawned it does not follow it. Scope by kind instead:

scope_agent_types: ["subagent"]

One policy then applies to every sub-agent in the tenant, regardless of what it is called or which framework spawned it. Policies with no scope continue to apply to everything, parents and sub-agents alike.

Delegation itself is evaluated at the spawn.before surface before the sub-agent starts, which is where spawn-limit, concurrent-spawn, delegation-depth and cross-agent-isolation policies decide. Set inherit_policies: true on a delegation policy to be told when a delegate runs under narrower scope than its parent.

See Multi-Agent for how this applies to LangGraph and deep agents.

Next Steps​