Sub-Workflows / verschachtelte Workflows

Anforderung

Ein Workflow-Schritt soll eine Instanz eines anderen Workflow-Templates als “Sub-Workflow” starten koennen. Der aeussere Prozess kann auf das Ergebnis warten oder fire-and-forget weiterlaufen. Eingabe-Daten gehen in den Sub-Workflow, Ergebnis-Daten kommen ggf. zurueck.

Aktueller Stand

Die Workflow-Engine ist bereits graph-basiert und gut modular aufgebaut (Step-Registry-Pattern). Wichtig: workflow_instance.parent_instance_id existiert bereits seit Migration 022 — die Sub-Workflow-Verschachtelung war bei der Foundation-Architektur schon vorgesehen, aber nicht umgesetzt.

Tabellen (41 Stueck, siehe scripts/schema_current.sql):

  • workflow_template, workflow_step, workflow_step_edge — Definition
  • workflow_instance (mit parent_instance_id), workflow_step_instance — Laufzeit
  • Pro Step-Typ je eine typisierte Tabelle (workflow_step_form, workflow_step_document, workflow_step_notification, workflow_step_appointment, workflow_step_authorization, workflow_step_timer, workflow_step_branch/_route, workflow_step_fork, workflow_step_join, workflow_step_end) — siehe ADR-077.

Step-Typen (CHECK-Constraint chk_wf_step_type): form, document, notification, appointment, authorization, timer, reminder, start, end, branch, fork, join. Implementierungen unter includes/classes/Service/Workflow/Step/ mit StepRegistry (Plug-in-faehig).

Start-Mechanismus: WorkflowExecutionService::startInstance($templateId, $tenantId, $createdByAccountId, $subjectLabel) — aufgerufen von:

  • Controller/WorkflowController (Studio-UI)
  • html/portal.php (externe Akteure via Magic-Link)
  • Tests

Ausfuehrung: kein Scheduler, sondern event-driven. completeStep() ruft advanceByEdges(), das die ausgehenden Edges nach from_port (Branch-Routing) bzw. join_strategy (Fan-in) abarbeitet und Ziel-Steps via activateStepRow() startet. Auto-Steps (document, notification, start, end, timer) werden direkt ausgefuehrt; interaktive Steps warten auf User. WorkflowTimerService cron-tickt nur fuer Timer-Steps.

Verschachtelung heute: Innerhalb eines Templates ja (group_key, fork/join, branch). Sub-Workflows / Cross-Template-Aufrufe gibt es nichtparent_instance_id ist im Schema definiert, wird aber nirgendwo geschrieben oder gelesen.

Was geaendert werden muss

DB / Schema

  • Neue Tabelle workflow_step_subworkflow (analog zu anderen typed-step-Tabellen):
    • step_id (PK, FK auf workflow_step)
    • child_template_id (FK auf workflow_template)
    • wait_mode (sync = blockiert bis Sub fertig / async = fire-and-forget)
    • input_mapping_json (JSON: aeussere Felder/Variablen Sub-Template-Felder)
    • output_mapping_json (JSON: Sub-Template-Output aeussere Variablen / response_json)
    • max_depth (Rekursionsschutz, default 5)
  • chk_wf_step_type um 'subworkflow' erweitern
  • workflow_step_instance: optional child_instance_id BIGINT REFERENCES workflow_instance(id) als Rueckverweis (sonst per parent_instance_id invertierbar)
  • workflow_instance.parent_instance_id existiert bereits — zusaetzlich parent_step_instance_id BIGINT waere sinnvoll, um beim Sub-Abschluss den exakten Wartepunkt im Eltern-Workflow zu finden
  • Neue Migration 074_workflow_subworkflow.sql

Backend (PHP)

  • Neuer Step: Service/Workflow/Step/SubWorkflowStep.class.php (type() = 'subworkflow', validateConfig, execute)
  • StepRegistry::default() um Eintrag erweitern
  • WorkflowExecutionService:
    • activateStepRow() neuer Branch fuer 'subworkflow': ruft rekursiv startInstance() mit parent_instance_id/parent_step_instance_id
    • Neue Methode notifyChildCompleted(int $childInstanceId) — wird vom Sub-Workflow am Ende aufgerufen, schliesst den wartenden Eltern-Step ab und ruft advanceByEdges()
    • Hook am Workflow-Ende (in autoExecuteEndStep() bzw. wo immer der Workflow 'completed' wird): wenn parent_instance_id gesetzt, Output-Mapping anwenden und Eltern-Step abschliessen
    • Rekursionsschutz: vor startInstance Tiefe der parent_instance_id-Kette pruefen (max_depth)
    • Loop-Schutz: gleiches Template darf nicht in eigener Vorfahrenkette stehen (Endlosschleife vermeiden)
  • Datenfluss: kleine SubWorkflowDataMapper-Klasse, die input_mapping_json gegen response_json der bisherigen Step-Instanzen aufloest und beim Output zurueck in das Eltern-response_json schreibt
  • Repo: WorkflowStepSubWorkflowRepository (analog zu WorkflowStepFormRepository etc.)
  • TypedStepConfigRepository muss neuen Typ kennen
  • Berechtigungs-Check: Sub-Template muss im selben Tenant + published + live sein. Sysadmin-Templates (scope=system) sind aufrufbar. Cross-Tenant-Aufrufe verbieten.

