Skip to Content
BlogTicket-Workflow neu anstoßen

Ticket-Workflow neu anstoßen

Martin MöllenbeckProcessCube CLIProcessCube EngineBPMNjqBash

Dieses Beispiel zeigt, wie ein laufender Ticket-Bearbeitungs-Workflow in der Engine kontrolliert beendet und an einer definierten Stelle wieder aufgenommen wird. Das Szenario stammt aus dem Kundendienst-Setup: jedes Helpdesk-Ticket wird durch drei aufeinanderfolgende Prozessmodelle bearbeitet, und wir wollen die jüngste Verteilung erneut an einer bestimmten Aktivität anstoßen — abhängig davon, ob das Ticket als bug oder feature getaggt ist.

Worum geht’s?

Pro Ticket laufen drei Prozessmodelle in Reihe:

Support_Ticket_verarbeiten_Process Support_Ticket_verteilen_Process Ticket_umsetzen_Process

Korrelations-ID aller drei Instanzen ist die Ticket-Referenz aus dem Helpdesk (z. B. 01179).

Wir wollen für eine Menge ausgewählter Tickets:

  1. alle laufenden Support_Ticket_verarbeiten_Process-Instanzen stoppen,
  2. 10 Sekunden warten, damit die Engine die Zustandsübergänge sauber abschließt,
  3. die jüngste laufende Support_Ticket_verteilen_Process-Instanz pro Ticket gezielt an einer Flow-Node erneut starten:
    • bei Tag feature an Activity_1y0wldz
    • bei Tag bug (case-insensitiv) an Activity_1ypk07j

Dieses Beispiel verändert Engine-Zustände (stop, retry). Führe es nur auf einer Engine aus, auf der das ausdrücklich gewünscht ist. Mach immer zuerst einen Trockenlauf (report-…), bevor du echte Aktionen auslöst.

Voraussetzungen

  • pc4.10
  • Aktive Engine-Session: pc engine login <engineUrl> (Device Flow)
  • jq installiert
  • Eine JSON-Liste von Tickets mit Feldern ticket_ref, tags[] und den zugeordneten Process-Instances. Im Folgenden nutzen wir tickets-wip-review-enriched.json (siehe nächster Schritt).

Eingabedaten

Wir gehen davon aus, dass die Tickets schon in einer JSON-Datei vorliegen (z. B. exportiert aus dem Helpdesk und anschließend mit Engine-Instanzen angereichert). Pro Ticket erwartet das Beispiel mindestens folgende Felder:

{ "ticket_ref": "01176", "tags": ["ProcessCube.LowCode", "feature"], "instances": [ { "processInstanceId": "2b45e142-f4b7-4096-a121-ab3b79ce88ec", "processModelId": "Support_Ticket_verarbeiten_Process", "state": "running", "createdAt": "2026-06-17T10:14:11.686Z" }, { "processInstanceId": "22888103-f50e-434a-80b7-5e5d1470e574", "processModelId": "Support_Ticket_verteilen_Process", "state": "running", "createdAt": "2026-06-17T10:14:18.346Z" } ] }

Das Beispiel nimmt an, dass die instances-Liste bereits gefiltert ist (state == "running"). Wenn nicht, kannst du Filter in den jq-Ausdrücken unten entsprechend ergänzen.

Schritt für Schritt

Tickets filtern, die wir verarbeiten wollen

Wir interessieren uns nur für Tickets, deren Tags entweder feature (genau so) oder bug (case-insensitiv) enthalten. Tickets ohne passenden Tag werden übersprungen.

jq '[.[] | select( (.tags | map(ascii_downcase) | index("bug")) or (.tags | index("feature")) )]' tickets-wip-review-enriched.json > tickets-relevant.json

ascii_downcase macht den Vergleich für bug case-insensitiv (matched Bug, BUG, bug). feature wird exakt verglichen.

Pro Ticket Tag → Flow-Node-ID auflösen

Die flowNodeId ist die im BPMN-Modell vergebene Aktivitäts-ID. Wir bilden sie aus den Ticket-Tags ab:

jq 'map(. + { flowNodeId: ( if (.tags | index("feature")) then "Activity_1y0wldz" elif (.tags | map(ascii_downcase) | index("bug")) then "Activity_1ypk07j" else null end ) }) | map(select(.flowNodeId != null))' tickets-relevant.json > tickets-with-flow.json

