New to Kestra?
Use blueprints to kickstart your first workflows.
Audit SurrealDB database schema state and conditionally execute table and index migration statements with Slack reporting.
Running DEFINE TABLE/DEFINE FIELD/DEFINE INDEX statements directly against a
SurrealDB database has no built-in dry-run: there is no safe way to confirm the database
is even reachable and healthy before mutating its schema. This blueprint splits that into
two stages. It first runs a read-only SurrealQL preflight audit (INFO FOR DB;), which
returns the database's current schema metadata without changing anything. Only if that
audit actually returned a row, and only if apply_migration was explicitly set to true,
does it run the DEFINE statements for real. Every run (audited, gated, or migrated) ends with one Slack message.
weekly_schema_audit (io.kestra.plugin.core.trigger.Schedule) runs once a week. Shipped disabled so you can validate a manual run first.preflight_schema_audit (io.kestra.plugin.surrealdb.Query) connects with host/port/username/password/namespace/database and runs INFO FOR DB; with fetchType: FETCH_ONE, which populates the row output with the database's schema metadata object (tables, users, functions, params).evaluate_migration_gate (io.kestra.plugin.core.flow.If) branches on inputs.apply_migration and outputs.preflight_schema_audit.row is defined. exitCode is not part of this condition: io.kestra.plugin.surrealdb.Query has no such property, and neither is result/results: FETCH_ONE populates row, not result.then: apply_schema_migration re-runs with the same connection and fetchType: NONE, executing DEFINE TABLE OVERWRITE user SCHEMAFULL; DEFINE FIELD OVERWRITE email ON TABLE user TYPE string; DEFINE INDEX OVERWRITE user_email ON TABLE user COLUMNS email UNIQUE;.else: log_audit_only records why nothing ran: audit failed to return a row, or it succeeded but apply_migration was false.notify_slack posts one summary message every run: whether the audit returned a row, the apply_migration input value, and whether the migration task actually executed.apply_migration) standing between "audited" and "migrated", instead of trusting a DEFINE statement to be safe to run unattended.OVERWRITE migration statements that can be re-run without first checking whether the table/field/index already exists.DEFINE migrations to require a conscious flag, not an automatic side effect of a scheduled job.SurrealQL has no concept of "audit first, then ask before mutating" across two separate statements: that sequencing, the gate in between, and the audit trail of which branch ran are not things a raw surreal sql script tracks for you. Kestra's If task turns the gate into a first-class, inspectable branch; the schedule, the explicit apply_migration input, and the execution history make every audit-only or migrated run reviewable after the fact.
docker run -d -p 8000:8000 surrealdb/surrealdb:latest start --user root --pass root.root-capable user (or a scoped user with permission to run INFO FOR DB and DEFINE statements) in the target namespace/database.SURREALDB_PASSWORD: password for the username (root by default) used by both Query tasks.SLACK_WEBHOOK_URL: Slack incoming webhook used by notify_slack.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_SURREALDB_PASSWORD=$(echo -n 'root' | 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.apply_migration (BOOL, default false): must be explicitly true for the migration branch to run, even if the audit succeeds.surrealdb_host (STRING, default localhost): bare hostname, no scheme or path.surrealdb_namespace (STRING, default production): target namespace.surrealdb_database (STRING, default app_data): target database.outputs.preflight_schema_audit.row: the INFO FOR DB metadata object (tables, users, functions, params) when the audit succeeds.outputs.preflight_schema_audit.size: row count from the audit query (always present).outputs.apply_schema_migration: present only when the migration branch ran; fetchType: NONE means it carries no row/rows/uri, only confirms the task executed.SURREALDB_PASSWORD and SLACK_WEBHOOK_URL secrets.preflight_schema_audit returns a row and, with apply_migration: false, only log_audit_only and notify_slack run.apply_migration: true against a namespace/database you are comfortable testing on, and confirm the user table, email field, and user_email index are created.weekly_schema_audit once you trust the audit step; keep apply_migration as a manual, reviewed flag rather than enabling it on the schedule.outputs.preflight_schema_audit.row.tables to branch differently depending on which tables already exist, instead of a flat true/false gate.Pause approval task between audit and migration instead of (or in addition to) the static apply_migration input.DEFINE statements to apply_schema_migration's query as your schema grows, keeping the OVERWRITE clause so re-runs stay idempotent.port property requires a literal integer. The SurrealDB plugin's port property is declared as a plain int in Java without a dynamic/templatable marker. Using a literal integer 8000 directly on the tasks prevents String-to-Int deserialization errors.host/port, not a single URL. io.kestra.plugin.surrealdb.Query has no url property; it opens new SurrealWebSocketConnection(host, port, useTls) internally. A ws://host:port/rpc-style value in host would be passed through as a literal hostname string, not parsed.FETCH_ONE populates row, not result. Checking outputs.preflight_schema_audit.result is defined (or exitCode, which does not exist on this task) would always be false; this blueprint checks .row is defined instead, which is the verified output field for fetchType: FETCH_ONE.OVERWRITE makes migration statements idempotent, but destructive on structural changes. DEFINE ... OVERWRITE replaces the existing definition rather than erroring if it already exists, which is what makes repeated runs safe - but changing a field's TYPE or an index's columns on a table with existing data can still reject or silently coerce rows that violate the new definition; test against representative data first.