Edictum
Reference

Lifecycle Callbacks

Edictum provides two lifecycle callbacks on the `Edictum` constructor for reacting to

AI Assistance

Right page if: you need to react to allow/block decisions in real time using `on_block`, `on_allow`, or `on_postcondition_warn` callbacks, or configure a HITL `approval_backend`. Wrong page if: you need persistent decision logs -- see https://docs.edictum.ai/docs/reference/audit-sinks. For production observability with OTel, see https://docs.edictum.ai/docs/reference/telemetry. Gotcha: `on_block` does not fire in per-rule observe mode (the call is allowed through, so `on_allow` fires instead). If `action: ask` fires but no `approval_backend` is configured, the pipeline raises `EdictumDenied` immediately.

Edictum provides two lifecycle callbacks on the Edictum constructor for reacting to allow/block decisions in real time. Unlike postcondition findings, which fire after tool execution, these callbacks fire before the tool runs -- at the moment the pipeline decides whether to allow or block.

Signatures

guard = Edictum(
    rules=[...],
    on_block=lambda tool_call, reason, decision_name: ...,
    on_allow=lambda envelope: ...,
)
CallbackSignatureWhen it fires
on_block(tool_call: ToolCall, reason: str, decision_name: str | None) -> NoneA tool call is blocked in enforce mode
on_allow(tool_call: ToolCall) -> NoneA tool call passes all pre-execution checks

Both callbacks are sync. If a callback raises an exception, it is caught and logged -- the pipeline continues normally.

When They Fire (and Don't)

Scenarioon_blockon_allow
Precondition blocks in enforce modeFires--
Session rule blocksFires--
Limit exceeded (max_attempts, max_tool_calls)Fires--
All checks pass--Fires
Per-rule observe mode converts block to allow--Fires
Approval granted (action: ask)--Fires
Approval blocked or timed out (action: ask)Fires--
Postcondition warns after execution----

on_block does not fire in per-rule observe mode. In per-rule observe mode, the call is allowed through (with a CALL_WOULD_DENY audit event), so on_allow fires instead.

In global observe mode (mode: observe on the Edictum instance), neither on_block nor on_allow fires on would-block paths. The pipeline emits a CALL_WOULD_DENY audit event but skips both callbacks.

Use Cases

Real-time alerting

React to blocks immediately instead of parsing audit logs after the fact:

def alert_on_block(tool_call, reason, decision_name):
    slack.post(f"BLOCKED {tool_call.tool_name}: {reason} (decision: {decision_name})")

guard = Edictum.from_yaml("rules.yaml", on_block=alert_on_block)

Metrics and dashboards

Track allow/block rates without OTel infrastructure:

from prometheus_client import Counter

blocked = Counter("edictum_blocked_total", "Blocked tool calls", ["tool", "decision"])
allowed = Counter("edictum_allowed_total", "Allowed tool calls", ["tool"])

guard = Edictum(
    rules=[...],
    on_block=lambda env, reason, name: blocked.labels(tool=env.tool_name, decision=name or "").inc(),
    on_allow=lambda env: allowed.labels(tool=env.tool_name).inc(),
)

Circuit breaker

Disable the agent after too many blocks in a window:

blocked_count = 0

def circuit_breaker(tool_call, reason, decision_name):
    global blocked_count
    blocked_count += 1
    if blocked_count > 10:
        raise SystemExit("Agent stuck in a block loop -- shutting down")

guard = Edictum(rules=[...], on_block=circuit_breaker)

Development debugging

Print block events to the console during development:

guard = Edictum(
    rules=[...],
    on_block=lambda env, reason, name: print(f"BLOCKED {env.tool_name}: {reason} [{name}]"),
    on_allow=lambda env: print(f"ALLOWED {env.tool_name}"),
)

Callback Arguments

on_block

ArgumentTypeDescription
envelopeToolCallFull context: tool name, args, principal, side effect, environment
reasonstrHuman-readable block reason from the rule, workflow stage, approval path, or limit
decision_namestr | NoneRule ID, workflow stage ID, or limit name that caused the block

on_allow

ArgumentTypeDescription
envelopeToolCallFull context: tool name, args, principal, side effect, environment

Works With All Entry Points

The callbacks are available on every way to create an Edictum instance:

# Constructor
guard = Edictum(rules=[...], on_block=my_handler, on_allow=my_handler)

# YAML
guard = Edictum.from_yaml("rules.yaml", on_block=my_handler, on_allow=my_handler)

# Template
guard = Edictum.from_template("file-agent", on_block=my_handler, on_allow=my_handler)

# Merged guards (inherits from first guard)
merged = Edictum.from_multiple([guard1, guard2])

from_multiple() merges rulesets from all supplied guards, including observe-mode rulesets. Prior to edictum 1.10, observe-mode rulesets were silently dropped during the merge — they are now preserved.

All 8 Adapters

Lifecycle callbacks fire in every adapter -- they are invoked by the run() method and adapters that call it. This means the same on_block / on_allow functions work regardless of which framework you use.

Relationship to Other Features

FeaturePurposeFires when
on_blockReact to blocks in real timePre-execution block (enforce mode)
on_allowReact to allowed calls in real timePre-execution allow
on_postcondition_warnRemediate bad tool outputPost-execution postcondition failure
approval_backendHuman-in-the-loop approval for tool callsPre-execution when action: ask fires
Audit sinksPersistent record of all decisionsEvery decision (allow, block, execute, fail)
OTel spansProduction observabilityEvery decision (with full trace context)

Lifecycle callbacks are the lightweight, zero-dependency option for users who need real-time reactions without setting up audit sink parsing or OTel infrastructure. For production observability at scale, use OTel. For persistent decision logs, use audit sinks.

Approval Backend

The approval_backend parameter enables human-in-the-loop approval workflows. When a precondition with action: ask fires, the pipeline pauses and delegates to the configured backend.

from edictum import Edictum, LocalApprovalBackend

guard = Edictum.from_yaml(
    "rules.yaml",
    approval_backend=LocalApprovalBackend(),
)

The ApprovalBackend protocol requires two async methods:

MethodDescription
request_approval(tool_name, tool_args, message, *, timeout, timeout_action, principal)Creates an approval request and returns an ApprovalRequest
wait_for_decision(approval_id, timeout)Blocks until the request is approved, blocked, or times out. Returns an ApprovalDecision

LocalApprovalBackend prompts on stdout and reads from stdin -- suitable for local development and testing. For production use, implement ApprovalBackend with your own backend (Slack bot, web control plane, approval queue).

If action: ask fires but no approval_backend is configured, the pipeline raises EdictumDenied immediately.

Last updated on

On this page