Desarrollo IATutorial

Agentes Durables en 2026: Que Tu Agente de IA Sobreviva a Timeouts, Deploys y Humanos Lentos

14 de octubre de 2026
13 min de lectura
Guía de agentes durables comparando Vercel Workflow, Temporal e Inngest
Compartir:

Este es el tipo de agente que rompe los setups serverless ingenuos: redacta un reembolso con un LLM, le avisa a un manager, espera un sí que puede tardar dos días y recién ahí mueve plata. Un solo request no aguanta eso. Una función de 300 segundos muere en plena espera, un redeploy borra el loop en memoria y un retry descuidado le cobra dos veces al cliente.

Esta guía cubre el arreglo, la ejecución durable, y compara las tres opciones que preseleccionaría un equipo TypeScript en 2026: Vercel Workflow, Temporal e Inngest. Después armás un agente de punta a punta: un paso con LLM, un paso de herramienta con clave de idempotencia, una aprobación humana y la reanudación. Cada API y cada límite lo verifiqué contra las docs de los proveedores el 2026-10-14. No hice pruebas de carga en estas plataformas, así que leé el ejemplo como un patrón, no como código de producción.

1. El Problema: Un Agente Es un Proceso, Serverless Te Da un Request

Un runtime pensado para requests asume que el trabajo termina rápido. En Vercel con Fluid compute una función dura 300 segundos por defecto; Pro y Enterprise pueden subirla a 800 segundos, y un máximo extendido de 1.800 segundos sigue en beta. El trabajo de un agente ignora esos números: las llamadas al modelo fallan y piden reintentos, las herramientas son lentas y una aprobación humana puede tardar días. Y mientras el run espera, vas a deployar código nuevo.

El Error Más Común

Guardar la posición del agente en memoria: un while con setTimeout o polling dentro de un route handler, o un flag en la base más un cron que relee un estado a medio escribir. En la demo funciona. El primer timeout, crash o redeploy pierde dónde estaba el run, y el retry que le agregás encima repite un efecto secundario que ya había ocurrido.

La ejecución durable saca el ’dónde estoy’ de la memoria del proceso y lo mete en un log persistido. Seguís escribiendo una función async común, pero cada paso completado queda registrado; si el proceso muere, el run se reconstruye desde el log y sigue donde se había detenido. Las docs de Vercel dicen que sus runs sobreviven a deploys y crashes mediante replays determinísticos, la misma idea que Temporal e Inngest implementan a su manera.

No es lo mismo que los checkpoints de LangGraph

Mi tutorial de LangGraph cubre el estado de conversación con checkpointer y los interrupts dentro de un solo proceso de agente. La ejecución durable está una capa más abajo: hace que el proceso mismo pueda desaparecer. Resuelven problemas vecinos y podés usar cualquiera sin el otro, así que acá no repito el checkpointing.

Leer el tutorial de LangGraph

2. Conceptos Mínimos Antes de Tocar Código

Seis ideas cubren todo el modelo. Los nombres cambian según el producto, la forma no.

📜

Historial de eventos

la fuente de verdad

Cada transición de estado (paso agendado, completado, sleep, hook recibido) se agrega a un log, y el estado del run se deriva de ahí. Vercel lo llama event log, Temporal Event History.

🔁

Replay determinístico

cómo se reanuda un run

Tras un crash o deploy el motor vuelve a ejecutar tu función workflow desde el principio y le devuelve los resultados registrados en vez de rehacer el trabajo. Solo funciona si el código toma las mismas decisiones con el mismo historial.

🧱

Steps / activities

donde viven los efectos

Todo lo externo o no determinístico (llamada al LLM, request HTTP, escritura en la base) va en un step en Vercel e Inngest, en una activity en Temporal. Su resultado queda registrado y se reutiliza en el replay.

♻️

Reintentos

por paso, no por run

Un paso que falla se reintenta sin volver a correr los pasos que ya salieron bien. Los defaults difieren: tres reintentos en Vercel, cuatro en Inngest, ilimitados con backoff en las activities de Temporal.

🙋

Hooks / signals / events

esperar a un humano

El run se suspende y no usa cómputo hasta que llega un mensaje: un hook en Vercel, una signal en Temporal, step.waitForEvent en Inngest.

⏳

Timers durables

dormir por días

Un sleep queda registrado en el log, así que ’dos días’ sobrevive a reinicios. Competir un timer contra una aprobación le da a cada espera humana un timeout definido.

La regla a conservar