Die if … elif-Reihenfolge legt fest, was bei Tickets passiert, die beide Tags tragen: feature gewinnt. Falls das umgekehrt sein soll, einfach die beiden Zweige tauschen.

Alle laufenden verarbeiten-Instanzen stoppen

Wir extrahieren je Ticket alle Process-Instance-IDs des Modells Support_Ticket_verarbeiten_Process und stoppen sie. pc engine stop nimmt mehrere IDs als Argumente entgegen.

jq -r '[ .[] | .instances[] | select(.processModelId == "Support_Ticket_verarbeiten_Process") | .processInstanceId ] | join(" ")' tickets-with-flow.json | xargs -r pc engine stop

xargs -r sorgt dafür, dass pc engine stop nicht ohne Argumente aufgerufen wird, falls die Liste leer ist.

Es werden bewusst alle laufenden Instanzen dieses Modells gestoppt, nicht nur die jüngste — sonst bliebe der Workflow doppelt offen.

10 Sekunden warten

Die Engine verarbeitet stop-Aufrufe asynchron: angeschlossene External-Task Worker müssen ihren Abbruch bestätigen und der Persistenz-Layer muss den Endzustand schreiben. Ohne diese Pause kann ein anschließender retry auf einem Zustand aufsetzen, der gerade noch im Übergang ist.

sleep 10

Wenn du auf Nummer sicher gehen willst, prüfe stattdessen explizit, dass die Instanzen den Endzustand erreicht haben:

pc engine lsi --filter-by-correlation-id 01176 --output json \ | jq '[.result[] | select(.state == "running")] | length'

Erst weitermachen, wenn der Wert für alle Tickets auf 0 (für das jeweilige Modell) gefallen ist.

Pro Ticket die jüngste verteilen-Instanz ermitteln

Falls für ein Ticket mehrere Support_Ticket_verteilen_Process-Instanzen existieren (z. B. nach früheren Retries), nehmen wir die mit dem größten createdAt — also die jüngste.

jq 'map(. + { targetInstance: ( [.instances[] | select(.processModelId == "Support_Ticket_verteilen_Process")] | sort_by(.createdAt) | last ) }) | map(select(.targetInstance != null))' tickets-with-flow.json > tickets-target.json

Tickets ohne laufende verteilen-Instanz werden hier verworfen — wir überspringen sie wie vereinbart.

flowNodeInstanceId auflösen

pc engine retry --flowNodeInstanceId erwartet die Laufzeit-ID eines Flow-Nodes (flowNodeInstanceId), nicht die BPMN-Modell-ID (flowNodeId). Dafür holen wir die Liste der Flow-Node-Instanzen pro Process-Instance über pc engine show und filtern nach der gewünschten flowNodeId. Wir nehmen die jüngste noch nicht beendete Flow-Node-Instanz — typischerweise eine mit dem Zustand suspended.

jq -c '.[]' tickets-target.json | while read -r TICKET; do REF=$(jq -r '.ticket_ref' <<<"$TICKET") PI=$(jq -r '.targetInstance.processInstanceId' <<<"$TICKET") FN=$(jq -r '.flowNodeId' <<<"$TICKET") # Flow-Node-Instanz mit passender flowNodeId, im Idealfall im Zustand suspended. FNI=$(pc engine show "$PI" --output json < /dev/null \ | jq -r --arg fn "$FN" ' [.result[0].flowNodeInstances[] | select(.flowNodeId == $fn) | select(.state != "finished")] | last // empty | .flowNodeInstanceId') if [ -z "$FNI" ]; then echo "Skip $REF: keine passende flowNodeInstanceId fuer $FN" >&2 continue fi echo "$REF -> $PI @ $FN ($FNI)" done

Diese Schleife produziert pro Ticket eine Zeile mit den drei IDs, die wir für den eigentlichen Retry brauchen.

select(.state != "finished") ist eine bewusst lockere Bedingung: typische Retry-Kandidaten stehen auf suspended, in Ausnahmefällen aber auch auf error. Beendete Knoten (finished) sind nie ein sinnvoller Retry-Punkt.

Retry an der gewählten Flow-Node ausführen

Erst jetzt wird der Zustand wirklich verändert. Wir erweitern die Schleife oben um einen pc engine retry-Aufruf pro Ticket:

