Cold Leads

Cold-Leads-Tools für CrewAI- und LangChain-Agenten

Für wen
Python-Entwickler, die Agenten mit CrewAI oder LangChain bauen
Das Problem
Ein Agent, den man nach Kontaktdaten fragt, rät, und eine geratene Adresse sieht genauso aus wie eine echte. Ohne ein Prüf-Tool können weder der Agent noch die Person, die seine Ausgabe liest, den Unterschied erkennen.
Die Lösung
Geben Sie dem Agenten drei eng gefasste Tools auf Basis der Cold-Leads-API: das eigene CRM des Nutzers durchsuchen, eine Adresse prüfen und eine Adresse aus Name und Domain erraten, mit einer ehrlichen Angabe der method. Fehler kommen als Daten zurück, sodass der Agent reagieren kann, statt abzustürzen.
Was Sie bekommen
Ein gemeinsamer API-Client, Tool-Klassen für CrewAI und für LangChain sowie eine minimale Crew und ein minimaler Agent, die sie nutzen.

Adressen, IDs und Ergebnisse in den Beispielen sind illustrativ. example.com ist für Dokumentation reserviert, eine echte Prüfung dieser Adressen liefert daher invalid.

Die Codebeispiele sind in allen Sprachen gleich, ihre Kommentare sind auf Englisch.

Die drei Tools

ToolEndpunktKostenWas der Agent erhält
search_crm_contactsGET /api/v1/leadsKostenlosKontakte, die bereits in Ihrem Cold-Leads-CRM sind, zu einer Firmendomain (bis zu 50), mit Phase, Tags, Prüfergebnis und do_not_contact.
verify_emailPOST /api/v1/verify1 Creditstatus, score, reasons, mx, catch_all, disposable und role.
find_emailPOST /api/v1/find1 CreditDie wahrscheinlichste Adresse, method (verified, pattern oder none), confidence und candidates.

Cold Leads hat keine Personen- oder Firmendatenbank. search_crm_contacts kann keine neuen Interessenten entdecken; es liefert nur Kontakte, die der Nutzer schon hat. Schreiben Sie das in die Tool-Beschreibung, sonst erwartet das Modell etwas anderes.

Ein gemeinsamer Client

Beide Frameworks nutzen denselben kleinen Client. Er gibt jede Antwort als JSON-String zurück und macht aus API-Fehlern (no_credits, rate_limited, bad_api_key) Daten, die das Modell lesen kann, statt eine Exception auszulösen.

coldleads_tools.py
# coldleads_tools.py - shared by the CrewAI and LangChain examples (pip install httpx)
import json
import os

import httpx

VERIFY_DESC = (
    "Verify one e-mail address with Cold Leads (costs 1 credit). Returns status valid, risky or invalid, "
    "a score from 0 to 100 and reason codes. The last reason says what was checked: ok means a mail server "
    "accepted the mailbox, smtp_unreachable means the mailbox itself was not checked, catch_all means the "
    "domain accepts any address, mailbox_missing, no_mx and syntax mean the address is invalid."
)
FIND_DESC = (
    "Guess a person's work e-mail address from first name, last name and company domain (costs 1 credit). "
    "Returns the most likely address, method (verified only when a mail server confirmed the mailbox, "
    "otherwise pattern), confidence from 0 to 100 and candidates. Treat a pattern result as unconfirmed."
)
SEARCH_DESC = (
    "Search the contacts already saved in the user's own Cold Leads CRM for a company domain (free). "
    "role is an optional keyword matched against name, e-mail local part, tags, notes and contact type. "
    "This is not a people database: it only returns contacts the user already has. "
    "Never e-mail contacts whose do_not_contact is true."
)


class ColdLeadsClient:
    def __init__(self, api_key=None, base_url="https://coldleads.app/api/v1"):
        self.http = httpx.Client(
            base_url=base_url,
            headers={"x-api-key": api_key or os.environ["COLDLEADS_API_KEY"]},
            timeout=40.0,
        )

    def _call(self, method, path, **kwargs) -> str:
        r = self.http.request(method, path, **kwargs)
        try:
            data = r.json()
        except ValueError:
            data = {}
        if r.status_code >= 400:
            # hand API errors (no_credits, rate_limited with retry_after_seconds, ...) to the model as data
            return json.dumps({"http_status": r.status_code, "error": f"http_{r.status_code}", **data})
        return json.dumps(data)

    def verify(self, email: str) -> str:
        return self._call("POST", "/verify", json={"email": email, "timeout_ms": 10000})

    def find(self, first: str, last: str, domain: str) -> str:
        return self._call("POST", "/find", json={"first": first, "last": last, "domain": domain})

    def search_crm(self, domain: str, role: str = "", limit: int = 10) -> str:
        params = {"domain": domain, "limit": limit}
        if role:
            params["role"] = role
        return self._call("GET", "/leads", params=params)