Decidí en el workflow, actuá en los steps. La función workflow es un plan que se re-ejecuta, así que tiene que ser determinística. Los steps son donde se toca el mundo: cada uno corre, queda registrado y su resultado registrado se reutiliza en el replay.

3. Vercel Workflow vs Temporal vs Inngest: Qué Cambia de Verdad

Los tres te dan replay, reintentos, timers y esperas externas. Se diferencian en dónde corre tu código, qué tan estricto es el modelo de programación y cómo pagás. Los datos de abajo salen de las docs de cada proveedor al 2026-10-14; los límites y precios se mueven, así que revisalos de nuevo antes de presupuestar.

TemaVercel WorkflowTemporalInngest
Modelo de programaciónDirectivas ’use workflow’ y ’use step’ sobre funciones async comunes (TypeScript o Python). Workflow SDK open source, paquete npm workflow 5.2.2.Funciones workflow en un sandbox determinístico que llaman Activities con proxyActivities. SDK de TypeScript 1.24.0 en npm.Funciones con step.run, step.sleep y step.waitForEvent. El handler se re-invoca y los pasos terminados se memoizan, sin reglas de runtime propias. Paquete npm inngest 4.24.0.
HostingGestionado en Vercel: Functions ejecutan el código, Queues lo despachan y la persistencia gestionada guarda el estado. Local World en dev; self-hosting con Worlds de la comunidad o de Postgres.Un Temporal Service (Cloud o self-hosted) más procesos Worker que desarrollás y operás en tus propios hosts.Inngest llama a un endpoint HTTP que servís vos, lo que funciona en hosts serverless; Connect es la alternativa con workers persistentes. El código está en GitHub para self-hosting.
ReintentosTres reintentos por defecto (cuatro intentos), maxRetries por paso. FatalError saltea los reintentos; RetryableError define retryAfter.Activities: intentos ilimitados, 1 s de intervalo inicial, backoff x2, tope de 100 s. Los workflows no reintentan por defecto.Cuatro reintentos tras el primer intento, contados por paso. NonRetriableError corta; RetryAfterError controla el momento.
Esperar días a un humanodefineHook, create({ token }), await; se reanuda desde una API route. Competilo con sleep para el timeout. Las docs no listan un límite de duración para sleep.defineSignal y setHandler, luego condition(fn, timeout); el cliente llama handle.signal. Los timers pueden dormir meses.step.waitForEvent con timeout devuelve el evento o null. Limitado por la duración de run del plan: 30 días Free, 90 Pro, 366 Business.
Desarrollo localCero configuración. Datos en .workflow-data/; inspeccioná con npx workflow web o npx workflow inspect runs.temporal server start-dev (UI en :8233) y un Worker que dejás corriendo. Node.js 20 o superior.npx inngest-cli@latest dev (UI en :8288) e INNGEST_DEV=1 en tu app.
Modelo de preciosEvents ($0,02 cada 1K), datos escritos ($0,50/GB) y datos retenidos ($0,50/GB-mes), más Functions y Queues. Hobby incluye 50.000 events por mes.Cloud cobra Actions ($50 por millón en el tramo de entrada) y almacenamiento, más un fee de plan (Developer: 10% del uso). O lo alojás y operás vos.Se cobra por ejecución y los steps cuentan: cinco step.run suman seis ejecuciones. Free: 50k por mes. Pro desde $99 por mes con 1M incluido.
Límites a conocer50 MB por payload; 25.000 events y 10.000 steps por run; un intento de replay de más de 240 s puede abortar el run. Datos del run guardados 1 día en Hobby, 7 en Pro.2 MB por payload y 4 MB por request gRPC; historial con tope de 51.200 eventos o 50 MB.4 MiB por retorno de step, 32 MiB de estado del run, 1.000 steps por función.

La misma espera de aprobación en tres dialectos

// Vercel Workflow - hook + timer race
const hook = approvalHook.create({ token: "refund:" + orderId });
const approval = hook.then((p) => p);
const decision = await Promise.race([approval, sleep("2 days").then(() => "timeout" as const)]);
// resume: await approvalHook.resume("refund:" + orderId, { approved: true });

// Temporal - signal + condition with timeout
export const approve = defineSignal<[{ approved: boolean }]>("approve");
let decision: { approved: boolean } | undefined;
setHandler(approve, (d) => { decision = d; });
const arrived = await condition(() => decision !== undefined, "2 days"); // false on timeout
// client: await handle.signal(approve, { approved: true });

