الكتابات/tutorial/2026/05
Tutorial16 مايو 2026·30 دقيقة

Cloudflare Workflows: بناء تطبيقات متعددة الخطوات وقابلة للاستمرار باستخدام TypeScript في 2026

تعلّم كيف تبني تطبيقات متعددة الخطوات قابلة للاستمرار ومقاومة للأعطال باستخدام Cloudflare Workflows وTypeScript. ستبني خط معالجة طلبات حقيقي مع إعادة المحاولات والانتظار والتدخل البشري — يعمل بدون خوادم على حافة الشبكة.

مهام طويلة الأمد تنجو من الأعطال وإعادة المحاولات وإعادة التشغيل — بدون طوابير أو قواعد بيانات أو حتى خادم واحد. يقدّم Cloudflare Workflows مفهوم التنفيذ القابل للاستمرار إلى Workers: اكتب شيفرتك متعددة الخطوات وكأنها متزامنة، ودع المنصة تتولى الحفظ وإعادة المحاولة والاستعادة نيابة عنك.

ما الذي ستبنيه؟

في هذا الدليل العملي، ستبني خط معالجة طلبات (Order Pipeline) متكامل على هيئة Cloudflare Workflow. يقوم الخط بـ:

  1. التحقق من صحة الطلب الوارد
  2. خصم قيمة الطلب من بطاقة العميل عبر واجهة دفع خارجية
  3. حجز المخزون في خدمة منفصلة
  4. الانتظار حتى يوم العمل التالي قبل جدولة الشحن
  5. إرسال رسالة تأكيد إلى العميل
  6. الاستعادة بسلاسة من أي فشل عبر إعادة المحاولة التلقائية

في النهاية سيكون لديك سير عمل جاهز للإنتاج، عديم التكرار (idempotent)، يعمل على حافة الشبكة، ينجو من إعادة تشغيل Workers، ويستطيع التوقف لساعات أو أيام دون استهلاك وقت معالجة.


المتطلبات

قبل أن تبدأ، تأكد من توفر:

  • Node.js 20+ مثبت على جهازك (التحميل)
  • حساب Cloudflare — الباقة المجانية تدعم Workflows (التسجيل)
  • Wrangler CLI الإصدار 3.99+ — أداة Cloudflare للمطورين (سنثبتها أدناه)
  • إلمام أساسي بـ TypeScript وآلية async/await
  • محرر شيفرة (يُفضّل VS Code)

لماذا Workflows؟ سكربتات Workers التقليدية يجب أن تنتهي خلال ثوانٍ. أما Workflows فيمكن أن تعمل لدقائق أو ساعات أو حتى أيام. صُمّمت للتعامل مع الواقع الفوضوي للأنظمة الموزعة: واجهات خارجية تتعطل، عمليات دفع تحتاج إعادة محاولة، وموافقات بشرية تستغرق وقتاً.


الخطوة 1: تثبيت Wrangler وإنشاء المشروع

ابدأ بتثبيت Wrangler عالمياً وإنشاء مشروع Workers جديد بدعم Workflows:

npm install -g wrangler@latest
wrangler login

ستفتح نافذة المتصفح لتسجيل الدخول إلى Cloudflare. بعد العودة إلى الطرفية، أنشئ مشروع TypeScript جديداً:

npm create cloudflare@latest noqta-order-pipeline -- \
  --type=hello-world \
  --lang=ts \
  --git=true \
  --deploy=false
 
cd noqta-order-pipeline

يُنشئ هذا الأمر Worker بسيطاً مع TypeScript وVitest وملف إعدادات wrangler.jsonc.


الخطوة 2: تفعيل Workflows في wrangler.jsonc

افتح ملف wrangler.jsonc وأضف ربط workflows. هذا يخبر Cloudflare بأن Worker الخاص بك يُصدّر صنف Workflow اسمه 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
  }
}

يُستخدم binding للإشارة إلى سير العمل من معالج HTTP، بينما يجب أن يطابق class_name اسم الصنف المُصدَّر في شيفرتك.


الخطوة 3: نمذجة بيانات الطلب

أنشئ ملف src/types.ts بأنواع صارمة لبيانات الطلب التي ستمر عبر خط المعالجة:

// 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;
}

