Ejecución durable para agentes largos: checkpoint por paso, resume sin repetir
Resumen
Un agente que corre 40 minutos muere a mitad por timeout, deploy, OOM o un 429: sin ejecución durable, el reintento repite todo desde cero, duplica efectos y quema presupuesto. Checkpoint por paso, resume que omite éxitos y efecto→confirmación→checkpoint con Temporal, LangGraph persistence o Cloudflare Workflows. Cero cron que re-ejecuta todo.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Todo agente largo muere a mitad. Timeout del proveedor, deploy que reinicia el contenedor, OOM en el worker, un 429 que pausa la serie. Sin ejecución durable, el reintento repite todo desde cero: los pasos que ya salieron bien se re-ejecutan, los efectos se duplican (dos cargos, dos mensajes, dos PRs) y el presupuesto se quema dos veces. El cron no resuelve esto: el cron dispara, pero lo que termina el trabajo es la ejecución durable.
Ejecución durable significa tres cosas: checkpoint por paso, resume que omite éxitos y efecto→confirmación→checkpoint. El resto —Temporal, LangGraph, Cloudflare Workflows— son implementaciones de ese contrato. Si tu agente corre más de unos minutos o toca efectos externos, este es el siguiente techo después de los reintentos con backoff.
No es cron en UTC. El cron decide cuándo arranca; la ejecución durable decide cómo sobrevive. Ni es graceful shutdown: el apagado elegante da segundos para cerrar limpio; lo durable deja continuar mañana donde quedaste anoche.
Contrato: cada paso deja checkpoint antes de seguir. Al reanudar, los pasos con checkpoint se omiten. Ningún efecto externo se considera hecho hasta confirmar y registrar. El cron dispara; lo durable termina.
El modelo mental: historial, no memoria
Los motores durables no guardan "el estado" como una foto: guardan el historial de eventos. Temporal lo documenta así en su página de workflows (verificada hoy): el workflow es código que se re-ejecuta desde el historial tras cada fallo, y las activities son los pasos con efecto. LangGraph hace lo mismo con su checkpointer: cada paso del grafo persiste en un thread y el resume continúa desde el último estado guardado. Cloudflare Workflows ofrece la versión serverless: pasos con reintento automático y estado que sobrevive entre ejecuciones.
La consecuencia práctica: tu código debe estar escrito para re-ejecutarse. Todo lo no determinista vive dentro de un paso con checkpoint, nunca suelto en el flujo. Si un paso puede correrse dos veces sin daño, es idempotente; si no, necesita key de idempotencia.

Las tres reglas
1. Checkpoint por paso, no por corrida. Un checkpoint al final no sirve: si el paso 11 de 14 falla, quieres reanudar en el 11. Cada llamada con efecto y cada escritura externa van solas; los pasos baratos y puros pueden agruparse.
2. Resume que omite éxitos. Reanudar es continuar: el motor revisa el historial, salta lo que ya tiene checkpoint y ejecuta solo lo pendiente. El try/catch vive en memoria y muere con el proceso; el historial vive en disco y sobrevive al deploy.
3. Efecto→confirmación→checkpoint, en ese orden. Primero ejecutas el efecto, luego confirmas que ocurrió (response 200, ID de transacción, lectura de vuelta), y solo entonces registras el checkpoint. Chequear antes de confirmar es marcar como hecho algo que quizá falló; confirmar después del checkpoint es registrar algo que quizá no ocurrió. Ese orden de tres tiempos es toda la diferencia entre "exactamente una vez" y "a veces dos".
Qué motor usar
| Opción | Dónde vive | Mejor cuando | Ojo con |
|---|---|---|---|
| Temporal | Cluster propio o Cloud | Flujos de horas/días, señales humanas, miles de ejecuciones | Operar el cluster tiene costo; no lo montes para un cron de 2 min |
| LangGraph persistence | Tu proceso Python/JS + store | El agente ya es un grafo LangGraph y quieres resume por thread | El checkpointer necesita store real (Postgres), no memoria |
| Cloudflare Workflows | Edge serverless | Pasos con I/O, sin infra que operar, reintento built-in | Límites de CPU/memoria por paso; no es para cómputo pesado |
| Cron + script con checkpoints | Tu VPS, cero dependencias | Una tarea, un tenant, pasos idempotentes a archivos | Tú implementas resume, locks y alertas; no escala a N tenants |
Si el agente ya es un grafo LangGraph, el checkpointer es el camino de menor fricción: mismo código, un store, resume por thread_id. Si el flujo cruza servicios, dura horas e incluye esperas humanas, Temporal es la herramienta diseñada para eso. Si no quieres operar nada, Workflows serverless. Y si es una sola tarea interna, un script con checkpoints en sqlite le gana a cualquier cluster.
Idempotencia: el impuesto que sí se paga
Cada paso con efecto externo lleva key de idempotencia: un ID estable (tenant + fecha + paso, o el ID del evento origen) que el proveedor usa para deduplicar. Y el checkpoint guarda la confirmación del proveedor (ID de cargo, message ID), no solo "paso 7 OK": la confirmación es lo que te permite verificar en vez de re-ejecutar.
Reglas compactas: keys deterministas derivadas del evento, no UUIDs aleatorios por intento (un UUID nuevo por reintento derrota la deduplicación). Ventana de retención de la key mayor que tu peor reintento. La mayoría de APIs de pagos, colas y webhooks aceptan alguna forma de key o permiten lecturas de confirmación. Sin key, el reintento legítimo del paso 7 crea el segundo cargo.
Esto conecta directo con los reintentos: el backoff decide cuándo reintentar; la key decide qué tan seguro es hacerlo. Uno sin el otro es media solución.
Timeouts por paso y señales
Un timeout global para una corrida de 40 minutos es decorativo: o no protege nada o mata corridas sanas. Cada paso lleva su propio presupuesto: la llamada LLM 120s, el webhook 30s, la espera humana días. El motor convierte el timeout de un paso en un evento del historial, no en una muerte: reintenta, escala a humano, o compensa.
Las señales (Temporal, interrupts en LangGraph, waitForEvent serverless) son el mecanismo para lo impredecible: el humano aprueba, el pago confirma, el proveedor avisa. El flujo duerme sin consumir recursos y despierta con la señal — mejor que un poll loop quemando tokens cada 5 minutos por un cambio que llega en 6 horas.

