Skip to content

BDD-165 — Planifier l'exécution automatique des workflows

1. User Story

En tant qu'utilisateur, je veux pouvoir planifier l'exécution automatique d'un workflow (quotidienne, hebdomadaire ou mensuelle, à une heure de mon choix), afin de ne plus avoir à cliquer manuellement sur « Exécuter » pour les traitements récurrents.

2. Critères d'acceptance

#CritèreStatut
1Depuis l'éditeur d'un workflow, l'utilisateur peut créer une règle de planification : fréquence (quotidienne / hebdomadaire / mensuelle) + heure libre (HH:MM)
2Pour une fréquence hebdomadaire, l'utilisateur sélectionne un ou plusieurs jours de la semaine
3Pour une fréquence mensuelle, l'utilisateur sélectionne un jour du mois (1-31)
4Si le jour du mois sélectionné n'existe pas dans un mois donné (ex. le 31 en février), le workflow s'exécute automatiquement le dernier jour de ce mois
5Un workflow peut avoir plusieurs règles de planification actives simultanément
6L'utilisateur peut activer/désactiver, modifier ou supprimer une règle existante
7L'exécution planifiée réutilise exactement le même moteur d'exécution que le bouton « Exécuter » manuel
8Chaque exécution planifiée apparaît dans l'historique des runs avec une distinction claire (manuelle / automatique)

3. Décision d'architecture : mono-processus (@nestjs/schedule)

Deux options ont été comparées avant tout codage (schémas soumis au PO) :

  • Option 1 — mono-processus : le tick de planification tourne dans le même processus Node que l'API, via @nestjs/schedule (@Cron). Aucune infrastructure supplémentaire (pas de Redis/BullMQ, pas de worker dédié).
  • Option 2 — Back + Worker séparés : un second processus dédié à l'exécution des jobs planifiés, avec une file (Redis/BullMQ) pour distribuer le travail entre plusieurs instances.

Décision retenue : Option 1, validée par le PO. Limitation actée : le tick de planification ne peut pas tourner sur plusieurs instances du processus API sans déclencher un même workflow plusieurs fois (pas de scale horizontal du tick). Cette limitation est documentée dans le ticket Jira comme un compromis accepté pour ce périmètre — une migration vers l'Option 2 reste possible ultérieurement sans changer le modèle de données (workflow_schedules et triggered_by/schedule_id sur workflow_runs ne dépendent pas du mécanisme de déclenchement). Voir ADR-07.

4. Décision de conception : next_run_at calculé, pas une expression cron

Le stockage sous forme d'expression cron brute a été envisagé puis écarté : les sémantiques cron standard ignorent un jour du mois qui n'existe pas (ex. 31 ne se déclenche jamais en février) au lieu de le ramener au dernier jour du mois — ce qui contredit directement le critère d'acceptance 4.

Choix retenu : chaque règle stocke ses paramètres structurés (frequency, time, days_of_week, day_of_month, timezone) et une colonne next_run_at calculée explicitement via Luxon, avec clampage du jour du mois au dernier jour disponible (workflow-schedule-next-run.util.ts).

5. Périmètre fonctionnel

Base de données

FichierRôle
sql/tool_25_workflow_schedules.sqlTable workflow_schedules, enums WorkflowScheduleFrequency / WorkflowRunTriggeredBy, colonnes workflow_runs.triggered_by / workflow_runs.schedule_id
back/prisma/schema.prismaModèles workflow_schedules, extension de workflow_runs et workflows

Backend

FichierRôle
back/src/api/workflows/scheduling/workflow-schedule-next-run.util.tsCalcul pur de next_run_at (daily/weekly/monthly), clampage du jour du mois
back/src/api/workflows/scheduling/dto/workflow-schedule.dto.tsDTO create/update/response des règles de planification
back/src/api/workflows/scheduling/workflow-schedule.service.tsCRUD des règles, scoping entreprise, recalcul de next_run_at à la création/modification
back/src/api/workflows/scheduling/workflow-schedule.controller.tsPOST/GET /workflows/:workflowUuid/schedules, PATCH/DELETE /workflows/:workflowUuid/schedules/:scheduleUuid
back/src/api/workflows/scheduling/workflow-schedule-cron.service.tsTick @Cron (chaque minute) : réclame les règles à échéance (updateMany atomique), résout l'utilisateur créateur du workflow, déclenche run() avec triggeredBy=scheduled
back/src/app.module.tsImport conditionnel de ScheduleModule.forRoot() (désactivé si NODE_ENV=test)
back/src/api/workflows/staging/workflow-run-staging-execution.service.tsrun()/executeRun() acceptent triggeredBy/scheduleId, transmis à persistRun()
back/src/api/workflows/workflow-run-history.service.tspersistRun() enregistre triggered_by/schedule_id ; listByWorkflow()/getRunDetail() exposent triggeredBy
back/src/exceptions/workflow.exceptions.tsWorkflowScheduleNotFoundException

