← Volver al Blog

Cron para agentes de IA: 4 patrones para detectar fallos silenciosos

Un agente de IA programado con cron puede fallar en silencio durante días. Yo tuve uno que dejó de ejecutarse un miércoles y me di cuenta el sábado siguiente, cuando abrí el dashboard y vi tres días sin resultados. El proceso terminaba con exit code 0. El cron log decía “OK.” La API de Claude estaba devolviendo 200. Y aun así el agente no había hecho nada útil desde el miércoles.

El problema con los agentes programados no es que fallen. El problema es que cuando fallan bien, no fallan de manera ruidosa. El cron da por hecho que todo salió bien si el proceso terminó sin error, y los agentes de IA son especialmente hábiles para “terminar sin error” mientras no producen nada de valor.

En este texto voy a mostrar los 4 patrones de observabilidad que aplico hoy a cualquier agente que se ejecuta por cron o por scheduler. Cada uno cubre un modo de fallo distinto. Uso ejemplos concretos con expresiones cron reales y un healthcheck HTTP mínimo que se puede copiar y pegar.

Contexto: qué cuenta como “agente por cron”

Cuando digo “agente de IA por cron” me refiero a cualquiera de estos:

  • Un job de cron o systemd timer en un servidor que llama a la API de Claude o de OpenAI.
  • Un workflow de GitHub Actions con schedule: que ejecuta un script con un LLM adentro.
  • Un scheduled agent nativo de la plataforma (Anthropic y otros proveedores empezaron a exponer “background runs” en 2026, pero la mayoría de la gente sigue en cron plano).

Los 4 patrones aplican a los tres casos. Cambian los detalles de implementación, no el principio.

Patrón 1: deadman switch (el agente tiene que probar que corrió)

El error mental más común es asumir que “cron ejecutó el script” equivale a “el agente hizo su trabajo.” No equivale.

La solución es invertir la señal: en lugar de esperar una alerta cuando algo falla, esperar una señal cuando algo funciona. Si la señal no llega, alertar. Esto se llama deadman switch, o “healthcheck de heartbeat.”

Yo uso healthchecks.io porque es gratis hasta 20 checks y evita tener que operar el receptor. El patrón se ve así:

#!/bin/bash
# /usr/local/bin/run-agent.sh
set -euo pipefail

CHECK_ID="a1b2c3d4-...-cambia-esto"
PING_URL="https://hc-ping.com/${CHECK_ID}"

# Avisar que empezó
curl -fsS -m 10 --retry 3 "${PING_URL}/start" > /dev/null

# Correr el agente
python3 /opt/agents/summarizer.py

# Avisar que terminó bien
curl -fsS -m 10 --retry 3 "${PING_URL}" > /dev/null

El cron queda simple:

# Cada hora en el minuto 5
5 * * * * /usr/local/bin/run-agent.sh >> /var/log/agent.log 2>&1

En healthchecks.io configuro el “grace period” en 15 minutos. Si a los 75 minutos del ping anterior no llegó uno nuevo, me llega un correo y un mensaje a Telegram. No importa qué falló: el servidor se apagó, cron se rompió, el script tiró excepción, la API de Claude bloqueó por rate limit. Todos se ven como “no llegó el ping.”

Esto solo cubre “el agente no corrió.” Todavía falta cubrir “corrió pero no hizo nada útil.”

Patrón 2: sanity check sobre el output del agente

Aquí es donde los agentes de IA se ponen creativos con las formas de fallar.

Casos que vi el último año:

  • El agente devolvió "" (string vacío) porque la API cortó por límite de tokens y el código no verificó.
  • El agente devolvió "I cannot help with that request" en inglés cuando el resto del pipeline esperaba español.
  • El agente devolvió JSON con las llaves esperadas pero todos los valores en null.
  • El agente devolvió el prompt del sistema literal (“You are a helpful assistant…”) porque el modelo entró en modo de repetición.

Ninguno de estos casos genera un error. Todos pasan el patrón 1. Todos son fallos.

La contramedida es sencilla: antes de considerar el resultado como bueno, aplicar 3-4 asserts baratos. Nada de LLM-as-a-judge por ahora, solo reglas duras.

def validate_agent_output(text: str) -> None:
    """Falla ruidosamente si el output no cumple lo mínimo."""
    if not text or len(text.strip()) < 50:
        raise ValueError(f"Output demasiado corto: {len(text)} chars")

    if text.strip().startswith(("I cannot", "I'm sorry", "Lo siento")):
        raise ValueError(f"Modelo se negó: {text[:100]}")

    if "You are a helpful assistant" in text:
        raise ValueError("Modelo devolvió su system prompt")

    try:
        parsed = json.loads(text)
    except json.JSONDecodeError:
        raise ValueError(f"Output no es JSON válido: {text[:200]}")

    if all(v is None for v in parsed.values()):
        raise ValueError("Todos los campos son null")

Si algún assert falla, el script termina con exit code no cero, el deadman switch del patrón 1 nunca ve el ping final, y a los 15 minutos me alerta. Los patrones se apilan.

La regla que me llevó tiempo aceptar: es mejor un falso positivo por mes que un falso negativo por semana. Un falso positivo me hace mirar 30 segundos y confirmar que no era nada. Un falso negativo me hace descubrir el sábado que llevo tres días sin ejecutar.

Patrón 3: exit code y stderr con alerta separada

Cron por defecto no te avisa si el script falló. Solo escribe a MAILTO si está configurado, y en 2026 casi nadie tiene un MAILTO que funcione en su servidor personal.

