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— Definitionworkflow_instance(mitparent_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 nicht — parent_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_typeum'subworkflow'erweiternworkflow_step_instance: optionalchild_instance_id BIGINT REFERENCES workflow_instance(id)als Rueckverweis (sonst perparent_instance_idinvertierbar)workflow_instance.parent_instance_idexistiert bereits — zusaetzlichparent_step_instance_id BIGINTwaere 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 erweiternWorkflowExecutionService:activateStepRow()neuer Branch fuer'subworkflow': ruft rekursivstartInstance()mitparent_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 ruftadvanceByEdges() - Hook am Workflow-Ende (in
autoExecuteEndStep()bzw. wo immer der Workflow'completed'wird): wennparent_instance_idgesetzt, Output-Mapping anwenden und Eltern-Step abschliessen - Rekursionsschutz: vor
startInstanceTiefe derparent_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 gegenresponse_jsonder bisherigen Step-Instanzen aufloest und beim Output zurueck in das Eltern-response_jsonschreibt - Repo:
WorkflowStepSubWorkflowRepository(analog zuWorkflowStepFormRepositoryetc.) TypedStepConfigRepositorymuss neuen Typ kennen- Berechtigungs-Check: Sub-Template muss im selben Tenant +
published+livesein. Sysadmin-Templates (scope=system) sind aufrufbar. Cross-Tenant-Aufrufe verbieten.
Frontend (Studio)
- Workflow-Canvas-Sidebar (
app/views/partials/workflow/step-config/): neue Dateisubworkflow.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-Mappingtests/Integration/Service/Workflow/SubWorkflowReturnTest.php— Output-Mapping bei Abschlusstests/Integration/Service/Workflow/SubWorkflowAsyncTest.php— fire-and-forgettests/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 erweiternproject/00-meta/decisions/ADR-082_sub-workflows.md— neuer ADR mit Begruendung Sync vs Async, Mapping-Format, Rekursionsschutzproject/00-meta/CHANGELOG.md+ Migration imschema_current.sqlproject/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
cancelledoderrejectedwird? 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 RESTRICTaufworkflow_instance.workflow_template_id— gleiche Semantik fuer den Step-Typ noetig (ON DELETE RESTRICTvonworkflow_step_subworkflow.child_template_id). - Berechtigungsvererbung: Wer ist
created_by_account_idder 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:
EagerTokenGeneratormuss bei Sub-Start ebenfalls feuern. Pruefen, ob das Token-Mapping ueber Workflow-Grenzen sauber ist (eigentlich prostep_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=asyncausgrauen. - Endlosschleifen bei Templates die sich gegenseitig aufrufen (A → B → A): Rekursionscheck muss die ganze Vorfahrenkette per
parent_instance_idtraversieren, 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 derparent_instance_idurspruenglich angelegt wurde