id: camunda-process-outcome-compliance-gate
namespace: company.team
description: |
Start a critical Camunda 8 process instance, wait for it to complete, and
classify the result HEALTHY (outcome variable matches the expected
value), AT_RISK (completed but the outcome variable says otherwise), or
UNREACHABLE (completed but the outcome variable never appeared at all),
alert Slack, and optionally retrigger one fresh run. auto_remediate and
dry_run are two independent gates: the original process instance is
never cancelled, altered, or re-run - only a brand new one may start.
triggers:
- id: periodic_outcome_check
type: io.kestra.plugin.core.trigger.Schedule
description: Run the monitored process and check its outcome every hour. Shipped
disabled; enable it once camunda_rest_address/process_id point at a real
Camunda cluster and a manual run has been verified. Always fires in safe
mode (auto_remediate false, dry_run true) regardless of this flow's own
defaults.
cron: "0 * * * *"
disabled: true
inputs:
auto_remediate: "false"
dry_run: "true"
inputs:
- id: camunda_rest_address
type: STRING
displayName: "Camunda REST API Base URL"
defaults: "http://localhost:8080"
description: Passed as restAddress to every io.kestra.plugin.camunda task in this flow.
- id: process_id
type: STRING
displayName: "Monitored BPMN Process ID"
defaults: "order-fulfillment"
description: The BPMN process id (processId) started and watched by this gate.
Must already be deployed on the target cluster.
- id: expected_outcome_variable
type: STRING
displayName: "Expected Outcome Variable"
defaults: "status"
description: Process variable name the monitored BPMN model is expected to set
before it ends. Only this one variable is fetched from the completed
instance.
- id: expected_outcome_value
type: STRING
displayName: "Expected Outcome Value"
defaults: "approved"
description: Value expected_outcome_variable must equal for the run to count as HEALTHY.
- id: auto_remediate
type: SELECT
displayName: "Auto Remediate"
defaults: "false"
values:
- "false"
- "true"
description: If "true" and the monitored run is AT_RISK or UNREACHABLE, start
one fresh retry instance of process_id (still gated by dry_run below). If
"false", the gap is only reported.
- id: dry_run
type: SELECT
displayName: "Dry Run"
defaults: "true"
values:
- "true"
- "false"
description: If "true", remediation is only logged, no retry instance is
started. Must be explicitly "false", together with auto_remediate "true",
for a retry to actually run.
tasks:
- id: run_monitored_process
type: io.kestra.plugin.camunda.CreateProcessInstance
description: "With awaitCompletion: true, this blocks until the instance ends
and returns its variables map (verified from the plugin's own source) -
there is no separate state field like Airflow's TriggerDagRun; the only
usable signal is whatever variables the BPMN model itself set before
ending. fetchVariables limits the returned map to
expected_outcome_variable."
restAddress: "{{ inputs.camunda_rest_address }}"
username: "{{ secret('CAMUNDA_USERNAME') }}"
password: "{{ secret('CAMUNDA_PASSWORD') }}"
processId: "{{ inputs.process_id }}"
awaitCompletion: true
fetchVariables:
- "{{ inputs.expected_outcome_variable }}"
requestTimeout: PT5M
- id: evaluate_outcome_presence
type: io.kestra.plugin.core.flow.If
description: UNREACHABLE when expected_outcome_variable never appears in the
completed instance's variables at all - the process reached an end event,
but not the one (or not in the shape) this gate was told to expect.
condition: "{{
outputs.run_monitored_process.variables[inputs.expected_outcome_variable]
is defined }}"
then:
- id: evaluate_outcome_value
type: io.kestra.plugin.core.flow.If
description: AT_RISK when the variable is present but does not equal
expected_outcome_value - the process completed, but the outcome it
recorded is not the expected one.
condition: "{{
outputs.run_monitored_process.variables[inputs.expected_outcome_varia\
ble] == inputs.expected_outcome_value }}"
then:
- id: log_outcome_healthy
type: io.kestra.plugin.core.log.Log
message: "HEALTHY: process {{ inputs.process_id }} (instance {{
outputs.run_monitored_process.processInstanceKey }}) completed
with {{ inputs.expected_outcome_variable }}={{
outputs.run_monitored_process.variables[inputs.expected_outcome_v\
ariable] }}."
else:
- id: route_remediation
type: io.kestra.plugin.core.flow.Switch
description: auto_remediate only decides whether a retry is considered at all;
dry_run, checked separately below, decides whether
CreateProcessInstance actually starts a new instance.
value: "{{ inputs.auto_remediate }}"
cases:
"true":
- id: check_dry_run
type: io.kestra.plugin.core.flow.If
condition: "{{ inputs.dry_run == \"false\" }}"
then:
- id: retry_process_instance
type: io.kestra.plugin.camunda.CreateProcessInstance
description: Starts one fresh instance of process_id and waits for its own
outcome too, so the retry's result is logged rather than
fired blind. It does not touch the original instance in
any way.
restAddress: "{{ inputs.camunda_rest_address }}"
username: "{{ secret('CAMUNDA_USERNAME') }}"
password: "{{ secret('CAMUNDA_PASSWORD') }}"
processId: "{{ inputs.process_id }}"
awaitCompletion: true
fetchVariables:
- "{{ inputs.expected_outcome_variable }}"
requestTimeout: PT5M
- id: log_remediation_applied
type: io.kestra.plugin.core.log.Log
message: "Retried {{ inputs.process_id }}: new instance {{
outputs.retry_process_instance.processInstanceKey }}
completed with {{ inputs.expected_outcome_variable }}={{
outputs.retry_process_instance.variables[inputs.expecte\
d_outcome_variable] }}."
else:
- id: log_dry_run_remediation
type: io.kestra.plugin.core.log.Log
message: "DRY RUN: would start a retry instance of {{ inputs.process_id }}
(dry_run is true). No new instance was started."
"false":
- id: log_remediation_disabled
type: io.kestra.plugin.core.log.Log
message: "auto_remediate is false: instance {{
outputs.run_monitored_process.processInstanceKey }} of {{
inputs.process_id }} recorded {{
inputs.expected_outcome_variable }}={{
outputs.run_monitored_process.variables[inputs.expected_out\
come_variable] }}, not {{ inputs.expected_outcome_value }},
but no retry was attempted."
- id: alert_outcome_at_risk
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Always alert on AT_RISK, independent of whether a retry ran.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Camunda process outcome gate: AT_RISK. {{ inputs.process_id }} instance {{ outputs.run_monitored_process.processInstanceKey }} recorded {{ inputs.expected_outcome_variable }}={{ outputs.run_monitored_process.variables[inputs.expected_outcome_variable] }}, expected {{ inputs.expected_outcome_value }}. auto_remediate: {{ inputs.auto_remediate }}. dry_run: {{ inputs.dry_run }}. Retried: {{ outputs.retry_process_instance is defined }}. Execution {{ execution.id }}."
}
else:
- id: alert_outcome_unreachable
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: UNREACHABLE means the instance completed but
expected_outcome_variable never came back at all - distinct from
AT_RISK, where the variable is present with an unexpected value.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Camunda process outcome gate: UNREACHABLE. {{ inputs.process_id }} instance {{ outputs.run_monitored_process.processInstanceKey }} completed without ever setting {{ inputs.expected_outcome_variable }} - check whether the BPMN model actually sets that variable on every end event. Execution {{ execution.id }}."
}
- id: log_audit_complete
type: io.kestra.plugin.core.log.Log
description: Final status line for every run, independent of which branch fired,
so the execution history always shows the headline numbers.
message: "Process outcome audit complete for {{ inputs.process_id }}.
instance={{ outputs.run_monitored_process.processInstanceKey }}.
variable_present={{
outputs.run_monitored_process.variables[inputs.expected_outcome_variable]
is defined }}. auto_remediate={{ inputs.auto_remediate }}. dry_run={{
inputs.dry_run }}."
errors:
- id: alert_execution_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Alert when the flow itself fails outside the handled branches, for
example an unreachable Camunda cluster, bad credentials, or the monitored
instance never reaching an end event within requestTimeout.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Camunda process outcome gate FAILED in execution {{ execution.id }} for {{ inputs.process_id }}. The monitored instance did not complete or Camunda could not be reached at all; investigate before trusting the last known HEALTHY/AT_RISK/UNREACHABLE status."
}