CrewAI

In this guide you'll give a DevOps crew a provision_gpus tool that asks a human before spending real money. Python, about ten minutes.

Before you start

  1. A Confirm account and API key. Sign in, open Dashboard → API Keys, create a key, and copy it (it's shown once).
  2. An Anthropic API key (CrewAI talks to it through LiteLLM).
  3. Python 3.10+.
terminal
pip install crewai requests
.env / shell
export CONFIRM_API_KEY=cfm_live_...
export ANTHROPIC_API_KEY=sk-ant-...

Step 1: the approval helper

Same helper as every integration: create the request, poll until a human decides. Copy it as-is (it's identical to the LangChain guide's file, reuse it if you have it).

confirm_client.py
import os
import time
import requests

BASE = "https://confirm.dev/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CONFIRM_API_KEY']}"}


def require_approval(summary: str, payload: dict, notify: str, ttl_minutes: int = 60, **fields) -> dict:
    """Create an approval request and block until a human decides."""
    res = requests.post(
        f"{BASE}/requests",
        headers=HEADERS,
        json={"summary": summary, "payload": payload, "notify": notify,
              "ttlMinutes": ttl_minutes, **fields},  # reasoning, recentActions, agentState
        timeout=30,
    )
    res.raise_for_status()
    request = res.json()

    while True:
        time.sleep(5)
        current = requests.get(
            f"{BASE}/requests/{request['id']}", headers=HEADERS, timeout=30
        ).json()
        if current["status"] != "PENDING":
            return current

Step 2: the gated tool and the crew

CrewAI tools subclass BaseTool. The description is what the agent reads, so it says plainly that approval takes time and denial is a real outcome.

crew.py
from crewai import Agent, Crew, Task, LLM
from crewai.tools import BaseTool
from pydantic import BaseModel, Field

from confirm_client import require_approval


class ProvisionInput(BaseModel):
    instance_type: str = Field(description="e.g. a100-80gb")
    count: int = Field(description="Number of instances")


class ProvisionGPUs(BaseTool):
    name: str = "provision_gpus"
    description: str = (
        "Provision GPU instances. Requests above $500/day require human approval: "
        "the tool blocks until someone decides, and may return a denial with a reason. "
        "Do not retry a denied request."
    )
    args_schema: type[BaseModel] = ProvisionInput

    def _run(self, instance_type: str, count: int) -> str:
        daily_cost = estimate_daily_cost(instance_type, count)  # your pricing logic
        if daily_cost <= 500:
            return provision(instance_type, count)              # your real provisioning

        verdict = require_approval(
            summary=f"Provision {count} x {instance_type} (~${daily_cost:,.0f}/day)",
            payload={"instance": instance_type, "count": count, "dailyCost": daily_cost},
            notify="group:platform",  # or "vp-eng@company.com" on Hobby
            reasoning="The research team's eval run needs this capacity for about a week.",
            recentActions=["Read the task brief", f"Estimated cost: ${daily_cost:,.0f}/day"],
        )
        if verdict["status"] != "APPROVED":
            note = verdict.get("resolutionNote") or verdict["status"].lower()
            return f"Denied by human reviewer: {note}"

        approved = verdict["effectivePayload"]  # the human may have lowered the count
        return provision(approved["instance"], approved["count"])


infra_agent = Agent(
    role="DevOps engineer",
    goal="Provision the compute the team needs, at reasonable cost",
    backstory="You manage cloud infrastructure and respect spending controls.",
    tools=[ProvisionGPUs()],
    llm=LLM(model="anthropic/claude-opus-4-8"),
)

task = Task(
    description="The research team needs 20 A100 80GB instances for a week-long eval run.",
    expected_output="A report of what was provisioned, or why it wasn't.",
    agent=infra_agent,
)

crew = Crew(agents=[infra_agent], tasks=[task])
print(crew.kickoff())

Step 3: run it and edit the request down

terminal
python crew.py

What happens:

  1. The agent calls provision_gpus with 20 instances. That's well over $500/day.
  2. The crew pauses mid-task while the tool polls Confirm.
  3. The approver's email arrives with the summary and the daily cost. Open it.
  4. Click Edit payload first, change "count": 20 to "count": 8, note "start smaller, scale if needed", and approve.
  5. The tool executes effectivePayload: 8 instances, not 20. The crew's final report reflects exactly what was actually provisioned.
A crew task blocks while its tool waits, so keep ttl_minutes short for interactive runs. For long-running crews, give the task a generous execution timeout or route approval-gated steps through a queue and resume on the webhook.

Next steps

  • Approval groups: send infra spend to the platform team, not one inbox.
  • Webhooks for event-driven flows.
  • The API reference for metadata: attach crew and task IDs so the audit log ties back to your runs.