Frontend

FichierRôle
front/src/stores/workflow-store.jsActions fetchWorkflowSchedules, createWorkflowSchedule, updateWorkflowSchedule, deleteWorkflowSchedule
front/src/composables/useWorkflowScheduling.jsChargement/CRUD des règles, gestion des erreurs (toasts)
front/src/pages/admin/workflows/workflow-scheduling.format.jsLibellés fréquence/jours, résumé de récurrence lisible
front/src/components/workflows/WorkflowSchedulingDialog.vueDialogue « Planification » (formulaire + liste des règles), accessible depuis le menu « Autres actions » de l'éditeur
front/src/pages/admin/workflows/WorkflowEditorPage.vueEntrée de menu « Planifier l'exécution automatique »
front/src/pages/admin/workflows/workflow-run-history.format.jstriggeredByLabel/triggeredByBadgeClass (badge Manuelle/Automatique)
front/src/components/workflows/WorkflowRunHistoryPanels.vueAffichage du badge manuel/automatique dans la liste des runs
front/src/css/workflow-run-semantic.scssClasses .wf-run-badge--manual / .wf-run-badge--scheduled

6. Comportement du tick de planification

Le tick (WorkflowScheduleCronService.handleTick) s'exécute chaque minute :

  1. Recherche les règles enabled=true, deleted_at IS NULL, next_run_at <= now.
  2. Pour chaque règle due, réclame l'exécution via UPDATE ... WHERE id = ? AND next_run_at <= now (le prochain next_run_at est déjà calculé et écrit) — évite un double déclenchement si un tick précédent chevauche le suivant.
  3. Résout l'utilisateur créateur du workflow (workflows.creator) pour construire le contexte d'exécution (UserCache) — un workflow dont le créateur n'a plus de rôle/entreprise valide est ignoré (log warn).
  4. Déclenche WorkflowRunStagingExecutionService.run(user, workflowUuid, { triggeredBy: 'scheduled', scheduleId }) — le même moteur d'exécution que le bouton « Exécuter » (critère 7).

Un verrou en mémoire (ticking) empêche deux ticks de tourner en parallèle dans le même processus si un tick précédent dépasse la minute.

7. Tests

Backend

FichierCouverture
back/src/api/workflows/scheduling/workflow-schedule-next-run.util.spec.tsdaily/weekly/monthly, clampage du jour du mois (31 → 28/29/30 → retour à 31), erreurs heure/fuseau invalides
back/src/api/workflows/scheduling/workflow-schedule.service.spec.tsCRUD scopé entreprise, recalcul de next_run_at, désactivation
back/src/api/workflows/scheduling/workflow-schedule-cron.service.spec.tsRéclamation atomique, chevauchement de tick ignoré, workflow/créateur introuvable, échec de run n'interrompt pas le tick
back/src/api/workflows/staging/workflow-run-staging-execution.service.spec.tsPropagation de triggeredBy/scheduleId jusqu'à persistRun
back/src/api/workflows/workflow-run-history.service.spec.tstriggered_by par défaut manual, schedule_id persisté, exposition dans l'historique
back/test/e2e/workflow-schedules-controller.e2e-spec.ts401/403/404/400, création/liste/modification/suppression, clamp mensuel

Frontend

FichierCouverture
front/src/__tests__/stores/workflow-store.spec.js4 actions CRUD planification
front/src/__tests__/composables/useWorkflowScheduling.spec.jsChargement, création, modification, suppression, gestion des erreurs
front/src/__tests__/pages/admin/workflows/workflow-scheduling.format.spec.jsLibellés fréquence/jours, résumé de récurrence
front/src/__tests__/pages/admin/workflows/workflow-run-history.format.spec.jsBadge manuel/automatique
front/src/__tests__/components/workflows/WorkflowSchedulingDialog.spec.jsChargement, création, édition, suppression (avec confirmation)

8. Hors périmètre

  • Scale horizontal du tick de planification (plusieurs instances du processus API) — voir ADR-07.
  • Historique des tentatives échouées avec retry automatique (une exécution planifiée qui échoue attend simplement la prochaine échéance).
  • Notification (email, etc.) en cas d'échec d'une exécution planifiée.