CrewAI Integration
Require human approval in multi-agent CrewAI workflows before sensitive tasks execute.
CrewAI is a multi-agent framework where specialised agents collaborate on tasks — each with a role, a goal, and a backstory that shapes how it behaves. Adding The Handover means your crew pauses at sensitive steps and waits for a human to sign off before continuing.
How it works
Three concepts to know:
- Agent — a specialised worker. Has a role, goal, backstory, and a list of tools it can call.
- Task — a unit of work assigned to an agent. Defines what to do and what a completed output looks like.
- Crew — the team. Runs agents and tasks in sequence (or hierarchically) and returns the final result.
The approval tool lives on an agent. When that agent decides an action needs sign-off, it calls the tool — which sends the request to The Handover and blocks until the approver responds.
Installation
pip install crewai requests
Step 1: Define the approval tool
Use BaseTool from crewai.tools with a Pydantic args_schema. The schema is how the agent understands what to pass — without it, the model has to guess from the description alone.
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Literal, Type
import requests, time
class ApprovalInput(BaseModel):
action: str = Field(description="What you want to do — be specific")
context: str = Field(description="Why, and any relevant background")
urgency: Literal["low", "medium", "high", "critical"] = Field(
default="medium", description="How time-sensitive this is"
)
class HumanApprovalTool(BaseTool):
name: str = "Request Human Approval"
description: str = (
"Request human approval before taking a sensitive or irreversible action. "
"Use for financial transactions, customer emails, data deletion, "
"deployments, or anything a human should review before it happens."
)
args_schema: Type[BaseModel] = ApprovalInput
api_key: str = "ho_your_key_here" # from your dashboard
approver: str = "approver@yourcompany.com"
def _run(self, action: str, context: str, urgency: str = "medium") -> str:
headers = {"Authorization": f"Bearer {self.api_key}"}
# Create the decision — approver gets an email with Approve/Deny buttons
resp = requests.post("https://thehandover.xyz/decisions", headers=headers, json={
"action": action, "context": context,
"urgency": urgency, "approver": self.approver,
})
resp.raise_for_status()
did = resp.json()["id"]
# Poll every 5 seconds for up to 10 minutes
for _ in range(120):
time.sleep(5)
r = requests.get(f"https://thehandover.xyz/decisions/{did}", headers=headers).json()
if r["status"] != "pending":
notes = r.get("response_notes") or "none"
return f"Decision: {r['status']}. Approver notes: {notes}"
return "Approval timed out — no response within 10 minutes. Do not proceed."
Step 2: Create an agent with the tool
Assign the tool to any agent that handles consequential actions. The backstory is what shapes the agent's judgment — put your approval rules there, not just in the task description.
from crewai import Agent
approval_tool = HumanApprovalTool()
finance_agent = Agent(
role="Finance Operations Agent",
goal="Process refunds and payments accurately and safely",
backstory=(
"You handle financial operations on behalf of the company. "
"Before issuing any refund, processing any payment, or modifying any account balance, "
"you MUST call Request Human Approval first. Do not proceed without an approved decision."
),
tools=[approval_tool],
verbose=True,
)
Step 3: Define a task and run the crew
from crewai import Task, Crew, Process
task = Task(
description="Process the refund request for customer #4821 (€340).",
expected_output="A confirmation message stating whether the refund was approved and processed.",
agent=finance_agent,
)
crew = Crew(
agents=[finance_agent],
tasks=[task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result)
Multi-agent example
In a real crew you typically separate analysis from action. Give the approval tool only to agents that take action — not to analytical or planning agents. This keeps the crew fast while protecting what matters.
from crewai import Agent, Task, Crew, Process
approval_tool = HumanApprovalTool()
# Analytical agent — no approval tool needed, it only researches
analyst = Agent(
role="Customer Account Analyst",
goal="Research customer accounts and summarise relevant history",
backstory="You review account data and flag issues, but you never take any actions yourself.",
verbose=True,
)
# Action agent — has the approval tool, must use it before acting
operations = Agent(
role="Operations Agent",
goal="Execute approved account actions safely",
backstory=(
"You carry out account actions based on the analyst's research. "
"You MUST call Request Human Approval before any refund, account change, or deletion. "
"Never proceed if the decision comes back as denied."
),
tools=[approval_tool],
verbose=True,
)
research_task = Task(
description="Look up customer #4821 and summarise their account status and refund eligibility.",
expected_output="A summary of the customer's account status and whether a refund is warranted.",
agent=analyst,
)
action_task = Task(
description="Based on the analyst's findings, process the refund if warranted. Get human approval first.",
expected_output="Confirmation that the refund was approved and processed, or a reason it was not.",
agent=operations,
)
crew = Crew(
agents=[analyst, operations],
tasks=[research_task, action_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result)
Writing an effective backstory
The backstory is the most important lever for controlling when an agent calls the approval tool. Vague backstories lead to the agent skipping the tool. Be explicit:
- Name the actions: "any refund", "any email to a customer", "any record deletion"
- Use MUST: "you MUST call Request Human Approval" is more reliable than "you should"
- State consequences: "Do not proceed if the decision is denied" closes the loophole of the agent continuing anyway
- Reinforce in the task: Mention approval in the task description too — agents consider both when deciding what to do
Production tip: use a callback instead of polling
Polling inside the tool blocks the agent for up to 10 minutes per approval. For workflows where approvals could take longer, pass a callback_url instead:
requests.post("https://thehandover.xyz/decisions", headers=headers, json={
"action": action, "context": context, "urgency": urgency,
"approver": self.approver,
"callback_url": "https://your-server.com/webhook/handover",
})
Your webhook fires the instant the approver clicks. See the webhooks docs for the payload format.
Ready to add human oversight?
Free to start. No credit card required. Five minutes to integrate.
Get Started Free