هذه الأنواع هي العقد بين كل خطوة وأخرى. وبما أن Workflows تحفظ مخرجات كل خطوة في تخزين دائم، فيجب أن تكون كل القيم قابلة للتحويل إلى JSON — أي أنواع أولية فقط.


الخطوة 4: كتابة صنف الـ Workflow

والآن قلب التطبيق. استبدل محتوى src/index.ts بما يلي:

// 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>;

نقاط جديرة بالتأمل:

  • step.do(name, ...) هو الوحدة الأساسية للتنفيذ القابل للاستمرار. تُنفَّذ كل خطوة بنجاح مرة واحدة فقط، وتُحفظ قيمتها المرتجعة لإعادة التشغيل عند الاستئناف.
  • إعدادات retries تصريحية. إذا أطلقت chargeCard استثناءً، تتولى المنصة إعادة المحاولة بتراجع أسّي — تبقى شيفرتك نظيفة.
  • step.sleepUntil يحرر موارد المعالجة. لا يستهلك سير العمل أي وقت معالجة أثناء النوم، ويستأنف العمل في الوقت المحدد.
  • إرجاع قيمة من step.do يجعلها متاحة للخطوات اللاحقة حتى بعد إزالة Worker من الذاكرة.

الخطوة 5: تنفيذ النداءات الخارجية

أضِف الدوال المساعدة إلى src/index.ts (أو ضعها في وحدة مستقلة):

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;
}

مفاتيح عدم التكرار (Idempotency) ضرورية. قد تُنفَّذ خطوة Workflow أكثر من مرة إذا قُتل Worker في المنتصف. مرّر مفتاح Idempotency لكل واجهة خارجية تعدّل حالة، مرتبطاً باسم الخطوة ومعرّف نسخة سير العمل.


الخطوة 6: إضافة الأسرار والربط المحلي

تقرأ Workflows الأسرار بنفس طريقة Workers العادية. للتطوير المحلي، أنشئ ملف .dev.vars في جذر المشروع:

PAYMENT_API_KEY="test_sk_local_xxxxxxxxxxxxxxxx"
INVENTORY_API_URL="https://staging.inventory.example.com"
EMAIL_API_URL="https://staging.email.example.com"

للإنتاج، ادفع الأسرار إلى Cloudflare:

wrangler secret put PAYMENT_API_KEY
wrangler secret put INVENTORY_API_URL
wrangler secret put EMAIL_API_URL

سيطلب كل أمر منك إدخال القيمة، ثم يحفظها مشفّرة في مخزن أسرار Cloudflare.


الخطوة 7: تشغيل الـ Workflow محلياً

شغّل خادم التطوير مع محاكاة كاملة لسير العمل:

wrangler dev --x-dev-env

ستطبع الأداة عنواناً محلياً مثل http://127.0.0.1:8787. من طرفية أخرى، أطلق طلباً:

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 }
    ]
  }'

ستحصل على استجابة JSON تحتوي على instanceId. افتح http://127.0.0.1:8787/__workflows في المتصفح (يوفر Wrangler مفتشاً مدمجاً) وراقب انتقال كل خطوة عبر الحالات running ثم success ثم مرحلة sleeping الطويلة.


الخطوة 8: فحص النسخ النشطة والتحكم بها

أضِف مساراً ثانياً للاستعلام عن سير العمل والتحكم به. استبدل معالج fetch بهذا الموجّه الأكثر ثراءً:

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>;

أصبح بإمكانك الآن:

  • POST /orders — بدء سير عمل طلب جديد
  • GET /orders?id=ord_2026_001 — قراءة الخطوة الحالية وحالة التنفيذ والوقت المنقضي
  • POST /orders/pause?id=... — إيقاف سير العمل مؤقتاً لأجل غير مسمى
  • POST /orders/resume?id=... — استئناف سير العمل المتوقف

هذا هو الأساس لبناء لوحة تحكم إدارية أو أداة دعم تتيح للموظفين إيقاف الشحنات في منتصف الطريق.


الخطوة 9: إضافة خطوة موافقة بشرية

للطلبات عالية القيمة، لنفترض أنك تريد موافقة بشرية قبل الدفع. استخدم 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" };
  }
}

ثم اكشف مساراً لتسليم الحدث:

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 });
}