jq -c '.[]' tickets-target.json | while read -r TICKET; do REF=$(jq -r '.ticket_ref' <<<"$TICKET") PI=$(jq -r '.targetInstance.processInstanceId' <<<"$TICKET") FN=$(jq -r '.flowNodeId' <<<"$TICKET") FNI=$(pc engine show "$PI" --output json < /dev/null \ | jq -r --arg fn "$FN" ' [.result[0].flowNodeInstances[] | select(.flowNodeId == $fn) | select(.state != "finished")] | last // empty | .flowNodeInstanceId') if [ -z "$FNI" ]; then echo "Skip $REF: keine passende flowNodeInstanceId fuer $FN" >&2 continue fi echo "Retry $REF: $PI @ $FN ($FNI)" pc engine retry "$PI" --flowNodeInstanceId "$FNI" < /dev/null done

Ergebnis prüfen

Für jedes verarbeitete Ticket sollte jetzt eine neue laufende Instanz im Workflow sichtbar sein. Stichprobe:

pc engine lsi --filter-by-correlation-id 01176 --output json \ | jq '[.result[] | {processModelId, state, createdAt}]'

Trockenlauf-Report-Skript

Read-only-Variante: druckt eine Tabelle mit Tickets, gewählter flowNodeId, jüngster verteilen-Instanz und aufgelöster flowNodeInstanceId — ohne Zustandsänderung an der Engine. Immer vor dem Apply-Script ausführen.

./scripts/report-retry-ticket-workflow.sh tickets-wip-review-enriched.json # optional als strukturierter JSON-Report: ./scripts/report-retry-ticket-workflow.sh \ tickets-wip-review-enriched.json --json retry-report.json

Beispielausgabe (gekürzt):

=============================================================== TROCKENLAUF — keine Engine-Aenderungen =============================================================== Eingabe-Tickets: 29 Relevante Tickets: 29 (mit bug/feature) Geplante Stop-Aufrufe: 29 verarbeiten-Instanzen =============================================================== Ticket Tag flowNode verteilen-PI (juengste) flowNodeInstanceId -------- -------- ------------------- ------------------------------------ ------------------------------------ 01176 feature Activity_1y0wldz 22888103-f50e-434a-80b7-5e5d1470e574 092aa3c0-5aed-44b0-a53f-70144b1e51a0 01174 bug Activity_1ypk07j edc05e19-c6dd-41e3-ba31-384eef0f50b8 SKIP: kein passender FlowNode-Zustand ... --------------------------------------------------------------- Zusammenfassung --------------------------------------------------------------- ✓ Retry geplant: 27 Ticket(s) · Skip — kein bug/feature-Tag: 0 · Skip — keine verteilen-Instanz: 0 · Skip — kein passender FlowNode: 2 ---------------------------------------------------------------

Mit --json enthält jeder Eintrag: ticket_ref, matchedTag, flowNodeId, targetProcessInstanceId, flowNodeInstanceId, stopIds[], plan ("retry" oder "skip").

Quellcode scripts/report-retry-ticket-workflow.sh:

#!/bin/bash # Trockenlauf-Report fuer scripts/retry-ticket-workflow.sh # Liest die Ticket-JSON, fuehrt alle Lese-Schritte aus (pc engine show), # aendert aber NICHTS am Engine-Zustand. Gibt eine Tabelle aus, was ein # echter Lauf tun wuerde. # # Aufruf: ./scripts/report-retry-ticket-workflow.sh <input.json> [--json <out.json>] set -uo pipefail INPUT="${1:?Eingabe-JSON fehlt}" OUTPUT_JSON="" if [ "${2:-}" = "--json" ] && [ -n "${3:-}" ]; then OUTPUT_JSON="$3" fi if ! command -v jq >/dev/null 2>&1; then echo "Fehler: jq wird benoetigt." >&2 exit 1 fi WORK=$(mktemp -d -t report-tw.XXXXXX) trap 'rm -rf "$WORK"' EXIT # 1. Relevante Tickets (Tag feature oder bug case-insensitiv) jq '[.[] | select( (.tags | map(ascii_downcase) | index("bug")) or (.tags | index("feature")) )]' "$INPUT" > "$WORK/relevant.json" # 2. Tag -> flowNodeId (feature gewinnt vor bug) jq 'map(. + { flowNodeId: ( if (.tags | index("feature")) then "Activity_1y0wldz" elif (.tags | map(ascii_downcase) | index("bug")) then "Activity_1ypk07j" else null end ), matchedTag: ( if (.tags | index("feature")) then "feature" elif (.tags | map(ascii_downcase) | index("bug")) then "bug" else null end ) }) | map(select(.flowNodeId != null))' "$WORK/relevant.json" > "$WORK/with-flow.json" # Reine Berechnung: pro Ticket # - stopIds: alle verarbeiten-PIs # - targetInstance: jüngste verteilen-PI jq 'map(. + { stopIds: [.instances[] | select(.processModelId == "Support_Ticket_verarbeiten_Process") | .processInstanceId], targetInstance: ( [.instances[] | select(.processModelId == "Support_Ticket_verteilen_Process")] | sort_by(.createdAt) | last ) })' "$WORK/with-flow.json" > "$WORK/plan.json" TOTAL_INPUT=$(jq 'length' "$INPUT") TOTAL_RELEVANT=$(jq 'length' "$WORK/with-flow.json") TOTAL_STOPS=$(jq '[.[].stopIds | length] | add // 0' "$WORK/plan.json") echo "" echo "===============================================================" echo " TROCKENLAUF — keine Engine-Aenderungen" echo "===============================================================" echo " Eingabe-Tickets: $TOTAL_INPUT" echo " Relevante Tickets: $TOTAL_RELEVANT (mit bug/feature)" echo " Geplante Stop-Aufrufe: $TOTAL_STOPS verarbeiten-Instanzen" echo "===============================================================" echo "" # Tabellenkopf printf '%-8s %-8s %-19s %-36s %s\n' \ "Ticket" "Tag" "flowNode" "verteilen-PI (juengste)" "flowNodeInstanceId" printf '%-8s %-8s %-19s %-36s %s\n' \ "--------" "--------" "-------------------" \ "------------------------------------" \ "------------------------------------" SKIPPED_NO_VERTEILEN=0 SKIPPED_NO_FNI=0 PLAN_OK=0 ENTRIES_FILE="$WORK/entries.ndjson" SHOW_JSON="$WORK/show.json" : > "$ENTRIES_FILE" while read -r TICKET; do REF=$(jq -r '.ticket_ref' <<<"$TICKET") TAG=$(jq -r '.matchedTag' <<<"$TICKET") FN=$(jq -r '.flowNodeId' <<<"$TICKET") PI=$(jq -r '.targetInstance.processInstanceId // empty' <<<"$TICKET") STOPS=$(jq -c '.stopIds' <<<"$TICKET") if [ -z "$PI" ]; then SKIPPED_NO_VERTEILEN=$((SKIPPED_NO_VERTEILEN + 1)) printf '%-8s %-8s %-19s %-36s %s\n' \ "$REF" "$TAG" "$FN" "—" "SKIP: keine verteilen-Instanz" continue fi pc engine show "$PI" --output json < /dev/null > "$SHOW_JSON" 2>/dev/null || true FNI=$(jq -r --arg fn "$FN" ' [.result[0].flowNodeInstances[] | select(.flowNodeId == $fn) | select(.state != "finished")] | last // empty | .flowNodeInstanceId' "$SHOW_JSON" 2>/dev/null || echo "") if [ -z "$FNI" ]; then SKIPPED_NO_FNI=$((SKIPPED_NO_FNI + 1)) printf '%-8s %-8s %-19s %-36s %s\n' \ "$REF" "$TAG" "$FN" "$PI" "SKIP: kein passender FlowNode-Zustand" else PLAN_OK=$((PLAN_OK + 1)) printf '%-8s %-8s %-19s %-36s %s\n' \ "$REF" "$TAG" "$FN" "$PI" "$FNI" fi if [ -n "$OUTPUT_JSON" ]; then jq -nc \ --arg ref "$REF" --arg tag "$TAG" --arg fn "$FN" \ --arg pi "$PI" --arg fni "$FNI" --argjson stops "$STOPS" \ '{ticket_ref: $ref, matchedTag: $tag, flowNodeId: $fn, targetProcessInstanceId: $pi, flowNodeInstanceId: ($fni | select(. != "")), stopIds: $stops, plan: (if $fni == "" then "skip" else "retry" end)}' \ >> "$ENTRIES_FILE" fi done < <(jq -c '.[]' "$WORK/plan.json") SKIPPED_NO_TAG=$((TOTAL_INPUT - TOTAL_RELEVANT)) echo "" echo "---------------------------------------------------------------" echo " Zusammenfassung" echo "---------------------------------------------------------------" echo " ✓ Retry geplant: $PLAN_OK Ticket(s)" echo " · Skip — kein bug/feature-Tag: $SKIPPED_NO_TAG" echo " · Skip — keine verteilen-Instanz: $SKIPPED_NO_VERTEILEN" echo " · Skip — kein passender FlowNode: $SKIPPED_NO_FNI" echo "---------------------------------------------------------------" if [ -n "$OUTPUT_JSON" ]; then jq -s '.' "$ENTRIES_FILE" > "$OUTPUT_JSON" echo " JSON-Report: $OUTPUT_JSON" fi echo ""

