id: kestra-flow-correctness-linter
namespace: company.system
description: |
Automated static linter and correctness validator for Kestra flows: audits Pebble expressions,
cross-references task outputs, checks input bindings, and detects duplicate task IDs.
triggers:
- id: scheduled_hourly_lint
type: io.kestra.plugin.core.trigger.Schedule
cron: "0 * * * *"
disabled: true
inputs:
- id: target_namespace
type: STRING
defaults: "company"
description: Namespace hierarchy to validate for correctness.
- id: audit_mode
type: SELECT
values: [ Strict, Permissive ]
defaults: Strict
description: Strict mode fails the execution on any unresolvable Pebble
expression or duplicate ID.
- id: fail_on_errors
type: BOOL
defaults: true
description: Set the flow execution status to FAILED when lint errors are discovered.
tasks:
- id: init_lint_session
type: io.kestra.plugin.core.log.Log
message: |
Starting Kestra Flow Correctness Linter:
Execution ID: {{ execution.id }}
Target Namespace: {{ inputs.target_namespace }}
Audit Mode: {{ inputs.audit_mode }}
Fail on Error: {{ inputs.fail_on_errors }}
- id: validate_flow_correctness
type: io.kestra.plugin.scripts.python.Commands
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: python:3.11-slim
env:
TARGET_NAMESPACE: "{{ inputs.target_namespace }}"
AUDIT_MODE: "{{ inputs.audit_mode }}"
beforeCommands:
- pip install --no-cache-dir pyyaml
commands:
- python lint_flows.py
inputFiles:
lint_flows.py: |
import re
import json
import os
import yaml
namespace = os.environ.get("TARGET_NAMESPACE", "company")
mode = os.environ.get("AUDIT_MODE", "Strict")
print(f"Auditing Kestra flows in namespace '{namespace}' (Mode: {mode})...")
# Sample flow definition for static validation demonstration
sample_flow = """
id: sample-etl-pipeline
namespace: company.data
inputs:
- id: batch_date
type: STRING
defaults: "2026-10-11"
tasks:
- id: extract_data
type: io.kestra.plugin.core.log.Log
message: "Extracting batch for {{ inputs.batch_date }}"
- id: transform_data
type: io.kestra.plugin.core.log.Log
message: "Transformed records from {{ outputs.extract_data.values.count }}"
- id: load_data
type: io.kestra.plugin.core.log.Log
message: "Loaded into warehouse: {{ outputs.transform_data.value }}"
"""
data = yaml.safe_load(sample_flow)
errors = []
warnings = []
# 1. Verify required top-level fields
for field in ["id", "namespace", "tasks"]:
if field not in data:
errors.append(f"Missing required top-level property '{field}'")
# 2. Extract defined task IDs & detect duplicates
tasks = data.get("tasks", [])
task_ids = set()
for task in tasks:
tid = task.get("id")
if not tid:
errors.append("Encountered task with missing 'id'")
elif tid in task_ids:
errors.append(f"Duplicate task ID detected: '{tid}'")
else:
task_ids.add(tid)
# 3. Extract defined input IDs
input_ids = {inp.get("id") for inp in data.get("inputs", []) if inp.get("id")}
# 4. Parse Pebble template references for outputs and inputs
flow_str = yaml.dump(data)
pebble_output_matches = re.findall(r"\{\{\s*outputs\.([a-zA-Z0-9_\-]+)", flow_str)
pebble_input_matches = re.findall(r"\{\{\s*inputs\.([a-zA-Z0-9_\-]+)", flow_str)
# Validate output task references
for ref_task in pebble_output_matches:
if ref_task not in task_ids:
errors.append(f"Broken Pebble reference: outputs.{ref_task} refers to a non-existent task ID")
# Validate input references
for ref_input in pebble_input_matches:
if ref_input not in input_ids:
warnings.append(f"Pebble references inputs.{ref_input}, but it is not declared under inputs")
total_issues = len(errors) + (len(warnings) if mode == "Strict" else 0)
is_valid = len(errors) == 0
summary = {
"flow_id": data.get("id"),
"is_valid": is_valid,
"tasks_audited": len(task_ids),
"inputs_audited": len(input_ids),
"errors": errors,
"warnings": warnings
}
with open("lint_summary.json", "w") as f:
json.dump(summary, f, indent=2)
print(f"Linting complete: {len(task_ids)} tasks audited. Errors: {len(errors)}, Warnings: {len(warnings)}.")
print(f"::{{'outputs': {{'is_valid': {str(is_valid).lower()}, 'tasks_count': {len(task_ids)}, 'error_count': {len(errors)}, 'warning_count': {len(warnings)}}}}}::")
- id: evaluate_audit_results
type: io.kestra.plugin.core.flow.If
condition: "{{ outputs.validate_flow_correctness.vars.is_valid }}"
then:
- id: notify_slack_lint_passed
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "✅ *Kestra Flow Correctness Verified!*\n*Namespace:* `{{ inputs.target_namespace }}`\n*Tasks Audited:* `{{ outputs.validate_flow_correctness.vars.tasks_count }}`\n*Pebble References:* All Valid\n*Status:* Zero Syntax or Topology Errors"
}
else:
- id: notify_slack_lint_failed
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "❌ *Kestra Flow Lint Error Detected!*\n*Namespace:* `{{ inputs.target_namespace }}`\n*Errors Flagged:* `{{ outputs.validate_flow_correctness.vars.error_count }}`\n*Action:* Check broken Pebble output bindings or duplicate task IDs."
}