أثناء الانتظار لا يستهلك سير العمل أي وقت معالجة وينجو من إعادة تشغيل Worker لأجل غير مسمى. وعندما يوافق المدير عبر واجهتك الإدارية، يستأنف سير العمل من حيث توقف بالضبط.


الخطوة 10: كتابة اختبار تكامل بـ Vitest

سير العمل قابل للاختبار من الطرف إلى الطرف عبر مجمّع اختبارات Cloudflare Workers. أضِف هذا إلى 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();
  });
});

شغّل الاختبارات:

npm test

تعمل الاختبارات على نسخة محلية من Workers — بلا شبكة وبلا نداءات حقيقية لـ Cloudflare.


الخطوة 11: النشر إلى الإنتاج

عندما تصبح جاهزاً للإطلاق:

wrangler deploy

سيقوم Wrangler برفع Worker الخاص بك، وتسجيل صنف OrderPipeline، وطباعة عنوان عام. من هذه اللحظة، يعمل سير العمل عبر شبكة Cloudflare العالمية مع حفظ مدمج للحالة.

اختبره بطلب حقيقي:

curl -X POST https://noqta-order-pipeline.your-subdomain.workers.dev/orders \
  -H "Content-Type: application/json" \
  -d @order.json

افتح لوحة تحكم Cloudflare، انتقل إلى Workers and Pages → Workflows، وانقر على نسختك لترى خط زمني تفصيلي لكل عملية إعادة محاولة ونوم وحدث.


اختبار التطبيق

تحقق من سير العمل وفق هذه القائمة:

  • الطلب الصحيح يعيد instanceId ويتقدم عبر كل الخطوات.
  • إيقاف خادم التطوير في المنتصف ثم تشغيله من جديد يستأنف من آخر خطوة مكتملة.
  • إجبار chargeCard على إطلاق استثناء يُظهر إعادة المحاولات بتراجع أسّي.
  • استدعاء /orders/pause يوقف التنفيذ، و/orders/resume يستأنف من نفس الخطوة.
  • الطلبات عالية القيمة تتوقف عند wait-for-manager-approval حتى تُرسل الحدث.

استكشاف الأخطاء وإصلاحها

Workflow class not found — تأكد أن class_name في wrangler.jsonc يطابق تماماً اسم الصنف المُصدَّر.

Step output is not serializable — أرجع فقط قيماً قابلة للتحويل إلى JSON من step.do. غلّف كائنات Date بـ .toISOString() وتجنّب Map وSet ونسخ الأصناف.

تنفيذ الخطوات أكثر من مرة — هذا متوقع عند تعطّل Worker في منتصف الخطوة. استخدم مفاتيح Idempotency لكل عملية تعديل خارجية.

النوم ينتهي فوراً في التطوير المحلي — كانت الإصدارات الأقدم من Wrangler تختصر فترات النوم. حدّث إلى wrangler@3.99 أو أحدث، أو مرّر --x-dev-env لاستخدام محاكاة المؤقتات الإنتاجية.

Cannot find module cloudflare:workers — تأكد أن ملف tsconfig.json يحتوي "types": ["@cloudflare/workers-types/2026-05-01"] وأنك ثبّت @cloudflare/workers-types.


الخطوات التالية


الخلاصة

يعيد Cloudflare Workflows صياغة الطريقة التي نكتب بها الشيفرات طويلة الأمد بدون خوادم. بدلاً من تجميع الطوابير والمجدولات وصناديق الرسائل الميتة وآلات الحالة لإعادة المحاولة، تكتب TypeScript خطّياً وتترك المنصة تتولى الاستمرارية. بدمج Workers و D1 و Queues و R2، تكتمل القصة المتكاملة لـ Cloudflare — ويصبح التنفيذ القابل للاستمرار في متناول أي فريق يجيد npm install.

في هذا الدليل انتقلت من مشروع فارغ إلى خط معالجة طلبات جاهز للإنتاج مع إعادة محاولات وفترات نوم وإيقاف واستئناف وموافقات بشرية واختبارات تكامل. اعتبره قالباً: بدّل واجهاتك الخارجية مكان الواجهات الافتراضية، وسيكون لديك خلفية صلبة تتوسع إلى الصفر وتكلّف فلسات لكل مليون استدعاء وتعمل في كل مركز بيانات لـ Cloudflare على وجه الأرض.