Multi-Step Business Workflows: DAG, מקביליות ו-Versioning בתהליכים מורכבים

מאת צוות מדיה דיל · 08.08.2026 · Automation · 10 דק׳

מדריך ארכיטקטוני לתהליכים עסקיים מרובי-שלבים: מודל DAG, ריצה מקבילה, ניהול State עשיר, Versioning ולולאות חזרה בתהליכים לא ליניאריים.

חברת ביטוח בונה תהליך טיפול בתביעה: קליטת מסמכים, בדיקת פוליסה, הערכת נזק (לעיתים דורשת שמאי חיצוני), חישוב פיצוי, אישור ניהולי, והעברת תשלום. זהו לא תהליך בן שלושה שלבים — זהו גרף מסועף עם עשרות תרחישים אפשריים, כל אחד עם משך זמן שונה, תלויות שונות, ואפשרות לחזור אחורה (תביעה שנדחתה יכולה להיפתח מחדש בערעור). ניהול תהליך כזה כרצף קוד ליניארי הוא מתכון בטוח לכאוס. Multi-Step Business Workflows דורשים חשיבה ארכיטקטונית שונה לגמרי מסקריפט רגיל — הם צריכים לטפל בהסתעפויות, בתלויות מקבילות, בכשלים חלקיים ובשינויי דרישות שמגיעים תוך כדי תנועה. המאמר הזה סוקר את הדפוסים שמאפשרים לבנות תהליכים כאלה בצורה שניתן להבין, לתחזק ולהרחיב.

מ-Flowchart לגרף ניתן לביצוע: DAG כמודל חשיבה

הדרך הנכונה לחשוב על תהליך עסקי מרובה שלבים היא לא כרצף ליניארי אלא כ-Directed Acyclic Graph (DAG) — צמתים שמייצגים משימות, וקשתות שמייצגות תלויות ביניהן. חלק מהצמתים יכולים לרוץ במקביל (בדיקת פוליסה והערכת נזק ראשונית יכולות להתבצע בו-זמנית, שתיהן לא תלויות זו בזו), וחלק חייבים להמתין לתוצאה של אחרים (חישוב הפיצוי תלוי בסיום שתי הבדיקות). המודל הזה, שמוכר גם מכלים כמו Apache Airflow או Dagster בעולם ה-data engineering, מתאים באותה מידה לתהליכים עסקיים — הוא מכריח לחשוב במפורש על תלויות, במקום להניח סדר שרירותי שמקורו רק בסדר הכתיבה של הקוד.

היתרון המעשי של חשיבה בגרף: כשמתגלה שצריך להוסיף שלב חדש (למשל בדיקת הונאה שנוספת כדרישה רגולטורית), קל להבין בדיוק היכן להכניס אותו בגרף התלויות ומה ההשפעה שלו על שאר השלבים — הרבה יותר קל מאשר לחפש היכן "לתחוב" קוד חדש בתוך רצף if-else מקונן.

ריצה מקבילה מול ריצה סדרתית: מתי ולמה

ריצה מקבילה של שלבים בלתי תלויים מקצרת משמעותית את זמן הריצה הכולל של תהליך, אבל היא מוסיפה מורכבות: מה קורה אם שלב אחד מתוך שניים מקבילים נכשל בעוד השני הצליח? הגישה הנכונה תלויה באופי התלות: אם שני השלבים המקבילים עצמאיים לחלוטין (לא משפיעים זה על זה), כשל באחד לא אמור לעצור את השני — הוא ממשיך וממתין, וכשל בודד מטופל בנפרד (retry או escalation לאותו שלב בלבד). אם לעומת זאת יש תלות עקיפה (שני השלבים כותבים לאותה ישות ועלולים להתנגש), צריך לתאם ביניהם דרך locking או תזמון מפורש.

כלל אצבע מעשי: מקבילות משתלמת כשעלות התיאום הנוספת (locking, error handling מורכב יותר) נמוכה מהחיסכון בזמן. בתהליכים שממתינים ממילא לפעולה אנושית ארוכת-טווח (ימים), החיסכון בכמה שניות של ריצה מקבילה כמעט לא מורגש — שם עדיף פשטות ובהירות של ריצה סדרתית, גם במחיר של כמה שניות נוספות בזמן ריצה כולל, כי הפשטות משתלמת בטווח הארוך מבחינת עלות תחזוקה. במקומות שבהם השלבים הם קריאות API מהירות שיחד יוצרות latency מצטבר משמעותי, מקבילות משתלמת בבירור.

ניהול State מורכב: יותר מ-"הצליח / נכשל"

