本页还没有译文
本页 32 段还没译成简体中文,下面显示的是英文原文。Execution Boundary Hooks
还没翻译译自英文(docs.crewai.com)本页 32 段都还没译
Execution boundary hooks intercept the outermost edges of a run — before any work starts, when inputs are resolved, when the final result is ready, and when the execution finishes. They fire for both crews and flows and are the right place for run-level policy checks, input rewriting, and output sanitization.
Overview
Four interception points cover the boundaries:
| Point | When | ctx.payload |
|---|---|---|
EXECUTION_START | A crew or flow is about to begin | inputs dict |
INPUT | Resolved inputs for the execution | inputs dict |
OUTPUT | The final result is ready | the output object |
EXECUTION_END | The execution has finished (success or failure) | the output object, or None on failure |
For a crew, the output payload is a CrewOutput. For a flow, it is the final
flow-method result.
Hook Signature
from crewai.hooks import on, HookAborted, InterceptionPoint
@on(InterceptionPoint.EXECUTION_START)
def boundary_hook(ctx) -> Any | None:
# Mutate ctx.payload in place, or
# return a non-None value to replace it, or
# raise HookAborted(reason, source) to stop the run
return NoneBoundary hooks follow the standard contract: proceed (return None), mutate in
place, replace by returning, or abort by raising
HookAborted. An abort at any
boundary propagates out of kickoff() with its reason.
Context Schema
Each point receives a typed context. All contexts share the base fields:
class InterceptionContext:
payload: Any # The interceptable value (see table above)
agent: Any = None # Not populated at execution boundaries
agent_role: str | None # Not populated at execution boundaries
task: Any = None # Not populated at execution boundaries
crew: Any = None # The Crew instance (crew runs only)
flow: Any = None # The Flow instance (flow runs only)The per-point contexts add a named alias for the payload:
class ExecutionStartContext(InterceptionContext):
inputs: dict # Same dict as payload
class InputContext(InterceptionContext):
inputs: dict # Same dict as payload
class OutputContext(InterceptionContext):
output: Any # The output object
class ExecutionEndContext(InterceptionContext):
output: Any # The output object (None when status == "failed")
status: str # "completed" or "failed"
error: BaseException | None # The exception when status == "failed"Note
ctx.inputs aliases the original inputs dict, so in-place edits through
either name are equivalent. If an earlier hook replaced the payload by
returning a new dict, only ctx.payload is rebound — always read and write
ctx.payload when hooks might chain.
Crew Runs vs. Flow Runs
Boundary hooks fire on both runtimes, and crew execution internally rides on a
flow runtime. During a crew.kickoff(), a global boundary hook therefore fires
for the crew boundary (ctx.crew set, ctx.flow None) and for the
internal flow (ctx.flow set, ctx.crew None). Discriminate by runtime:
@on(InterceptionPoint.OUTPUT)
def crew_output_only(ctx):
if ctx.crew is None:
return None # Skip the internal flow (or a bare flow)
ctx.payload.raw = ctx.payload.raw.strip()Common Use Cases
Policy Check at Start
@on(InterceptionPoint.EXECUTION_START)
def enforce_policy(ctx):
if ctx.crew is not None and not ctx.payload.get("authorized"):
raise HookAborted(reason="unauthorized execution", source="access-control")Input Rewriting
@on(InterceptionPoint.INPUT)
def add_defaults(ctx):
if ctx.crew is None:
return None
ctx.payload.setdefault("locale", "en-US")
ctx.payload["topic"] = ctx.payload["topic"].strip().lower()Rewritten inputs flow into task interpolation, so the run behaves as if it was kicked off with the modified dict.
Prefer INPUT for rewriting and treat EXECUTION_START as the allow/deny
gate. Rewrites at EXECUTION_START are still honored — on crews they also
feed the before_kickoff callbacks; on flows they land exactly like an
INPUT rewrite.
Output Sanitization
import re
@on(InterceptionPoint.OUTPUT)
def redact_emails(ctx):
if ctx.crew is None:
return None
ctx.payload.raw = re.sub(
r"\b[\w.+-]+@[\w-]+\.[\w.]+\b", "[EMAIL-REDACTED]", ctx.payload.raw
)OUTPUT runs before EXECUTION_END, and both see the (possibly replaced)
payload from earlier hooks; the final rewritten value is what kickoff()
returns.
Observing Failures
EXECUTION_END fires exactly once per execution, on success and on failure
alike. When the run raises — a task error, a flow-method exception, or a
HookAborted from an earlier point — the hook receives status="failed" with
the exception in ctx.error, and the original exception still propagates out
of kickoff() unchanged:
@on(InterceptionPoint.EXECUTION_END)
def report_outcome(ctx):
if ctx.status == "failed":
notify_policy_engine(status="failed", error=repr(ctx.error))
else:
notify_policy_engine(status="completed")Two caveats: EXECUTION_END does not fire when EXECUTION_START never
dispatched (an abort at start means the boundary never opened, so there is no
end to pair), and raising HookAborted from a failure-path EXECUTION_END
dispatch is ignored — there is nothing left to abort, and the original error
wins.
Ordering
For a crew run the boundary order is:
EXECUTION_START → before_kickoff callbacks → INPUT → tasks execute → OUTPUT → EXECUTION_END
For a flow run, the boundary hooks resolve the inputs before the lifecycle events begin:
EXECUTION_START → INPUT → FlowStartedEvent → flow methods execute → OUTPUT → EXECUTION_END → FlowFinishedEvent
FlowStartedEvent carries the hook-resolved inputs, and rewriting
inputs["id"] in a boundary hook redirects state restoration. An abort at
EXECUTION_START still surfaces as FlowStartedEvent followed by
FlowFailedEvent, emitted at the abort with the payload as resolved by the
hooks that ran before it.
Hooks at the same point run in registration order, global hooks first, then
crew-scoped hooks. Telemetry (HookDispatchedEvent) is emitted per dispatch.
Managing Hooks in Tests
from crewai.hooks import clear_all_hooks
clear_all_hooks() # Clears every point, including boundaries