Yo lo resuelvo con un wrapper que captura exit code y stderr, y los manda a Telegram si hubo problema. Este es el mismo bash del patrón 1 con un poco más de peso:

#!/bin/bash
# /usr/local/bin/run-agent.sh
set -uo pipefail  # notar: sin -e adrede

CHECK_ID="a1b2c3d4-..."
PING_URL="https://hc-ping.com/${CHECK_ID}"
TELEGRAM_TOKEN="..."
TELEGRAM_CHAT="..."

curl -fsS -m 10 "${PING_URL}/start" > /dev/null

STDERR_FILE=$(mktemp)
python3 /opt/agents/summarizer.py 2> "${STDERR_FILE}"
EXIT_CODE=$?

if [ "${EXIT_CODE}" -ne 0 ]; then
    ERR=$(head -c 3000 "${STDERR_FILE}")
    curl -fsS -X POST \
        "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
        -d "chat_id=${TELEGRAM_CHAT}" \
        -d "text=Agent falló (exit ${EXIT_CODE}):\n${ERR}"
    curl -fsS -m 10 "${PING_URL}/fail" > /dev/null
    rm -f "${STDERR_FILE}"
    exit "${EXIT_CODE}"
fi

curl -fsS -m 10 "${PING_URL}" > /dev/null
rm -f "${STDERR_FILE}"

Detalles que aprendí a la mala:

  • set -e desactivado a propósito para poder capturar el exit code y mandar la alerta antes de morir. Si dejo set -e, el script muere en la línea del python3 y nunca llega al curl de Telegram.
  • head -c 3000 porque Telegram tiene límite de 4096 caracteres por mensaje. Un stack trace de Python largo pasa fácil ese límite.
  • ${PING_URL}/fail (no ${PING_URL}) para que healthchecks.io registre el intento como fallo y no como éxito.

Para GitHub Actions el equivalente es más fácil:

# .github/workflows/scheduled-agent.yml
on:
  schedule:
    - cron: '5 */4 * * *'  # cada 4 horas en el minuto 5

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run agent
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python3 agents/summarizer.py
      - name: Notify on failure
        if: failure()
        uses: appleboy/telegram-action@v1.0.0
        with:
          to: ${{ secrets.TELEGRAM_CHAT }}
          token: ${{ secrets.TELEGRAM_TOKEN }}
          message: "Agent falló en ${{ github.workflow }} run ${{ github.run_id }}"

Un detalle importante de GitHub Actions: los workflows programados con schedule: se desactivan automáticamente después de 60 días sin actividad en el repositorio. Si nadie hace commits durante dos meses, tu agente deja de ejecutarse y GitHub no te avisa de forma prominente. Este es un modo de fallo silencioso que el patrón 1 detecta (deja de llegar el ping), pero conviene saberlo antes de armar el pipeline.

Patrón 4: circuit breaker de costo o tokens

El cuarto modo de fallo es el que más me duele en el bolsillo: el agente entró en un bucle y quemó dinero en API calls durante horas.

Vi esto pasar dos veces. Una fue con un agente que reintentaba una llamada con exponential backoff mal calibrado y terminó gastando 40 USD en un fin de semana. La otra fue con un agente que se enredó en un loop de tool calls y facturó 12 USD en 20 minutos antes de que yo revisara Slack.

La contramedida es un budget check dentro del propio agente. La lógica es tan aburrida como necesaria:

# agents/summarizer.py
import os
from anthropic import Anthropic

MAX_INPUT_TOKENS_PER_RUN = 500_000
MAX_OUTPUT_TOKENS_PER_RUN = 100_000

client = Anthropic()
total_in = 0
total_out = 0

def call_with_budget(**kwargs):
    global total_in, total_out
    if total_in > MAX_INPUT_TOKENS_PER_RUN:
        raise RuntimeError(f"Budget de input excedido: {total_in}")
    if total_out > MAX_OUTPUT_TOKENS_PER_RUN:
        raise RuntimeError(f"Budget de output excedido: {total_out}")
    resp = client.messages.create(**kwargs)
    total_in += resp.usage.input_tokens
    total_out += resp.usage.output_tokens
    return resp

Cuando el budget salta, el raise combina con el patrón 3 para mandarme una alerta. El costo de la alerta es cero. El costo de no tenerla es 40 USD el fin de semana.

Ojo con este patrón: el número mismo (500k input, 100k output) es específico de mi caso. Si tu agente hace ingest de PDFs largos, los límites tienen que ser mayores; si es un agente de clasificación con prompts cortos, mucho menores. Lo que no debería cambiar es que haya un límite y que su violación sea ruidosa.

Los 4 patrones apilados: cobertura por modo de fallo

4 patrones de observabilidad para agentes por cron

Para cerrar, así es como cada patrón cubre un modo de fallo distinto:

Modo de falloPatrón que lo detecta
El script no corrió (cron, servidor, permisos)1 (deadman switch)
Corrió pero el output está vacío o degenerado2 (sanity checks)
El proceso murió con excepción3 (exit code + stderr → Telegram)
Bucle infinito quemando dinero4 (budget circuit breaker)

Los 4 juntos ocupan unas 150 líneas de bash y Python. Es probablemente la mejor relación costo/beneficio que conozco en observabilidad de agentes.

Si ya tienes agentes corriendo 24 horas sin supervisión, este otro texto sobre seguridad de agentes autónomos cubre el ángulo de permisos, que es complementario al de observabilidad. Los dos temas tienden a resolverse juntos, porque un agente sin permisos limitados que además falla en silencio es una combinación con la que no vas a dormir tranquilo.


ken imoto · WebRTC & Voice AI engineer · kenimoto.dev