Telemetry — MANDATORY. Every api.fabric.microsoft.com call must carry
x-ms-fabric-skill: e2e-medallion-architecture (az rest: --headers "x-ms-fabric-skill=e2e-medallion-architecture"),
including every LRO poll, fabric_lro and retry. Snippets omit it — add it anyway.
CRITICAL NOTES
- To find the workspace details (including its ID) from workspace name: list all workspaces and, then, use JMESPath filtering
- To find the item details (including its ID) from workspace ID, item type, and item name: list all items of that type in that workspace and, then, use JMESPath filtering
End-to-End Medallion Architecture
Prerequisite Knowledge
Read these companion documents — they contain the foundational context this skill depends on:
- COMMON-CORE.md — Fabric REST API patterns, authentication, token audiences, item discovery
- COMMON-CLI.md —
az rest, az login, token acquisition, Fabric REST via CLI
- SPARK-AUTHORING-CORE.md — Notebook deployment, lakehouse creation, job execution
- notebook-api-operations.md — Required for notebook creation —
.ipynb structure requirements, cell format, getDefinition/updateDefinition workflow
For Spark-specific optimization details, see data-engineering-patterns.md.
Architecture Overview
Medallion Architecture is a data lakehouse pattern with three progressive layers:
| Layer |
Purpose |
Optimization Profile |
Use Case |
| Bronze (Raw) |
Land raw data exactly as received |
Write-optimized, append-only, partitioned by ingestion date |
Audit trail, reprocessing, lineage |
| Silver (Cleaned) |
Deduplicated, validated, conformed data |
Balanced read/write, partitioned by business date |
Feature engineering, operational reporting |
| Gold (Aggregated) |
Pre-calculated metrics for analytics |
Read-optimized (ZORDER, compaction), partitioned by month/year |
Power BI reports, dashboards, ad-hoc analytics via SQL endpoint |
- Bronze: Schema-on-read — flexible schema, Delta time travel supports audit and rollback
- Silver: Schema enforcement — reject non-conforming writes; handle schema evolution with
mergeSchema when sources change
- Gold: Strict schema governance — curated, business-approved datasets only
Must/Prefer/Avoid
MUST DO
- Choose lakehouse architecture based on schema-enabled availability (see infrastructure-orchestration.md):
- Preferred: Schema-enabled lakehouse → create ONE workspace + ONE lakehouse with
bronze, silver, gold schemas
- Legacy: Non-schema-enabled → create separate workspaces per layer (Bronze, Silver, Gold) for governance and access control
- Use Livy API for schema and table creation — to create schemas and tables in a schema-enabled lakehouse, submit Spark SQL statements via Livy sessions (
POST /livyApi/versions/2023-12-01/sessions → POST .../statements). This is the only programmatic REST path for DDL operations (CREATE SCHEMA, CREATE TABLE) in Fabric lakehouses.
- Add metadata columns in Bronze: ingestion timestamp, source file, batch ID
- Apply data quality rules in the Bronze-to-Silver transformation (deduplication, null handling, range validation)
- Use Delta Lake format for all medallion layer tables
- Use partition-aware overwrite in Silver/Gold writes to avoid reprocessing unchanged data
- Include validation steps after each layer (row counts, schema checks, anomaly detection)
- Follow the
.ipynb validation + Fabric nuances in notebook-api-operations.md when creating notebooks via REST API — every code cell must include "outputs": [] and "execution_count": null
- Complete the full end-to-end flow — do not stop after creating notebooks; always bind lakehouses, execute notebooks sequentially (Bronze → Silver → Gold), verify results, and connect Power BI to the Gold layer unless the user explicitly requests a partial setup
- In every MLV-versus-notebook recommendation, state that MLVs require a schema-enabled lakehouse. Hand off MLV definition and incremental-refresh review to
spark-cli authoring mode; hand off scheduling, refresh, monitoring, and failure diagnosis to spark-cli mlv mode.
- For recurring MLV refresh, give the exact interactive path Lakehouse → Materialized lake views → Manage → Schedules. For automation, use
POST /workspaces/{workspaceId}/lakehouses/{lakehouseId}/jobs/refreshMaterializedLakeViews/schedules; never invent /mlvRefreshSchedules or route recurring refresh through notebook scheduling.
PREFER
- Incremental processing (watermark pattern) over full refresh
- Separate notebooks per layer for independent testing and debugging
- ZORDER on frequently filtered columns in Gold tables
- Running OPTIMIZE after writes in Silver and Gold layers
- Environment-specific Spark configs (write-heavy for Bronze, balanced for Silver, read-heavy for Gold)
- OneLake shortcuts to expose Gold data to consumer workspaces without duplication
- Clear layer ownership: engineers own Bronze/Silver, analysts own Gold
- Fabric Variable Libraries to centralize paths and configuration across layers
- Multi-workspace deployment patterns for medium/high governance requirements (Bronze/Silver/Gold in separate workspaces)
- Use Materialized Lake Views (MLVs) for Silver/Gold tables when the transformation is expressible in Spark SQL and benefits from declarative refresh semantics. See spark-cli — Materialized Lake View patterns and MLV incremental refresh patterns.
- Treat "materialized view", "spark materialized view", and "MLV" as the same Fabric feature.
AVOID
- Storing all layers in a single lakehouse WITHOUT schemas — non-schema lakehouses require notebook init cells or Environment configuration to enable OneLake Spark Catalog for RLS/CLS and MLVs. Use separate lakehouses for isolation if schemas aren't available.
- Creating 3 separate lakehouses when schema-enabled lakehouse is available — use schemas within one lakehouse instead (cleaner, no boilerplate init cells, more efficient for MLV cross-schema transformations)
- Skipping the Silver layer and going directly from Bronze to Gold
- Hardcoded workspace IDs, lakehouse IDs, or FQDNs — discover via REST API
- SELECT * without LIMIT on Bronze tables (they grow unboundedly)
- Running VACUUM without checking downstream dependencies
- Chaining OneLake shortcuts between medallion layers (Bronze→Silver→Gold) — each layer must be physically materialized for lineage and governance
- Copying complete implementation code into skills — guide the LLM to generate instead
- Reading from external HTTP/HTTPS URLs directly in Spark — Fabric Spark cannot access arbitrary external URLs; land data in lakehouse
Files/ first (via curl, OneLake API, or Fabric pipeline Copy activity), then read from the lakehouse path
- Creating notebooks via REST API without validating
.ipynb structure — missing execution_count: null or outputs: [] on code cells causes silent failures or "Job instance failed without detail error"
Workspace Setup Guidance
When setting up a medallion workspace, choose your architecture pattern first (see infrastructure-orchestration.md for detailed guidance):
Option A: Schema-Enabled Lakehouse (Preferred)
- Create single workspace:
{project}-{env}
- Create one lakehouse with schemas:
{project}_lakehouse
- Create schemas within the lakehouse:
bronze schema for raw ingestion
silver schema for cleaned/validated data
gold schema for aggregated analytics
- Choose transformation approach:
- Option 4a: Use notebooks for each layer (PySpark or Spark SQL transformations)
- Option 4b: Use Materialized Lake Views (Spark SQL) for declarative transformations with incremental refresh (when query is IR-eligible) — see materialized-lake-view-patterns.md and mlv-incremental-refresh-patterns.md
- Note: PySpark MLVs exist but use full refresh only (no incremental) — use when you need UDFs/complex Python logic
- MLV benefit: OneLake Spark Catalog is automatically enabled for schema-enabled lakehouses — MLVs work out-of-box with no notebook init cells or Environment configuration required
- RBAC (optional): Use row-level security and column masking within schemas for fine-grained access control (also requires OneLake Spark Catalog)
Option B: Separate Lakehouses (Legacy)
- Create three workspaces:
{project}-bronze-{env}
{project}-silver-{env}
{project}-gold-{env}
- Create one lakehouse per workspace:
- Bronze workspace →
{project}_bronze lakehouse
- Silver workspace →
{project}_silver lakehouse
- Gold workspace →
{project}_gold lakehouse
- Assign RBAC per layer workspace:
- Bronze: ingestion/engineering write permissions
- Silver: engineering/data quality permissions
- Gold: analytics/BI consumer access with stricter curation controls
- Enable OneLake Spark Catalog for non-schema lakehouses (required for RLS/CLS and catalog-backed access patterns):
- Primary: Set
spark.sql.fabric.catalog.enable-schemaless-lakehouses=true in an Environment and attach it to notebooks.
- Alternative: Omit default lakehouse binding from notebooks. Use four-part fully-qualified references (
workspace.lakehouse.schema.table). OneLake Spark Catalog auto-enables when no default lakehouse is set.
- Alternative (internal/unsupported): Add this as the first cell in every notebook:
%%pyspark
!echo "spark.sql.fabric.catalog.enable-schemaless-lakehouses=true" >> /home/trusted-service-user/.trident-context
- ⚠️ Note: This workaround uses an internal runtime configuration path that may change in future Fabric releases. Prefer schema-enabled lakehouses for stable, documented OneLake Spark Catalog support.
- With this configuration, non-schema lakehouses support:
- ✅ Row-level security (RLS) and column-level security (CLS)
- Note: MLVs require schema-enabled lakehouses (Option A). For non-schema lakehouses, use notebooks with Delta tables.
Common Steps (Both Options)
After completing Option A or Option B above, perform these steps:
- Create notebooks for each layer (one per transformation stage) — follow
.ipynb validation + Fabric nuances
- Bind each notebook to its lakehouse — set
metadata.dependencies.lakehouse with the correct lakehouse ID (see notebook-api-operations.md § Default Lakehouse Binding):
- Option A: All notebooks → same lakehouse, use schema prefixes (
bronze.table, silver.table)
- Option B:
- Bronze notebook → Bronze workspace/lakehouse
- Silver notebook → Silver workspace/lakehouse (reads Bronze via cross-workspace OneLake access / fully qualified references)
- Gold notebook → Gold workspace/lakehouse (reads Silver via cross-workspace access)
- Confirm notebook deployment — check that
updateDefinition returned Succeeded; this is sufficient confirmation that content and lakehouse binding persisted. Do NOT call getDefinition to re-verify — it is an async LRO and adds unnecessary latency.
- Execute notebooks sequentially — Bronze first, then Silver, then Gold — using
POST .../jobs/instances?jobType=RunNotebook with the correct defaultLakehouse in execution config (both id and name required)
- Connect Power BI to Gold layer — discover the Gold lakehouse SQL endpoint, create a Direct Lake semantic model, create a report with visuals on the Gold summary table (see Gold Layer → Power BI Consumption)
- Create pipeline to orchestrate the Bronze → Silver → Gold flow for recurring execution
Explicit Override: Single Workspace
If the user explicitly asks for a single workspace deployment (for example, POC/small team/monolithic pattern), keep the current approach:
- One workspace with separate Bronze/Silver/Gold lakehouses
- Preserve layer separation logically even when workspace is shared
- Call out governance trade-offs versus multi-workspace design
Parameterize by environment: workspace name suffix (-dev, -prod), data volume (sample vs full), capacity SKU, and Bronze retention period.
Bronze Layer — Ingestion Patterns
When a user requests data ingestion into the Bronze layer, guide LLM to:
- Land data in lakehouse first: External data must be staged into the lakehouse
Files/ folder before Spark can read it — use one of:
- Fabric Pipeline Copy activity (preferred for recurring loads) — connects to external sources (HTTP, FTP, databases, cloud storage) and writes to OneLake
- OneLake API /
curl — upload files via REST API using storage.azure.com token (see COMMON-CLI.md § OneLake Data Access)
- OneLake Shortcut — for data already in Azure ADLS Gen2, S3, or another OneLake location
notebookutils.fs — copy from mounted storage paths within a notebook
- ⚠️ Fabric Spark cannot read from arbitrary HTTP/HTTPS URLs —
spark.read.format("csv").load("https://...") will fail
- Read from lakehouse path: Once data is in
Files/, read using lakehouse-relative paths (e.g., spark.read.format("csv").load("Files/landing/daily/"))
- Add metadata and write: Tracking columns (ingestion timestamp, source file, batch ID), Delta table with descriptive name, partition by ingestion date, append mode
- Validate: Log row counts, validate schema structure, flag anomalies vs historical patterns
Silver Layer — Transformation Patterns
When a user requests Bronze-to-Silver transformation, guide LLM to:
- Quality rules: Deduplicate on natural/composite key, filter invalid ranges, handle nulls (drop required, fill optional), validate logical constraints
- Schema conformance: snake_case column names, standardized data types, derived columns (durations, percentages, categories)
- Schema evolution: Use
mergeSchema option when source schemas change; coordinate downstream updates to Gold tables and Power BI datasets
- Write strategy: Partition by business date, partition-aware overwrite, run OPTIMIZE after write, log before/after metrics
Gold Layer — Aggregation Patterns
When a user requests Gold analytics tables, guide LLM to generate:
- Common aggregates: Daily/weekly/monthly summaries, dimensional analysis (by location, category, type), trend breakdowns over time, demand patterns (hour-of-day, day-of-week)
- Spark session config — set these properties in the Gold notebook before any write operations:
spark.conf.set("spark.sql.parquet.vorder.default", "true")
spark.conf.set("spark.databricks.delta.optimizeWrite.enabled", "true")
spark.conf.set("spark.databricks.delta.optimizeWrite.binSize", "1g")
- V-Order (
vorder.default) — applies Fabric's columnar sort optimization to all Parquet files, dramatically improving Direct Lake and SQL endpoint read performance
- Optimize Write (
optimizeWrite.enabled) — coalesces small partitions into optimally-sized files (target ~1 GB per binSize), reducing file count and improving scan efficiency
- Optimization: ZORDER on filter columns, run OPTIMIZE after writes, pre-aggregate metrics to avoid runtime computation
End-to-End Execution Flow
When setting up medallion architecture end-to-end, the LLM must not stop after creating notebooks and deploying code. The complete lifecycle is:
Create Resources → Deploy Content → Bind Lakehouses → Execute → Verify Results
Step-by-Step
- Create layer workspaces and lakehouses (default) — one workspace and one lakehouse per layer (Bronze, Silver, Gold); capture workspace IDs and lakehouse IDs
- Create notebooks — one per layer, with valid
.ipynb structure (see notebook-api-operations.md)
- Bind lakehouse to each notebook — include
metadata.dependencies.lakehouse in the .ipynb payload with:
default_lakehouse: the target lakehouse GUID
default_lakehouse_name: the lakehouse display name
default_lakehouse_workspace_id: the workspace GUID
- Deploy notebook content —
updateDefinition with the Base64-encoded .ipynb payload (content + lakehouse binding together)
- Confirm deployment — check that each
updateDefinition LRO returned Succeeded; that is sufficient. Do NOT call getDefinition to re-verify — it is an async LRO and adds significant latency per notebook.
- Execute notebooks sequentially — use
POST .../jobs/instances?jobType=RunNotebook:
- Pass
defaultLakehouse with both id and name in executionData.configuration
- Run Bronze first → poll until
Completed → run Silver → poll → run Gold → poll
- Check for recent jobs before submitting (prevent duplicates — see SPARK-AUTHORING-CORE.md)
- Verify results — after each notebook completes, confirm expected tables exist and row counts are reasonable
- Connect Power BI to Gold — create semantic model + report on Gold summary tables (see Gold Layer → Power BI Consumption)
Common Failure: Stopping After Notebook Creation
If the flow stops after deploying notebook code without binding or executing:
- Notebooks will have no lakehouse context →
spark.sql() and relative paths (Tables/, Files/) fail at runtime
- The user sees no output or results — the architecture is set up but never tested
- Always complete through step 7 unless the user explicitly asks to stop at a specific step
Gold Layer → Power BI Consumption
After Gold tables are populated, connect Power BI to surface the analytics.
Build a semantic model on top of the Gold lakehouse, using DirectLake.
Step-by-Step
- Discover the Gold lakehouse SQL endpoint — call
GET /v1/workspaces/{workspaceId}/lakehouses/{goldLakehouseId} and extract properties.sqlEndpointProperties.connectionString and provisioningStatus; wait until status is Success
- Verify Gold tables via SQL — connect to the SQL endpoint using
sqlcmd (see COMMON-CLI.md § SQL / TDS Data-Plane Access) and confirm the target table exists:SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'nyc_taxi_daily_summary'
- Create a semantic model — use the semantic-model-authoring skill for semantic model creation and TMDL deployment. Create via
POST /v1/workspaces/{workspaceId}/items with type: "SemanticModel" then deploy definition via updateDefinition using TMDL format (see ITEM-DEFINITIONS-CORE.md § SemanticModel):
- The model must reference the Gold lakehouse SQL endpoint as its data source
- Define a table mapping to the Gold summary table (e.g.,
nyc_taxi_daily_summary)
- Use Direct Lake mode — this connects directly to Delta tables in OneLake without data import
- Include measures for key aggregations you find interesting (e.g.,
Total Trips, Avg Fare, Total Revenue, Month over Month Growth)
- Create a Power BI report —
POST /v1/workspaces/{workspaceId}/items with type: "Report" then deploy definition via updateDefinition using PBIR format (see ITEM-DEFINITIONS-CORE.md § Report):
- Reference the semantic model created in step 3 via
definition.pbir
- Define at least one page with visuals on the Gold summary table
- Suggested visuals: line chart (daily trend), card (KPI totals), bar chart (by category), table (detail view)
- Verify end-to-end — use the
semantic-model-authoring skill for metadata discovery running DAX queries against the semantic model and confirm data flows from Gold tables through to the report
Principles
- Discover SQL endpoint dynamically — the connection string is in
properties.sqlEndpointProperties.connectionString on the lakehouse response; never hardcode it
- Wait for SQL endpoint provisioning — status must be
Success before connecting; newly created lakehouses may take minutes to provision
- Prefer Direct Lake mode — avoids data duplication; semantic model reads directly from OneLake Delta tables
- Match table/column names exactly — the semantic model table definition must use the exact Delta table and column names from the Gold lakehouse
- For semantic model authoring (TMDL, refresh, permissions), cross-reference the semantic-model-authoring skill
- For DAX query validation, cross-reference the semantic-model-authoring skill for metadata discovery and DAX queries for validation.
Pipeline Orchestration
When a user requests a pipeline for the medallion flow, guide LLM to design with:
- Structure: Sequential activities (Bronze → Silver → Gold), each waiting for previous success; independent Gold aggregations can run in parallel; include validation and notification activities
- Parameterization: Pipeline-level processing date (defaults to yesterday), passed to all notebooks; dynamic date expressions
- Scheduling: Daily aligned with source refresh, watermark-based incremental processing, periodic full refresh for corrections
- Error handling: Retry with backoff for transient failures, alerting for persistent failures, graceful degradation (downstream uses previous data if upstream fails)
Environment Optimization
For detailed Spark configurations and optimization strategies, see data-engineering-patterns.md.
| Layer |
Profile |
Key Settings |
| Bronze |
Write-heavy |
Disable V-Order, enable autoCompact, large file targets, partition by ingestion_date |
| Silver |
Balanced |
Enable V-Order, adaptive query execution, partition by business date, ZORDER on filtered columns |
| Gold |
Read-heavy |
V-Order (spark.sql.parquet.vorder.default=true), Optimize Write (optimizeWrite.enabled=true, binSize=1g), vectorized readers, adaptive execution, ZORDER on all filter columns, pre-aggregate metrics |
Examples
Example 1: Set Up Medallion Workspaces (Default)
Prompt: "Set up medallion architecture with separate Bronze, Silver, and Gold workspaces for sales analytics"
What the LLM should generate: REST API calls to:
- Create workspaces:
sales-bronze-dev, sales-silver-dev, sales-gold-dev
- Create one lakehouse in each workspace:
sales_bronze, sales_silver, sales_gold
- Assign RBAC roles per workspace/layer
# Workspace creation (see COMMON-CLI.md for full patterns)
cat > /tmp/body.json << 'EOF'
{"displayName": "sales-analytics-dev"}
EOF
workspace_id=$(az rest --method post --resource "https://api.fabric.microsoft.com" \
--url "https://api.fabric.microsoft.com/v1/workspaces" \
--body @/tmp/body.json --query "id" --output tsv)
# Create Bronze lakehouse
cat > /tmp/body.json << 'EOF'
{"displayName": "sales_bronze", "type": "Lakehouse"}
EOF
az rest --method post --resource "https://api.fabric.microsoft.com" \
--url "https://api.fabric.microsoft.com/v1/workspaces/$workspace_id/items" \
--body @/tmp/body.json
Example 2: Design Bronze Ingestion
Prompt: "Ingest daily CSV files into bronze lakehouse with metadata columns"
What the LLM should generate: PySpark notebook that:
- Reads source files with schema inference or explicit schema
- Adds
ingestion_timestamp, source_file, batch_id columns
- Writes to Delta table partitioned by ingestion date
- Logs row count and validation metrics
# Bronze ingestion pattern (guide LLM to generate full implementation)
from pyspark.sql.functions import current_timestamp, input_file_name, lit
import uuid
batch_id = str(uuid.uuid4())
df = (spark.read.format("csv").option("header", True).load("/Files/landing/daily/")
.withColumn("ingestion_timestamp", current_timestamp())
.withColumn("source_file", input_file_name())
.withColumn("batch_id", lit(batch_id)))
df.write.mode("append").partitionBy("ingestion_date").format("delta").saveAsTable("bronze.events_raw")
Example 3: Bronze-to-Silver Transformation
Prompt: "Clean bronze data: remove duplicates, filter invalid records, add derived columns, write to silver"
What the LLM should generate: PySpark notebook applying quality rules, schema conformance, and partitioned write with optimization.
Example 4: End-to-End Pipeline
Prompt: "Create a pipeline that runs bronze ingestion, then silver transformation, then gold aggregation daily at 2 AM"
What the LLM should generate: Pipeline JSON definition with sequential notebook activities, date parameter, retry logic, and schedule trigger.
1---2name: e2e-medallion-architecture3description: Plan and implement end-to-end Microsoft Fabric data platforms and Medallion Architecture (Bronze/Silver/Gold) lakehouse patterns using PySpark, Delta Lake, Lakehouse/Warehouse items, Fabric Pipelines, and semantic-model handoff. Use for multi-layer workspaces, ingestion-to-analytics pipelines, Bronze-to-Silver-to-Gold notebook orchestration, layer-specific Fabric Spark tuning, and Materialized Lake View versus notebook decisions for Silver and Gold. For natural-language questions over existing Power BI report data, use `fabriciq`. Triggers: "medallion architecture", "bronze silver gold", "lakehouse layers", "e2e data pipeline", "end-to-end Fabric data platform", "medallion Spark tuning", "Silver Gold MLV or notebooks".4---56> **Telemetry — MANDATORY.** Every `api.fabric.microsoft.com` call must carry7> `x-ms-fabric-skill: e2e-medallion-architecture` (`az rest`: `--headers "x-ms-fabric-skill=e2e-medallion-architecture"`),8> including every LRO poll, `fabric_lro` and retry. Snippets omit it — add it anyway.910> **CRITICAL NOTES**11> 1. To find the workspace details (including its ID) from workspace name: list all workspaces and, then, use JMESPath filtering12> 2. To find the item details (including its ID) from workspace ID, item type, and item name: list all items of that type in that workspace and, then, use JMESPath filtering1314# End-to-End Medallion Architecture1516## Prerequisite Knowledge1718Read these companion documents — they contain the foundational context this skill depends on:1920- [COMMON-CORE.md](../../common/COMMON-CORE.md) — Fabric REST API patterns, authentication, token audiences, item discovery21- [COMMON-CLI.md](../../common/COMMON-CLI.md) — `az rest`, `az login`, token acquisition, Fabric REST via CLI22- [SPARK-AUTHORING-CORE.md](../../common/SPARK-AUTHORING-CORE.md) — Notebook deployment, lakehouse creation, job execution23- [notebook-api-operations.md](../spark-cli/references/authoring/resources/notebook-api-operations.md) — **Required for notebook creation** — `.ipynb` structure requirements, cell format, `getDefinition`/`updateDefinition` workflow2425For Spark-specific optimization details, see [data-engineering-patterns.md](../spark-cli/references/authoring/resources/data-engineering-patterns.md).2627---2829## Architecture Overview3031**Medallion Architecture** is a data lakehouse pattern with three progressive layers:3233| Layer | Purpose | Optimization Profile | Use Case |34|-------|---------|---------------------|----------|35| **Bronze** (Raw) | Land raw data exactly as received | Write-optimized, append-only, partitioned by ingestion date | Audit trail, reprocessing, lineage |36| **Silver** (Cleaned) | Deduplicated, validated, conformed data | Balanced read/write, partitioned by business date | Feature engineering, operational reporting |37| **Gold** (Aggregated) | Pre-calculated metrics for analytics | Read-optimized (ZORDER, compaction), partitioned by month/year | Power BI reports, dashboards, ad-hoc analytics via SQL endpoint |3839- **Bronze**: Schema-on-read — flexible schema, Delta time travel supports audit and rollback40- **Silver**: Schema enforcement — reject non-conforming writes; handle schema evolution with `mergeSchema` when sources change41- **Gold**: Strict schema governance — curated, business-approved datasets only4243---4445## Must/Prefer/Avoid4647### MUST DO48- **Choose lakehouse architecture** based on schema-enabled availability (see [infrastructure-orchestration.md](../spark-cli/references/authoring/resources/infrastructure-orchestration.md)):49 - **Preferred:** Schema-enabled lakehouse → create ONE workspace + ONE lakehouse with `bronze`, `silver`, `gold` schemas50 - **Legacy:** Non-schema-enabled → create separate workspaces per layer (Bronze, Silver, Gold) for governance and access control51- **Use Livy API for schema and table creation** — to create schemas and tables in a schema-enabled lakehouse, submit Spark SQL statements via Livy sessions (`POST /livyApi/versions/2023-12-01/sessions` → `POST .../statements`). This is the only programmatic REST path for DDL operations (CREATE SCHEMA, CREATE TABLE) in Fabric lakehouses.52- Add **metadata columns** in Bronze: ingestion timestamp, source file, batch ID53- Apply **data quality rules** in the Bronze-to-Silver transformation (deduplication, null handling, range validation)54- Use **Delta Lake format** for all medallion layer tables55- Use **partition-aware overwrite** in Silver/Gold writes to avoid reprocessing unchanged data56- Include **validation steps** after each layer (row counts, schema checks, anomaly detection)57- Follow the **`.ipynb` validation + Fabric nuances** in [notebook-api-operations.md](../spark-cli/references/authoring/resources/notebook-api-operations.md#ipynb-validation--fabric-nuances) when creating notebooks via REST API — every code cell must include `"outputs": []` and `"execution_count": null`58- **Complete the full end-to-end flow** — do not stop after creating notebooks; always bind lakehouses, execute notebooks sequentially (Bronze → Silver → Gold), verify results, and connect Power BI to the Gold layer unless the user explicitly requests a partial setup59- In every MLV-versus-notebook recommendation, state that MLVs require a **schema-enabled lakehouse**. Hand off MLV definition and incremental-refresh review to `spark-cli` **authoring** mode; hand off scheduling, refresh, monitoring, and failure diagnosis to `spark-cli` **mlv** mode.60- For recurring MLV refresh, give the exact interactive path **Lakehouse → Materialized lake views → Manage → Schedules**. For automation, use `POST /workspaces/{workspaceId}/lakehouses/{lakehouseId}/jobs/refreshMaterializedLakeViews/schedules`; never invent `/mlvRefreshSchedules` or route recurring refresh through notebook scheduling.6162### PREFER63- Incremental processing (watermark pattern) over full refresh64- Separate notebooks per layer for independent testing and debugging65- ZORDER on frequently filtered columns in Gold tables66- Running OPTIMIZE after writes in Silver and Gold layers67- Environment-specific Spark configs (write-heavy for Bronze, balanced for Silver, read-heavy for Gold)68- OneLake shortcuts to expose Gold data to consumer workspaces without duplication69- Clear layer ownership: engineers own Bronze/Silver, analysts own Gold70- Fabric Variable Libraries to centralize paths and configuration across layers71- Multi-workspace deployment patterns for medium/high governance requirements (Bronze/Silver/Gold in separate workspaces)72- Use Materialized Lake Views (MLVs) for Silver/Gold tables when the transformation is expressible in Spark SQL and benefits from declarative refresh semantics. See [spark-cli — Materialized Lake View patterns](../spark-cli/references/authoring/resources/materialized-lake-view-patterns.md) and [MLV incremental refresh patterns](../spark-cli/references/authoring/resources/mlv-incremental-refresh-patterns.md).73- Treat "materialized view", "spark materialized view", and "MLV" as the same Fabric feature.7475### AVOID76- **Storing all layers in a single lakehouse WITHOUT schemas** — non-schema lakehouses require notebook init cells or Environment configuration to enable OneLake Spark Catalog for RLS/CLS and MLVs. Use separate lakehouses for isolation if schemas aren't available.77- **Creating 3 separate lakehouses when schema-enabled lakehouse is available** — use schemas within one lakehouse instead (cleaner, no boilerplate init cells, more efficient for MLV cross-schema transformations)78- Skipping the Silver layer and going directly from Bronze to Gold79- Hardcoded workspace IDs, lakehouse IDs, or FQDNs — discover via REST API80- SELECT * without LIMIT on Bronze tables (they grow unboundedly)81- Running VACUUM without checking downstream dependencies82- Chaining OneLake shortcuts between medallion layers (Bronze→Silver→Gold) — each layer must be physically materialized for lineage and governance83- Copying complete implementation code into skills — guide the LLM to generate instead84- Reading from **external HTTP/HTTPS URLs** directly in Spark — Fabric Spark cannot access arbitrary external URLs; land data in lakehouse `Files/` first (via `curl`, OneLake API, or Fabric pipeline Copy activity), then read from the lakehouse path85- Creating notebooks via REST API **without validating `.ipynb` structure** — missing `execution_count: null` or `outputs: []` on code cells causes silent failures or "Job instance failed without detail error"8687---8889## Workspace Setup Guidance9091When setting up a medallion workspace, choose your architecture pattern first (see [infrastructure-orchestration.md](../spark-cli/references/authoring/resources/infrastructure-orchestration.md) for detailed guidance):9293### Option A: Schema-Enabled Lakehouse (Preferred)94951. **Create single workspace**: `{project}-{env}`962. **Create one lakehouse** with schemas: `{project}_lakehouse`973. **Create schemas within the lakehouse**:98 - `bronze` schema for raw ingestion99 - `silver` schema for cleaned/validated data100 - `gold` schema for aggregated analytics1014. **Choose transformation approach**:102 - **Option 4a:** Use notebooks for each layer (PySpark or Spark SQL transformations)103 - **Option 4b:** Use Materialized Lake Views (Spark SQL) for declarative transformations with incremental refresh (when query is IR-eligible) — see [materialized-lake-view-patterns.md](../spark-cli/references/authoring/resources/materialized-lake-view-patterns.md) and [mlv-incremental-refresh-patterns.md](../spark-cli/references/authoring/resources/mlv-incremental-refresh-patterns.md)104 - **Note:** PySpark MLVs exist but use full refresh only (no incremental) — use when you need UDFs/complex Python logic105 - **MLV benefit:** OneLake Spark Catalog is **automatically enabled** for schema-enabled lakehouses — MLVs work out-of-box with no notebook init cells or Environment configuration required1065. **RBAC** (optional): Use row-level security and column masking within schemas for fine-grained access control (also requires OneLake Spark Catalog)107108### Option B: Separate Lakehouses (Legacy)1091101. **Create three workspaces**:111 - `{project}-bronze-{env}`112 - `{project}-silver-{env}`113 - `{project}-gold-{env}`1142. **Create one lakehouse per workspace**:115 - Bronze workspace → `{project}_bronze` lakehouse116 - Silver workspace → `{project}_silver` lakehouse117 - Gold workspace → `{project}_gold` lakehouse1183. **Assign RBAC per layer workspace**:119 - Bronze: ingestion/engineering write permissions120 - Silver: engineering/data quality permissions121 - Gold: analytics/BI consumer access with stricter curation controls1224. **Enable OneLake Spark Catalog for non-schema lakehouses** (required for RLS/CLS and catalog-backed access patterns):123 - **Primary:** Set `spark.sql.fabric.catalog.enable-schemaless-lakehouses=true` in an Environment and attach it to notebooks.124 - **Alternative:** Omit default lakehouse binding from notebooks. Use four-part fully-qualified references (`workspace.lakehouse.schema.table`). OneLake Spark Catalog auto-enables when no default lakehouse is set.125 - **Alternative (internal/unsupported):** Add this as the **first cell** in every notebook:126 ```python127 %%pyspark128 !echo "spark.sql.fabric.catalog.enable-schemaless-lakehouses=true" >> /home/trusted-service-user/.trident-context129 ```130 - **⚠️ Note:** This workaround uses an internal runtime configuration path that may change in future Fabric releases. **Prefer schema-enabled lakehouses** for stable, documented OneLake Spark Catalog support.131 - With this configuration, non-schema lakehouses support:132 - ✅ Row-level security (RLS) and column-level security (CLS)133 - **Note:** MLVs require schema-enabled lakehouses (Option A). For non-schema lakehouses, use notebooks with Delta tables.134135### Common Steps (Both Options)136137After completing Option A or Option B above, perform these steps:1381391. **Create notebooks** for each layer (one per transformation stage) — follow `.ipynb` validation + Fabric nuances1402. **Bind each notebook to its lakehouse** — set `metadata.dependencies.lakehouse` with the correct lakehouse ID (see [notebook-api-operations.md § Default Lakehouse Binding](../spark-cli/references/authoring/resources/notebook-api-operations.md#default-lakehouse-binding)):141 - Option A: All notebooks → same lakehouse, use schema prefixes (`bronze.table`, `silver.table`)142 - Option B:143 - Bronze notebook → Bronze workspace/lakehouse144 - Silver notebook → Silver workspace/lakehouse (reads Bronze via cross-workspace OneLake access / fully qualified references)145 - Gold notebook → Gold workspace/lakehouse (reads Silver via cross-workspace access)1463. **Confirm notebook deployment** — check that `updateDefinition` returned `Succeeded`; this is sufficient confirmation that content and lakehouse binding persisted. Do NOT call `getDefinition` to re-verify — it is an async LRO and adds unnecessary latency.1474. **Execute notebooks** sequentially — Bronze first, then Silver, then Gold — using `POST .../jobs/instances?jobType=RunNotebook` with the correct `defaultLakehouse` in execution config (both `id` and `name` required)1485. **Connect Power BI to Gold layer** — discover the Gold lakehouse SQL endpoint, create a Direct Lake semantic model, create a report with visuals on the Gold summary table (see [Gold Layer → Power BI Consumption](#gold-layer--power-bi-consumption))1496. **Create pipeline** to orchestrate the Bronze → Silver → Gold flow for recurring execution150151### Explicit Override: Single Workspace152153If the user explicitly asks for a single workspace deployment (for example, POC/small team/monolithic pattern), keep the current approach:154155- One workspace with separate Bronze/Silver/Gold lakehouses156- Preserve layer separation logically even when workspace is shared157- Call out governance trade-offs versus multi-workspace design158159Parameterize by environment: workspace name suffix (`-dev`, `-prod`), data volume (sample vs full), capacity SKU, and Bronze retention period.160161---162163## Bronze Layer — Ingestion Patterns164165When a user requests data ingestion into the Bronze layer, guide LLM to:1661671. **Land data in lakehouse first**: External data must be staged into the lakehouse `Files/` folder before Spark can read it — use one of:168 - **Fabric Pipeline Copy activity** (preferred for recurring loads) — connects to external sources (HTTP, FTP, databases, cloud storage) and writes to OneLake169 - **OneLake API / `curl`** — upload files via REST API using `storage.azure.com` token (see COMMON-CLI.md § OneLake Data Access)170 - **OneLake Shortcut** — for data already in Azure ADLS Gen2, S3, or another OneLake location171 - **`notebookutils.fs`** — copy from mounted storage paths within a notebook172 - ⚠️ **Fabric Spark cannot read from arbitrary HTTP/HTTPS URLs** — `spark.read.format("csv").load("https://...")` will fail1732. **Read from lakehouse path**: Once data is in `Files/`, read using lakehouse-relative paths (e.g., `spark.read.format("csv").load("Files/landing/daily/")`)1743. **Add metadata and write**: Tracking columns (ingestion timestamp, source file, batch ID), Delta table with descriptive name, partition by ingestion date, append mode1754. **Validate**: Log row counts, validate schema structure, flag anomalies vs historical patterns176177---178179## Silver Layer — Transformation Patterns180181When a user requests Bronze-to-Silver transformation, guide LLM to:182183- **Quality rules**: Deduplicate on natural/composite key, filter invalid ranges, handle nulls (drop required, fill optional), validate logical constraints184- **Schema conformance**: snake_case column names, standardized data types, derived columns (durations, percentages, categories)185- **Schema evolution**: Use `mergeSchema` option when source schemas change; coordinate downstream updates to Gold tables and Power BI datasets186- **Write strategy**: Partition by business date, partition-aware overwrite, run OPTIMIZE after write, log before/after metrics187188---189190## Gold Layer — Aggregation Patterns191192When a user requests Gold analytics tables, guide LLM to generate:193194- **Common aggregates**: Daily/weekly/monthly summaries, dimensional analysis (by location, category, type), trend breakdowns over time, demand patterns (hour-of-day, day-of-week)195- **Spark session config** — set these properties in the Gold notebook **before** any write operations:196 ```python197 spark.conf.set("spark.sql.parquet.vorder.default", "true")198 spark.conf.set("spark.databricks.delta.optimizeWrite.enabled", "true")199 spark.conf.set("spark.databricks.delta.optimizeWrite.binSize", "1g")200 ```201 - **V-Order** (`vorder.default`) — applies Fabric's columnar sort optimization to all Parquet files, dramatically improving Direct Lake and SQL endpoint read performance202 - **Optimize Write** (`optimizeWrite.enabled`) — coalesces small partitions into optimally-sized files (target ~1 GB per `binSize`), reducing file count and improving scan efficiency203- **Optimization**: ZORDER on filter columns, run OPTIMIZE after writes, pre-aggregate metrics to avoid runtime computation204205---206207## End-to-End Execution Flow208209When setting up medallion architecture end-to-end, the LLM **must not stop** after creating notebooks and deploying code. The complete lifecycle is:210211```212Create Resources → Deploy Content → Bind Lakehouses → Execute → Verify Results213```214215### Step-by-Step2162171. **Create layer workspaces and lakehouses (default)** — one workspace and one lakehouse per layer (Bronze, Silver, Gold); capture workspace IDs and lakehouse IDs2182. **Create notebooks** — one per layer, with valid `.ipynb` structure (see [notebook-api-operations.md](../spark-cli/references/authoring/resources/notebook-api-operations.md))2193. **Bind lakehouse to each notebook** — include `metadata.dependencies.lakehouse` in the `.ipynb` payload with:220 - `default_lakehouse`: the target lakehouse GUID221 - `default_lakehouse_name`: the lakehouse display name222 - `default_lakehouse_workspace_id`: the workspace GUID2234. **Deploy notebook content** — `updateDefinition` with the Base64-encoded `.ipynb` payload (content + lakehouse binding together)2245. **Confirm deployment** — check that each `updateDefinition` LRO returned `Succeeded`; that is sufficient. Do NOT call `getDefinition` to re-verify — it is an async LRO and adds significant latency per notebook.2256. **Execute notebooks sequentially** — use `POST .../jobs/instances?jobType=RunNotebook`:226 - Pass `defaultLakehouse` with both `id` and `name` in `executionData.configuration`227 - Run Bronze first → poll until `Completed` → run Silver → poll → run Gold → poll228 - Check for recent jobs before submitting (prevent duplicates — see SPARK-AUTHORING-CORE.md)2297. **Verify results** — after each notebook completes, confirm expected tables exist and row counts are reasonable2308. **Connect Power BI to Gold** — create semantic model + report on Gold summary tables (see [Gold Layer → Power BI Consumption](#gold-layer--power-bi-consumption))231232### Common Failure: Stopping After Notebook Creation233234If the flow stops after deploying notebook code without binding or executing:235- Notebooks will have no lakehouse context → `spark.sql()` and relative paths (`Tables/`, `Files/`) fail at runtime236- The user sees no output or results — the architecture is set up but never tested237- **Always complete through step 7** unless the user explicitly asks to stop at a specific step238239---240241## Gold Layer → Power BI Consumption242243After Gold tables are populated, connect Power BI to surface the analytics.244Build a semantic model on top of the Gold lakehouse, using DirectLake.245246247### Step-by-Step2482491. **Discover the Gold lakehouse SQL endpoint** — call `GET /v1/workspaces/{workspaceId}/lakehouses/{goldLakehouseId}` and extract `properties.sqlEndpointProperties.connectionString` and `provisioningStatus`; wait until status is `Success`2502. **Verify Gold tables via SQL** — connect to the SQL endpoint using `sqlcmd` (see [COMMON-CLI.md § SQL / TDS Data-Plane Access](../../common/COMMON-CLI.md#sql--tds-data-plane-access)) and confirm the target table exists:251 ```sql252 SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'nyc_taxi_daily_summary'253 ```2543. **Create a semantic model** — use the [semantic-model-authoring](../semantic-model-authoring/SKILL.md) skill for semantic model creation and TMDL deployment. Create via `POST /v1/workspaces/{workspaceId}/items` with `type: "SemanticModel"` then deploy definition via `updateDefinition` using TMDL format (see [ITEM-DEFINITIONS-CORE.md § SemanticModel](../../common/ITEM-DEFINITIONS-CORE.md#semanticmodel)):255 - The model must reference the Gold lakehouse SQL endpoint as its data source256 - Define a table mapping to the Gold summary table (e.g., `nyc_taxi_daily_summary`)257 - Use **Direct Lake** mode — this connects directly to Delta tables in OneLake without data import258 - Include measures for key aggregations you find interesting (e.g., `Total Trips`, `Avg Fare`, `Total Revenue`, `Month over Month Growth`)2594. **Create a Power BI report** — `POST /v1/workspaces/{workspaceId}/items` with `type: "Report"` then deploy definition via `updateDefinition` using PBIR format (see [ITEM-DEFINITIONS-CORE.md § Report](../../common/ITEM-DEFINITIONS-CORE.md#report)):260 - Reference the semantic model created in step 3 via `definition.pbir`261 - Define at least one page with visuals on the Gold summary table262 - Suggested visuals: line chart (daily trend), card (KPI totals), bar chart (by category), table (detail view)2635. **Verify end-to-end** — use the `semantic-model-authoring` skill for metadata discovery running DAX queries against the semantic model and confirm data flows from Gold tables through to the report264265### Principles266267- **Discover SQL endpoint dynamically** — the connection string is in `properties.sqlEndpointProperties.connectionString` on the lakehouse response; never hardcode it268- **Wait for SQL endpoint provisioning** — status must be `Success` before connecting; newly created lakehouses may take minutes to provision269- **Prefer Direct Lake mode** — avoids data duplication; semantic model reads directly from OneLake Delta tables270- **Match table/column names exactly** — the semantic model table definition must use the exact Delta table and column names from the Gold lakehouse271- **For semantic model authoring** (TMDL, refresh, permissions), cross-reference the [semantic-model-authoring](../semantic-model-authoring/SKILL.md) skill272- **For DAX query validation**, cross-reference the [semantic-model-authoring](../semantic-model-authoring/SKILL.md) skill for metadata discovery and DAX queries for validation.273274---275276## Pipeline Orchestration277278When a user requests a pipeline for the medallion flow, guide LLM to design with:279280- **Structure**: Sequential activities (Bronze → Silver → Gold), each waiting for previous success; independent Gold aggregations can run in parallel; include validation and notification activities281- **Parameterization**: Pipeline-level processing date (defaults to yesterday), passed to all notebooks; dynamic date expressions282- **Scheduling**: Daily aligned with source refresh, watermark-based incremental processing, periodic full refresh for corrections283- **Error handling**: Retry with backoff for transient failures, alerting for persistent failures, graceful degradation (downstream uses previous data if upstream fails)284285---286287## Environment Optimization288289**For detailed Spark configurations and optimization strategies, see [data-engineering-patterns.md](../spark-cli/references/authoring/resources/data-engineering-patterns.md).**290291| Layer | Profile | Key Settings |292|-------|---------|-------------|293| Bronze | Write-heavy | Disable V-Order, enable autoCompact, large file targets, partition by ingestion_date |294| Silver | Balanced | Enable V-Order, adaptive query execution, partition by business date, ZORDER on filtered columns |295| Gold | Read-heavy | V-Order (`spark.sql.parquet.vorder.default=true`), Optimize Write (`optimizeWrite.enabled=true`, `binSize=1g`), vectorized readers, adaptive execution, ZORDER on all filter columns, pre-aggregate metrics |296297---298299## Examples300301### Example 1: Set Up Medallion Workspaces (Default)302303**Prompt**: "Set up medallion architecture with separate Bronze, Silver, and Gold workspaces for sales analytics"304305**What the LLM should generate**: REST API calls to:3061. Create workspaces: `sales-bronze-dev`, `sales-silver-dev`, `sales-gold-dev`3072. Create one lakehouse in each workspace: `sales_bronze`, `sales_silver`, `sales_gold`3083. Assign RBAC roles per workspace/layer309310```bash311# Workspace creation (see COMMON-CLI.md for full patterns)312cat > /tmp/body.json << 'EOF'313{"displayName": "sales-analytics-dev"}314EOF315workspace_id=$(az rest --method post --resource "https://api.fabric.microsoft.com" \316 --url "https://api.fabric.microsoft.com/v1/workspaces" \317 --body @/tmp/body.json --query "id" --output tsv)318319# Create Bronze lakehouse320cat > /tmp/body.json << 'EOF'321{"displayName": "sales_bronze", "type": "Lakehouse"}322EOF323az rest --method post --resource "https://api.fabric.microsoft.com" \324 --url "https://api.fabric.microsoft.com/v1/workspaces/$workspace_id/items" \325 --body @/tmp/body.json326```327328### Example 2: Design Bronze Ingestion329330**Prompt**: "Ingest daily CSV files into bronze lakehouse with metadata columns"331332**What the LLM should generate**: PySpark notebook that:3331. Reads source files with schema inference or explicit schema3342. Adds `ingestion_timestamp`, `source_file`, `batch_id` columns3353. Writes to Delta table partitioned by ingestion date3364. Logs row count and validation metrics337338```python339# Bronze ingestion pattern (guide LLM to generate full implementation)340from pyspark.sql.functions import current_timestamp, input_file_name, lit341import uuid342343batch_id = str(uuid.uuid4())344df = (spark.read.format("csv").option("header", True).load("/Files/landing/daily/")345 .withColumn("ingestion_timestamp", current_timestamp())346 .withColumn("source_file", input_file_name())347 .withColumn("batch_id", lit(batch_id)))348df.write.mode("append").partitionBy("ingestion_date").format("delta").saveAsTable("bronze.events_raw")349```350351### Example 3: Bronze-to-Silver Transformation352353**Prompt**: "Clean bronze data: remove duplicates, filter invalid records, add derived columns, write to silver"354355**What the LLM should generate**: PySpark notebook applying quality rules, schema conformance, and partitioned write with optimization.356357### Example 4: End-to-End Pipeline358359**Prompt**: "Create a pipeline that runs bronze ingestion, then silver transformation, then gold aggregation daily at 2 AM"360361**What the LLM should generate**: Pipeline JSON definition with sequential notebook activities, date parameter, retry logic, and schedule trigger.