Workflow-Kontrollfluss & Edit-Lifecycle

Workflow-Ausführung und Workflow-Bearbeitung sind getrennte Abläufe. Executions laufen asynchron. Änderungen an einer Workflow-Definition erfordern einen Edit-Lock.

Execution

POST /api/workflows/{id}/execute — asynchron, 202 + ExecutionId. Body:

{ "parameters": {}, "timeoutSeconds": 120, "debug": false }

Fortschritt via SignalR. Mit debug: true → Breakpoints, StepPaused, Resume via POST /executions/{id}/resume.

EndpointSemantik
POST /executeStartet Lauf
POST /enable / /disableKill-Switch. enable verlangt einen lock-freien Workflow — ein bestehender Lock (auch der eigene) liefert 423; aus dem Edit-Modus heraus wird stattdessen publiziert. enable führt an der gespeicherten Definition dieselben Aktivierungsprüfungen aus wie Publish: ein Verweis auf eine nicht aufgelöste importierte Ressource (leere GUID) liefert 400 unresolved_workflow_dependency, ein schwaches Webhook-HMAC-Secret liefert 400 weak_webhook_hmac_secret, ein scheduleTrigger, dessen Cron Quartz nicht parst, liefert 400 invalid_cron_expression, eine ungültige Agent-Konfiguration liefert 400 invalid_agent_configuration, eine waitNofM-Junction, deren requiredCount die aktiven Eingänge übersteigt, liefert 400 invalid_junction_required_count. Dabei wird das Runtime-Principal (PublishedByUserId) mit dem aktivierenden User gefüllt, falls der Workflow keines hat; ein vorhandenes wird nie ersetzt — ein Caller ohne User-Id bekommt 401 statt eines Workflows, der nie feuern könnte. Ein bereits aktiver Workflow ist ein No-Op (204) und wird deshalb nicht gestempelt: dafür Disable-dann-Enable oder einmal Publish. disable ignoriert Locks und schreibt das Principal nie.
POST /cancel-allCancelt alle Running- und Pending-Executions des Workflows
PUT /concurrency-limitBegrenzt, wie viele Ausführungen gleichzeitig laufen. Body: {"maxConcurrentExecutions": 5}, oder null für unbegrenzt. Die Property ist Pflicht — ein leerer Body ist ein 400, damit ein Client das Limit nie durch Weglassen löscht. 0 wird abgelehnt; dafür den Workflow deaktivieren.
`POST /executions/{id}/cancelretry

Disable + cancel-all = Quarantäne.

Parallelitätslimit

MaxConcurrentExecutions begrenzt, wie viele Ausführungen eines Workflows gleichzeitig laufen. Das Limit gilt über alle Aufrufer hinweg — manuelle Läufe, Schedule-/File-/Database-/ EventLog-Trigger, Webhooks, External-Trigger und Sub-Workflow-Aufrufe aus startWorkflow und forEach. Zwei Eltern-Workflows, die auf dasselbe Kind fächern, können es zusammen also nicht überschreiten.

Bei dauerhaft eingereihten Starts bleibt ein Lauf bei erreichtem Limit Pending und startet, sobald ein Platz frei wird. Das gilt auch für asynchrones startWorkflow (waitForCompletion=false). Das Limit allein lässt solche Aufträge nicht fehlschlagen.

Synchrone startWorkflow-Aufrufe und forEach warten dagegen innerhalb des bereits laufenden Eltern-Workflows auf einen Platz. Diese Wartezeit zählt zum Schritt- beziehungsweise Element-Timeout und kann mit einem Timeout enden, bevor das Kind startet; dabei entsteht keine dauerhafte Pending-Ausführung des Kindes. maxParallelism begrenzt weiterhin die einzelne Schleife, das Workflow-Limit das Kind über alle Aufrufer hinweg.

Eine Änderung wirkt sofort. Wird das Limit unter die Zahl der aktiven Läufe gesenkt, bricht nichts ab — der Überhang läuft aus, danach greift die Grenze. Eine Anhebung gibt wartende Läufe sofort frei, nicht einen pro abgeschlossenem Lauf.

Das Setzen braucht keinen Edit-Lock und erzeugt keine neue Workflow-Version, weil es eine Betriebseinstellung ist und keine Änderung der Workflow-Definition.

Edit-Lock (SCOrch-style)

Workflows haben einen per-User Edit-Lock (CheckedOutByUserId + CheckedOutAt). Mutierende Endpoints liefern 423 Locked, wenn der Caller nicht Lock-Owner ist. disable ist nicht lock-gegated (Incident-Kill-Switch).

EndpointVerhalten
POST /lockAtomar IsEnabled=false + Lock-Fields setzen. 409 wenn schon gelockt.
POST /unlockLock-Fields auf null. IsEnabled bleibt unverändert.
POST /publishAtomar: Save + IsEnabled=true + Unlock. Wie PUT verlangt es expectedVersion; ein veralteter Entwurf liefert 409 workflow_version_conflict. Die übergebene Definition wird vorher validiert: ein Verweis auf eine nicht aufgelöste importierte Ressource liefert 400 unresolved_workflow_dependency, eine ungültige Agent-Konfiguration liefert 400 invalid_agent_configuration, ein schwaches Webhook-HMAC-Secret liefert 400 weak_webhook_hmac_secret, eine für Quartz ungültige Cron-Expression liefert 400 invalid_cron_expression, eine waitNofM-Junction, die auf mehr Eingänge wartet als sie hat, liefert 400 invalid_junction_required_count.
POST /force-unlockAdmin-only. Bricht fremden Lock.

UX-Flow

  1. Workflow ist Productive (Enabled, kein Lock) → Designer read-only. Toolbar: Edit + Disable.
  2. Edit → lock → Locked-by-Me + Disabled. Save wird sichtbar, Disable-Slot wird zu Publish.
  3. Save → Zwischenstand (PUT, kein Status-Wechsel). Repeatierbar.
  4. Publish → atomar Save + Enable + Unlock. Workflow ist Productive.
  5. Alternative: Exit → unlock. Workflow bleibt Disabled. Publish-Slot callt /enable (reaktivieren ohne Edit-Roundtrip).

Publish/Disable-Toggle (ein Button-Slot, vier States)

Workflow-StateLabelEndpoint
IsEnabled=true, kein Lock"Disable" (red)/disable
IsEnabled=false, lock-by-me"Publish" (primary)/publish
IsEnabled=false, kein Lock"Publish" (primary)/enable
IsEnabled=false, lock-by-other"Publish" disabled(none — Tooltip nennt Lock-Owner)

Sichtbarkeit gegated durch roleCanWrite (Admin/Operator), nicht durch lock-by-me. Viewer sehen den Slot nicht.

canWrite-Regel

canWrite = role !== 'Viewer' && checkedOutByUserId === currentUserId

Alle nodesDraggable/nodesConnectable/Save/Tidy-Affordances folgen automatisch — kein separates Edit-Mode-Toggle. currentUserId via /auth/me und LoginResponse.userId (das JWT ist ein httpOnly-Cookie, den die SPA nicht dekodieren kann).

Workflow-Version-History

Update / Rollback snapshotten die vorherige Definition.

Idempotency-Keys

POST /api/trigger/{name} akzeptiert Idempotency-Key-Header.

Trigger-los / Empty-Workflow

  • Kein (aktiver) Trigger (trigger-los oder nur Zyklen) → 0 Roots → Failed (ErrorMessage nennt den fehlenden Trigger/Start) + Warning. Roots sind ausschließlich Trigger-Nodes — kein inDegree==0-Fallback.
  • Leer (0 Nodes) → läuft mit 0 Steps durch (Succeeded).

Node-Level disabled

data.disabled: true → Node wird Skipped, Downstream ohne andere Quellen ebenfalls.

Beispiele

NP=http://localhost:5000, Auth via -b cookie.jar (siehe Authentifizierung). JSON-Property-Keys sind camelCase; Enum-Werte als PascalCase-String ("status":"Succeeded").

Die folgenden Bash-Beispiele setzen eine Anmeldung gemäß Authentifizierung und eine cookie.jar für diesen Server voraus. Diese Funktion vor den Beispielen definieren; sie liest den aktuellen CSRF-Wert auch nach einem Refresh:

NP=http://localhost:5000
csrf_token() {
  awk '$6 == "np_csrf" {sub(/\r$/, "", $7); print $7}' cookie.jar
}

Ausführen

# Asynchron starten — 202 + ExecutionResponse, Location-Header auf /api/executions/{id}
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../execute" \
  -H 'Content-Type: application/json' \
  -d '{ "parameters": { "version":"2.1.0", "env":"prod" },
        "timeoutSeconds": 300, "debug": false }'
# 202 → {"id":"7e3f...","workflowId":"...","status":"Pending","triggeredBy":"manual",...}

# Debug-Run mit Pause — Resume via /api/executions/{id}/resume
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../execute" \
  -H 'Content-Type: application/json' \
  -d '{ "parameters":{}, "debug": true }'

# Admin kann eine Maintenance-Window bypassen
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../execute?force=true" \
  -H 'Content-Type: application/json' -d '{}'

Fortschritt via SignalR (/hubs/execution). parameters-Keys mit __-Prefix → 400. Disabled Workflow → 400. Maintenance-Window-Block → 423 {"message":"...","windowId":"...","activeUntil":"..."}.

Edit-Lock-Lifecycle

# Lock anfordern — atomar IsEnabled=false + Lock-Fields. 409 bei vorhandenem fremdem Lock.
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../lock"
# 200 → WorkflowResponse (Locked-by-Me, Disabled)

# Zwischenstand speichern (PUT, kein Status-Wechsel) — 423 ohne eigenen Lock
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X PUT "$NP/api/workflows/21f1c0d4-..." \
  -H 'Content-Type: application/json' \
  -d '{ "name":"Deploy App", "description":"wip",
        "definitionJson":"{\"nodes\":[...],\"edges\":[...]}", "expectedVersion": 3 }' -i   # 204

# Publish — atomar Save + Enable + Unlock. Workflow ist wieder Productive.
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../publish" \
  -H 'Content-Type: application/json' \
  -d '{ "name":"Deploy App", "description":null,
        "definitionJson":"{\"nodes\":[...],\"edges\":[...]}", "expectedVersion": 4 }'

# Exit ohne Publish: nur Unlock — Workflow bleibt Disabled
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../unlock"

# Reaktivieren ohne Edit-Roundtrip (IsEnabled=false, kein Lock → /enable)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../enable" -i   # 204

# Fremden Lock brechen (Admin-only)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../force-unlock"

Quarantäne & Einzelne Lauf-Kontrolle

# Quarantäne = Disable + cancel-all
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../disable" -i       # 204, ignoriert Locks
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../cancel-all"
# 200 → {"total":3,"signalled":2}

# Einzelne Execution
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/executions/7e3f.../cancel" -i            # 204
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/executions/7e3f.../retry"  -i            # 202 + Location

# Debug-Resume — stepId required, mode: continue|stepOver|stop
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/executions/7e3f.../resume" \
  -H 'Content-Type: application/json' \
  -d '{ "stepId":"runHealth", "mode":"stepOver",
        "overrides": { "vars.targetHost":"srv02" } }' -i                        # 204

resume 409 {"message":"No paused step with this id ..."} wenn die Pause schon vorbei ist; 403 wenn nicht Debug-Session-Owner (und nicht Admin). Ein Override auf einen globals.*- oder reservierten __*-Schlüssel ergibt 400, der Schritt bleibt pausiert.

GET /api/executions/{id}/paused-steps listet jeden wartenden Breakpoint mit stepId, stepLabel, pausedAt, reason (breakpoint/stepOver) und seinen redigierten variables.

Alle Kapitel