Anti-patrones caros
- Cron que re-ejecuta todo. Sin checkpoints, cada disparo repite efectos. El cron dispara; sin historial no hay resume.
- Estado en memoria. Dicts, globales,
/tmp: todo muere con el contenedor. Lo que no está en el store no existe. - Checkpoint optimista. Marcar hecho antes de confirmar. Al resumir, el motor omite un paso que nunca ocurrió.
- Reintentar la corrida entera. El reintento es por paso; reintentar todo multiplica costo y efectos.
- Historial con PII. El historial persiste: si lleva PII, creaste una base regulada. Aplica detección y redacción antes de persistir.
- Timeout global único. Un número para toda la corrida no protege ningún paso. Presupuesto por paso o nada.
Observar lo durable
El historial es tu mejor trace: pasos, reintentos, señales y tiempos sin instrumentación extra. Expón cuatro cosas en tu tablero: ejecuciones activas por antigüedad, pasos con más reintentos, tiempo medio por paso y señales pendientes viejas. Los pasos con reintentos crónicos son tus siguientes casos de eval.
Checklist
- Cada paso con efecto deja checkpoint antes de continuar
- Resume omite pasos con checkpoint válido (verificado con un kill -9 a mitad, no de palabra)
- Orden efecto→confirmación→checkpoint en todos los pasos externos
- Keys de idempotencia deterministas, retenidas más que el peor reintento
- Timeout propio por paso; espera humana vía señal, nunca poll loop
- Historial sin PII (redacción en entrada y salida)
- Tablero: activas por antigüedad, reintentos por paso, señales viejas
FAQ
¿Un cron con lock file ya es durable? No. El lock evita duplicados concurrentes pero no da resume: si muere a mitad, el lock huérfano bloquea la siguiente corrida. Lock + checkpoints en disco es el mínimo viable.
¿LangGraph checkpointer en memoria sirve? Solo para desarrollo. En producción el store debe sobrevivir al proceso: Postgres o equivalente. Checkpointer en memoria + deploy = historial perdido = corrida que empieza de cero.
¿Cuánto historial guardo? Retención mayor que tu ventana de disputa: si un cargo puede reclamarse en 30 días, el checkpoint con su confirmación vive 30+ días.
Siguiente paso: si tu agente aún se instala a mano en cada máquina, el flujo guiado de /curso/instalar-agente deja el entorno listo; y si tus corridas mueren por el contenedor antes que por la lógica, revisa graceful shutdown para el cierre limpio y cron en UTC para el disparo.
Lecturas relacionadas
Sigue explorando AgentOps y otras piezas para builders.

Versionar tool schemas de agentes: cambios solo aditivos y rollout sin romper producción

Timeouts en tools HTTP: AbortSignal.timeout, no un sleep eterno

Tests de tools de agentes sin LLM: Vitest primero, evals después
