Cold Leads

Herramientas de Cold Leads para agentes de CrewAI y LangChain

Para quién
Desarrolladores de Python que crean agentes con CrewAI o LangChain
El problema
Un agente al que se le piden datos de contacto los adivinará, y una dirección adivinada parece exactamente una real. Sin una herramienta de verificación, ni el agente ni la persona que lee su resultado pueden distinguirlas.
La solución
Dé al agente tres herramientas acotadas basadas en la API de Cold Leads: buscar en el propio CRM del usuario, verificar una dirección y deducir una dirección a partir de un nombre y un dominio con una etiqueta de método honesta. Los errores vuelven como datos, así que el agente puede reaccionar en lugar de fallar.
Qué obtiene
Un cliente de API compartido, clases de herramientas para CrewAI y para LangChain, y una crew y un agente mínimos que las usan.

Las direcciones, los identificadores y los resultados de los ejemplos son ilustrativos. example.com está reservado para documentación, así que una comprobación real de estas direcciones devuelve invalid.

Los ejemplos de código son iguales en todos los idiomas; sus comentarios están en inglés.

Las tres herramientas

HerramientaEndpointCosteQué recibe el agente
search_crm_contactsGET /api/v1/leadsGratisContactos que ya están en su CRM de Cold Leads en el dominio de una empresa (hasta 50), con fase, etiquetas, verificación y do_not_contact.
verify_emailPOST /api/v1/verify1 créditostatus, score, reasons, mx, catch_all, disposable y role.
find_emailPOST /api/v1/find1 créditoLa dirección más probable, method (verified, pattern o none), confidence y candidates.

Cold Leads no tiene una base de datos de personas ni de empresas. search_crm_contacts no puede descubrir prospectos nuevos; solo devuelve contactos que el usuario ya tiene. Dígalo en la descripción de la herramienta o el modelo esperará otra cosa.

Un cliente compartido

Ambos frameworks usan el mismo cliente pequeño. Devuelve cada respuesta como una cadena JSON y convierte los errores de la API (no_credits, rate_limited, bad_api_key) en datos que el modelo puede leer, en lugar de lanzar excepciones.

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)
  • Cada herramienta es una subclase de crewai.tools.BaseTool con name, description, args_schema (un modelo de Pydantic) y _run. env_vars declara la clave que necesita la herramienta; el propio cliente lee COLDLEADS_API_KEY del entorno.
  • Las herramientas se pasan al agente como instancias, y {domain} y {role} de la tarea se rellenan desde kickoff(inputs=...).
  • CrewAI necesita Python de 3.10 a 3.13. La línea llm usa Claude Opus 5.5; sirve cualquier modelo compatible con CrewAI que admita llamadas a herramientas.

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"))
  • Las herramientas usan el decorador @tool de langchain.tools con name, description y args_schema explícitos.
  • create_agent de langchain.agents ejecuta el bucle de herramientas. Sustituye a create_react_agent de LangGraph, que está obsoleto.
  • El modelo se indica como provider:model. El mensaje final puede contener varios bloques de contenido, así que el ejemplo solo imprime sus bloques de texto.

Descripciones con las que el modelo puede actuar

  • Indique el coste en la descripción (1 crédito para verify_email y find_email, gratis para la búsqueda en el CRM), para que el modelo sepa qué llamadas gastan créditos.
  • Explique el último código de motivo: ok significa que un servidor de correo aceptó el buzón, smtp_unreachable que el buzón no se comprobó y catch_all que el dominio acepta cualquier dirección.
  • Diga que un resultado de find_email con method pattern es una suposición y que a los contactos con do_not_contact true no se les debe enviar correo.
  • Mantenga las herramientas acotadas. Un agente que solo puede buscar, verificar y deducir no puede enviar correos ni cambiar registros por error.

Límites y costes

  • verify_email y find_email cuestan 1 crédito por llamada, incluidos los resultados de la caché de 30 días; la búsqueda en el CRM es gratis.
  • El cliente pide un margen de verificación de 10 segundos; cuando se agota, el resultado es risky con el motivo timeout y no se guarda en caché.
  • 120 peticiones por minuto por clave. Un bucle de agente que compruebe muchas direcciones puede alcanzar ese límite; el error rate_limited que recibe entonces incluye retry_after_seconds.
  • La API forma parte del plan Business: $99 al mes con 10 000 créditos al mes; los paquetes extra de 1 000 créditos cuestan $5.

Preguntas frecuentes

¿Puede el agente encontrar prospectos nuevos en una empresa?

No. La búsqueda en el CRM solo devuelve contactos que ya están en su espacio de trabajo de Cold Leads, y la búsqueda de direcciones necesita el nombre de una persona. Cold Leads no proporciona datos de personas.

¿Por qué las herramientas devuelven los errores en lugar de lanzarlos?

Una excepción dentro de una herramienta puede terminar la ejecución del agente. Devuelto como JSON, un error como no_credits o rate_limited llega al modelo, que puede informar de él o esperar en lugar de fallar.

¿Se puede hacer sin escribir herramientas?

Sí, para la verificación y la búsqueda en el CRM: el servidor MCP de Cold Leads expone search_leads y verify_email a cualquier cliente MCP. No tiene herramienta de búsqueda de direcciones.