AI Templates
AI templates install starter files for AI agents that work on Bruin repositories. They are installed with bruin init, but they are not pipeline templates.
AI templates install files into the Git repository root when Bruin can find one, otherwise into the current directory. They do not create a pipeline folder, initialize Git, or create .bruin.yml by default. They also do not accept a folder argument or --in-place.
# Open the AI template selector
bruin init ai
# Install AGENTS.md directly
bruin init ai-agents-md
# Install the troubleshooting skill pack directly
bruin init ai-skill-self-healAvailable AI Templates
| Template | What it installs |
|---|---|
ai-agents-md | A generic AGENTS.md starter with Bruin-oriented agent instructions. |
ai-skill-self-heal | Starter troubleshooting skills under .agents/skills/. |
The self-healing starter is a pack of focused skills:
pipeline-diagnoseschema-drift-checkduplicate-investigatefreshness-checkquality-check-investigatemaintenance-action
Each skill includes a placeholder Actions section for repository-specific behavior. Until customized, the skills only diagnose and report findings.
Runtime Expectations
The starter skills are primarily meant for AI agents configured inside Bruin Cloud. In that environment, agents should use Bruin Cloud MCP tools when available. If they use the CLI, the relevant Cloud commands include:
# Diagnose the latest failed or recent run
bruin cloud runs diagnose --project-id <project-id> --pipeline <pipeline-name> --latest
# Read run and asset logs
bruin cloud runs get --project-id <project-id> --run-id <run-id>
bruin cloud instances logs --project-id <project-id> --run-id <run-id> --asset <asset-name>
bruin cloud instances failed-logs --project-id <project-id> --run-id <run-id>
# Create or rerun Cloud runs
bruin cloud runs trigger --project-id <project-id> --pipeline <pipeline-name>
bruin cloud runs rerun --project-id <project-id> --run-id <run-id> --only-failed
# Enable or disable Cloud pipelines
bruin cloud pipelines enable --project-id <project-id> --pipeline <pipeline-name>
bruin cloud pipelines disable --project-id <project-id> --pipeline <pipeline-name>For local development, the skills should rely on local terminal commands such as bruin validate, bruin render, bruin query, and bruin run. Local troubleshooting should read terminal output and the local logs/ folder, especially logs/runs, query logs, and export logs when they exist. Local runs should be created with bruin run rather than Bruin Cloud run commands.
When troubleshooting data issues, the skills guide agents to find one specific failing row, key, partition, or timestamp first and keep upstream queries filtered to that example. For layered pipelines such as bronze, silver, and gold, agents should start at the asset where the issue appears, trace upstream through lineage until the issue first appears, then inspect that asset's SQL or Python logic to isolate the likely join, filter, cast, incremental condition, function, or transformation step. If the user has allowed fixes, agents should change only the isolated logic, verify the same bad example first, and only then run the broader quality check, freshness check, duplicate check, or pipeline command.
If investigation or verification requires actual asset or pipeline runs, agents should prefer a dev or shadow environment. If one is not available, they should ask whether to run in production or create temporary copies of affected tables to reproduce and test the issue.
For any other agent runtime or orchestrator, customize the installed skills with the correct log source and action mechanism before using them to read logs, trigger runs, enable or disable pipelines, mark statuses, or change external systems.
Safe Sample Copies
When an agent needs to investigate real data issues, instruct it to reproduce the smallest useful slice of the problem outside production before changing pipeline logic. The safest pattern is:
- Find one failing row, key, partition, timestamp, or column first.
- Create dev, shadow, or temporary copies of only the affected upstream and downstream tables.
- Prefer a small sample filtered to the failing example plus a small amount of surrounding context, such as the same date partition, tenant, customer, order, or batch.
- Preserve enough lineage to reproduce the issue locally or in a dev warehouse: source sample, intermediate sample, final asset sample, and the SQL or Python logic under test.
- Run the suspected fix against the sampled copies first.
- Verify the known bad example before running broader checks, reruns, or backfills.
For large tables, avoid full-table copies unless the user explicitly approves the cost and blast radius. A useful agent instruction is:
Before changing production logic, create the smallest safe dev sample that reproduces the issue.
Copy only the affected keys, partitions, or time window from each upstream table needed for lineage.
Run the current logic against the sample, prove the failure, apply the proposed fix to the sample, and prove the same key or partition is fixed before touching the real asset.Sample Pipeline and Test
This section is a fully mocked local demo. It creates a dummy DuckDB pipeline, deliberately introduces common data and pipeline issues, then gives agent prompts and proof commands for testing the installed self-healing skills end to end.
Use a throwaway local DuckDB project so agents can safely investigate, run assets, and test fixes without production data. Recreate the fixture before each scenario unless you intentionally want to test multiple failures at once.
Create the Sample Pipeline
BRUIN=${BRUIN:-bruin}
WORKDIR=$(mktemp -d)
cd "$WORKDIR"
git init
$BRUIN init ai-skill-self-heal
mkdir -p assets
cat > .bruin.yml <<'YAML'
default_environment: default
environments:
default:
connections:
duckdb:
- name: duckdb-skill-test
path: skill-test.duckdb
YAML
cat > pipeline.yml <<'YAML'
name: ai-skill-test
default_connections:
duckdb: duckdb-skill-test
YAML
cat > assets/bronze_orders.sql <<'SQL'
/* @bruin
name: bronze_orders
type: duckdb.sql
materialization:
type: table
columns:
- name: order_id
type: INTEGER
- name: user_id
type: INTEGER
- name: transaction_date
type: DATE
- name: amount
type: DOUBLE
- name: status
type: VARCHAR
@bruin */
SELECT 1001 AS order_id, 501 AS user_id, DATE '2025-01-01' AS transaction_date, 25.00 AS amount, 'paid' AS status
UNION ALL
SELECT 1002 AS order_id, 502 AS user_id, DATE '2025-01-01' AS transaction_date, 35.00 AS amount, 'paid' AS status
UNION ALL
SELECT 1003 AS order_id, 503 AS user_id, DATE '2025-01-02' AS transaction_date, 40.00 AS amount, 'paid' AS status
UNION ALL
SELECT 1004 AS order_id, 504 AS user_id, DATE '2025-01-03' AS transaction_date, 50.00 AS amount, 'paid' AS status;
SQL
cat > assets/silver_orders.sql <<'SQL'
/* @bruin
name: silver_orders
type: duckdb.sql
materialization:
type: table
depends:
- bronze_orders
columns:
- name: order_id
type: INTEGER
- name: user_id
type: INTEGER
- name: transaction_date
type: DATE
- name: amount
type: DOUBLE
- name: status
type: VARCHAR
@bruin */
WITH typed_orders AS (
SELECT
order_id,
user_id,
transaction_date,
amount AS amount,
status
FROM bronze_orders
WHERE status <> 'cancelled'
)
SELECT * FROM typed_orders
UNION ALL
SELECT * FROM typed_orders WHERE order_id = 1002;
SQL
cat > assets/gold_order_report.sql <<'SQL'
/* @bruin
name: gold_order_report
type: duckdb.sql
materialization:
type: table
depends:
- silver_orders
columns:
- name: order_id
type: INTEGER
primary_key: true
checks:
- name: not_null
- name: unique
- name: user_id
type: INTEGER
- name: transaction_date
type: DATE
checks:
- name: not_null
- name: amount
type: DOUBLE
checks:
- name: positive
- name: status
type: VARCHAR
custom_checks:
- name: latest partition exists
query: SELECT CASE WHEN max(transaction_date) = DATE '2025-01-03' THEN 1 ELSE 0 END FROM gold_order_report
value: 1
@bruin */
SELECT
order_id,
user_id,
transaction_date,
amount,
status
FROM silver_orders;
SQL
$BRUIN validate .
$BRUIN render assets/gold_order_report.sql
$BRUIN query --connection duckdb-skill-test --query "SELECT current_database() AS duckdb_database;"The setup should print a successful validation for three assets, render SQL that reads from silver_orders, and return the active DuckDB database. That proves the skills, pipeline files, and DuckDB connection are in place before you create any issue.
Run Agent Test Scenarios
Use one fresh copy of the fixture for each scenario. If you are testing with an agent runner that supports parallel work, create one sub-agent per scenario and give each sub-agent its own copy of the throwaway directory. Ask every agent to read the named skill file under .agents/skills/, print the commands it runs, capture the row or error that proves the issue exists, isolate the smallest upstream cause, apply only the approved fix, and print the rerun/backfill proof.
Use this prompt shape for each scenario:
Use .agents/skills/<skill-name>/SKILL.md.
Create the documented issue in this throwaway DuckDB pipeline.
Prove the tables were created and prove the issue exists with a filtered query or failing Bruin command.
Trace lineage from gold_order_report to silver_orders to bronze_orders, keeping queries filtered to the failing key, date, or column.
Fix only the isolated cause.
Rerun with bruin run --full-refresh --backfill-id <scenario>-fix --backfill-total 1 .
Verify the specific bad example first, then run bruin run --only checks .
Report the commands, important output, changed file, and final status.For these local table-materialization tests, --full-refresh makes reruns deterministic after an agent edits SQL. It is not a requirement for every real pipeline fix.
Mock Duplicate Investigation
The base fixture already contains a duplicate row for order_id = 1002 in silver_orders.
$BRUIN run . || true
$BRUIN query --connection duckdb-skill-test --query "SHOW TABLES;"
$BRUIN query --connection duckdb-skill-test --query "SELECT order_id, count(*) AS row_count FROM gold_order_report GROUP BY 1 HAVING count(*) > 1;"
$BRUIN query --connection duckdb-skill-test --query "SELECT 'gold' AS layer, count(*) AS row_count FROM gold_order_report WHERE order_id = 1002 UNION ALL SELECT 'silver' AS layer, count(*) AS row_count FROM silver_orders WHERE order_id = 1002 UNION ALL SELECT 'bronze' AS layer, count(*) AS row_count FROM bronze_orders WHERE order_id = 1002;"Expected proof:
bruin runfailsgold_order_report:order_id:unique.SHOW TABLESlistsbronze_orders,silver_orders, andgold_order_report.- The duplicate query returns
1002withrow_count = 2. - The lineage query shows the duplicate first appears in
silver_orders, notbronze_orders.
Ask the agent to use duplicate-investigate, start from order_id = 1002, trace upstream through gold_order_report -> silver_orders -> bronze_orders, isolate the extra UNION ALL in assets/silver_orders.sql, and fix it only if you explicitly allow fixes. After the fix, verify the specific key first, then run the broader check and a tagged rerun:
$BRUIN run --full-refresh --backfill-id duplicate-fix --backfill-total 1 .
$BRUIN query --connection duckdb-skill-test --query "SELECT order_id, count(*) AS row_count FROM gold_order_report WHERE order_id = 1002 GROUP BY 1 HAVING count(*) > 1;"
$BRUIN run --only checks .The filtered duplicate query should return no rows, and the check-only run should pass.
Mock Quality Check Investigation
Create a negative amount after removing the duplicate from the base fixture:
perl -0pi -e 's/\nUNION ALL\nSELECT \* FROM typed_orders WHERE order_id = 1002;//' assets/silver_orders.sql
perl -0pi -e 's/amount AS amount/CASE WHEN order_id = 1003 THEN -amount ELSE amount END AS amount/' assets/silver_orders.sql
$BRUIN run --full-refresh . || true
$BRUIN query --connection duckdb-skill-test --query "SHOW TABLES;"
$BRUIN query --connection duckdb-skill-test --query "SELECT order_id, amount FROM gold_order_report WHERE order_id = 1003;"
$BRUIN query --connection duckdb-skill-test --query "SELECT 'gold' AS layer, amount FROM gold_order_report WHERE order_id = 1003 UNION ALL SELECT 'silver' AS layer, amount FROM silver_orders WHERE order_id = 1003 UNION ALL SELECT 'bronze' AS layer, amount FROM bronze_orders WHERE order_id = 1003;"Expected proof:
bruin runfailsgold_order_report:amount:positive.SHOW TABLESlistsbronze_orders,silver_orders, andgold_order_report.order_id = 1003has a negative amount ingold_order_report.- The lineage query shows the negative amount first appears in
silver_orders.
Ask the agent to use quality-check-investigate, start from order_id = 1003, trace the negative amount upstream, isolate the expression in silver_orders, and fix only that expression if fixes are allowed. Verify the row, then rerun:
$BRUIN run --full-refresh --backfill-id quality-fix --backfill-total 1 .
$BRUIN query --connection duckdb-skill-test --query "SELECT order_id, amount FROM gold_order_report WHERE order_id = 1003;"
$BRUIN run --only checks .The row should show a positive amount, and the check-only run should pass.
Mock Freshness Check
Create a stale latest partition after removing the duplicate from the base fixture:
perl -0pi -e 's/\nUNION ALL\nSELECT \* FROM typed_orders WHERE order_id = 1002;//' assets/silver_orders.sql
perl -0pi -e "s/WHERE status <> 'cancelled'/WHERE status <> 'cancelled' AND transaction_date < DATE '2025-01-03'/" assets/silver_orders.sql
$BRUIN run . || true
$BRUIN query --connection duckdb-skill-test --query "SHOW TABLES;"
$BRUIN query --connection duckdb-skill-test --query "SELECT max(transaction_date) AS max_transaction_date FROM gold_order_report;"
$BRUIN query --connection duckdb-skill-test --query "SELECT 'gold' AS layer, count(*) AS row_count FROM gold_order_report WHERE transaction_date = DATE '2025-01-03' UNION ALL SELECT 'silver' AS layer, count(*) AS row_count FROM silver_orders WHERE transaction_date = DATE '2025-01-03' UNION ALL SELECT 'bronze' AS layer, count(*) AS row_count FROM bronze_orders WHERE transaction_date = DATE '2025-01-03';"Expected proof:
bruin runfails thelatest partition existscustom check.SHOW TABLESlistsbronze_orders,silver_orders, andgold_order_report.max(transaction_date)is2025-01-02.- The lineage query shows
2025-01-03exists inbronze_ordersbut not insilver_ordersorgold_order_report.
Ask the agent to use freshness-check, start from the missing transaction_date = DATE '2025-01-03', trace upstream, isolate the stale filter in silver_orders, and fix it only if allowed. Verify the date, then rerun:
$BRUIN run --full-refresh --backfill-id freshness-fix --backfill-total 1 .
$BRUIN query --connection duckdb-skill-test --query "SELECT max(transaction_date) AS max_transaction_date FROM gold_order_report;"
$BRUIN run --only checks .The max date should be 2025-01-03, and the check-only run should pass.
Mock Schema Drift Check
Create a source column rename after removing the duplicate from the base fixture:
perl -0pi -e 's/\nUNION ALL\nSELECT \* FROM typed_orders WHERE order_id = 1002;//' assets/silver_orders.sql
perl -0pi -e 's/ AS amount/ AS gross_amount/g' assets/bronze_orders.sql
$BRUIN render assets/bronze_orders.sql --raw-query
$BRUIN render assets/silver_orders.sql --raw-query
$BRUIN run --full-refresh . || true
$BRUIN query --connection duckdb-skill-test --query "DESCRIBE bronze_orders;"Expected proof:
bruin runfails while executingsilver_orders.- The error says the
amountcolumn cannot be found and suggestsgross_amount. - Rendered SQL shows
assets/bronze_orders.sqlnow emitsgross_amountwhileassets/silver_orders.sqlstill selectsamount AS amount. DESCRIBE bronze_ordersshowsgross_amount.
Ask the agent to use schema-drift-check, identify that bronze_orders.amount became gross_amount, confirm silver_orders still references amount, and update only the affected mapping if fixes are allowed. Verify with render or the smallest failing command, then rerun:
$BRUIN render assets/silver_orders.sql
$BRUIN run --full-refresh --backfill-id schema-drift-fix --backfill-total 1 .
$BRUIN query --connection duckdb-skill-test --query "DESCRIBE silver_orders;"
$BRUIN run --only checks .The run should rebuild all three tables, DESCRIBE silver_orders should show the downstream amount column again, and the check-only run should pass.
Mock Pipeline Diagnose and Maintenance Action
Create a broken table reference after removing the duplicate from the base fixture:
perl -0pi -e 's/\nUNION ALL\nSELECT \* FROM typed_orders WHERE order_id = 1002;//' assets/silver_orders.sql
perl -0pi -e 's/FROM silver_orders/FROM missing_table/' assets/gold_order_report.sql
$BRUIN run . || true
$BRUIN render assets/gold_order_report.sqlExpected proof:
bruin runfails while executinggold_order_report.- The error says
missing_tabledoes not exist. bruin render assets/gold_order_report.sqlshowsFROM missing_table.
Ask the agent to use pipeline-diagnose, classify the failure, inspect the failing asset, and isolate the broken table reference. Then explicitly approve maintenance-action to fix only that reference. Verify the specific command, then rerun:
$BRUIN render assets/gold_order_report.sql
$BRUIN validate .
$BRUIN run --full-refresh --backfill-id pipeline-diagnose-fix --backfill-total 1 .
$BRUIN run --only checks .The rendered SQL should read from silver_orders, validation should pass, and both reruns should pass.
Publish the Fix
In a real repository, have the agent show its diff and publish the fix after the focused verification passes:
git diff
git status --short
git add assets/<fixed-asset>.sql
git commit -m "Fix <scenario> in ai skill test pipeline"
# Only when the repository has a remote:
git push -u origin <branch-name>The proof to save in the agent's final report is the failing command output, the filtered investigation query, the fixed diff, the bruin run --backfill-id ... output, the bruin run --only checks . output, and the commit or push reference.
AI Template Conflicts
If an AI template target already exists, Bruin asks what to do:
add: append or update the marked Bruin AI section inAGENTS.md; for skill files, keep existing files.overwrite: replace the existing target file.skip: leave the existing target unchanged.
In non-interactive shells, existing targets fail safely and Bruin prints the path that needs a choice.
Optional AI Connections
Some AI starter skills can use Bruin Cloud or GitHub context. If bruin init ai-skill-self-heal does not find matching connections in .bruin.yml, Bruin asks whether to add placeholder connections:
default_environment: default
environments:
default:
connections:
bruin:
- name: bruin-cloud
api_token: "${BRUIN_CLOUD_API_TOKEN}"
github:
- name: github
access_token: "${GITHUB_TOKEN}"
owner: "<github-owner>"
repo: "<github-repo>"Bruin infers owner and repo from the origin remote when possible. Existing connections are never duplicated.