Ticket-Workflow neu anstoßen
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_ProcessKorrelations-ID aller drei Instanzen ist die Ticket-Referenz aus dem
Helpdesk (z. B. 01179).
Wir wollen für eine Menge ausgewählter Tickets:
- alle laufenden
Support_Ticket_verarbeiten_Process-Instanzen stoppen, - 10 Sekunden warten, damit die Engine die Zustandsübergänge sauber abschließt,
- die jüngste laufende
Support_Ticket_verteilen_Process-Instanz pro Ticket gezielt an einer Flow-Node erneut starten:- bei Tag
featureanActivity_1y0wldz - bei Tag
bug(case-insensitiv) anActivity_1ypk07j
- bei Tag
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
pc≥ 4.10- Aktive Engine-Session:
pc engine login <engineUrl>(Device Flow) jqinstalliert- Eine JSON-Liste von Tickets mit Feldern
ticket_ref,tags[]und den zugeordneten Process-Instances. Im Folgenden nutzen wirtickets-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.jsonascii_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.jsonDie 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 stopxargs -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 10Wenn 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.jsonTickets 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)"
doneDiese 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
doneErgebnis 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.jsonBeispielausgabe (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.jsonQuellcode 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
doneEdge Cases
- Ticket ohne
bug/feature-Tag: wird in Schritt 1 herausgefiltert. - Ticket mit beiden Tags:
featuregewinnt (sieheif/elif-Reihenfolge). - Ticket ohne laufende
verteilen-Instanz: wird in Schritt 5 verworfen (kein automatischer Neustart einesverarbeiten-Prozesses). flowNodeIdnicht 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
flowNodeInstanceszur selbenflowNodeId(Loops/Retries): Wir nehmen die jüngste, noch nicht beendete (state != "finished").
Weiterführende Themen
pc engine stop-process-instancepc engine retry-process-instancepc engine show-process-instance- Template-Pipes — für noch kompaktere Pipelines ohne jq-Zwischenschritte