CrewAI

crewai_coldleads.py
# pip install crewai httpx   (CrewAI needs Python 3.10-3.13)
from crewai import LLM, Agent, Crew, Task
from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, Field

from coldleads_tools import FIND_DESC, SEARCH_DESC, VERIFY_DESC, ColdLeadsClient

client = ColdLeadsClient()
KEY = [EnvVar(name="COLDLEADS_API_KEY", description="Cold Leads secret API key (sk_...)", required=True)]


class VerifyInput(BaseModel):
    email: str = Field(..., description="E-mail address to verify, e.g. anna@example.com")


class FindInput(BaseModel):
    first: str = Field(..., description="First name")
    last: str = Field(..., description="Last name")
    domain: str = Field(..., description="Company domain, e.g. example.com")


class SearchInput(BaseModel):
    domain: str = Field(..., description="Company domain, e.g. example.com")
    role: str = Field("", description="Optional keyword such as ceo, sales or marketing")
    limit: int = Field(10, ge=1, le=50, description="Maximum number of contacts")


class VerifyEmailTool(BaseTool):
    name: str = "verify_email"
    description: str = VERIFY_DESC
    args_schema: type[BaseModel] = VerifyInput
    env_vars: list[EnvVar] = KEY

    def _run(self, email: str) -> str:
        return client.verify(email)


class FindEmailTool(BaseTool):
    name: str = "find_email"
    description: str = FIND_DESC
    args_schema: type[BaseModel] = FindInput
    env_vars: list[EnvVar] = KEY

    def _run(self, first: str, last: str, domain: str) -> str:
        return client.find(first, last, domain)


class SearchCrmTool(BaseTool):
    name: str = "search_crm_contacts"
    description: str = SEARCH_DESC
    args_schema: type[BaseModel] = SearchInput
    env_vars: list[EnvVar] = KEY

    def _run(self, domain: str, role: str = "", limit: int = 10) -> str:
        return client.search_crm(domain, role, limit)


researcher = Agent(
    role="B2B outreach researcher",
    goal="Prepare a short list of contacts whose addresses were checked with Cold Leads",
    backstory="You never invent contacts or addresses and you report what each check actually confirmed.",
    tools=[SearchCrmTool(), VerifyEmailTool(), FindEmailTool()],
    llm=LLM(model="anthropic/claude-opus-5-5", max_tokens=4096),  # any tool-calling model CrewAI supports
)
task = Task(
    description=(
        "Find the contacts at {domain} in my Cold Leads CRM whose role keyword matches {role}. "
        "Skip anyone with do_not_contact true, verify the others and report status and the last reason code."
    ),
    expected_output="A table with e-mail, name, status, score and last reason code.",
    agent=researcher,
)

if __name__ == "__main__":
    result = Crew(agents=[researcher], tasks=[task]).kickoff(inputs={"domain": "example.com", "role": "cto"})
    print(result.raw)
  • Jedes Tool ist eine Unterklasse von crewai.tools.BaseTool mit name, description, args_schema (ein Pydantic-Modell) und _run. env_vars deklariert den Schlüssel, den das Tool braucht; der Client selbst liest COLDLEADS_API_KEY aus der Umgebung.
  • Tools werden dem Agenten als Instanzen übergeben, und {domain} und {role} im Task werden aus kickoff(inputs=...) befüllt.
  • CrewAI braucht Python 3.10 bis 3.13. Die llm-Zeile nutzt Claude Opus 5.5; jedes Modell mit Tool-Calling, das CrewAI unterstützt, funktioniert.

LangChain

langchain_coldleads.py
# pip install -U "langchain[anthropic]" httpx   (LangChain 1.x, Python 3.10+)
from langchain.agents import create_agent
from langchain.tools import tool
from pydantic import BaseModel, Field

from coldleads_tools import FIND_DESC, SEARCH_DESC, VERIFY_DESC, ColdLeadsClient

client = ColdLeadsClient()


class VerifyInput(BaseModel):
    email: str = Field(description="E-mail address to verify, e.g. anna@example.com")


class FindInput(BaseModel):
    first: str = Field(description="First name")
    last: str = Field(description="Last name")
    domain: str = Field(description="Company domain, e.g. example.com")