בתהליך פשוט, מספיק status בינארי. בתהליך עסקי אמיתי, כל שלב יכול להיות באחד ממספר מצבים: not_started, in_progress, waiting_for_input, completed, failed, skipped (רלוונטי כשתנאי מסוים לא מתקיים ושלב מדלג על עצמו), או retrying. מודל State עשיר כזה מאפשר לענות על שאלות עסקיות אמיתיות — "כמה תביעות תקועות בהמתנה לשמאי יותר משבוע" — בלי לפרש היוריסטית לוגים לא מובנים. שדה status עשיר כזה גם מאפשר לבנות אינדיקטורים תפעוליים אמיתיים: זמן שהייה ממוצע בכל מצב, ואיזה שלב סטטיסטית גורם לרוב הכשלים, במקום להסתמך על תחושת בטן של הצוות התפעולי.

חשוב גם להבחין בין state של תהליך שלם (overall workflow state) לבין state של כל שלב בנפרד — תהליך יכול להיות "in_progress" ברמה הכללית בזמן שחלק מהשלבים כבר completed וחלק עדיין pending. הפרדה ברורה בין שתי הרמות האלה מונעת בלבול נפוץ שבו דוחות מציגים סטטוס לא מדויק כי מישהו בלבל בין המצב הכולל למצב של שלב יחיד.

שינויי תהליך תוך כדי ריצה: הבעיה שאף אחד לא מתכנן אליה

אחת הבעיות הקשות ביותר בתהליכים ארוכי-טווח היא מה קורה כשהתהליך העסקי עצמו משתנה בזמן שיש instances פעילים שרצים לפי הגרסה הישנה. אם מוסיפים שלב חדש חובה (למשל בדיקת הונאה נוספת שהרגולטור דרש), תביעות שכבר עברו את הנקודה הזו בגרף לא אמורות "לחזור אחורה" ולעבור אותו, אבל תביעות חדשות כן. הפתרון הנכון הוא versioning ברמת ה-workflow definition עצמו — כל instance נוצר עם reference לגרסת התהליך שהייתה בתוקף ברגע יצירתו, וממשיך לפעול לפיה עד סיומו, גם אם הגרסה החדשה כבר בתוקף לתביעות חדשות. בפועל, זה אומר שבמערכת בשלה קיימות בו-זמנית כמה "גרסאות חיות" של אותו תהליך, וכלי הניטור צריכים לדעת להציג זאת בבירור — כדי שמישהו שבודק דוח סטטוס לא יתבלבל בין תביעה שרצה לפי v3 לתביעה שרצה לפי v4.

Loops וחזרות: כשתהליך לא ליניארי באמת

לא כל תהליך עסקי מתקדם רק קדימה. תביעת ביטוח שנדחתה יכולה להיפתח מחדש בערעור, מה שמחזיר אותה לשלב מוקדם יותר בגרף עם קונטקסט חדש (נימוקי הערעור). מודל התהליך צריך לתמוך במפורש בקשתות "חזרה לאחור" (loops), לא רק בקשתות קדימה, ולשמור בכל חזרה כזו את ההיסטוריה המלאה — לא רק את הניסיון הנוכחי, אלא גם את כל הניסיונות הקודמים, כדי שאפשר יהיה לראות בבירור "זו הפעם השנייה שהתביעה הזו נדחית ועוברת ערעור" ולא רק את המצב הנוכחי בבידוד. מבחינת מודל הנתונים, זה אומר שרשומת "ניסיון" (attempt) היא ישות נפרדת מרשומת "תביעה" — תביעה אחת יכולה להכיל כמה attempts לאורך זמן, וכל attempt שומר את ההקשר המלא שהיה רלוונטי לו, כולל למה הוא נפתח מחדש ומי יזם זאת.

בדיקות ל-Workflows מורכבים

בדיקת יחידה רגילה לא מספיקה לתהליך עם עשרות תרחישים אפשריים והסתעפויות. הגישה הנכונה כוללת שתי שכבות: בדיקות יחידה לכל שלב בנפרד (מבודד מהתהליך הכולל, עם mocks לתלויות חיצוניות), ובדיקות אינטגרציה שמריצות תרחישי-קצה מלאים על התהליך כולו — כולל תרחישי כשל (מה קורה אם שמאי לא מגיב בזמן, מה קורה אם שני שלבים מקבילים נכשלים בדיוק בו-זמנית). כדאי לבנות ספריית תרחישי בדיקה (fixtures) שמייצגת את כל הנתיבים האפשריים בגרף, ולהריץ אותם אוטומטית בכל שינוי בקוד התהליך — כי שינוי קטן בשלב אחד עלול לשבור נתיב שלם שאף אחד לא חשב עליו בזמן הכתיבה. חשוב גם לתחזק את הפיקסצ'רים האלה כחלק אינטגרלי מהקוד עצמו — לא כמסמך Word נפרד שמישהו כתב פעם אחת ואף אחד לא מעדכן — כדי שכל שינוי בגרף התלויות יחייב מפורשות לבדוק אם התרחישים הקיימים עדיין רלוונטיים או שצריך להוסיף חדשים. גישה שעובדת היטב בפועל היא לגזור חלק מהפיקסצ'רים ישירות מתביעות אמיתיות (מתועתקות ומטוהרות ממידע אישי) שגרמו לבאג בעבר, כך שכל תיקון תקלה מלווה אוטומטית בהוספת מקרה בדיקה שמונע הישנות שלה.

