Generates Liquibase database changesets via a structured workflow: resolve schema+instance (from context or via API discovery), confirm DB engine type, generate Liquibase YAML, and present for HITL review with Accept / Deny / Accept & Commit actions. Supports update mode (refine existing changeset) and error-recovery mode (fix a failed migration).
61
72%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./plugins/claude/skills/dbops-changeset/SKILL.mdGenerate Liquibase database changesets from natural language descriptions, anchored to a real schema and instance from the Harness DBOPS service.
When the user asks to:
The Skill tool loads this file (SKILL.md) automatically — do not Read it again.
Before generating Liquibase YAML, Read reference.md using the exact path listed in
the Skill tool result. It contains all Liquibase YAML rules, change types, rollback
patterns, batch migration guidelines, MongoDB JSON string requirements, and the
clarifying questions policy. Apply those rules in Step 4 when generating the changeset.
At conversation start, the initial message may contain a JSON block with pre-selected identifiers injected by the Harness frontend UI:
{
"dbops_schema_id": "users-schema",
"dbops_instance_id": "prod-instance"
}Read this JSON block (if present) before doing any tool calls.
| What's in context | What to do |
|---|---|
dbops_schema_id + dbops_instance_id | Skip Steps 1 AND 2 — but immediately fetch instance metadata via harness_get(resource_type="database_instance", resource_id="<dbops_instance_id>", dbschema_id="<dbops_schema_id>") to obtain connector (needed in Step 3) and check hasSnapshot (needed in Step 3-B). |
Only dbops_schema_id | Skip Step 1 — use this schema ID. Then go to Step 2-A |
| Neither | Full Steps 1 and 2 |
dbops_schema_id is in context)Call harness_list for the available schemas:
harness_list(resource_type="database_schema")The response is a plain JSON array of objects. Each object has:
identifier — used as the schema ID in subsequent callsname — human-readable namemigrationType — "Liquibase" or "Flyway" (only Liquibase supported)instanceCount — number of linked instancesprimaryDbInstanceId — the designated instance for LLM authoring (may be empty). If non-empty, Step 2-A uses this directly as the instance_id without further discovery.Disambiguation:
AskUserQuestion (selection mode):
Show: [<name> (<identifier>) — <migrationType>, <instanceCount> instances] for each.migrationType is "Flyway": "This skill supports Liquibase schemas only."dbops_schema_id is known)When you have a schema_id but no instance_id (from context OR after Step 1 selection):
harness_get to fetch the full schema:harness_get(resource_type="database_schema", dbschema_id="<schema_id>")primaryDbInstanceId field in the response:instance_id. Inform the user:
"Using primary instance <primaryDbInstanceId> for schema <name>."
Then fetch the instance to obtain the connector field needed in Step 3:harness_get(resource_type="database_instance", resource_id="<primaryDbInstanceId>",
dbschema_id="<schema_id>")Read connector from the response.
primaryDbInstanceId is empty)Use the database_default_authoring_instance resource to let the server pick the best
instance (oldest with a metadata snapshot; falls back to oldest overall):
harness_get(resource_type="database_default_authoring_instance", dbschema_id="<schema_identifier>")The response is { identifier: string, name: string, hasSnapshot: boolean, connector: string }.
Handling the response:
identifier as the instance_id and connector as the
connector identifier for Step 3 (no separate instance detail fetch needed). Inform the user:
"Using instance <name> (<identifier>) for schema <schema_name>."
If hasSnapshot is true, the instance has metadata available for snapshot lookups — proceed
to Step 3-B for any changeset intent (create, modify, or drop). If false, skip Step 3-B entirely.<schema_name>. Please add an instance in Harness DBOPS first — open Harness → DBOPS → Schemas → <schema_name> → Add Instance." — STOP. Do NOT prompt the user for a connector identifier. Do NOT proceed to Step 3. Do NOT generate a changeset.Invariant. By this point you MUST have a non-empty connector value from Step 0 (fast-path
instance fetch), Step 2-A, or Step 2-B — whichever applied. If you do not — for any reason —
STOP and surface the Step 2 "instance not found" message to the user. Never ask the user to
provide a connector identifier directly; connectors are always derived from an existing DBOPS
instance.
Use the connector field obtained in Step 0, Step 2-A, or Step 2-B (whichever ran). Fetch the
connector to determine the engine type:
harness_get(resource_type="connector", resource_id="<connector>",
org_id="<org_id>", project_id="<project_id>")The connector is always a JDBC connector. Read connector.spec.url and map the prefix:
connector.spec.url prefix | Engine |
|---|---|
jdbc:mysql:// or jdbc:mariadb:// | MySQL |
jdbc:postgresql:// | PostgreSQL |
jdbc:oracle:thin: | Oracle |
jdbc:sqlserver:// | MSSQL |
jdbc:mongodb:// | MongoDB |
jdbc:cloudspanner: | Spanner |
jdbc:db2:// | DB2 |
jdbc:sybase: | Sybase |
jdbc:sqlite: | SQLite |
If the URL prefix is unrecognised, proceed with generic ANSI SQL. Do NOT ask the user.
Run this step for any changeset intent (create, modify, or drop) when hasSnapshot is true.
Purpose:
Call with dbschema_id, dbinstance_id, and the relevant object_type ("Table", "Index", "View", etc.):
harness_list(
resource_type="database_snapshot_object",
filters={
dbschema_id: "<schema_id>",
dbinstance_id: "<instance_id>",
object_type: "Table"
}
)Returns { data: string[] } — names of objects of that type in the current snapshot.
If the list API errors or returns an empty data array, treat the snapshot as stale — proceed to Step 4 without blocking the user (stale-snapshot tolerance).
If the user's intent is to create an object (new table, index, view, etc.) and the target name
appears in the data list returned above:
"The
<object_type><name>already exists in the current snapshot for instance<instance_name>. A CREATE changeset would fail. Did you mean to modify it instead? I can generate an ALTER TABLE (add column / modify column / add index / add constraint) changeset if you describe the change you need."
If the name is not in the list (expected case for a genuine new object), proceed to Step 4.
When modifying or dropping an existing object, fetch its full metadata (columns, indexes, etc.) — requires non-empty object_names:
harness_get(
resource_type="database_snapshot_object",
resource_id="<instance_id>",
params={
dbschema_id: "<schema_id>",
object_type: "Table",
object_names: ["<table_name>"]
}
)If the target name is not in the snapshot list for a modify/drop intent, proceed anyway — the snapshot may be stale; do not block the user.
Run when the intent involves adding or modifying a foreign key (addForeignKeyConstraint),
including FK constraints added during createTable.
Fetch snapshot metadata in one harness_get call. Which tables to include depends on intent:
addForeignKeyConstraint on an existing table: fetch both baseTableName and
referencedTableName — needed for snapshot name alignment and column scoring on both sides.createTable with inline FK constraints: fetch only referencedTableName — the
new base table is not in the snapshot yet; base columns come from the changeset definition.harness_get(
resource_type="database_snapshot_object",
resource_id="<instance_id>",
params={
dbschema_id: "<schema_id>",
object_type: "Table",
object_names: ["<base_table>", "<referenced_table>"]
}
)For createTable with inline FK, use object_names: ["<referenced_table>"] only.
Apply column-resolution rules in reference.md → Foreign Key Column Resolution →
Column resolution. If snapshot metadata is unavailable, proceed using user-provided
names only.
Canonical DBOPS resource_type values (see dbops toolset): database_schema, database_instance, database_default_authoring_instance, database_snapshot_object, database_execute_llm_authoring_pipeline.
Produce a valid Liquibase YAML changeset. All YAML generation rules (structure,
required fields, rollback, preconditions, timestamps, batch migrations, MongoDB
formats, clarifying questions) are in **reference.md** in this directory — follow
them exactly.
Changeset author: Derive the author name from .harness_context/user_context.json (if present) or the ## User Context: block in the conversation. Apply these rules in order:
user.display_name, uid, or username in the user context.- (hyphen)a-z, 0-9, and -)email field:@ symbol (e.g., john.doe@harness.io → johndoe)harness-ai.Examples:
| Context Value | Derived Author |
|---|---|
display_name: "John Doe" | john-doe |
display_name: "Jane_Smith-123" | jane-smith-123 |
email: "dev.user@company.com" (no username) | devuser |
| Neither available | harness-ai |
Multi-turn accumulation (follow-up prompts): If the conversation already contains a
previously generated databaseChangeLog YAML (from any prior turn, whether accepted, denied,
or still pending review), that YAML is the running baseline. On every follow-up turn you
MUST produce the full accumulated changelog — not just the delta for this turn.
Classify the user's intent and merge accordingly:
| Intent | Merge behaviour |
|---|---|
| Add a new object (new table, column, index, etc.) | Append one or more new changeSet blocks to the end of the baseline databaseChangeLog |
Modify an object that already has a changeSet in the baseline | Update the matching changeSet block in-place; preserve its id and author |
Drop / remove an object that has a changeSet in the baseline | Remove or replace the matching changeSet block with the appropriate dropTable / dropColumn etc. changeSet |
| Undo / clear / start over | Discard the entire baseline; generate a fresh databaseChangeLog |
Always present the complete, merged databaseChangeLog (all accumulated changeSet entries)
for review — never only the new delta.
Update mode (refining a single changeSet): If the user is explicitly refining or correcting
one specific changeSet from the baseline (e.g., "change the column type in the dog table
changeset"), apply the modification to that changeSet block in-place. Preserve the id and
author from that changeSet and apply the requested modifications. Still present the full
accumulated changelog.
Error recovery mode: See Error Recovery Mode section below.
The present_for_review tool is not available. Present the change for review in chat:
yaml code block.Example prompt after the YAML:
Please review the changeset above and reply with one of: Accept, Accept & Commit, or Deny.
CRITICAL:
databaseChangeLog — never only the new delta."accept" — Save onlyConfirm the changeset is saved. Do NOT execute any pipeline. Reply with a short confirmation message referencing the schema and instance names.
"deny" — DiscardAsk what they want to change and regenerate from Step 4. DO NOT re-run the schema/instance discovery — the schema and instance are already selected.
"accept_commit" — Save + Execute PipelineAfter the user accepts:
IMPORTANT — use already-resolved identifiers: The schema_id, instance_id, and the
instance's connector were resolved in Steps 1–2 and are still in context. Do NOT re-list
schemas, re-fetch instances, or list connectors — use the values you already have.
IMPORTANT — changeset encoding: Send the changeset field as PLAIN-TEXT YAML, exactly as
the user accepted it. Do NOT base64-encode it..
IMPORTANT — conversation_id (REQUIRED before harness_execute): Always pass conversation_id explicitly when calling harness_execute for the database_execute_llm_authoring_pipeline resource. Resolve it from agent context in this order:
conversation_id — Harness chat / platform-injected conversation ID (preferred)session_id — use the session ID as the conversation_id value when present in agent or system contextconversation_id without asking the user:
session_id, composerId, chatId).agent-transcripts/<uuid>/ directory (typically ~/.cursor/projects/<project-slug>/agent-transcripts/<uuid>/<uuid>.jsonl). Match the transcript that contains the current conversation’s recent user query (or the most recently updated transcript for this chat), and use that <uuid> as conversation_id.It is a hard requirement before making the execute-pipeline tool call. Prefer platform-injected IDs when both a Harness conversation_id and a Cursor session ID exist. Never fabricate a UUID from scratch — conversation_id is the billing / idempotency key. Only if steps 1–3 all fail, STOP — do not call harness_execute. Tell the user pipeline execution cannot proceed without a conversation/session ID.
IMPORTANT — chat runner gate protocol. The user already consented via
Accept & Commit, but the chat runner may render a separate permission
prompt before forwarding harness_execute. Handle it deterministically:
| Tool response or system event contains… | What to do |
|---|---|
waiting for user to confirm / waiting for user to review / awaiting confirmation / pending user approval (case-insensitive) | Reply EXACTLY: "Pipeline execution is awaiting your approval — please approve the permission prompt to trigger the run. I will not call additional tools until you do." Do NOT call any tool. Each retry stacks another prompt. |
user confirmed / user approved / run approved (case-insensitive) | Re-issue harness_execute ONCE with the EXACT same args you used pre-gate (same schema_id, instance_id, changeset, conversation_id, branch flag). Do NOT re-walk Steps A–E. If a duplicate confirmation event arrives before the call returns, ignore it. After harness_execute returns, treat any further confirmation events for this turn as no-ops — do NOT call harness_execute a third time. |
user declined / user cancelled / request cancelled (case-insensitive) | Do NOT call harness_execute. Reply: "Pipeline trigger cancelled. Click Accept & Commit again to retry, or ask me to modify the changeset first." |
Branch-lock. Once Step A picks custom vs default, the decision is locked
until harness_execute returns a result or the user cancels — never re-evaluate
it after a gate event, even if the NG setting could have changed.
Args lost to context compaction. If a user confirmed event arrives but
your context no longer holds the original harness_execute args, reply:
"Approval received but I need to regenerate. Can you re-confirm Accept & Commit on the changeset?" — do NOT speculatively reconstruct args.
Post the success message ONLY after harness_execute returns a non-empty
executionId.
harness_list(resource_type="setting", filters={ category: "DBOPS", scope: "project" }, compact=false)Find the entry with identifier == "dbops_llm_authoring_pipeline". Read its value field.
value is a non-empty string → custom-pipeline branch (continue to Step B).value is empty or the setting is absent → default-pipeline branch (skip to Step E).harness_get(
resource_type="runtime_input_template",
resource_id="<value from Step A>",
account_id="<account>", org_id="<org>", project_id="<project>"
)The response's inputSetTemplateYaml lists every <+input> token the admin's pipeline
declares. The skill is responsible for these three reserved tokens (the server expects these
EXACT YAML keys — see the dbops database_execute_llm_authoring_pipeline resource schema):
dbSchema — auto-filled with schema_id.dbInstance — auto-filled with instance_id.changeset — auto-filled with the PLAIN-TEXT changeset YAML.The pipeline's stepGroupInfra.spec.connectorRef is also a <+input> token but is filled
server-side from the DBOPS instance — the skill never sends a value, never asks the user
for it.
If a template field's name does not match any of the canonical names or aliases above, treat
it as non-reserved and elicit it in Step C. Do NOT silently auto-fill it from a similarly-named
context value (e.g. a template field called schema or db_schema is non-reserved — ask the user).
Server-side note (current state): the db-devops-service execute endpoint currently rejects any non-empty
runtime_inputsmap with the error"custom runtimeInputs are not yet supported (pipeline-service contract pending)". Until that contract lands, if the template declares any required field outside the reserved set, stop and tell the user the configured pipeline is incompatible with chat execution. Skip the elicitation loop below entirely. Once the server lands custom-input support, remove this note and follow the loop as documented.
For every non-reserved template field, ask the user for a value via AskUserQuestion —
treat all such fields as required (we cannot reliably infer optionality from the template).
AskUserQuestion is single-question by design; do not batch.Collect responses into a runtime_inputs map.
Cancellation rule. Stop and do NOT call Step D if any of these hold:
AskUserQuestion returns cancelled (no answer, user dismissed the prompt).cancel, skip, stop, or abort (case-insensitive whole word).Any other freeform text is a real answer — copy verbatim into the runtime_inputs map.
Do not proceed to Step D with a partial map.
Call the consolidated dbops resource (single tool call — server triggers execution and records the billing event atomically):
harness_execute(
resource_type="database_execute_llm_authoring_pipeline",
action="run",
schema_id="<schema_id>",
instance_id="<instance_id>",
conversation_id="<conversation_id or session_id from agent context>",
changeset="<plain-text accepted YAML>",
pipeline_identifier="<value from Step A>",
runtime_inputs={ <field>: <value>, ... } # OMIT this key entirely when no fields were elicited; do NOT pass {}
)If the call returns an error or times out, surface the exact error message verbatim and stop.
Do NOT retry automatically and do NOT fall back to the default-pipeline branch.
Do NOT fabricate navigation paths, execution status claims, or retry invitations.
The openInHarness link MUST only be shown when the response provides it — never construct it manually.
Skip to Step F.
harness_execute(
resource_type="database_execute_llm_authoring_pipeline",
action="run",
schema_id="<schema_id>",
instance_id="<instance_id>",
conversation_id="<conversation_id or session_id from agent context>",
changeset="<plain-text accepted YAML>",
use_default_pipeline=true
)The server performs get-or-create of dbops_default_pipeline on first use, then triggers the
execution. The server resolves the K8s connector for the step-group infra from the DBOPS
instance's connector — the skill sends no infra hints.
If the call returns an error or times out, surface the exact error message verbatim and stop.
Do NOT fabricate navigation paths, execution status claims, or retry invitations.
The openInHarness link MUST only be shown when the response provides it — never construct it manually.
Proceed to Step F.
Both branches return:
{ executionId, pipelineIdentifier, openInHarness }Post immediately — only if openInHarness is a non-empty string in the response:
Pipeline started — <a href="{openInHarness}" target="_blank">view execution</a>If openInHarness is absent or empty, post instead:
Pipeline started — execution ID: `<executionId>`.Do NOT construct the URL manually. The link must come from the response.
Do NOT proactively poll. When the user asks for status, call
harness_get(resource_type="execution", execution_id="<executionId>") once — this hits the
same pipeline-service execution endpoint that powers the openInHarness link.
Status triage:
<stepName> is <stepStatus>".openInHarness is present, append — <a href="{openInHarness}" target="_blank">view execution</a>. Do NOT construct the URL manually.failureInfo.responseMessages[0].message.
changeset/liquibase/migration OR the message
contains those keywords or checksum / precondition / validation error — this is a
changeset error. Offer error recovery: enter error-recovery mode (see Error
Recovery Mode section below) and regenerate a corrected changeset.<stepName>:
<message> — view execution. This is an
infrastructure or configuration issue — please check your Harness pipeline setup."harness_execute for database_execute_llm_authoring_pipeline, resolve conversation_id from agent context (conversation_id, else session_id, else Cursor chat/transcript UUID). Do not execute without it, and do not ask the user for a Cursor chat ID when the transcript UUID can be resolved locally.accept_commit, post the openInHarness link (if present) and stop. Do NOT proactively poll — the server reconciles status asynchronously. If the user later asks for status, call harness_get once as described in Step F.deny, skip re-discovery — schema and instance are already known.Trigger: Enter this mode ONLY for changeset/Liquibase failures:
FAILED pipeline status event resumes the conversation (automated polling path), ORDo NOT enter this mode for infrastructure or configuration failures (broken connector, K8s scheduling failure, missing secret, pipeline misconfiguration, etc.). For those, report the error verbatim and reply: "Pipeline failed at step <stepName>: <message> — this is an infrastructure or configuration issue — please check your Harness pipeline setup." Do NOT attempt to regenerate the changeset.
Do NOT re-run Steps 1–2 (schema/instance discovery). schema_id, instance_id, and
connector are already resolved from the original session. Jump directly to the steps below.
1. Extract the failure details from context:
failed_step, failure_message, execution_id, open_in_harness link.execution_id but no failure_message, call:harness_diagnose(resource_type="execution", execution_id="<execution_id>")Read the full step log from the response to get the Liquibase error text before proceeding to root-cause analysis.
1a. Classify the failure before proceeding:
| Failure contains | Classification | Action |
|---|---|---|
liquibase, changeset, checksum, precondition, validation, OPA policy, migration | Changeset error | Continue to step 2 below |
| Connector error, K8s / pod failure, secret not found, network timeout, pipeline config | Infrastructure error | Report verbatim + stop. Do NOT regenerate. |
| Ambiguous | Treat as infrastructure error — do not guess at changeset root cause |
2. Identify the root cause.
Read reference.md → "Author, Update Mode, and Error Recovery" section. Common causes:
rollback blockDATETIME2 vs TIMESTAMP)changeSet id within the changelogonFail3. Patch the offending changeSet:
id, author, and all unaffected changeSets.databaseChangeLog (never emit just the delta).4. Explain the fix in a text message BEFORE presenting the corrected YAML: State: what the error was, which changeSet was affected, and what was changed to fix it.
5. Present the corrected YAML for review (Step 5) with all three options: Accept, Accept & Commit, Deny.
23e9b8b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.