class SearchInput(BaseModel):
    domain: str = Field(description="Company domain, e.g. example.com")
    role: str = Field(default="", description="Optional keyword such as ceo, sales or marketing")
    limit: int = Field(default=10, ge=1, le=50, description="Maximum number of contacts")


@tool("verify_email", description=VERIFY_DESC, args_schema=VerifyInput)
def verify_email(email: str) -> str:
    return client.verify(email)


@tool("find_email", description=FIND_DESC, args_schema=FindInput)
def find_email(first: str, last: str, domain: str) -> str:
    return client.find(first, last, domain)


@tool("search_crm_contacts", description=SEARCH_DESC, args_schema=SearchInput)
def search_crm_contacts(domain: str, role: str = "", limit: int = 10) -> str:
    return client.search_crm(domain, role, limit)


agent = create_agent(
    model="anthropic:claude-opus-5-5",  # any tool-calling chat model in provider:model form
    tools=[search_crm_contacts, verify_email, find_email],
    system_prompt=(
        "You help with B2B outreach research. Use only the Cold Leads tools for contact data and never invent "
        "addresses. For every verification, report the status and the last reason code."
    ),
)

if __name__ == "__main__":
    result = agent.invoke({"messages": [{"role": "user", "content": "List my CRM contacts at example.com tagged cto and verify their addresses."}]})
    final = result["messages"][-1]
    print("".join(block["text"] for block in final.content_blocks if block["type"] == "text"))
  • Die Tools nutzen den Decorator @tool aus langchain.tools mit explizitem name, description und args_schema.
  • create_agent aus langchain.agents führt die Tool-Schleife aus. Es ersetzt create_react_agent aus LangGraph, das veraltet ist.
  • Das Modell wird als provider:model angegeben. Die letzte Nachricht kann mehrere Content-Blöcke enthalten, daher gibt das Beispiel nur ihre Textblöcke aus.

Beschreibungen, mit denen das Modell arbeiten kann

  • Nennen Sie die Kosten in der Beschreibung (1 Credit für verify_email und find_email, kostenlos für die CRM-Suche), damit das Modell weiß, welche Aufrufe Credits verbrauchen.
  • Erklären Sie den letzten Grundcode: ok heißt, ein Mailserver hat das Postfach akzeptiert, smtp_unreachable heißt, das Postfach wurde nicht geprüft, catch_all heißt, die Domain akzeptiert jede Adresse.
  • Schreiben Sie, dass ein find_email-Ergebnis mit method pattern eine Vermutung ist und dass Kontakte mit do_not_contact true keine E-Mail erhalten dürfen.
  • Halten Sie die Tools eng gefasst. Ein Agent, der nur suchen, prüfen und raten kann, kann nicht versehentlich E-Mails senden oder Datensätze ändern.

Limits und Kosten

  • verify_email und find_email kosten 1 Credit pro Aufruf, auch für Ergebnisse aus dem 30-Tage-Cache; die CRM-Suche ist kostenlos.
  • Der Client fordert ein Prüfbudget von 10 Sekunden an; läuft es ab, ist das Ergebnis risky mit dem Grund timeout und wird nicht zwischengespeichert.
  • 120 Anfragen pro Minute und Schlüssel. Eine Agentenschleife, die viele Adressen prüft, kann dieses Limit erreichen; der Fehler rate_limited, den sie dann erhält, enthält retry_after_seconds.
  • Die API ist Teil des Business-Tarifs: $99 im Monat mit 10.000 Credits im Monat; zusätzliche Pakete zu 1.000 Credits kosten $5.

FAQ

Kann der Agent neue Interessenten in einem Unternehmen finden?

Nein. Die CRM-Suche liefert nur Kontakte, die bereits in Ihrem Cold-Leads-Arbeitsbereich sind, und die Adresssuche braucht den Namen einer Person. Cold Leads stellt keine Personendaten bereit.

Warum geben die Tools Fehler zurück, statt Exceptions auszulösen?

Eine Exception in einem Tool kann den Agentenlauf beenden. Als JSON zurückgegeben, erreicht ein Fehler wie no_credits oder rate_limited das Modell, das ihn melden oder warten kann, statt zu scheitern.

Geht das auch, ohne Tools zu schreiben?

Ja, für Prüfung und CRM-Suche: Der Cold-Leads-MCP-Server stellt jedem MCP-Client search_leads und verify_email bereit. Ein Tool für die Adresssuche hat er nicht.