wandres.dev
CONTAINERS Y WORKFLOWS · lo nuevo en compute

Los pasos de un Workflow: step.do, step.sleep y el estado

Si la ejecución durable es la idea, los pasos son las herramientas. Dominamos step.do como unidad reintentable —con reintentos, backoff, timeout y NonRetryableError—, step.sleep para esperar días o semanas sin consumir cómputo, y el estado que viaja entre pasos a través de los valores de retorno persistidos. Y la regla de oro que separa el código correcto del roto: solo lo que vive dentro de un paso es durable; el cuerpo que lo rodea se reejecuta en cada reproducción y debe ser puro.

⏱ 17 min

La ejecución durable es la idea; los pasos son las herramientas con las que se ejerce. Hay tres que debes dominar: step.do, la unidad de trabajo reintentable cuyo resultado se persiste; step.sleep, que pausa el proceso durante segundos, días o semanas sin gastar cómputo; y el estado que viaja de un paso a otro a través de los valores de retorno. Y por encima de las tres, una regla sutil que decide si tu Workflow es correcto o defectuoso: solo lo que ocurre dentro de un paso es duradero, mientras que el código a su alrededor se reejecuta en cada reproducción y debe ser puro.

🎯 Al terminar esta lección sabrás
  • Configurar step.do como unidad reintentable: reintentos, backoff, timeout y NonRetryableError.
  • Usar step.sleep y step.sleepUntil para esperar días sin consumir cómputo.
  • Pasar estado entre pasos a través de los valores de retorno persistidos.
  • Respetar la regla de oro: los efectos secundarios viven dentro de step.do, nunca fuera.

step.do: la unidad reintentable

Cada step.do es a la vez el átomo de la durabilidad y el átomo del reintento. Recibe un nombre, una configuración opcional y una función. Si la función lanza una excepción, ese paso —y solo ese— se reintenta según su política: un límite de intentos, un retardo, una estrategia de backoff (linear, exponential o constant) y un timeout. Cuando por fin tiene éxito, su valor de retorno se persiste y no se vuelve a ejecutar nunca.

import { NonRetryableError } from "cloudflare:workflows";

const datos = await step.do(
  "consultar proveedor",
  { retries: { limit: 5, delay: "10 seconds", backoff: "exponential" }, timeout: "1 minute" },
  async () => {
    const r = await fetch(event.payload.url);
    if (r.status === 429) throw new Error("saturado, conviene reintentar");
    return await r.json();
  },
);

await step.do("validar", async () => {
  if (!datos.ok) throw new NonRetryableError("payload invalido");
});

El nombre no es decorativo: es la identidad del checkpoint. Y la distinción entre fallo transitorio y fallo terminal es crucial: un 429 merece reintentarse, pero un dato inválido o un fallo de autenticación no van a mejorar por insistir. Para esos casos lanzas NonRetryableError, que detiene la instancia de inmediato en vez de quemar los cinco intentos en vano.

step.sleep: esperar sin consumir

step.sleep pausa la instancia durante un tiempo relativo —"30 seconds", "3 days", hasta un año— y step.sleepUntil la duerme hasta una fecha concreta. Mientras duerme, el Workflow no consume nada: ni CPU, ni una instancia caliente reservada. El tiempo, literalmente, sale gratis, y las esperas ni siquiera cuentan para el límite de pasos.

// Esperar tres dias sin gastar computo
await step.sleep("periodo de gracia", "3 days");

// O despertar en una fecha exacta
await step.sleepUntil("informe semanal", Date.parse("2026-08-03T09:00:00Z"));

Esto es imposible en un modelo facturado por invocación, donde “espera tres días” obliga a mantener algo vivo o a montar a mano un cron que consulte una tabla. Aquí el motor solo anota “reanuda esto en tal instante” y libera todo lo demás; de hecho, una instancia que despierta de dormir tiene prioridad sobre las recién encoladas, para que el trabajo antiguo llegue a completarse.

