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 sinksRecords emitted governance eventsCompleted paths after event construction
OTel spansProduction observabilityInstrumented paths after span creation

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 decision logs, use audit sinks with external retention and immutability controls where required.

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, reference stack, approval queue).

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

Last updated on

On this page