// Inngest - wait for an event
const review = await step.waitForEvent("wait-for-approval", {
  event: "refund/approved",
  timeout: "2d",
  match: "data.orderId",
}); // null when the timeout expires

Cómo elegiría yo

Next.js en Vercel y sin ganas de infraestructura: Vercel Workflow, aceptando que el motor gestionado está atado a Vercel (existe un World de Postgres para self-hosting). Versionado estricto de workflows, historiales muy largos y equipos políglotas, y no te molesta operar workers o pagar Cloud: Temporal. Producto orientado a eventos donde un endpoint HTTP en tu host serverless es lo único que querés operar: Inngest. Es mi lectura de las docs, no un benchmark.

4. Tutorial: Un Agente con Aprobación Humana en Vercel Workflow

Elegí Vercel Workflow para el ejemplo porque su quickstart de Next.js es el más corto de los tres, corre en local sin servicios extra y un sitio Next.js en Vercel ya aloja los steps. Versiones verificadas en npm el 2026-10-14: workflow 5.2.2 (línea estable desde 5.0.0, el 2026-09-30), ai 7.0.140 y zod 4.6.7. El agente propone un reembolso con un LLM, espera a un humano y recién ahí llama a la herramienta de pagos, una sola vez.

Paso 1 — Instalá y envolvé la config de Next.js

Instalá los paquetes y envolvé tu config con withWorkflow. Es el único cambio de configuración obligatorio; las directivas hacen el resto.

Terminal + next.config.ts

npm i workflow ai zod

// next.config.ts
import type { NextConfig } from "next";
import { withWorkflow } from "workflow/next";

const nextConfig: NextConfig = {};

export default withWorkflow(nextConfig);

Paso 2 — El paso con LLM, el paso de herramienta, el hook y el workflow

Cinco piezas en un archivo. proposeRefund llama al modelo y devuelve un objeto tipado. requestReview le avisa al revisor. approvalHook es la espera tipada. issueRefund es el único paso que mueve plata, así que lleva una clave de idempotencia. refundAgent solo orquesta. El string del modelo pasa por Vercel AI Gateway, así que configurá sus credenciales (o pasá una instancia de proveedor) antes de la primera corrida.

TypeScript — workflows/refund-agent.ts

// workflows/refund-agent.ts
import { defineHook, sleep, getStepMetadata, FatalError } from "workflow";
import { generateText, Output } from "ai";
import { z } from "zod";

type Ticket = { orderId: string; message: string };
type Plan = { amountCents: number; reason: string };

// The human decision, validated before the run resumes
export const approvalHook = defineHook({
  schema: z.object({ approved: z.boolean(), comment: z.string().optional() }),
});

// LLM step: proposes, never executes
async function proposeRefund(ticket: Ticket): Promise<Plan> {
  "use step";
  const { output } = await generateText({
    model: "anthropic/claude-sonnet-5.5", // any AI Gateway "provider/model" string
    instructions: "You triage refund requests. Propose a refund amount in cents.",
    prompt: ticket.message,
    output: Output.object({
      schema: z.object({ amountCents: z.number(), reason: z.string() }),
    }),
  });
  return output;
}

// Notification step: a retry may ping twice - fine for a ping, not for money
async function requestReview(ticket: Ticket, plan: Plan) {
  "use step";
  await fetch(process.env.REVIEW_WEBHOOK_URL!, { // the secret stays inside the step
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      text: "Refund " + plan.amountCents + " cents for order " + ticket.orderId + ": " + plan.reason,
    }),
  });
}

// Tool step: the only place money moves
async function issueRefund(orderId: string, amountCents: number) {
  "use step";
  const { stepId } = getStepMetadata();
  const res = await fetch("https://payments.example.com/v1/refunds", { // placeholder provider
    method: "POST",
    headers: {
      authorization: "Bearer " + process.env.PAYMENTS_API_KEY,
      "content-type": "application/json",
      "idempotency-key": "refund:" + stepId, // stable across retries of this step
    },
    body: JSON.stringify({ orderId, amountCents }),
  });
  if (res.status === 429 || res.status >= 500) throw new Error("Payments API " + res.status); // retried
  if (!res.ok) throw new FatalError("Refund rejected: " + res.status); // not retried
  return (await res.json()) as { refundId: string };
}

