CrewAICrewAI 1.15 · Python 3.10 to 3.13
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
32 small wins to finish your pathNext lesson

Refunds need approval: tool hooks

A hook is a function CrewAI calls at a fixed point of every run. A hook before each tool call can block the call, which is how a refund waits for a manager.

The shop's rule from lesson 2: a refund needs a manager's approval. The clerk gets a second tool, and the rule goes in a hook, outside the model, where no prompt can talk its way round it.

Exampletools.py, with refunds
from crewai.tools import tool

ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}


@tool
def lookup_order(order_id: str) -> str:
    """Look up an order's shipping status by its id, such as A17."""
    status = ORDERS.get(order_id)
    return f"{order_id} {status}." if status else f"{order_id} is not an order we have."


@tool
def refund_order(order_id: str) -> str:
    """Refund an order in full. Every refund needs a manager's approval."""
    return f"Refund for {order_id} sent."

refund_order would send the money. Its docstring mentions approval, but a model is free to ignore a docstring.

Examplecrew.py, the clerk with both tools
from crewai import Agent, Crew, Task
from shop_llm import ShopLLM
from tools import lookup_order, refund_order

clerk = Agent(
    role="Order clerk",
    goal="Find the status of customers' orders",
    backstory="You can look up any order in the shop's system.",
    llm=ShopLLM(model="shop"),
    tools=[lookup_order, refund_order],
)
task = Task(description="Answer the customer: {question}",
            expected_output="The order's status in one sentence.", agent=clerk)
crew = Crew(agents=[clerk], tasks=[task])

The clerk can now look an order up or refund it, which is exactly the pair worth stopping before it runs.

A hook before the tool runs

Exampleapproval.py
from crewai.hooks import HookAborted, InterceptionPoint, on

APPROVED = set()


@on(InterceptionPoint.PRE_TOOL_CALL, tools=["refund_order"])
def needs_approval(ctx):
    order_id = ctx.tool_input["order_id"]
    if order_id not in APPROVED:
        raise HookAborted(reason=f"refund for {order_id} needs approval")

@on registers a function for an interception point, here PRE_TOOL_CALL, before a tool runs. tools= limits it to refund_order. The hook receives a context with the tool's name and arguments. Raising HookAborted stops the call; returning normally lets it through.

Example
result = crew.kickoff(inputs={"question": "Please refund order A17."})
print(result.raw)

The model asked for refund_order, the hook refused, and the tool never ran. What the model got back as the tool's result is Tool execution blocked by hook and the tool's name. The reason you gave is not in it, although the hooks page says the reason propagates; in 1.15.22 the model is not told why.

After a manager says yes

Example
APPROVED.add("A17")
result = crew.kickoff(inputs={"question": "Please refund order A17."})
print(result.raw)

The same crew, the same question, and this time the refund ran. The rule lives in your code, so a manager's approval is a change to APPROVED, not to a prompt. Lesson 24 shows a flow that pauses until a person answers.

Where a hook can step in
allowedblockedThe agent loopmodel and toolsPRE_MODEL_CALLbefore each callThe modeldecidesPOST_MODEL_CALLafter each callPRE_TOOL_CALLapproval gaterefund_ordersends the money
Hover or tap a piece to see what it is and which lesson built it.
Trace a refund

Pick one to watch it run, step by step.

The four points a hook can sit at, around the loop from lesson 9. Two of them are this lesson's; the other two are the next lesson's.

Hooks are global
A hook registered with @on stays registered for every crew in the process, as a listener does. Running the cell that defines one a second time registers it again, and it then runs twice per call. In a notebook, run clear_all_hooks() from crewai.hooks first.
Try it yourself
  • Ask to refund C40 after approving only A17.
  • Remove tools=["refund_order"] from @on and ask about A17's status.
  • Print ctx.agent_role inside the hook.

You understood something today that you didn't yesterday.