דוגמת קוד: הגדרת גרף תלויות עם ריצה מקבילה

הנה דוגמה שממחישה איך מגדירים תהליך כגרף תלויות מפורש, שבו כל שלב מכריז מראש על מה הוא תלוי, והמנוע קובע בעצמו אילו שלבים יכולים לרוץ במקביל:

const claimWorkflow = {
  steps: {
    intake: { dependsOn: [], handler: intakeDocuments },
    policyCheck: { dependsOn: ['intake'], handler: verifyPolicy },
    damageAssessment: { dependsOn: ['intake'], handler: assessDamage },
    fraudCheck: { dependsOn: ['intake'], handler: runFraudCheck },
    calculatePayout: {
      dependsOn: ['policyCheck', 'damageAssessment', 'fraudCheck'],
      handler: calculatePayout
    },
    managerApproval: { dependsOn: ['calculatePayout'], handler: requestApproval },
    disburse: { dependsOn: ['managerApproval'], handler: disbursePayment }
  }
};

async function runWorkflow(def, instanceId) {
  const completed = new Set();
  const state = await loadState(instanceId);

  while (completed.size < Object.keys(def.steps).length) {
    const ready = Object.entries(def.steps).filter(
      ([name, step]) =>
        !completed.has(name) &&
        step.dependsOn.every(dep => completed.has(dep))
    );
    await Promise.all(ready.map(async ([name, step]) => {
      await step.handler(state);
      completed.add(name);
      await persistStepCompletion(instanceId, name);
    }));
  }
}

שימו לב ששלושת השלבים policyCheck, damageAssessment ו-fraudCheck כולם תלויים רק ב-intake, ולכן המנוע מריץ אותם במקביל אוטומטית — הלוגיקה שמזהה "אילו שלבים מוכנים לרוץ עכשיו" היא כללית ולא צריכה לדעת דבר ספציפי על תביעות ביטוח. calculatePayout, לעומת זאת, ממתין ששלושתם יסתיימו לפני שהוא מתחיל, כי הוא תלוי מהותית בתוצאה של כל אחד מהם בנפרד. המבנה הזה הופך את השאלה "מה קורה אם נוסיף שלב חדש" לשינוי מקומי בהגדרת התהליך, ולא לשינוי בלוגיקת הריצה עצמה.

טיפול בכשל חלקי בענפים מקבילים

כשמריצים כמה שלבים במקביל, חייבים להחליט מראש מה קורה כשענף אחד נכשל בעוד האחרים עדיין רצים. יש שלוש אסטרטגיות נפוצות: fail-fast, שבה כשל בענף אחד עוצר מיד את כל הענפים האחרים (מתאים כשהתוצאה הסופית חסרת משמעות עסקית בלי כל הענפים שהצליחו במלואם); fail-slow, שבה כל הענפים ממשיכים לרוץ עד הסוף גם אם אחד נכשל, וכשל מדווח רק בסיום (מתאים כשענפים עצמאיים לחלוטין ורוצים לקבל כמה שיותר מידע גם אם חלקו חסר); ו-partial-continue, שבה ענף שנכשל מסומן כ-skipped עם ברירת מחדל, והתהליך ממשיך עם ערך חלופי (מתאים לענפים לא קריטיים, כמו העשרת נתונים אופציונלית). הבחירה בין השלוש היא החלטה עסקית, לא רק טכנית, וצריכה להיקבע מראש לכל שלב בגרף, לא כברירת מחדל גורפת לכל התהליך.

סיכום

תהליכים עסקיים מרובי-שלבים אמיתיים דורשים מודל מנטלי של גרף תלויות, לא רצף ליניארי — עם תמיכה במקבילות, ב-state עשיר לכל שלב, בversioning כשהתהליך משתנה, ובלולאות חזרה כשהעסק לא באמת ליניארי. ההשקעה בעיצוב הנכון מלכתחילה חוסכת חודשים של תיקוני-טלאי מאוחר יותר, כשמתגלה שהתהליך "האמיתי" תמיד היה מורכב יותר ממה שהמודל הראשוני הניח. תהליכים כאלה נבנים לרוב מעל מנוע workflow ייעודי — ראו את הדיון המעמיק ב-Workflow Automation — ולעיתים משלבים גם שלבי אישור אנושי שמתוארים ב-Approval Workflow Automation. השילוב בין שני הדפוסים — מנוע הרצה שיודע לטפל בכשל חלקי ובמקבילות, יחד עם מודל נתונים שמאפשר לשאול שאלות עסקיות אמיתיות על התהליך — הוא מה שבסופו של דבר מבדיל בין מערכת אוטומציה שהצוות סומך עליה לבין מערכת שמישהו צריך לבדוק ידנית "רק כדי לוודא".

תגיות: Multi-Step Workflows · DAG · Business Process · Parallel Execution · Workflow Versioning · State Management

← חזרה לבלוג · צור קשר