// The workflow: deterministic orchestration only
export async function refundAgent(ticket: Ticket) {
  "use workflow";

  const plan = await proposeRefund(ticket);
  if (plan.amountCents > 50_000) return { status: "needs-manager", plan };

  const hook = approvalHook.create({ token: "refund:" + ticket.orderId }); // BEFORE notifying
  await requestReview(ticket, plan);

  const approval = hook.then((payload) => payload);
  const decision = await Promise.race([
    approval,
    sleep("2 days").then(() => "timeout" as const),
  ]);

  if (decision === "timeout" || !decision.approved) {
    return { status: "closed", plan, decision };
  }

  const receipt = await issueRefund(ticket.orderId, plan.amountCents);
  return { status: "refunded", receipt };
}

Fijate en el orden: el hook se crea antes de avisar al revisor, así un sí rápido no llega cuando nadie escucha; y la espera compite contra un timer de dos días, así el silencio tiene un desenlace definido. El tope de 50.000 centavos vive en el código del workflow a propósito: el modelo propone, el código determinístico dispone.

Qué pasa cuando llega la aprobación

El motor vuelve a ejecutar refundAgent desde el principio. proposeRefund y requestReview devuelven al instante sus resultados registrados (sin segunda llamada al LLM ni segundo aviso), el hook se resuelve con el payload del revisor y solo issueRefund se ejecuta de verdad. Esperá un día o deployá en el medio: por defecto un run se queda en el deployment que lo inició, así que el run pausado se reanuda con el código con el que empezó.

Paso 3 — Iniciá el run y reanudalo desde route handlers

start() encola el run y vuelve enseguida. La ruta approve reanuda el hook por su token. Los tokens no son credenciales: el schema de Zod valida el payload, no a quien lo manda, así que autenticá al caller antes de llamar a resume().

TypeScript — dos route handlers

// app/api/refund/route.ts - start a run
import { start } from "workflow/api";
import { refundAgent } from "@/workflows/refund-agent";

export async function POST(req: Request) {
  const ticket = await req.json();
  const run = await start(refundAgent, [ticket]);
  return Response.json({ runId: run.runId });
}

// app/api/refund/approve/route.ts - resume it
import { approvalHook } from "@/workflows/refund-agent";
import { getReviewer } from "@/lib/auth"; // your own auth, not part of the SDK

export async function POST(req: Request) {
  const reviewer = await getReviewer(req);
  if (!reviewer?.canApproveRefunds) return new Response("Forbidden", { status: 403 });

  const { orderId, approved, comment } = await req.json();
  await approvalHook.resume("refund:" + orderId, { approved, comment });
  return Response.json({ ok: true });
}

Tip: tokens que podés reconstruir

Un token derivado de una clave de negocio (refund:A-1042) permite que la ruta approve encuentre el run sin guardar un run ID, y funciona de guarda contra duplicados: si un run activo ya es dueño de ese token, el SDK expone un conflicto (mirá hook.getConflict()) en vez de iniciar en silencio un segundo reembolso. Si tu app tiene proxy.ts o middleware, excluí .well-known/workflow/ de su matcher.

Paso 4 — Corrélo en local y leé la línea de tiempo

En local el SDK usa su Local World sin configuración. Iniciá un run, abrí el inspector y después aprobá. En Vercel los mismos runs aparecen en el dashboard, en Observability y luego Workflows. Una trampa de deploy que mencionan las docs: ’Enable access to System Environment Variables’ tiene que estar activo, o el deployment cae al Local World y falla por el filesystem de solo lectura.

Terminal — correr, inspeccionar, aprobar

npm run dev

# start a run
curl -X POST --json '{"orderId":"A-1042","message":"Charged twice, please refund the duplicate."}' http://localhost:3000/api/refund

# watch it: web UI or terminal
npx workflow web
npx workflow inspect runs

# the reviewer says yes (your auth must let this through)
curl -X POST --json '{"orderId":"A-1042","approved":true}' http://localhost:3000/api/refund/approve

5. Errores Comunes (el Código Durable Necesita Que Se Los Digan)

La ejecución durable te saca los timeouts de encima y agrega una clase nueva de bug: código que es correcto la primera vez e incorrecto la segunda. Estos cinco son los que rodean las docs de todos los proveedores.

💳

Efectos sin idempotencia

el bug del doble cobro

Los steps se reintentan, y un reintento puede seguir a un intento que ya hizo el trabajo pero perdió la respuesta. Pasá una clave que el proveedor deduplique: stepId de getStepMetadata() en Vercel (estable entre reintentos), run ID del workflow más activity ID en Temporal. En Inngest hacé idempotente la llamada vos mismo; sus docs usan un paso de reembolso como ejemplo. Primero pagos y emails.

🎲

