Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Creates bauplan data pipeline projects with SQL and Python models. Use when starting a new pipeline, defining DAG transformations, writing models, or setting up bauplan project structure from scratch.
.claude/skills/aiskillstore-creating-bauplan-pipelines/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 108% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 73% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 15% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 124% | 0% |
This skill guides you through creating a new bauplan data pipeline project from scratch, including the project configuration and SQL/Python transformation models.
> NEVER run pipelines on main branch. Always use a development branch.
Branch naming convention: <username>.<branch_name> (e.g., john.feature-pipeline). Get your username with bauplan info. See Workflow Checklist for exact commands.
Before creating the pipeline, verify that:
main)bauplan)A bauplan pipeline is a DAG of functions (models). Key rules:
bauplan.Model() referencestrips.sql → trips)def clean_trips() → clean_trips)Expectations: Data quality functions that take tables as input and return a boolean.
[lakehouse: taxi_fhvhv] ──→ [trips.sql] ──→ [clean_trips] ──→ [daily_summary]
↑
[lakehouse: taxi_zones] ────────────────────────┘In this example:
taxi_fhvhv and taxi_zones are source tables (already in lakehouse)trips.sql reads from taxi_fhvhv (SQL model, first node)clean_trips takes trips and taxi_zones as inputs (Python model, multiple inputs)daily_summary takes clean_trips as input (Python model, single input)Before writing a pipeline, you MUST gather the following information from the user:
bauplan table getREPLACE (default) or APPEND?--strict flag, which fails on issues like output column mismatches during dry-run, allowing immediate error detection and correction.If the user hasn't provided this information, ask before proceeding with implementation.
When strict mode is enabled, append --strict to all bauplan run commands:
bash# Without strict mode (default) bauplan run --dry-run bauplan run # With strict mode enabled bauplan run --dry-run --strict bauplan run --strict
Benefits of strict mode:
A bauplan project is a folder containing:
my-project/
bauplan_project.yml # Required: project configuration
model.sql # Optional: a single SQL model, one per file
models.py # Optional: Python models (one file can have >1 models, or be split into multiple files)
expectations.py # Optional: data quality tests (if any)Every project is a separate folder which requires this configuration file:
yamlproject: id: <unique-uuid> # Generate a unique UUID name: <project_name> # Descriptive name for the project
> IMPORTANT: SQL models should be LIMITED to first nodes in the pipeline graph only.
This ensures consistency and allows for better control over transformations, output schema validation, and documentation.
SQL models are .sql files where:
Use SQL models only when reading from existing lakehouse tables:
sql-- trips.sql -- First node: reads from taxi_fhvhv table in the lakehouse SELECT pickup_datetime, PULocationID, trip_miles FROM taxi_fhvhv WHERE pickup_datetime >= '2022-12-01'
Output table: trips (from filename) Input table: taxi_fhvhv (from FROM clause, exists in lakehouse)
Python models use decorators to define transformations. They should be used for all pipeline nodes except first nodes reading from the lakehouse.
@bauplan.model() - Registers function as a model@bauplan.model(columns=[...]) - Specify expected output columns for validation (Optional but recommended)@bauplan.model(materialization_strategy='REPLACE') - Persist output to lakehouse@bauplan.python('3.11', pip={'pandas': '1.5.3'}) - Specify Python version and packages> IMPORTANT: whenever possible, specify the columns parameter in @bauplan.model() to define the expected output schema. This enables automatic validation of your model's output.
First, check the schema of your source tables to understand input columns. Then specify the output columns based on your transformation:
python# If input has columns: [id, name, age, city] # And transformation drops 'city' column # Then output columns should be: [id, name, age] @bauplan.model(columns=['id', 'name', 'age'])
> IMPORTANT: Every Python model should have a docstring describing the transformation and showing the output table structure as an ASCII table (if the table is too wide, show only key columns, if values are too large, truncate them in the cells).
python@bauplan.model(columns=['id', 'name', 'age']) @bauplan.python('3.11') def clean_users(data=bauplan.Model('raw_users')): """ Cleans user data by removing invalid entries and dropping the city column. | id | name | age | |-----|---------|-----| | 1 | Alice | 30 | | 2 | Bob | 25 | """ # transformation logic return data.drop_columns(['city'])
columns and filter> IMPORTANT: whenever possible, use columns and filter parameters in bauplan.Model() to restrict the data read. This enables I/O pushdown, dramatically reducing the amount of data transferred and improving performance. Do not read columns you don't need.
pythonbauplan.Model( 'table_name', columns=['col1', 'col2', 'col3'], # Only read these columns filter="date >= '2022-01-01'" # Pre-filter at storage level )
Whenever possible, specify:
columns: List only the columns your model actually needsfilter: SQL-like filter expression to restrict rows at the storage level, if appropriatepythonimport bauplan @bauplan.model( columns=['pickup_datetime', 'PULocationID', 'trip_miles'], materialization_strategy='REPLACE' ) @bauplan.python('3.11', pip={'polars': '1.15.0'}) def clean_trips( # Use columns and filter for I/O pushdown data=bauplan.Model( 'trips', columns=['pickup_datetime', 'PULocationID', 'trip_miles'], filter="trip_miles > 0" ) ): """ Filters trips to include only those with positive mileage. | pickup_datetime | PULocationID | trip_miles | |---------------------|--------------|------------| | 2022-12-01 08:00:00 | 123 | 5.2 | """ import polars as pl df = pl.from_arrow(data) df = df.filter(pl.col('trip_miles') > 0.0) return df.to_arrow()
Models can take multiple tables as input - just add more bauplan.Model() parameters:
pythondef model_with_joins( table_a=bauplan.Model('source_a', columns=['id', 'value']), table_b=bauplan.Model('source_b', columns=['id', 'name']) ): # Join, transform, return Arrow table return table_a.join(table_b, 'id', 'id')
See examples.md for complete multi-input examples with Polars.
Copy this checklist and track your progress:
Pipeline Creation Progress:
- [ ] Step 1: Get username → bauplan info
- [ ] Step 2: Checkout main → bauplan branch checkout main
- [ ] Step 3: Create dev branch → bauplan branch create <username>.<branch_name>
- [ ] Step 4: Checkout dev branch → bauplan branch checkout <username>.<branch_name>
- [ ] Step 5: Verify source tables → bauplan table get <namespace>.<table_name>, Optional for data preview: bauplan query "SELECT * FROM <namespace>.<table_name> LIMIT 3"
- [ ] Step 6: Create project folder with bauplan_project.yml
- [ ] Step 7: Write SQL model(s) / Python model(s) for transformations respecting the guidelines
- [ ] Step 8: Verify materialization decorators (see Materialization Checklist below)
- [ ] Step 9: Dry run → bauplan run --dry-run [--strict if strict mode]
- [ ] Step 10: Run pipeline → bauplan run [--strict if strict mode]> CRITICAL: Never run on main branch. Steps 2-4 ensure you're on a development branch.
After writing models, verify that each model has the correct materialization_strategy based on user requirements:
| Model Type | No Materialization (intermediate) | Materialized Output | |------------|-----------------------------------|---------------------| | Python | @bauplan.model() (no strategy) | @bauplan.model(materialization_strategy='REPLACE') or 'APPEND' | | SQL | No comment needed | Add comment: -- bauplan: materialization_strategy=REPLACE or APPEND |
Verify for each model:
materialization_strategy specifiedmaterialization_strategy='REPLACE' (default) or 'APPEND'materialization_strategy='APPEND' is setExample Python decorator for materialized output:
python@bauplan.model(materialization_strategy='REPLACE', columns=['col1', 'col2'])
Example SQL comment for materialized output:
sql-- bauplan: materialization_strategy=REPLACE SELECT * FROM source_table
See examples.md for:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | fail→pass | 11,245 | 10,523 | -6% | 1 | 1 | 0% | 1,879 | 3,903 | +108% | 0 | 0 | — |
case-21 | pass→pass | 10,941 | 14,608 | +34% | 1 | 1 | 0% | 2,006 | 4,790 | +139% | 0 | 0 | — |
case-01 | fail→pass | 22,631 | 19,799 | -13% | 1 | 1 | 0% | 3,546 | 6,120 | +73% | 0 | 0 | — |
case-02 | fail→pass | 37,111 | 28,265 | -24% | 1 | 1 | 0% | 6,476 | 9,421 | +45% | 0 | 0 | — |
case-03 | fail→pass | 22,304 | 11,115 | -50% | 1 | 1 | 0% | 4,462 | 5,138 | +15% | 0 | 0 | — |
case-04 | fail→pass | 15,805 | 22,320 | +41% | 1 | 1 | 0% | 2,672 | 5,986 | +124% | 0 | 0 | — |
case-05 | fail→pass | 11,330 | 8,803 | -22% | 1 | 1 | 0% | 2,008 | 4,522 | +125% | 0 | 0 | — |
case-06 | fail→pass | 13,545 | 8,457 | -38% | 1 | 1 | 0% | 1,361 | 3,509 | +158% | 0 | 0 | — |
case-07 | fail→pass | 21,161 | 16,720 | -21% | 1 | 1 | 0% | 2,622 | 5,002 | +91% | 0 | 0 | — |
case-08 | pass→pass | 20,096 | 12,870 | -36% | 1 | 1 | 0% | 2,591 | 4,305 | +66% | 0 | 0 | — |
case-09 | fail→pass | 22,514 | 14,050 | -38% | 1 | 1 | 0% | 2,866 | 4,527 | +58% | 0 | 0 | — |
case-10 | fail→pass | 11,064 | 10,603 | -4% | 1 | 1 | 0% | 1,722 | 3,950 | +129% | 0 | 0 | — |
case-12 | pass→pass | 8,860 | 10,392 | +17% | 1 | 1 | 0% | 1,513 | 3,843 | +154% | 0 | 0 | — |
case-13 | fail→pass | 11,022 | 14,614 | +33% | 1 | 1 | 0% | 1,837 | 4,768 | +160% | 0 | 0 | — |
case-14 | pass→pass | 14,578 | 4,021 | -72% | 1 | 1 | 0% | 1,512 | 3,629 | +140% | 0 | 0 | — |
case-15 | pass→pass | 9,630 | 4,364 | -55% | 1 | 1 | 0% | 1,469 | 3,654 | +149% | 0 | 0 | — |
case-22 | pass→pass | 14,294 | 11,414 | -20% | 1 | 1 | 0% | 1,589 | 4,009 | +152% | 0 | 0 | — |
case-16 | pass→pass | 13,628 | 7,023 | -48% | 1 | 1 | 0% | 1,492 | 3,722 | +149% | 0 | 0 | — |
case-17 | fail→pass | 13,006 | 3,314 | -75% | 1 | 1 | 0% | 1,346 | 3,454 | +157% | 0 | 0 | — |
case-18 | fail→pass | 12,803 | 7,780 | -39% | 1 | 1 | 0% | 1,344 | 3,343 | +149% | 0 | 0 | — |
case-19 | pass→pass | 11,827 | 11,701 | -1% | 1 | 1 | 0% | 2,013 | 4,133 | +105% | 0 | 0 | — |
case-20 | pass→pass | 12,923 | 4,971 | -62% | 1 | 1 | 0% | 1,345 | 3,770 | +180% | 0 | 0 | — |
case-23 | pass→pass | 11,678 | 8,927 | -24% | 1 | 1 | 0% | 1,356 | 3,676 | +171% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +57 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.