§ Integration · CrewAI

A CrewAI scheduling agent that can't double-book.

Give a CrewAI agent a calendar tool by subclassing crewai.tools.BaseTool over the agentdraft Python SDK. The crew checks availability and commits bookings through AgentDraft's coordination layer, so when a second agent races your crew for the same slot the conflict is resolved at the storage layer and exactly one writer wins — the loser gets a sentence it can act on, not a double-booked human.

Updated


§ 01Install
pip install agentdraft crewai

There is no separate agentdraft-crewai package — CrewAI tools are small enough to own in your own codebase, and the framework-agnostic agentdraft SDK already carries the auth, retries, and conflict semantics. Set AGENTDRAFT_API_KEY to an avs_live_… agent key from the dashboard.


§ 02Two tools, native to CrewAI
from typing import Any, Optional

from crewai.tools import BaseTool
from pydantic import BaseModel, Field

from agentdraft import Client, Conflict


class BookingInput(BaseModel):
    start: str = Field(description="Meeting start, ISO 8601")
    end: str = Field(description="Meeting end, ISO 8601")
    title: Optional[str] = Field(default=None, description="Short meeting title")


class AgentDraftBookingTool(BaseTool):
    name: str = "agentdraft_commit_booking_safe"
    description: str = (
        "Commit a final booking through AgentDraft. If a higher-priority "
        "agent already holds the slot this returns 'OUTRANKED' instead of "
        "raising — propose a different time rather than retrying."
    )
    args_schema: type[BaseModel] = BookingInput
    client: Any = None

    def _run(self, start: str, end: str, title: Optional[str] = None) -> str:
        try:
            booking = self.client.bookings.commit(
                start=_parse(start), end=_parse(end), title=title,
            )
        except Conflict as exc:
            # A lost race is an expected outcome, not a failure.
            return (
                f"OUTRANKED — agent_id={exc.winning_agent_id} "
                f"(priority={exc.winning_agent_priority}) holds this slot. "
                "Call agentdraft_get_availability and propose another time."
            )
        return f"Committed booking {booking.booking_id}."

The companion tool wraps client.availability.list(...) the same way, and returns text rather than objects so the model can read it:

class AvailabilityInput(BaseModel):
    start: str = Field(description="Start of the search window, ISO 8601")
    end: str = Field(description="End of the search window, ISO 8601")
    duration_minutes: int = Field(default=30, description="Meeting length")


class AgentDraftAvailabilityTool(BaseTool):
    name: str = "agentdraft_get_availability"
    description: str = (
        "Get the calendar slots open to this agent between two times. "
        "Call this before proposing or committing a meeting."
    )
    args_schema: type[BaseModel] = AvailabilityInput
    client: Any = None

    def _run(self, start: str, end: str, duration_minutes: int = 30) -> str:
        slots = self.client.availability.list(
            start=_parse(start), end=_parse(end),
            duration_minutes=duration_minutes,
        )
        if not slots:
            return "No open slots in that window. Propose a different range."
        lines = "\n".join(
            f"- {s.start.isoformat()} to {s.end.isoformat()}" for s in slots
        )
        return f"{len(slots)} open slot(s):\n{lines}"

_parse is a three-line helper that swaps a trailing Z for +00:00 before datetime.fromisoformat — an LLM will emit the Z form almost every time, and Python won't accept it before 3.11. The same two tools and a runnable Crew ship as examples/crewai_quickstart.py in the agentdraft source distribution.


§ 03Wire them into a Crew
from crewai import Agent, Crew, Task
from agentdraft import Client

client = Client()                             # reads AGENTDRAFT_API_KEY
tools = [
    AgentDraftAvailabilityTool(client=client),
    AgentDraftBookingTool(client=client),
]

scheduler = Agent(
    role="Scheduler",
    goal="Book meetings without ever double-booking the human",
    backstory="You share a calendar with other autonomous agents.",
    tools=tools,
)
task = Task(
    description="Book a 30-minute call tomorrow at 2pm UTC.",
    expected_output="The booking id, or the alternate slot you chose.",
    agent=scheduler,
)
Crew(agents=[scheduler], tasks=[task]).kickoff()

§ 04Why not just hand CrewAI a LangChain tool?

Because it doesn't work, despite how often it's repeated — and we repeated it here too until we tested it. CrewAI validates Agent(tools=…) against its own crewai.tools.BaseTool, so a langchain_core.tools.BaseTool is rejected by Pydantic before the agent ever runs:

ValidationError: 2 validation errors for Agent
tools.0
  Input should be a valid dictionary or instance of BaseTool
  [type=model_type, input_value=AvailabilityTool(...), input_type=AvailabilityTool]

The documented bridge doesn't span the gap either. Tool.from_langchain(tool) requires a callable .func attribute, which only function-derived tools (StructuredTool, @tool) carry — a hand-written BaseTool subclass implements _run instead and raises ValueError: The provided tool must have a callable 'func' attribute.

Verified against crewai 1.15.20 and langchain-core 1.6.2 on 2026-09-07. AutoGen is the opposite case — its LangChainToolAdapter does accept our agentdraft-langchain tools, so that path stays supported.


§ 05What the coordination layer adds

A CrewAI tool that calls Google Calendar directly works fine for one crew. The moment a second agent — another crew, a founder's assistant, a sales bot — writes to the same calendar, the calendar API has no notion of "the other agent that was about to write here." Both writes succeed and the human gets overlapping events. That's the multi-agent calendar collision .

Routing the booking through AgentDraft resolves the race before it reaches the calendar: one TransactWriteItems with a conditional write per time bucket, so exactly one writer wins deterministically by agent priority. Only the winner reaches Google Calendar. The loser gets a typed 409 naming the winner, its priority, and the audit reference — which the tool above turns into the OUTRANKED sentence the LLM reads.


§ 06

Frequently asked

Can CrewAI use LangChain tools directly?

No. CrewAI validates an agent's tools list against its own crewai.tools.BaseTool, so passing a langchain_core.tools.BaseTool raises a Pydantic ValidationError. Tool.from_langchain() only converts tools that expose a callable .func, which excludes any hand-written BaseTool subclass. Subclass CrewAI's own BaseTool instead — it's about twenty lines over the underlying SDK. Checked against crewai 1.15.20 on 2026-09-07.

Is there an official agentdraft-crewai package?

No, and deliberately so. A CrewAI tool is a thin subclass over the agentdraft Python SDK, and owning those twenty lines in your own repo means your tool descriptions can be tuned to your crew's prompts. Copy examples/crewai_quickstart.py and edit it.

What happens when two CrewAI agents book the same slot?

AgentDraft's conflict engine resolves the race at the storage layer with a single conditional write per time bucket. Exactly one agent commits; the other's tool call returns an OUTRANKED message naming the winning agent and its priority, so the LLM proposes a different time instead of retrying a slot it cannot win.

Do the tools work inside a hierarchical or async Crew?

Yes. The tools hold one agentdraft.Client and make a single HTTP call per invocation, so they're safe to share across agents in a Crew regardless of process. Conflict resolution happens server-side, so two agents in the same crew racing each other resolve exactly as two agents in different crews would.


§ 07Further reading