Files
crewAI/docs/edge/en/learn/step-hooks.mdx
Lucas Gomide 6be349f2e7 docs: prefix hook cross-links with /en so they resolve in edge
The new edge-only hooks pages linked each other with versionless paths
like `/learn/step-hooks`, which mintlify resolves against the frozen
default version where those pages do not exist, breaking the CI link
check. Uses the `/en/learn/...` form the other edge pages already use.
2026-07-14 17:48:53 -03:00

143 lines
4.4 KiB
Plaintext

---
title: Step Hooks
description: Intercept task and flow-method steps with PRE_STEP and POST_STEP hooks in CrewAI
mode: "wide"
---
Step hooks intercept each unit of work inside an execution: every crew **task**
and every **flow method**. Use them to inspect or rewrite what goes into a
step, transform what comes out, or trace step-by-step progress — without
touching the level of individual LLM or tool calls.
## Overview
Two interception points cover steps:
| Point | When | `ctx.payload` |
|-------|------|---------------|
| `PRE_STEP` | Before a task or flow method runs | step input (see below) |
| `POST_STEP` | After a task or flow method runs | step output (see below) |
What the payload holds depends on `ctx.kind`:
| `ctx.kind` | `PRE_STEP` payload | `POST_STEP` payload |
|------------|--------------------|---------------------|
| `"task"` | The context string passed to the agent | The `TaskOutput` object |
| `"flow_method"` | The method's parameters as a `dict` | The method's return value |
For flow methods, positional arguments appear in the params dict under `_0`,
`_1`, ... keys and keyword arguments under their own names; edits and
replacements are mapped back onto the actual call.
## Hook Signature
```python
from crewai.hooks import on, HookAborted, InterceptionPoint
@on(InterceptionPoint.PRE_STEP)
def step_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 step
return None
```
## Context Schema
Both points receive a `StepContext`:
```python
class StepContext(InterceptionContext):
payload: Any # Step input (pre) or step output (post)
kind: str | None # "task" or "flow_method"
step_name: str | None # Task name/description, or flow method name
output: Any # POST_STEP only: same object as payload
agent: Any # Task steps: the executing agent (else None)
agent_role: str | None # Task steps: the agent's role (else None)
task: Any # Task steps: the Task instance (else None)
crew: Any # None for step points
flow: Any # Flow-method steps: the Flow instance (else None)
```
For task steps, `step_name` is the task's `name` (falling back to its
description). For flow-method steps, it is the method name.
## Common Use Cases
### Step Tracing
```python
@on(InterceptionPoint.POST_STEP)
def trace_steps(ctx):
print(f"{ctx.kind} '{ctx.step_name}' finished")
```
### Rewriting Task Context
```python
@on(InterceptionPoint.PRE_STEP)
def inject_disclaimer(ctx):
if ctx.kind != "task":
return None
return f"{ctx.payload}\n\nNote: treat all figures as estimates."
```
### Transforming Task Output
```python
@on(InterceptionPoint.POST_STEP)
def normalize_output(ctx):
if ctx.kind != "task":
return None
ctx.payload.raw = ctx.payload.raw.strip()
```
<Note>
`POST_STEP` runs before the task's output is stored, so rewrites propagate
everywhere the output is used: downstream task context, callbacks, the final
crew output, and the task's `output_file` on disk.
</Note>
### Guarding Flow Methods
```python
@on(InterceptionPoint.PRE_STEP)
def guard_publish(ctx):
if ctx.kind == "flow_method" and ctx.step_name == "publish":
if not ctx.flow.state.get("reviewed"):
raise HookAborted(reason="publish requires review", source="review-gate")
```
### Filtering by Agent
Step hooks support the same `agents=` filter as the other points (matched
against the executing agent's role on task steps):
```python
@on(InterceptionPoint.POST_STEP, agents=["Researcher"])
def log_research_steps(ctx):
print(f"research step done: {ctx.step_name}")
```
## Aborting a Step
Raising `HookAborted` in `PRE_STEP` stops the step before any agent or method
work happens, and the abort propagates out of the execution with its reason —
it is not swallowed. Any other exception raised by a step hook is swallowed
(fail-open), like at every other point.
## Managing Hooks in Tests
```python
from crewai.hooks import clear_all_hooks
clear_all_hooks() # Clears every point, including steps
```
## Related Documentation
- [Execution Hooks Overview →](/en/learn/execution-hooks)
- [Execution Boundary Hooks →](/en/learn/execution-boundary-hooks)
- [LLM Call Hooks →](/en/learn/llm-hooks)
- [Tool Call Hooks →](/en/learn/tool-hooks)