Skip to content

Commit b8cb5dc

Browse files
committed
docs: group execution hooks and document all hook contexts
The hooks pages sat flat in the learn nav and only documented the LLM and tool call contexts, with examples built on the legacy decorators. Groups them under a collapsible "Execution Hooks" section, adds `step-hooks` and `execution-boundary-hooks` pages covering every hook context from source, reworks the LLM and tool pages to lead with `@on` while keeping the decorators, and links the orphaned `before-and-after-kickoff-hooks` page into the group.
1 parent 0e5d0ec commit b8cb5dc

6 files changed

Lines changed: 694 additions & 782 deletions

File tree

docs/docs.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,17 @@
373373
"edge/en/learn/replay-tasks-from-latest-crew-kickoff",
374374
"edge/en/learn/sequential-process",
375375
"edge/en/learn/using-annotations",
376-
"edge/en/learn/execution-hooks",
377-
"edge/en/learn/llm-hooks",
378-
"edge/en/learn/tool-hooks"
376+
{
377+
"group": "Execution Hooks",
378+
"pages": [
379+
"edge/en/learn/execution-hooks",
380+
"edge/en/learn/llm-hooks",
381+
"edge/en/learn/tool-hooks",
382+
"edge/en/learn/execution-boundary-hooks",
383+
"edge/en/learn/step-hooks",
384+
"edge/en/learn/before-and-after-kickoff-hooks"
385+
]
386+
}
379387
]
380388
},
381389
{
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
---
2+
title: Execution Boundary Hooks
3+
description: Intercept the start, inputs, output, and end of crew and flow executions with the @on decorator
4+
mode: "wide"
5+
---
6+
7+
Execution boundary hooks intercept the outermost edges of a run — before any
8+
work starts, when inputs are resolved, when the final result is ready, and when
9+
the execution finishes. They fire for both crews and flows and are the right
10+
place for run-level policy checks, input rewriting, and output sanitization.
11+
12+
## Overview
13+
14+
Four interception points cover the boundaries:
15+
16+
| Point | When | `ctx.payload` |
17+
|-------|------|---------------|
18+
| `EXECUTION_START` | A crew or flow is about to begin | inputs `dict` |
19+
| `INPUT` | Resolved inputs for the execution | inputs `dict` |
20+
| `OUTPUT` | The final result is ready | the output object |
21+
| `EXECUTION_END` | The execution has finished | the output object |
22+
23+
For a crew, the output payload is a `CrewOutput`. For a flow, it is the final
24+
flow-method result.
25+
26+
## Hook Signature
27+
28+
```python
29+
from crewai.hooks import on, HookAborted, InterceptionPoint
30+
31+
@on(InterceptionPoint.EXECUTION_START)
32+
def boundary_hook(ctx) -> Any | None:
33+
# Mutate ctx.payload in place, or
34+
# return a non-None value to replace it, or
35+
# raise HookAborted(reason, source) to stop the run
36+
return None
37+
```
38+
39+
Boundary hooks follow the standard contract: proceed (`return None`), mutate in
40+
place, replace by returning, or abort by raising
41+
[`HookAborted`](/learn/execution-hooks#aborting-an-operation). An abort at any
42+
boundary propagates out of `kickoff()` with its reason.
43+
44+
## Context Schema
45+
46+
Each point receives a typed context. All contexts share the base fields:
47+
48+
```python
49+
class InterceptionContext:
50+
payload: Any # The interceptable value (see table above)
51+
agent: Any = None # Not populated at execution boundaries
52+
agent_role: str | None # Not populated at execution boundaries
53+
task: Any = None # Not populated at execution boundaries
54+
crew: Any = None # The Crew instance (crew runs only)
55+
flow: Any = None # The Flow instance (flow runs only)
56+
```
57+
58+
The per-point contexts add a named alias for the payload:
59+
60+
```python
61+
class ExecutionStartContext(InterceptionContext):
62+
inputs: dict # Same dict as payload
63+
64+
class InputContext(InterceptionContext):
65+
inputs: dict # Same dict as payload
66+
67+
class OutputContext(InterceptionContext):
68+
output: Any # The output object
69+
70+
class ExecutionEndContext(InterceptionContext):
71+
output: Any # The output object
72+
```
73+
74+
<Note>
75+
`ctx.inputs` aliases the **original** inputs dict, so in-place edits through
76+
either name are equivalent. If an earlier hook *replaced* the payload by
77+
returning a new dict, only `ctx.payload` is rebound — always read and write
78+
`ctx.payload` when hooks might chain.
79+
</Note>
80+
81+
## Crew Runs vs. Flow Runs
82+
83+
Boundary hooks fire on both runtimes, and crew execution internally rides on a
84+
flow runtime. During a `crew.kickoff()`, a global boundary hook therefore fires
85+
for the crew boundary (`ctx.crew` set, `ctx.flow` `None`) **and** for the
86+
internal flow (`ctx.flow` set, `ctx.crew` `None`). Discriminate by runtime:
87+
88+
```python
89+
@on(InterceptionPoint.OUTPUT)
90+
def crew_output_only(ctx):
91+
if ctx.crew is None:
92+
return None # Skip the internal flow (or a bare flow)
93+
ctx.payload.raw = ctx.payload.raw.strip()
94+
```
95+
96+
## Common Use Cases
97+
98+
### Policy Check at Start
99+
100+
```python
101+
@on(InterceptionPoint.EXECUTION_START)
102+
def enforce_policy(ctx):
103+
if ctx.crew is not None and not ctx.payload.get("authorized"):
104+
raise HookAborted(reason="unauthorized execution", source="access-control")
105+
```
106+
107+
### Input Rewriting
108+
109+
```python
110+
@on(InterceptionPoint.INPUT)
111+
def add_defaults(ctx):
112+
if ctx.crew is None:
113+
return None
114+
ctx.payload.setdefault("locale", "en-US")
115+
ctx.payload["topic"] = ctx.payload["topic"].strip().lower()
116+
```
117+
118+
Rewritten inputs flow into task interpolation, so the run behaves as if it was
119+
kicked off with the modified dict.
120+
121+
### Output Sanitization
122+
123+
```python
124+
import re
125+
126+
@on(InterceptionPoint.OUTPUT)
127+
def redact_emails(ctx):
128+
if ctx.crew is None:
129+
return None
130+
ctx.payload.raw = re.sub(
131+
r"\b[\w.+-]+@[\w-]+\.[\w.]+\b", "[EMAIL-REDACTED]", ctx.payload.raw
132+
)
133+
```
134+
135+
`OUTPUT` runs before `EXECUTION_END`, and both see the (possibly replaced)
136+
payload from earlier hooks; the final rewritten value is what `kickoff()`
137+
returns.
138+
139+
## Ordering
140+
141+
For a crew run the boundary order is:
142+
143+
```
144+
EXECUTION_START → before_kickoff callbacks → INPUT → tasks execute → OUTPUT → EXECUTION_END
145+
```
146+
147+
Hooks at the same point run in registration order, global hooks first, then
148+
crew-scoped hooks. Telemetry (`HookDispatchedEvent`) is emitted per dispatch.
149+
150+
## Managing Hooks in Tests
151+
152+
```python
153+
from crewai.hooks import clear_all_hooks
154+
155+
clear_all_hooks() # Clears every point, including boundaries
156+
```
157+
158+
## Related Documentation
159+
160+
- [Execution Hooks Overview →](/learn/execution-hooks)
161+
- [Step Hooks →](/learn/step-hooks)
162+
- [LLM Call Hooks →](/learn/llm-hooks)
163+
- [Tool Call Hooks →](/learn/tool-hooks)

docs/edge/en/learn/execution-hooks.mdx

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ from crewai.hooks import on, InterceptionPoint
6464

6565
@on(InterceptionPoint.POST_TOOL_CALL, agents=["researcher"], tools=["web_search"])
6666
def log_search_results(ctx):
67-
print(f"search returned: {str(ctx.payload)[:80]}")
67+
print(f"search returned: {(ctx.tool_result or '')[:80]}")
6868
```
6969

7070
Applied to a method inside a `@CrewBase` class, `@on` registers a
@@ -84,30 +84,36 @@ class MyProjCrew:
8484

8585
## Interception point catalog
8686

87-
`payload` is the value a hook may mutate or replace at each point.
87+
Each family has a detailed guide covering its context schema, payload
88+
semantics, and examples.
8889

89-
### Execution boundaries
90+
### [Execution boundaries](/learn/execution-boundary-hooks)
9091

91-
| Point | When | `payload` |
92-
|-------|------|-----------|
92+
| Point | When | `ctx.payload` |
93+
|-------|------|---------------|
9394
| `EXECUTION_START` | A crew or flow is about to begin | inputs `dict` |
9495
| `INPUT` | Resolved inputs for the execution | inputs `dict` |
9596
| `OUTPUT` | Final result is ready | the output object |
9697
| `EXECUTION_END` | A crew or flow has finished | the output object |
9798

98-
### Model & tool boundaries
99+
### [Model boundaries](/learn/llm-hooks) & [tool boundaries](/learn/tool-hooks)
99100

100-
| Point | When | `payload` |
101-
|-------|------|-----------|
101+
| Point | When | Hook receives |
102+
|-------|------|---------------|
102103
| `PRE_MODEL_CALL` | Before an LLM call | `LLMCallHookContext` |
103-
| `POST_MODEL_CALL` | After an LLM call | response |
104+
| `POST_MODEL_CALL` | After an LLM call | `LLMCallHookContext` (with `response` set) |
104105
| `PRE_TOOL_CALL` | Before a tool runs | `ToolCallHookContext` |
105-
| `POST_TOOL_CALL` | After a tool runs | tool result |
106+
| `POST_TOOL_CALL` | After a tool runs | `ToolCallHookContext` (with results set) |
106107

107-
### Step points
108+
At these four points the hook receives the rich legacy context **directly** as
109+
its argument — there is no separate `ctx.payload`. Mutate `ctx.messages` /
110+
`ctx.tool_input` in place, and return a string from a post hook to replace the
111+
response / tool result.
108112

109-
| Point | When | `payload` |
110-
|-------|------|-----------|
113+
### [Step points](/learn/step-hooks)
114+
115+
| Point | When | `ctx.payload` |
116+
|-------|------|---------------|
111117
| `PRE_STEP` | Before a task or flow-method step | step input |
112118
| `POST_STEP` | After a task or flow-method step | step output |
113119

@@ -147,12 +153,12 @@ def enforce_policy(ctx):
147153
@on(InterceptionPoint.PRE_TOOL_CALL)
148154
def block_dangerous_tools(ctx):
149155
dangerous = {"delete_file", "drop_table", "system_shutdown"}
150-
if ctx.payload.tool_name in dangerous:
151-
raise HookAborted(reason=f"{ctx.payload.tool_name} is blocked", source="safety-policy")
156+
if ctx.tool_name in dangerous:
157+
raise HookAborted(reason=f"{ctx.tool_name} is blocked", source="safety-policy")
152158

153159
@on(InterceptionPoint.PRE_MODEL_CALL)
154160
def iteration_limit(ctx):
155-
if ctx.payload.iterations > 15:
161+
if ctx.iterations > 15:
156162
raise HookAborted(reason="maximum iterations exceeded", source="loop-guard")
157163
```
158164

@@ -161,8 +167,8 @@ def iteration_limit(ctx):
161167
```python
162168
@on(InterceptionPoint.PRE_TOOL_CALL, tools=["send_email", "make_payment"])
163169
def require_approval(ctx):
164-
response = ctx.payload.request_human_input(
165-
prompt=f"Approve {ctx.payload.tool_name}?",
170+
response = ctx.request_human_input(
171+
prompt=f"Approve {ctx.tool_name}?",
166172
default_message="Type 'yes' to approve:",
167173
)
168174
if response.lower() != "yes":
@@ -171,8 +177,8 @@ def require_approval(ctx):
171177

172178
### Sanitizing outputs
173179

174-
A non-`None` return value replaces the payload, so transformations are plain
175-
return statements:
180+
A non-`None` return value replaces the interceptable value, so transformations
181+
are plain return statements:
176182

177183
```python
178184
import re
@@ -182,7 +188,7 @@ def redact_keys(ctx):
182188
return re.sub(
183189
r'(api[_-]?key)["\']?\s*[:=]\s*["\']?[\w-]+',
184190
r"\1: [REDACTED]",
185-
ctx.payload,
191+
ctx.response,
186192
flags=re.IGNORECASE,
187193
)
188194
```
@@ -254,10 +260,11 @@ Differences from `@on`:
254260
- They cover **only** the four model/tool points — no execution boundaries, no
255261
steps.
256262
- Blocking is `return False`, with no abort reason or source attached.
257-
- They receive rich point-specific contexts — `LLMCallHookContext` (with full
258-
executor access) and `ToolCallHookContext` — the same objects `@on` exposes
259-
as `ctx.payload` at the `PRE_MODEL_CALL` / `PRE_TOOL_CALL` points.
260-
- Crew-scoping uses the `*_crew` decorator variants inside `@CrewBase` classes.
263+
- They receive the same rich contexts — `LLMCallHookContext` (with full
264+
executor access) and `ToolCallHookContext` — that `@on` hooks receive at the
265+
model/tool points.
266+
- Crew-scoping works the same way: apply the decorator to a method inside a
267+
`@CrewBase` class.
261268
- They support the same `agents=` / `tools=` filters.
262269

263270
You might still prefer them for existing codebases that already use

0 commit comments

Comments
 (0)