Des tâches longues qui survivent aux crashs, aux retries et aux redémarrages — sans file d'attente, sans base de données, sans un seul serveur. Cloudflare Workflows apporte l'exécution durable aux Workers : écrivez du code multi-étapes comme s'il était synchrone, et laissez la plateforme gérer la persistance, les retries et la récupération.
Ce que vous allez construire
Dans ce tutoriel, vous construirez un pipeline de traitement de commandes complet sous forme de Cloudflare Workflow. Le pipeline :
- Valide une commande entrante
- Débite la carte du client via une API de paiement externe
- Réserve l'inventaire dans un service en aval
- Attend jusqu'au jour ouvré suivant avant de planifier l'expédition
- Envoie un email de confirmation
- Se remet de toute défaillance d'étape grâce aux retries automatiques
À la fin, vous disposerez d'un workflow idempotent prêt pour la production, qui s'exécute en périphérie, survit aux redémarrages des Workers et peut se mettre en pause pendant des heures ou des jours sans consommer de calcul.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (téléchargement)
- Un compte Cloudflare — l'offre gratuite prend en charge Workflows (inscription)
- Wrangler CLI v3.99+ — l'outil développeur de Cloudflare (installé ci-dessous)
- Des bases en TypeScript et async/await
- Un éditeur de code (VS Code recommandé)
Pourquoi Workflows ? Les scripts Workers traditionnels doivent se terminer en quelques secondes. Workflows peut s'exécuter pendant des minutes, des heures, voire des jours. Conçus pour la réalité chaotique des systèmes distribués : les API externes tombent, les paiements ont besoin de retries, et les humains ont besoin de temps pour approuver.
Étape 1 : installer Wrangler et créer le projet
Commencez par installer Wrangler globalement et créer un nouveau projet Workers avec support Workflows :
npm install -g wrangler@latest
wrangler loginUne fenêtre de navigateur s'ouvre pour authentifier votre compte Cloudflare. De retour dans le terminal, générez un projet TypeScript :
npm create cloudflare@latest noqta-order-pipeline -- \
--type=hello-world \
--lang=ts \
--git=true \
--deploy=false
cd noqta-order-pipelineCela crée un Worker minimal avec TypeScript, Vitest et un fichier de configuration wrangler.jsonc.
Étape 2 : activer Workflows dans wrangler.jsonc
Ouvrez wrangler.jsonc et ajoutez un binding workflows. Cela indique à Cloudflare que votre Worker expose une classe Workflow nommée OrderPipeline :
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "noqta-order-pipeline",
"main": "src/index.ts",
"compatibility_date": "2026-05-01",
"compatibility_flags": ["nodejs_compat"],
"workflows": [
{
"name": "order-pipeline",
"binding": "ORDER_PIPELINE",
"class_name": "OrderPipeline"
}
],
"observability": {
"enabled": true
}
}Le binding est utilisé par votre handler HTTP pour référencer le workflow à l'exécution. Le class_name doit correspondre exactement à la classe exportée dans votre code.
Étape 3 : modéliser la commande
Créez un fichier src/types.ts avec des types stricts pour les données qui traversent le pipeline :
// src/types.ts
export interface OrderItem {
sku: string;
quantity: number;
unitPriceCents: number;
}
export interface OrderParams {
orderId: string;
customerId: string;
customerEmail: string;
paymentToken: string;
items: OrderItem[];
}
export interface PaymentReceipt {
transactionId: string;
chargedCents: number;
chargedAt: string;
}
export interface InventoryReservation {
reservationId: string;
reservedAt: string;
}Ces types forment le contrat entre chaque étape du workflow. Comme Workflows persiste la sortie de chaque étape dans un stockage durable, toute valeur retournée doit être sérialisable en JSON — donc des interfaces avec uniquement des primitives.
Étape 4 : écrire la classe Workflow
Voici la pièce maîtresse. Remplacez le contenu de src/index.ts par ce qui suit :
// src/index.ts
import {
WorkflowEntrypoint,
WorkflowEvent,
WorkflowStep,
} from "cloudflare:workers";
import type {
OrderParams,
PaymentReceipt,
InventoryReservation,
} from "./types";
interface Env {
ORDER_PIPELINE: Workflow;
PAYMENT_API_KEY: string;
INVENTORY_API_URL: string;
EMAIL_API_URL: string;
}
export class OrderPipeline extends WorkflowEntrypoint<Env, OrderParams> {
async run(event: WorkflowEvent<OrderParams>, step: WorkflowStep) {
const order = event.payload;
await step.do("validate-order", async () => {
if (order.items.length === 0) {
throw new Error("Order has no items");
}
const total = order.items.reduce(
(sum, i) => sum + i.unitPriceCents * i.quantity,
0
);
if (total <= 0) throw new Error("Order total must be positive");
return { total };
});
const receipt = await step.do<PaymentReceipt>(
"charge-payment",
{
retries: {
limit: 5,
delay: "10 seconds",
backoff: "exponential",
},
timeout: "30 seconds",
},
async () => chargeCard(order, this.env.PAYMENT_API_KEY)
);
const reservation = await step.do<InventoryReservation>(
"reserve-inventory",
{ retries: { limit: 3, delay: "5 seconds", backoff: "exponential" } },
async () => reserveInventory(order, this.env.INVENTORY_API_URL)
);
await step.sleepUntil("wait-next-business-day", nextBusinessDay());
await step.do("send-confirmation", async () =>
sendEmail(order.customerEmail, receipt, reservation, this.env.EMAIL_API_URL)
);
return {
status: "completed",
transactionId: receipt.transactionId,
reservationId: reservation.reservationId,
};
}
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
if (req.method !== "POST") {
return new Response("Use POST to start an order", { status: 405 });
}
const body = (await req.json()) as OrderParams;
const instance = await env.ORDER_PIPELINE.create({
id: body.orderId,
params: body,
});
return Response.json({
instanceId: instance.id,
status: await instance.status(),
});
},
} satisfies ExportedHandler<Env>;Quelques points clés :
step.do(name, ...)est la primitive durable. Chaque étape nommée s'exécute avec succès au plus une fois ; sa valeur de retour est persistée et rejouée lors de la reprise.- La configuration
retriesest déclarative. SichargeCardlève une exception, la plateforme réessaie avec un backoff exponentiel — votre code reste propre. step.sleepUntillibère le calcul. Le workflow consomme zéro CPU pendant le sommeil et reprend à l'heure prévue.- Retourner une valeur depuis
step.dola rend disponible aux étapes suivantes, même après l'éviction du Worker de la mémoire.
Étape 5 : implémenter les appels externes
Ajoutez les fonctions utilitaires à src/index.ts (ou extrayez-les dans leur propre module) :
async function chargeCard(
order: OrderParams,
apiKey: string
): Promise<PaymentReceipt> {
const total = order.items.reduce(
(sum, i) => sum + i.unitPriceCents * i.quantity,
0
);
const res = await fetch("https://payments.example.com/charge", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": order.orderId,
},
body: JSON.stringify({
token: order.paymentToken,
amount: total,
currency: "USD",
}),
});
if (!res.ok) throw new Error(`Payment failed: ${res.status}`);
const data = (await res.json()) as { id: string; amount: number };
return {
transactionId: data.id,
chargedCents: data.amount,
chargedAt: new Date().toISOString(),
};
}
async function reserveInventory(
order: OrderParams,
apiUrl: string
): Promise<InventoryReservation> {
const res = await fetch(`${apiUrl}/reservations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": `${order.orderId}-inv`,
},
body: JSON.stringify({ orderId: order.orderId, items: order.items }),
});
if (!res.ok) throw new Error(`Inventory error: ${res.status}`);
const data = (await res.json()) as { id: string };
return {
reservationId: data.id,
reservedAt: new Date().toISOString(),
};
}
async function sendEmail(
to: string,
receipt: PaymentReceipt,
reservation: InventoryReservation,
apiUrl: string
) {
await fetch(`${apiUrl}/send`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to,
subject: "Order confirmation",
body: `Charged ${receipt.chargedCents} cents. Reservation: ${reservation.reservationId}`,
}),
});
}
function nextBusinessDay(): Date {
const now = new Date();
const next = new Date(now);
next.setUTCDate(now.getUTCDate() + 1);
next.setUTCHours(9, 0, 0, 0);
const day = next.getUTCDay();
if (day === 6) next.setUTCDate(next.getUTCDate() + 2);
if (day === 0) next.setUTCDate(next.getUTCDate() + 1);
return next;
}Les clés d'idempotence sont essentielles. Les étapes de workflow peuvent s'exécuter plus d'une fois si un Worker est tué en cours d'exécution. Passez une clé d'idempotence à toute API externe qui modifie un état, scopée au nom de l'étape et à l'ID de l'instance.
Étape 6 : ajouter les secrets et les bindings locaux
Workflows lit les secrets de la même manière que les Workers classiques. Pour le développement local, créez un fichier .dev.vars à la racine du projet :
PAYMENT_API_KEY="test_sk_local_xxxxxxxxxxxxxxxx"
INVENTORY_API_URL="https://staging.inventory.example.com"
EMAIL_API_URL="https://staging.email.example.com"Pour la production, poussez les secrets vers Cloudflare :
wrangler secret put PAYMENT_API_KEY
wrangler secret put INVENTORY_API_URL
wrangler secret put EMAIL_API_URLChaque commande vous demande la valeur et la stocke chiffrée dans le coffre à secrets Cloudflare.
Étape 7 : exécuter le Workflow en local
Lancez le serveur de développement avec la simulation complète des workflows :
wrangler dev --x-dev-envLa CLI affiche une URL locale comme http://127.0.0.1:8787. Dans un autre terminal, lancez une commande :
curl -X POST http://127.0.0.1:8787 \
-H "Content-Type: application/json" \
-d '{
"orderId": "ord_2026_001",
"customerId": "cust_42",
"customerEmail": "ada@example.com",
"paymentToken": "tok_fake_visa",
"items": [
{ "sku": "TSHIRT-RED-L", "quantity": 2, "unitPriceCents": 1999 }
]
}'Vous recevez une réponse JSON avec un instanceId. Ouvrez http://127.0.0.1:8787/__workflows dans votre navigateur (Wrangler expose un inspecteur intégré) et observez chaque étape passer par running, success, puis la longue phase sleeping.
Étape 8 : inspecter et contrôler les instances actives
Ajoutez un second endpoint pour interroger et piloter les workflows. Remplacez le handler fetch par ce routeur plus riche :
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
const id = url.searchParams.get("id");
if (req.method === "POST" && url.pathname === "/orders") {
const body = (await req.json()) as OrderParams;
const instance = await env.ORDER_PIPELINE.create({
id: body.orderId,
params: body,
});
return Response.json({ instanceId: instance.id });
}
if (req.method === "GET" && url.pathname === "/orders" && id) {
const instance = await env.ORDER_PIPELINE.get(id);
return Response.json(await instance.status());
}
if (req.method === "POST" && url.pathname === "/orders/pause" && id) {
const instance = await env.ORDER_PIPELINE.get(id);
await instance.pause();
return Response.json({ paused: true });
}
if (req.method === "POST" && url.pathname === "/orders/resume" && id) {
const instance = await env.ORDER_PIPELINE.get(id);
await instance.resume();
return Response.json({ resumed: true });
}
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;Vous pouvez désormais :
POST /orders— démarrer un nouveau workflow de commandeGET /orders?id=ord_2026_001— lire l'étape courante, le statut et le temps écouléPOST /orders/pause?id=...— mettre en pause un workflow indéfinimentPOST /orders/resume?id=...— reprendre un workflow en pause
C'est la base d'un tableau de bord d'administration ou d'un outil de support qui permet aux agents de suspendre des expéditions en cours.
Étape 9 : ajouter une étape de validation humaine
Pour les commandes de gros montant, supposons que vous souhaitiez une approbation humaine avant le paiement. Utilisez step.waitForEvent :
const total = order.items.reduce(
(sum, i) => sum + i.unitPriceCents * i.quantity,
0
);
if (total > 50000) {
const decision = await step.waitForEvent<{ approved: boolean }>(
"wait-for-manager-approval",
{
type: "order.approval",
timeout: "24 hours",
}
);
if (!decision.payload.approved) {
return { status: "rejected" };
}
}Puis exposez une route pour livrer l'événement :
if (req.method === "POST" && url.pathname === "/orders/approve" && id) {
const body = (await req.json()) as { approved: boolean };
const instance = await env.ORDER_PIPELINE.get(id);
await instance.sendEvent({ type: "order.approval", payload: body });
return Response.json({ delivered: true });
}Pendant l'attente, le workflow ne consomme aucune ressource de calcul et survit aux redémarrages indéfinis du Worker. Quand le manager approuve via votre interface d'administration, le workflow reprend exactement où il s'était arrêté.
Étape 10 : écrire un test d'intégration Vitest
Les workflows sont testables de bout en bout avec le pool de tests Cloudflare Workers. Ajoutez ce code dans test/order.spec.ts :
import { env, createExecutionContext, waitOnExecutionContext } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import worker from "../src/index";
describe("OrderPipeline", () => {
it("creates a workflow instance for a valid order", async () => {
const req = new Request("http://example.com/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
orderId: `ord_${crypto.randomUUID()}`,
customerId: "cust_1",
customerEmail: "test@example.com",
paymentToken: "tok_test",
items: [{ sku: "SKU1", quantity: 1, unitPriceCents: 500 }],
}),
});
const ctx = createExecutionContext();
const res = await worker.fetch(req, env, ctx);
await waitOnExecutionContext(ctx);
expect(res.status).toBe(200);
const body = (await res.json()) as { instanceId: string };
expect(body.instanceId).toBeDefined();
});
});Lancez-le :
npm testLes tests s'exécutent dans un runtime Workers en mémoire — aucun appel réseau, aucun appel réel à Cloudflare.
Étape 11 : déployer en production
Quand vous êtes prêt à livrer :
wrangler deployWrangler téléverse votre Worker, enregistre la classe OrderPipeline et affiche une URL publique. Désormais, le workflow s'exécute sur le réseau périphérique mondial de Cloudflare avec persistance intégrée.
Testez-le avec une vraie commande :
curl -X POST https://noqta-order-pipeline.your-subdomain.workers.dev/orders \
-H "Content-Type: application/json" \
-d @order.jsonOuvrez le tableau de bord Cloudflare, allez dans Workers and Pages → Workflows, et cliquez sur votre instance pour voir une chronologie détaillée de chaque retry, attente et événement.
Tester votre implémentation
Vérifiez le workflow avec cette checklist :
- Une commande valide retourne un
instanceIdet progresse à travers toutes les étapes. - Tuer le serveur de développement en cours puis le redémarrer reprend à la dernière étape complétée.
- Forcer
chargeCardà lever une exception montre les retries s'enchaîner avec backoff exponentiel. - Appeler
/orders/pausearrête l'exécution ;/orders/resumecontinue à la même étape. - Les commandes de gros montant bloquent sur
wait-for-manager-approvaljusqu'à livraison de l'événement.
Dépannage
Workflow class not found — confirmez que le class_name dans wrangler.jsonc correspond exactement à la classe exportée.
Step output is not serializable — ne retournez que des valeurs JSON-safe depuis step.do. Encapsulez les objets Date avec .toISOString() et évitez Map, Set ou les instances de classes.
Les étapes s'exécutent plusieurs fois — c'est volontaire quand un Worker crashe en cours d'étape. Utilisez des clés d'idempotence sur chaque mutation externe.
Les sleeps se déclenchent instantanément en dev local — les anciennes versions de Wrangler accéléraient les sleeps. Mettez à jour vers wrangler@3.99 ou plus récent, ou passez --x-dev-env pour utiliser la simulation des minuteurs de production.
Cannot find module cloudflare:workers — assurez-vous que votre tsconfig.json contient "types": ["@cloudflare/workers-types/2026-05-01"] et que vous avez installé @cloudflare/workers-types.
Pour aller plus loin
- Intercalez des Queues entre les requêtes utilisateur et la création de workflows pour éviter de saturer les API en aval lors des pics. Voir notre tutoriel Cloudflare Workers + Hono + D1.
- Persistez les sorties des workflows dans D1 ou R2 pour des requêtes long terme aux côtés de vos données existantes.
- Comparez avec Temporal pour l'exécution durable en TypeScript quand vous avez besoin de portabilité on-prem ou multi-cloud.
- Branchez les workflows sur les flux événementiels Inngest pour une observabilité plus riche.
- Ajoutez des logs structurés avec le traçage OpenTelemetry sur les frontières de chaque étape.
Conclusion
Cloudflare Workflows redéfinit la manière dont on écrit du code serverless de longue durée. Au lieu d'assembler des files d'attente, des planificateurs cron, des boîtes aux lettres mortes et des machines d'état pour les retries, vous écrivez du TypeScript linéaire et laissez la plateforme gérer la durabilité. Combiné à Workers, D1, Queues et R2, c'est la pièce manquante du puzzle full-stack Cloudflare — et l'exécution durable devient accessible à toute équipe à l'aise avec npm install.
Dans ce tutoriel, vous êtes passé d'un projet vierge à un pipeline de commandes prêt pour la production avec retries, sleeps, pause et reprise, validations humaines et tests d'intégration. Considérez-le comme un canevas : remplacez les API externes par les vôtres et vous obtenez un backend résilient qui passe à zéro, coûte quelques centimes par million d'invocations et s'exécute dans chaque data center Cloudflare de la planète.