Lifecycle Callbacks
Edictum provides two lifecycle callbacks on the `Edictum` constructor for reacting to
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: ...,
)| Callback | Signature | When it fires |
|---|---|---|
on_block | (tool_call: ToolCall, reason: str, decision_name: str | None) -> None | A tool call is blocked in enforce mode |
on_allow | (tool_call: ToolCall) -> None | A 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)
| Scenario | on_block | on_allow |
|---|---|---|
| Precondition blocks in enforce mode | Fires | -- |
| Session rule blocks | Fires | -- |
| 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
| Argument | Type | Description |
|---|---|---|
envelope | ToolCall | Full context: tool name, args, principal, side effect, environment |
reason | str | Human-readable block reason from the rule, workflow stage, approval path, or limit |
decision_name | str | None | Rule ID, workflow stage ID, or limit name that caused the block |
on_allow
| Argument | Type | Description |
|---|---|---|
envelope | ToolCall | Full 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
| Feature | Purpose | Fires when |
|---|---|---|
on_block | React to blocks in real time | Pre-execution block (enforce mode) |
on_allow | React to allowed calls in real time | Pre-execution allow |
on_postcondition_warn | Remediate bad tool output | Post-execution postcondition failure |
approval_backend | Human-in-the-loop approval for tool calls | Pre-execution when action: ask fires |
| Audit sinks | Records emitted governance events | Completed paths after event construction |
| OTel spans | Production observability | Instrumented 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:
| Method | Description |
|---|---|
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