Código de workflow no determinístico

el que rompe el replay

El código del workflow se re-ejecuta, así que tiene que decidir igual cada vez. Dejá relojes, aleatoriedad, lecturas de env, base de datos y HTTP en steps. Los SDKs parchean parte (Vercel fija Math.random y Date; Temporal los reemplaza y ofrece uuid4()), pero en Inngest todo lo que esté fuera de un step puede correr más de una vez. Editar la lógica del workflow bajo runs en curso es la versión de combustión lenta del mismo bug.

📦

Tamaño de payloads

todo termina en el log

Las entradas y salidas de los steps se persisten, y los límites varían mucho: 50 MB en Vercel, 2 MB por payload y 4 MB por request gRPC en Temporal, 4 MiB por retorno de step en Inngest. Pasá referencias (una URL de blob, un ID de fila), no documentos ni transcripciones enteras. Vercel además avisa que el replay se vuelve lento pasados los 2.000 eventos, así que agrupá trabajo por step o dividí en child workflows.

🔑

Secretos

el historial se puede leer

Argumentos y valores de retorno quedan registrados y los ve cualquiera con permiso para leer datos de runs (en Vercel: owners del team o el permiso Workflow Run Data Viewer). Leé las API keys del env dentro de los steps, pasá IDs en vez de tokens, generá tokens impredecibles en un step (la aleatoriedad con semilla no es secreta) y autenticá el endpoint que reanuda un hook.

🔍

Observabilidad y debugging por replay

el log es tu debugger

Abrí el run, encontrá el paso que falló y leé su input e intentos. Usá el dashboard o npx workflow web en Vercel Workflow, la Web UI de Temporal y el dev server de Inngest. Logueá desde los steps, ponéles nombres claros y mantené los payloads chicos para que la línea de tiempo se lea.

✅ Hacé esto

  • • Decidí en el workflow, actuá en los steps
  • • Derivá las claves de idempotencia de stepId o de una clave de negocio estable
  • • Creá el hook o registrá la espera antes de avisar al humano
  • • Autenticá el endpoint que reanuda un run
  • • Pasá IDs y referencias, y quedate lejos de los límites de payload
  • • Competí cada espera humana contra un timer y manejá la rama de timeout

❌ No esto

  • • Llamar a fetch o leer Date, env o la base en código de workflow
  • • Reintentar llamadas que mueven plata sin clave de idempotencia
  • • Poner API keys o documentos completos en argumentos o retornos de steps
  • • setTimeout, polling o cron más un flag de estado para esperas de días
  • • Reordenar pasos de un workflow en curso sin plan de versionado
  • • Tratar un token de hook como prueba de que el caller puede aprobar

Regla de oro

Si un paso puede correr dos veces, asumí que va a correr. Hacé que ’dos veces’ sea inofensivo (una clave de idempotencia, una API que deduplica, un resultado guardado) y el resto de la ejecución durable es plomería.

Conclusión

Los agentes largos fallan por razones aburridas: un límite de tiempo, un reinicio, un reintento que repite un pago. La ejecución durable responde a las tres con un solo truco, registrar cada paso y hacer replay para reanudar, y los tres productos de acá lo empaquetan distinto: directivas en Vercel, un workflow determinístico más workers en Temporal, steps memoizados sobre HTTP en Inngest.

Elegí un flujo de agente que hoy cuidás con un cron y una columna de estado. Mové la llamada al modelo y el efecto secundario a steps, dale al que mueve plata una clave de idempotencia, reemplazá el polling por un hook o signal y competilo contra un timer. Esa migración debería borrar más código pegamento del que agrega, y es la base de cada agente con humano en el loop que construyas después.

Machete del Tutorial

Vercel Workflow

  • • ’use workflow’ + ’use step’
  • • defineHook + sleep en carrera
  • • stepId como clave de idempotencia

Temporal

  • • Workflow + Activities + Worker
  • • defineSignal + condition(fn, t)
  • • Ojo al tope de 2 MB por payload

Inngest

  • • step.run + step.waitForEvent
  • • Cuatro reintentos por paso por defecto
  • • Cada step cuenta como ejecución

Fuentes

Diego Rodriguez

Diego Rodriguez

Ingeniero Senior Full-Stack & AI

Diego tiene mas de 9 anos de experiencia construyendo aplicaciones potenciadas por IA de produccion, desde orquestacion de LLMs y pipelines RAG hasta deteccion de riesgos con ML y sistemas de trading algoritmico.

Conoce mas sobre Diego →