Outbound AI: CRM comercial y automatización de emails
Definí el flujo de trabajo y las reglas de negocio de un CRM de prospección asistido por IA y lo construí con Claude Code.
Contexto
Proyecto personal de septiembre de 2026. Quería hacer prospección B2B personalizada sin escribir cada email a mano y sin enviar emails masivos genéricos.
Problema
Los primeros emails personalizados requieren mucha investigación, y los seguimientos se olvidan o, peor aún, se envían a alguien que ya ha respondido.
Qué hice
- Definí el flujo diario y las diez reglas que el sistema nunca debe romper (por ejemplo, un primer email nunca se envía automáticamente).
- Decidí qué es automático y qué necesita a una persona.
- Lo probé y lo uso con contactos reales. El código se escribió con Claude Code; no es experiencia profesional como ingeniero de software.
Solución
- Investigación y redacción
- Agentes de IA investigan cada empresa, encuentran un motivo contrastado para escribir, redactan el email y eliminan afirmaciones sin respaldo.
- CRM y campañas
- Contactos, empresas, fases del pipeline y campañas comparadas por tasa de cierre.
- Seguimientos
- Yo apruebo y envío cada primer email; los seguimientos se envían solos y se detienen en cuanto alguien responde.
Cómo funciona: sigue un ejemplo paso a paso (cliente ficticio)
Resultado
- Aplicación funcional: backend en Python/FastAPI, interfaz en React/TypeScript y 18 archivos de tests automáticos.
- En uso inicial: más de 400 contactos importados, primeros emails enviados y respuestas detectadas.
Evidencias
Ver código: Los controles antes de cada seguimiento
Extracto de backend/app/services/followup_engine.py. Cada seguimiento pasa estos controles, del más barato y decisivo al más costoso; la comprobación de respuesta se hace dos veces.
def _process_link(session, link, grace_days, at):
"""All gates for one follow-up, cheapest and most decisive first."""
contact = session.get(Contact, link.contact_id)
# 1. Block state and contactability.
contactable, reason = contact_service.is_contactable(contact)
if not contactable:
sequence_service.stop(session, link, reason=reason)
return outcome(False, True, reason)
# 2. Has the prospect replied? Re-checked here, never assumed from earlier.
if response_service.has_replied_since(session, contact.id):
sequence_service.stop(session, link, reason="Prospect replied — follow-up cancelled")
return outcome(False, True, "Prospect had already replied")
# 3. The previous email must be a verified send.
previous = _previous_sent_emails(session, link)
if not previous:
link.next_follow_up_at = utcnow() + timedelta(hours=6)
return outcome(False, False, "No verified sent email in this thread yet")
# … 4–7: grace period, sequence step, sender pinned to the original mailbox,
# daily capacity, reuse of a draft that survived a crash
# 8. Last look before handing anything to Mail.
if response_service.has_replied_since(session, contact.id):
email.status = str(EmailStatus.CANCELLED)
sequence_service.stop(session, link, reason="Prospect replied while the follow-up was being written")
return outcome(False, True, "Reply arrived during generation")
sent, error = mail_service.send_follow_up(session, email)La base de datos de contactos se queda en mi ordenador y nunca se sube al repositorio. El repositorio es privado y está disponible si se solicita.
Herramientas
Claude Code · Python · FastAPI · SQLAlchemy · SQLite · React · TypeScript · Anthropic API · AppleScript · Git