Frontend (Studio)

  • Workflow-Canvas-Sidebar (app/views/partials/workflow/step-config/): neue Datei subworkflow.php
    • Dropdown Sub-Template (gefiltert nach Tenant + published)
    • Auswahl wait_mode
    • UI fuer Input-Mapping (aeussere Variablen / Form-Felder → Sub-Template-Felder)
    • UI fuer Output-Mapping
  • Step-Toolbar in workflow-canvas.js: neuer Step-Typ “Sub-Workflow”
  • Diagram-Visualisierung (workflow-diagram.js): eigenes Icon, evtl. Klick → Sub-Template oeffnen
  • Instance-View (workflow-instance.js): bei aktivem Sub-Step Link auf laufende Sub-Instanz; bei abgeschlossenem Step Status anzeigen

Tests

  • tests/Integration/Service/Workflow/SubWorkflowStartTest.php — Sync + Input-Mapping
  • tests/Integration/Service/Workflow/SubWorkflowReturnTest.php — Output-Mapping bei Abschluss
  • tests/Integration/Service/Workflow/SubWorkflowAsyncTest.php — fire-and-forget
  • tests/Integration/Service/Workflow/SubWorkflowRecursionGuardTest.php — max_depth + Self-Loop
  • E2E im scripts/test_workflow_e2e.php

Doku

  • project/02-architecture/DATA_MODEL.md — Abschnitt Workflow um neue Tabelle + parent_instance_id-Semantik erweitern
  • project/00-meta/decisions/ADR-082_sub-workflows.md — neuer ADR mit Begruendung Sync vs Async, Mapping-Format, Rekursionsschutz
  • project/00-meta/CHANGELOG.md + Migration im schema_current.sql
  • project/04-implementation/API_SPEC.md — falls neue Endpoints (z.B. zum Abrufen Child-Instanz)

Komplexitaets-Einschaetzung

M (1-2 Wochen) fuer den minimalen produktiven Wurf — hauptsaechlich, weil parent_instance_id schon existiert, der StepRegistry-Plug-in-Mechanismus sauber ist und das typed-step-Pattern aus ADR-077 sich klar reproduzieren laesst.

Aufschluesselung:

  • DB-Migration + neuer Step-Typ + Repo: 2 Tage
  • Sync-Aufruf + Wait + Output-Hook im ExecutionService: 3 Tage (Tricky: das Auffinden und Vorschieben des Eltern-Step-Instance beim Child-Abschluss; sauber zu testen)
  • Input/Output-Mapping (PartyResolverService-Pattern wiederverwendbar): 2 Tage
  • Studio-UI (Sidebar-Panel + Diagram): 3 Tage
  • Rekursionsschutz + Tenant-Isolation + Tests: 2 Tage
  • ADR + DATA_MODEL + Code-Review: 1 Tag

L (3-4 Wochen) sobald Async + komplexere Output-Aggregation (z.B. mehrere parallele Sub-Workflows in einem Fork mit Aggregations-Strategy) gewuenscht ist.

Offene Punkte / Risiken

  • Transaktionalitaet: Was passiert, wenn der Sub-Workflow cancelled oder rejected wird? Eltern-Step → rejected-Port? Konfigurierbar pro Sub-Workflow-Step? Empfehlung: zwei Output-Ports (Port 1 = completed, Port 2 = rejected/cancelled) analog zum Authorization-Step.
  • Stale Templates: Wenn das Sub-Template archiviert/geloescht wird waehrend ein Eltern-Workflow es noch referenziert. Aktuell ON DELETE RESTRICT auf workflow_instance.workflow_template_id — gleiche Semantik fuer den Step-Typ noetig (ON DELETE RESTRICT von workflow_step_subworkflow.child_template_id).
  • Berechtigungsvererbung: Wer ist created_by_account_id der Sub-Instanz? Vererbung vom Parent oder Service-Account? Empfehlung: Parent-Creator vererben, das passt zu Tenant-Limit (Usage-Service).
  • Tenant-Limit / Usage-Service: Jede Sub-Instanz wird gezaehlt → kann Limit ueberraschen. Optional Sub-Instanzen separat zaehlen oder als Teil der Eltern-Instanz buchen.
  • Approval-Token / Magic-Links: EagerTokenGenerator muss bei Sub-Start ebenfalls feuern. Pruefen, ob das Token-Mapping ueber Workflow-Grenzen sauber ist (eigentlich pro step_instance_id, also unkritisch).
  • Cross-Tenant-Templates: System-Scope-Templates (z.B. zentrale Onboarding-Bausteine) sind verlockend, aber bringen Datenschutz-Komplexitaet. Empfehlung: in Phase 1 nur tenant-eigene Sub-Templates zulassen.
  • Async-Modus + Output-Mapping: passt nicht zusammen — fire-and-forget hat keinen Output. UI muss Output-Mapping bei wait_mode=async ausgrauen.
  • Endlosschleifen bei Templates die sich gegenseitig aufrufen (A → B → A): Rekursionscheck muss die ganze Vorfahrenkette per parent_instance_id traversieren, nicht nur direkten Parent.
  • Visualisierung: Werden Sub-Workflows expandierbar im Canvas dargestellt oder nur als Block? Empfehlung Phase 1: nur Block + Klick-Sprung in Sub-Template.

Zusammenhaenge

  • Index
  • ADR-077 (Typed Step Tables) — Pattern fuer neue typisierte Step-Tabelle
  • project/superpowers/plans/2026-03-29-workflow-engine-r71-foundation.md — Foundation, in der parent_instance_id urspruenglich angelegt wurde