New to Kestra?
Use blueprints to kickstart your first workflows.
Audit a Neo4j label's constraint coverage with SHOW CONSTRAINTS, alert on gaps, and optionally create the missing constraint.
A node label with no uniqueness constraint can silently accumulate duplicate
"unique" keys - two Account nodes with the same account_id, two Merchant
nodes with the same merchant_id - because Neo4j does not enforce uniqueness
unless a constraint says so. This blueprint queries Neo4j's own schema metadata
with SHOW CONSTRAINTS to check whether a target label's governance-critical
property is actually covered, alerts on a gap or on a label with no data to
govern, and can create the missing constraint with CREATE CONSTRAINT ... IF NOT EXISTS - schema-only, never touching existing node data.
periodic_constraint_check (io.kestra.plugin.core.trigger.Schedule) runs every 6 hours, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.check_label_population (io.kestra.plugin.neo4j.Query) runs MATCH (n:{{ target_label }}) RETURN count(n) AS node_count with storeType: FETCHONE, populating row.node_count.check_constraint_coverage (io.kestra.plugin.neo4j.Query) runs SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties WHERE entityType = 'NODE' AND '{{ target_label }}' IN labelsOrTypes AND '{{ required_unique_property }}' IN properties RETURN count(*) AS matching_constraints with storeType: FETCHONE, populating row.matching_constraints.evaluate_population (io.kestra.plugin.core.flow.If) branches on node_count > 0.else (UNREACHABLE): alert_label_unreachable reports that the label has no nodes, so coverage could not be evaluated.then (label populated): nested evaluate_constraint_presence (io.kestra.plugin.core.flow.If) branches on matching_constraints > 0.then (HEALTHY): log_constraint_healthy records a clean check.else (AT_RISK): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, remediate_constraint_gap runs CREATE CONSTRAINT ... IF NOT EXISTS FOR (n:{{ target_label }}) REQUIRE n.{{ required_unique_property }} IS UNIQUE. alert_constraint_gap always fires to Slack regardless of which remediation path ran.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails - an unreachable Neo4j instance or bad credentials must not read as "no gap".SHOW CONSTRAINTS metadata, to "is this property actually enforced unique on this label" - not inferred from sampling node data.IF NOT EXISTS - it cannot drop an existing constraint or touch node data.auto_remediate, dry_run) between "gap detected" and "constraint actually created".Query driving schema introspection (SHOW CONSTRAINTS) and schema mutation (CREATE CONSTRAINT), not just data reads.SHOW CONSTRAINTS run by hand answers one moment in time; it does not decide a cadence, classify the result into a three-state risk level, gate a schema change behind two independent confirmations, or notify anyone. Kestra supplies the schedule, the HEALTHY/AT_RISK/UNREACHABLE classification as a first-class branch, two independent authorization gates before any schema mutation, and an execution history that shows exactly when a label went uncovered and whether it was fixed.
SHOW CONSTRAINTS and CREATE CONSTRAINT ... IS UNIQUE are both available on Community Edition).target_label) that may or may not have nodes, and may or may not have a constraint on required_unique_property.Local testing:
docker run -d --name neo4j-gate -p 7475:7474 -p 7688:7687 -e NEO4J_AUTH=neo4j/<your-local-password> neo4j:5
docker network connect YOUR_KESTRA_NETWORK neo4j-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set neo4j_url to bolt://neo4j-gate:7687 from inside that network, or bolt://localhost:7688 from the host).
NEO4J_USERNAME / NEO4J_PASSWORD: basic-auth credentials used by every io.kestra.plugin.neo4j.Query task in this flow.SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_constraint_gap, alert_label_unreachable, and the errors block.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_NEO4J_PASSWORD=$(echo -n '<your-local-password>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.neo4j_url (STRING, default bolt://localhost:7687): Bolt endpoint.target_label (SELECT: "Account", "Device", "Merchant"; default "Account"): label audited. Bounded so it is never spliced into Cypher from unbounded input.required_unique_property (STRING, default account_id): property expected to be constraint-covered.auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for remediation to even be considered.dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for CREATE CONSTRAINT to actually run.outputs.check_label_population.row.node_count: node count for target_label, every run.outputs.check_constraint_coverage.row.matching_constraints: count of matching constraints from SHOW CONSTRAINTS, every run.Account nodes without a constraint: CREATE (:Account {account_id: "a1"}), (:Account {account_id: "a1"}) (two nodes sharing the same account_id on purpose).NEO4J_USERNAME, NEO4J_PASSWORD, and SLACK_WEBHOOK_URL secrets.alert_constraint_gap fires (AT_RISK: nodes exist, no constraint yet).auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the constraint without creating it.auto_remediate: "true" and dry_run: "false" and confirm remediate_constraint_gap runs, then verify with SHOW CONSTRAINTS in the Neo4j Browser that the constraint now exists.Account nodes and re-run to confirm alert_label_unreachable fires (UNREACHABLE) instead of either HEALTHY or AT_RISK.periodic_constraint_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.target_label/required_unique_property pairs with io.kestra.plugin.core.flow.Loop to run the same gate across multiple labels in one flow.SHOW CONSTRAINTS check for NODE KEY or NODE PROPERTY EXISTENCE constraints alongside uniqueness, to cover required-field compliance as well as duplicate-key risk.CREATE CONSTRAINT remediation with a report-only mode that writes the suggested Cypher to a file via io.kestra.plugin.core.storage.Write, for teams that want change control outside Kestra.Query and Batch exist under io.kestra.plugin.neo4j. Both schema introspection (SHOW CONSTRAINTS) and schema mutation (CREATE CONSTRAINT) go through the same generic Query task as ordinary Cypher text.target_label is a SELECT, not a free-text STRING. Because the label is spliced directly into MATCH (n:{{ target_label }}) and CREATE CONSTRAINT ... FOR (n:{{ target_label }}) (Cypher has no bind-parameter syntax for label names), bounding it to a fixed list is what keeps this safe from arbitrary Cypher injection through that field.auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for CREATE CONSTRAINT to run; setting only one leaves the other still blocking.Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.SHOW CONSTRAINTS YIELD ... WHERE ... RETURN count(*) and CREATE CONSTRAINT ... IF NOT EXISTS ... REQUIRE ... IS UNIQUE syntax is taken verbatim from Neo4j's own Cypher manual, and the Query task's properties (url, username, password, query, storeType) and outputs (row, rows, uri, size) are taken verbatim from the plugin's source on main - but no Docker container or Kestra engine was run to execute this flow end to end.