Construire une opération IA à étape unique est simple : prendre une entrée, appeler un LLM, retourner la sortie. Construire un moteur de workflow multi-étapes qui supporte le branchement, les boucles, l’exécution parallèle, les portes d’approbation humaine et les réessais automatiques est un tout autre type de problème.
Cet article couvre l’architecture du moteur de workflow de JieGou — le modèle d’exécution, les 8 types d’étapes, comment les données circulent entre les étapes, comment les approbations mettent en pause et reprennent l’exécution, et les garde-fous qui maintiennent la fiabilité.
Modèle d’exécution
Un workflow est un graphe dirigé d’étapes. L’exécution commence avec executeWorkflow(), qui crée un enregistrement WorkflowRun, construit un StepExecutionContext partagé et appelle executeStepList() pour traiter les étapes séquentiellement.
Le contexte porte une Map previousStepOutputs — un magasin clé-valeur où chaque étape terminée dépose sa sortie pour que les étapes en aval la référencent. C’est la colonne vertébrale du flux de données. L’étape B peut référencer la sortie de l’étape A en utilisant la syntaxe de template comme {{step.stepA.fieldName}}.
Chaque workflow a un timeout configurable (5 minutes par défaut), appliqué via un champ deadlineMs dans le contexte. Les étapes individuelles ont également leur propre timeout (60 secondes par défaut) appliqué par un wrapper withStepTimeout().
Les 8 types d’étapes
Étape recette
Le cheval de bataille. Exécute un template de prompt réutilisable via executeRecipe() et stocke la sortie parsée dans previousStepOutputs. Le mapping d’entrée résout les références aux entrées du workflow, aux sorties des étapes précédentes, aux valeurs statiques ou aux éléments de boucle.
Étape condition
Évalue une expression booléenne et exécute soit thenSteps soit elseSteps récursivement. C’est un vrai branchement — les deux chemins peuvent contenir tout type d’étape, y compris des conditions imbriquées. Le moteur appelle executeStepList() récursivement sur la branche choisie.
Étape boucle
Itère sur une collection et exécute une liste de sous-étapes pour chaque élément. La collection peut provenir de 4 sources : un tableau statique défini dans le workflow, la sortie d’une étape précédente, un champ d’entrée du workflow ou des éléments de boucle parent.
Chaque itération obtient sa propre Map loopContext, pour que les sous-étapes puissent référencer l’élément courant via {{loop_item.fieldName}}. Les résultats d’itération sont stockés comme un tableau avec des stepRuns imbriqués pour l’observabilité.
Étape parallèle
Exécute plusieurs branches concurremment via Promise.allSettled(). Chaque branche est une liste indépendante d’étapes avec son propre tableau stepRuns. C’est utile quand plusieurs opérations indépendantes peuvent s’exécuter simultanément — par exemple, enrichir un lead depuis trois sources de données différentes en même temps.
Étape approbation
Le type d’étape le plus intéressant architecturalement. Quand l’exécution atteint une étape d’approbation, elle lance une ApprovalPauseError. C’est une exception contrôlée — pas un crash.
L’erreur est attrapée au niveau supérieur, le WorkflowRun est persisté avec le statut pending_approval et les approbateurs éligibles sont notifiés par email. L’exécution s’arrête complètement. Aucune ressource n’est retenue.
Quand un approbateur agit (approuver ou rejeter via l’API), resumeWorkflowFromApproval() charge le run persisté, appelle reconstructPreviousOutputs() pour reconstruire la Map previousStepOutputs à partir des stepRuns sauvegardés et reprend l’exécution à partir de l’étape suivant la porte d’approbation.
La reconstruction est récursive — elle parcourt thenStepRuns, elseStepRuns, iterations[] et branchStepRuns[] pour restaurer chaque sortie imbriquée. Cela signifie que les approbations fonctionnent correctement même quand elles sont à l’intérieur d’une branche conditionnelle ou d’une itération de boucle.
Étape écriture en KB
Capture la sortie d’une étape et l’écrit dans un document de base de connaissances. Cela permet des workflows qui construisent des connaissances institutionnelles — un workflow de triage de support pourrait écrire des résumés de résolution dans une KB que les futures exécutions référencent via RAG.
Étape transfert
Notifie les utilisateurs d’un département cible par email et notification in-app. C’est un no-op pour l’exécution — elle ne produit pas de sortie ni ne bloque le pipeline. Utile pour les flux d’escalade où un humain doit être alerté mais le workflow doit continuer.
Étape action navigateur
Exécute un appel d’outil MCP via l’extension navigateur. Acquiert un client depuis le pool de connexions MCP, résout les arguments de template (champs d’entrée du workflow et sorties des étapes précédentes) et retourne le résultat de l’outil.
Flux de données : résolution de templates
Les étapes se référencent entre elles via une syntaxe de template résolue au moment de l’exécution :
{{workflow_input.fieldName}}— Référence les données d’entrée du workflow{{step.stepId.path.to.value}}— Référence la sortie d’une étape précédente en notation pointée{{loop_item.fieldName}}— Référence l’élément courant dans une itération de boucle
Le résolveur utilise getNestedValue() pour le parcours de chemins en notation pointée, gérant les tableaux et objets imbriqués. Les mappings d’entrée déclarent leur source explicitement : workflowInput, previousStep, static ou loopItem.
Réessai et gestion d’erreurs
Toutes les défaillances ne sont pas permanentes. Les limites de débit, les erreurs 5xx transitoires, les timeouts et les échecs de connexion sont réessayables. Les erreurs de validation côté client (4xx) ne le sont pas.
La stratégie de réessai utilise un backoff exponentiel avec jitter : Math.min(30000, 2000 * 2^attempt + jitter aléatoire). C’est environ 2 secondes, 4 secondes, 8 secondes, 16 secondes, plafonné à 30 secondes, avec un jitter aléatoire pour éviter les problèmes de troupeau tonnerre. Le nombre maximum de tentatives par défaut est 3, configurable par étape.
Chaque réessai est journalisé avec l’index de tentative et les détails de l’erreur. Si toutes les tentatives échouent, l’étape est marquée comme échouée et le workflow se termine (sauf si la gestion d’erreurs au niveau du workflow spécifie autrement).
Contrôle de concurrence
Chaque compte est limité à 10 appels LLM concurrents via un sémaphore Redis. Cela empêche un workflow par lots de monopoliser les connexions au fournisseur.
Les connexions MCP utilisent un client poolé avec éviction LRU (max 20 connexions, timeout d’inactivité de 60 secondes). Le pool gère la configuration de connexion, le keepalive et le nettoyage.
Les deux systèmes utilisent une sémantique fail-open sur les erreurs Redis — une couche de cache dégradée ne devrait jamais empêcher l’exécution du workflow.
Observabilité
Trois couches d’observabilité s’exécutent concurremment :
Métriques Prometheus suivent workflowExecutionsTotal et workflowDuration par statut. Cela fournit des tableaux de bord opérationnels pour les taux de succès et les percentiles de latence.
Spans OpenTelemetry créent une hiérarchie de traces avec des spans au niveau workflow et au niveau étape, portant l’ID du workflow, le nom et les métadonnées de l’étape. Ceux-ci s’intègrent avec tout backend compatible OTel.
Traces d’exécution sont un arbre de spans hiérarchique personnalisé persisté dans Firestore via fire-and-forget. Ceux-ci alimentent l’inspecteur d’exécution détaillé dans l’interface, montrant exactement ce qui s’est passé à chaque étape incluant entrées, sorties, timing et tentatives de réessai.
Vérifications pré-vol
Avant le début de l’exécution, le moteur lance la validation :
- Validation du schéma d’entrée — Les entrées du workflow sont vérifiées contre le schéma déclaré
- Validation des mappings — Tous les mappings d’entrée des étapes sont vérifiés (les étapes référencées existent, les chemins sont plausibles)
- Vérification MCP — Si des étapes nécessitent des actions navigateur, le moteur vérifie la connectivité du client MCP en amont plutôt que d’échouer en milieu d’exécution
- Résolution de contexte automatique — Les documents de base de connaissances sont résolus depuis les sources workflow, département et ID explicites, pour que le contexte RAG soit prêt avant l’exécution de la première étape
Leçons apprises
Les portes d’approbation comme exceptions simplifient l’architecture. Notre première conception utilisait une machine à états avec des états pause/reprise explicites. L’approche basée sur les exceptions est plus propre — le workflow avance jusqu’à rencontrer une approbation, lance l’exception, persiste et s’arrête. La reprise reconstruit l’état et continue. Pas de transitions d’état complexes à gérer.
La reconstruction récursive des sorties vaut la complexité. Reconstruire previousStepOutputs depuis les runs d’étapes persistés nécessite de parcourir chaque niveau d’imbrication — conditions, boucles, branches parallèles. C’est du code complexe, mais cela signifie que les approbations fonctionnent correctement à n’importe quelle profondeur d’imbrication sans traitement spécial.
Les timeouts par étape empêchent les retards en cascade. Un seul appel LLM lent ne devrait pas consommer le timeout entier du workflow. Le défaut de 60 secondes par étape attrape les requêtes bloquées tôt, et le timeout au niveau workflow agit comme filet de sécurité.
L’observabilité fire-and-forget garde le chemin critique rapide. Les traces d’exécution, les livraisons webhook et les sorties de destination sont toutes dispatchées de manière asynchrone après la fin de l’exécution principale. L’utilisateur reçoit son résultat sans attendre que les écritures d’observabilité se terminent.