CrewAI spending limit, one ceiling per crew run
Pythoncrewai 1.15.22Two hooks that ask AgentBill before each model call your crew makes and settle after it, with the job's task_ref. When the call would pass the job's ceiling, the hook raises HookAborted, CrewAI does not make the call, and kickoff() raises to your code.
Use the limits CrewAI ships
A CrewAI agent takes max_iter, max_rpm and max_execution_time, and a crew takes max_rpm. They count iterations, requests per minute and seconds. Use them.
The ceiling below counts something else: the units you assign to each model call, for one job. Crew-wide means one task_ref, passed by every agent in the crew and by every other crew or process working on the same job. Nothing groups the calls for you. The task_ref you pass is the grouping.
Install
pip install agentbill-sdk "crewai==1.15.22"
Give the job its ceiling before the crew runs, in the console or with PUT /tasks/:task_ref/ceiling. The samples on this page were run end to end against crewai 1.15.22 and agentbill-sdk 0.6.5, with a stub LLM in place of a provider.
The hooks
import contextvars import os from agentbill import AgentBillClient, TaskCeilingExceededError from crewai.hooks import HookAborted, InterceptionPoint, on client = AgentBillClient(api_key=os.environ["AGENTBILL_API_KEY"]) UNITS = 12 # what one model call is worth to you, in your own units JOB = contextvars.ContextVar("agentbill_task_ref", default=None) # the job's name @on(InterceptionPoint.PRE_MODEL_CALL) def ask_agentbill(ctx): job = JOB.get() if job is None: raise HookAborted(reason="no AgentBill task_ref is set", source="agentbill") role = getattr(ctx.agent, "role", None) or "crewai" try: client.preflight(agent_id=role, task_ref=job, estimated_units=UNITS) except TaskCeilingExceededError as e: raise HookAborted(reason=str(e), source="agentbill") from e @on(InterceptionPoint.POST_MODEL_CALL) def settle_agentbill(ctx): role = getattr(ctx.agent, "role", None) or "crewai" client.record(agent_id=role, task_ref=JOB.get(), units=UNITS)
Why the hook raises HookAborted
CrewAI runs these hooks fail-open. An exception other than HookAborted raised in a hook is caught, printed when the crew is verbose, and the model call goes ahead (crewai/hooks/dispatch.py in crewai 1.15.22). So a TaskCeilingExceededError left to escape the hook would change nothing: in a run with that version of the hook, every call after the ceiling was spent still reached the model. The hook turns the refusal into HookAborted, which CrewAI does raise to the caller, and it does the same when no job is set, rather than let a call through unasked.
The same rule covers AgentBill being unreachable. A network error inside the hook is an ordinary exception, so CrewAI prints it and makes the call. If you want the call refused instead, catch it in the hook and raise HookAborted.
Run a crew, and decide what a refusal means
JOB.set("job-142")
try:
result = crew.kickoff()
except HookAborted as e:
# Your code decides. e.reason is AgentBill's sentence, and on a ceiling
# refusal e.__cause__ is the TaskCeilingExceededError with the numbers.
print(e.reason)crew is your crew. Set JOB in the code that calls kickoff(); the hooks read it where the model call runs.
Several crews or processes on one job
The job's budget is one row, keyed on your account and the task_ref, with no clock on it. Every reservation is one conditional UPDATE on that row, so crews running at once, in one process or in several, cannot both be approved against the last units. Set the same JOB in each.
Settling, and what a crash does
The POST_MODEL_CALL hook settles the units preflight reserved, once the model has answered. When a model call raises instead, nothing settles it, and the reservation is held until it expires, 60 minutes by default, and then its units come back. The same happens when the process dies between the two hooks. In that window the job's ceiling is tighter, never looser.
What it does not do
- It does not count tokens. UNITS is the number you assign to one model call.
- It does not meter the provider or read its bill.
- It does not end the crew. Preflight answers, the hook raises, and your code decides.
- A refusal from AgentBill's own monthly quota is returned, not raised, so these hooks let that call run.