Speichern unter scripts/report-retry-ticket-workflow.sh, ausführbar machen (chmod +x scripts/report-retry-ticket-workflow.sh) und wie oben aufrufen.

Komplettes Script

Alle Schritte zusammengefasst in einem einzigen Bash-Script scripts/retry-ticket-workflow.sh (im Repo-Root erwartet).

Dieses Script verändert Engine-Zustände. Vorher den Trockenlauf-Report ausführen und Plan prüfen.

Aufruf:

chmod +x scripts/retry-ticket-workflow.sh ./scripts/retry-ticket-workflow.sh tickets-wip-review-enriched.json

Quellcode scripts/retry-ticket-workflow.sh:

#!/bin/bash set -uo pipefail INPUT="${1:?Eingabe-JSON fehlt}" WORK=$(mktemp -d -t retry-tw.XXXXXX) trap 'rm -rf "$WORK"' EXIT # 1. Relevante Tickets (Tag feature oder bug case-insensitiv) jq '[.[] | select( (.tags | map(ascii_downcase) | index("bug")) or (.tags | index("feature")) )]' "$INPUT" > "$WORK/relevant.json" # 2. Tag -> flowNodeId jq 'map(. + { flowNodeId: ( if (.tags | index("feature")) then "Activity_1y0wldz" elif (.tags | map(ascii_downcase) | index("bug")) then "Activity_1ypk07j" else null end ) }) | map(select(.flowNodeId != null))' "$WORK/relevant.json" > "$WORK/with-flow.json" # 3. Stop aller verarbeiten-Instanzen STOP_IDS=$(jq -r '[ .[] | .instances[] | select(.processModelId == "Support_Ticket_verarbeiten_Process") | .processInstanceId ] | join(" ")' "$WORK/with-flow.json") if [ -n "$STOP_IDS" ]; then echo "Stoppe: $STOP_IDS" echo "$STOP_IDS" | xargs pc engine stop < /dev/null fi # 4. 10 Sekunden warten echo "Warte 10s ..." sleep 10 # 5. Jüngste verteilen-Instanz pro Ticket jq 'map(. + { targetInstance: ( [.instances[] | select(.processModelId == "Support_Ticket_verteilen_Process")] | sort_by(.createdAt) | last ) }) | map(select(.targetInstance != null))' "$WORK/with-flow.json" > "$WORK/target.json" # 6. + 7. flowNodeInstanceId aufloesen und retry jq -c '.[]' "$WORK/target.json" | while read -r TICKET; do REF=$(jq -r '.ticket_ref' <<<"$TICKET") PI=$(jq -r '.targetInstance.processInstanceId' <<<"$TICKET") FN=$(jq -r '.flowNodeId' <<<"$TICKET") FNI=$(pc engine show "$PI" --output json < /dev/null \ | jq -r --arg fn "$FN" ' [.result[0].flowNodeInstances[] | select(.flowNodeId == $fn) | select(.state != "finished")] | last // empty | .flowNodeInstanceId') if [ -z "$FNI" ]; then echo "Skip $REF: keine passende flowNodeInstanceId fuer $FN" >&2 continue fi echo "Retry $REF: $PI @ $FN ($FNI)" pc engine retry "$PI" --flowNodeInstanceId "$FNI" < /dev/null done

Edge Cases

  • Ticket ohne bug/feature-Tag: wird in Schritt 1 herausgefiltert.
  • Ticket mit beiden Tags: feature gewinnt (siehe if/elif-Reihenfolge).
  • Ticket ohne laufende verteilen-Instanz: wird in Schritt 5 verworfen (kein automatischer Neustart eines verarbeiten-Prozesses).
  • flowNodeId nicht in der Instance vorhanden: Schritt 6 überspringt das Ticket mit einer Warnung — typischerweise ein Hinweis auf ein anderes BPMN-Modell oder einen schon weiter fortgeschrittenen Stand.
  • Mehrere flowNodeInstances zur selben flowNodeId (Loops/Retries): Wir nehmen die jüngste, noch nicht beendete (state != "finished").

Weiterführende Themen