El estado que viaja entre pasos

El valor que devuelve un step.do se almacena de forma duradera y queda disponible para todo el código posterior. Así fluye el estado hacia adelante: en el ejemplo, datos se obtiene al principio y sigue vivo tres días después, cuando el proceso despierta para el seguimiento.

// El estado del primer paso sigue disponible tras el sueño
await step.do("hacer seguimiento", async () => {
  await email(datos.email, datos.id);
});

Pero esto solo funciona si respetas la regla de oro. Los efectos secundarios y todo lo que quieras persistir tienen que vivir dentro de un step.do. El código del cuerpo de run que rodea a los pasos se reejecuta en cada reproducción, así que debe ser puro y determinista: una llamada fetch o una escritura colocada ahí fuera se repetiría en cada reanudación. Además, lo que devuelves debe ser serializable, porque se escribe en almacenamiento. Traza bien esa frontera y el Workflow es correcto; trázala mal y cosecharás efectos duplicados o estado perdido.

flowchart TD
A[step do consultar] -->|falla| R[reintento con backoff]
R --> A
A -->|exito| P[estado persistido]
P --> Z[step sleep 3 dias]
Z -.despierta y reanuda.-> B[step do seguimiento]
B --> C[completado]
style P fill:#a6e3a1,color:#11111b
style Z fill:#f9e2af,color:#11111b
style R fill:#fab387,color:#11111b
La frontera de un paso es la frontera de la durabilidad

Toda la disciplina de la ejecución durable se reduce a un solo acto: trazar las fronteras de los pasos. Dentro de un step.do, todo se confirma de forma atómica desde el punto de vista del Workflow, como una transacción; fuera de él, todo se reejecuta en cada reproducción y por tanto debe ser puro o idempotente. Interioriza esa única línea divisoria y habrás entendido todos los motores durables que existen: las activities de Temporal, las transacciones de DBOS, las tasks de las Step Functions codifican exactamente la misma ley. Y de ahí sale la reorientación del oficio: el trabajo real del programador no es escribir los pasos, sino decidir dónde empiezan y dónde acaban —qué debe quedar grabado en un checkpoint y qué debe ser seguro de repetir—. Un paso demasiado grande reejecuta trabajo caro cuando solo falló una parte; un paso demasiado pequeño llena el proceso de checkpoints inútiles; y un efecto secundario colocado fuera de todo paso es una bomba de relojería que estalla en la primera reanudación. Luego está el segundo superpoder, más silencioso, que revela step.sleep: el tiempo convertido en primitiva de primera clase y gratuita. Un proceso puede esperar tres días sin coste alguno —sin servidor, sin cron, sin sondeo— porque el motor solo guarda una marca temporal y desaparece hasta entonces. Cuando el cómputo y la espera se desacoplan por completo, categorías enteras de lógica de negocio que antes eran problemas de infraestructura —periodos de prueba, plazos de gracia, recordatorios, escalados— se encogen hasta ser tres líneas de código ordinario. Ese es el cambio de mentalidad: dejas de preguntarte cómo mantener vivo un proceso que espera y empiezas a escribir la espera como si fuera una instrucción más, porque para el Workflow lo es.

⚔️ Diseña los pasos y sus fronteras
  1. Escribe un step.do con una política de reintentos explícita y justifica el backoff que elegiste frente a las alternativas.
  2. Añade un paso que lance NonRetryableError ante un fallo terminal y explica qué habría pasado sin él.
  3. Inserta un step.sleep de varios días entre dos pasos y razona por qué esa espera no cuesta nada en este modelo pero sí en uno facturado por invocación.
  4. Toma una llamada fetch colocada por error en el cuerpo de run, fuera de todo paso, y explica exactamente qué falla en la primera reanudación.
  5. Divide un paso demasiado grande en dos y argumenta cómo cambia el comportamiento ante un fallo parcial.