
Refresh Semantic Model
- 33 installs
- 836 repo stars
- Updated July 29, 2026
- data-goblin/power-bi-agentic-development
Refresh, manage, and troubleshoot Power BI semantic models and refresh schedules via the Enhanced Refresh REST API and Fabric CLI.
About
Triggers, monitors, validates, and troubleshoots semantic model refreshes and refresh schedules using the Power BI Enhanced Refresh REST API and Fabric CLI. A developer uses it to run or configure a dataset refresh and diagnose failures.
- Manages refreshes and refresh schedules for datasets
- Monitors and troubleshoots refresh via REST API and fab
Refresh Semantic Model by the numbers
- 33 all-time installs (skills.sh)
- Ranked #1,088 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/data-goblin/power-bi-agentic-development --skill refresh-semantic-modelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 836 |
| Last updated | July 29, 2026 |
| Repository | data-goblin/power-bi-agentic-development ↗ |
What it does
Refresh, manage, and troubleshoot Power BI semantic models and refresh schedules via the Enhanced Refresh REST API and Fabric CLI.
Files
Refreshing Semantic Models
Trigger, monitor, validate, and troubleshoot semantic model refreshes via the Power BI Enhanced Refresh REST API and Fabric CLI.
Core Concepts
A semantic model refresh reloads data from upstream sources and/or recalculates dependent objects (calculated columns, calculated tables, measures). The scope can be the entire model, specific tables, or individual partitions.
Six refresh types are available via the REST API; a seventh (add) is TMSL-only:
| Type | Reloads Data | Recalculates | Primary Use Case | API |
|---|---|---|---|---|
full | Yes | Yes | Complete reload from scratch | REST |
automatic | Conditional | Conditional | Smart refresh; process only if needed | REST |
dataOnly | Yes | No* | Reload data; clear dependents | REST |
calculate | No | Yes | Recalculate without reloading data | REST |
clearValues | No | No | Empty data from objects | REST |
defragment | No | No | Clean up column dictionaries | REST |
add | Append | Yes | Append rows to a partition | TMSL |
*dataOnly clears dependent objects (calculated columns, calculated tables) but does not recalculate them. Follow with a calculate refresh to restore them.
For detailed descriptions, behavior with incremental refresh policies, commit modes, and parallelism options, consult `references/refresh-types.md`.
Refresh Workflow
Step 1: Resolve IDs
Extract the workspace and model GUIDs needed for API calls:
WS_ID=$(fab get "WorkspaceName.Workspace" -q "id" | tr -d '"')
MODEL_ID=$(fab get "WorkspaceName.Workspace/ModelName.SemanticModel" -q "id" | tr -d '"')Step 2: Query Baseline Data (Pre-Refresh Validation)
Before triggering the refresh, capture a baseline snapshot to later verify that data actually changed. Execute a DAX query against the model to get current row counts or max dates:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/executeQueries" \
-X post -i '{
"queries": [{"query": "EVALUATE ROW(\"RowCount\", COUNTROWS(FactSales), \"MaxDate\", MAX(FactSales[OrderDate]))"}],
"serializerSettings": {"includeNulls": true}
}'Record the output. This baseline is compared after refresh to confirm new data arrived.
Step 3: Trigger the Refresh
Full model refresh (simplest):
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"full"}'Refresh specific tables:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{
"type": "full",
"objects": [{"table": "FactSales"}, {"table": "DimProduct"}]
}'Refresh specific partitions:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{
"type": "full",
"objects": [
{"table": "FactSales", "partition": "FactSales_2024"},
{"table": "FactSales", "partition": "FactSales_2023"}
]
}'Data-only refresh (skip recalculation):
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"dataOnly","objects":[{"table":"FactSales"}]}'Calculate only (no data reload):
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"calculate"}'Clear values from a table:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"clearValues","objects":[{"table":"StagingTable"}]}'For the script-based approach with CLI arguments, use `scripts/refresh_model.py`.
Step 4: Monitor Status
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes?\$top=1"Status values: Unknown, InProgress, Completed, Failed, Disabled, Cancelled
Step 5: Post-Refresh Validation
After the refresh completes, re-run the same DAX query from Step 2 and compare:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/executeQueries" \
-X post -i '{
"queries": [{"query": "EVALUATE ROW(\"RowCount\", COUNTROWS(FactSales), \"MaxDate\", MAX(FactSales[OrderDate]))"}],
"serializerSettings": {"includeNulls": true}
}'If data has not changed after a successful refresh:
- The upstream data source has not been updated
- The ETL pipeline (Fabric pipeline, notebook, Data Factory, or other orchestration) needs to run first
- Check the lakehouse/warehouse/SQL database to verify fresh data exists
- For Fabric lakehouses: run
fab run "Workspace.Workspace/Pipeline.DataPipeline"or trigger the notebook - The refresh only pulls what the source provides; if the source is stale, the refresh will succeed but show no new data
Step 6: Cancel (if needed)
To cancel an in-progress enhanced refresh, first retrieve the requestId from the refresh history:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes?\$top=1"The response includes a requestId field. Use it to cancel:
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes/<requestId>" \
-X deleteOnly works for refreshes triggered via the Enhanced API (not scheduled or portal refreshes).
Using the Refresh Script
The `scripts/refresh_model.py` script wraps the Enhanced Refresh API with CLI arguments:
# Full refresh
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID
# Refresh specific tables
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID --tables Sales,Calendar
# Refresh specific partitions
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID --partitions Sales:Sales_2024
# Data-only then calculate (two-phase)
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID -t dataOnly --tables FactSales
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID -t calculate
# Partial batch with parallelism
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID --commit partialBatch --parallelism 4
# Skip incremental refresh policy
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID --no-policy
# Check status only
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID --status-only
# Poll until complete
uv run scripts/refresh_model.py -w $WS_ID -m $MODEL_ID --pollEnhanced Refresh Options
The Enhanced Refresh API (Premium/Fabric capacity required) extends the standard refresh with:
| Parameter | Default | Purpose |
|---|---|---|
type | automatic | Refresh type (full, automatic, dataOnly, calculate, etc.) |
commitMode | transactional | Atomic commit or per-object partial batch |
maxParallelism | 10 | Number of parallel processing threads |
retryCount | 0 | Automatic retries on failure |
objects | Entire model | Array of table/partition targets |
applyRefreshPolicy | true | Apply or skip incremental refresh policy |
effectiveDate | Current date | Override date for incremental policy window |
timeout | 05:00:00 | Per-attempt timeout (max total 24h with retries) |
Common Patterns
Two-Phase Refresh (Large Models)
Split data loading and recalculation for better control and failure isolation:
1. dataOnly with partialBatch to reload all tables (each committed independently) 2. calculate with transactional to recalculate everything atomically
Selective Partition Refresh
For tables with incremental refresh, refresh only specific time-range partitions rather than the entire table. To discover partition names, query the model's TMSL metadata via the XMLA endpoint using Tabular Editor, SSMS, or by exporting with the Fabric CLI:
fab export "Workspace.Workspace/Model.SemanticModel" -o /tmp/model -fInspect the exported TMDL table files; each partition block lists the partition name. Target specific partitions in the objects array of the refresh request.
Refresh After ETL
When orchestrating a data pipeline:
1. Run the upstream ETL (Fabric pipeline, notebook, ADF, or custom) 2. Verify fresh data in the source (lakehouse, warehouse, SQL) 3. Trigger the semantic model refresh 4. Validate with a DAX query that row counts or max dates changed 5. If unchanged, investigate the ETL output; the semantic model refresh succeeded but the source was stale
Troubleshooting
Quick reference for the most common failures. For the full troubleshooting guide with debugging workflows and detailed error tables, read `references/troubleshooting.md`.
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Failed with credential error | Credentials expired, missing, or didn't carry over after copy | Update in dataset settings; only shared cloud connections transfer with fab cp |
| Type mismatch on a table | Source column types don't match model column types | Check column data types in the model definition vs source schema; add Table.TransformColumnTypes in partition expression |
| Column does not exist | Source column renamed, removed, or differently cased | Check source schema; add Table.RenameColumns in partition expression |
| Timeout (2h shared / 5h Premium) | Model too large for a single refresh window | Implement incremental refresh; use partition-level refresh via XMLA; reduce model size |
| Calculated tables empty | dataOnly refresh clears but doesn't rebuild | Follow with a calculate refresh via the Enhanced Refresh API to rebuild calculated tables and calc groups |
| Throttled on Premium | Too many concurrent refreshes | Stagger refresh schedules; refresh during off-peak |
Debugging per-table failures
When a full refresh fails, isolate the failing table by refreshing individual tables via the Enhanced Refresh API:
# Refresh dimensions first
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"full","objects":[{"table":"Customers"}]}'
# Then facts
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"full","objects":[{"table":"Invoices"}]}'
# Then recalculate
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"calculate"}'Check the failing table's partition expression and compare source schema against the model's expected column names and types via fab export or fab table schema.
Large Model Strategies
Models over 1 GB or with refresh times exceeding an hour benefit from targeted approaches:
- Partition-level refresh: Refresh individual table partitions via the enhanced REST API or XMLA endpoint instead of the full model. Requires Premium/Fabric capacity.
- Incremental refresh: Automatically partition large tables by date; only recent data refreshes each cycle. Configure
RangeStart/RangeEndparameters in Power Query. Also supports detect-data-changes to skip unchanged partitions entirely. - Aggregations: Pre-aggregate large fact tables at a coarser grain into an import-mode aggregation table. Detail queries fall through to DirectQuery. Reduces both refresh time and memory.
- Hybrid tables: Historical partitions in import mode; a real-time DirectQuery partition for recent data. Related tables must be Dual storage mode.
- Scale-out: Isolate refresh from query workloads by enabling semantic model scale-out on Premium capacities. A read-only replica handles queries while the primary refreshes.
Capacity Limits
| Capacity Type | Max Refreshes/Day | Default Timeout | Enhanced Features |
|---|---|---|---|
| Pro | 8 | 2 hours | No |
| Premium Per User | 48 | 5 hours | Yes |
| Premium / Fabric | 48 | 5 hours | Yes |
Pro capacity supports only full-model standard refreshes. Enhanced refresh features (table/partition targeting, commit modes, parallelism, cancel, timeout override) require Premium or Fabric capacity.
Requirements
- Workspace contributor or higher permissions
fabCLI authenticated:fab auth login
Additional Resources
Reference Files
- `references/refresh-types.md` -- Complete reference for all 7 refresh types, commit modes, parallelism, incremental policy interaction, status values, XMLA/TMSL details, and the two-phase refresh pattern
- `references/troubleshooting.md` -- Comprehensive troubleshooting guide: credential errors, type/schema mismatches, timeouts, capacity limits, incremental refresh issues, debugging workflows, and large model strategies
Scripts
- `scripts/refresh_model.py` -- CLI tool for triggering and monitoring refreshes with all enhanced options
Related Skills
- `semantic-model` -- Model design, build, and quality/performance review
- `lineage-analysis` -- Downstream report discovery and impact analysis
- `standardize-naming-conventions` -- Naming audit and remediation
- `fabric-cli` (fabric-cli plugin) -- Workspace and item management via
fabCLI
Refresh Types Reference
Reference for all refresh types available in the Power BI Enhanced Refresh API and TMSL refresh command. These types apply to databases, tables, and partitions.
Refresh Type Summary
| Type | Reloads Data | Recalculates | Clears First | Scope | Use Case |
|---|---|---|---|---|---|
full | Yes | Yes | Yes | DB / Table / Part. | Complete reload and recalculation from scratch |
automatic | Conditional | Conditional | No | DB / Table / Part. | Smart refresh; only processes what needs updating |
dataOnly | Yes | No | Yes | DB / Table / Part. | Reload data without recalculating dependents |
calculate | No | Yes | No | DB / Table / Part. | Recalculate without reloading any data |
clearValues | No | No | Yes | DB / Table / Part. | Empty data from objects and all dependents |
add | Yes (append) | Yes | No | Partition only | Append new rows to an existing partition |
defragment | No | No | No | DB / Table | Clean up unused dictionary entries |
Detailed Descriptions
full
Reload all data from the source and recalculate all dependent objects. The most thorough but most expensive operation.
- Clears existing data before reloading
- Recalculates all calculated columns, calculated tables, and measures
- Processes all partitions in the specified scope
- For calculation partitions, recalculates the partition and all its dependents
- Use when data sources have changed structurally or after schema changes
- Use when partition states are inconsistent or in an error state
automatic
Conditionally refresh and recalculate only objects that need it. The default type when no type is specified.
- Checks the state of each object; only processes objects not in a
Readystate - If a partition already has data and is in Ready state, it is skipped
- More efficient than
fullfor incremental scenarios - Ideal for scheduled refreshes and general-purpose automation
- Behaves like
fullfor objects that need processing
dataOnly
Reload data from the source without recalculating any dependent objects.
- Clears existing data and reloads from source
- Does NOT recalculate calculated columns, calculated tables, or measures
- Dependents are cleared to an unprocessed state (calculated columns become empty, calculated tables lose data); they must be recalculated with a subsequent
calculaterefresh - Useful as the first step in a two-phase refresh (dataOnly + calculate)
- Reduces processing time when recalculation will happen in a separate step
- The model may be in a partially unprocessed state until the
calculatestep runs
calculate
Recalculate dependent objects without reloading any data from sources.
- No data source connections are made
- Recalculates calculated columns, calculated tables, and measures
- Only recalculates objects that need it (unless they are volatile formulas)
- Useful after a
dataOnlyrefresh to recalculate in a controlled step - Useful when only DAX logic has changed (new measures or modified expressions)
clearValues
Remove all data from the specified objects and their dependents.
- Does not reload data; simply empties the object
- Clears all dependent objects as well
- Leaves the object in an unprocessed state
- Useful to free memory before a selective reload
- Useful to reset a partition or table to empty state
add (Partition only; TMSL only)
Append new rows to an existing partition without removing current data. Not available via the REST API; requires XMLA/TMSL endpoint access (SSMS, Tabular Editor, PowerShell).
- Only valid for regular (import) partitions; not for calculation partitions
- Appends data from the partition source query
- Recalculates all dependents after appending
- Does NOT clear existing data first
- Useful for log/event tables where new data is appended
- Requires that the source query returns only the new rows to append
defragment
Clean up unused dictionary entries in column stores.
- No data is reloaded or recalculated
- Removes values from column dictionaries that no longer exist in actual data
- Reduces memory consumption after many add/remove operations
- Useful for models with frequent partition changes (incremental refresh)
- Minimal impact on query performance during execution
Two-Phase Refresh Pattern
For large models, split the refresh into two phases for better control:
Phase 1 -- Data reload:
{
"type": "dataOnly",
"commitMode": "partialBatch",
"maxParallelism": 4,
"objects": [
{"table": "FactSales"},
{"table": "FactInventory"}
]
}Phase 2 -- Recalculation:
{
"type": "calculate",
"commitMode": "transactional"
}Benefits:
- Data reload failures do not leave calculated columns in an inconsistent state
- Each phase can have different commit modes (partialBatch for data, transactional for calc)
- Better visibility into which phase failed
Commit Modes
transactional (default)
- All objects are committed atomically; either everything succeeds or nothing changes
- If any object fails, the entire operation rolls back
- Model remains in its pre-refresh state on failure
- Safer for production workloads
partialBatch
- Each object is committed individually as it completes
- If one table fails, previously committed tables retain their new data
- On failure, the model may contain a mix of old and new data
- Faster for large models with many independent tables
applyRefreshPolicymust befalsewhen using partialBatch
Incremental Refresh Policy Interaction
When applyRefreshPolicy is true (default) and the table has an incremental refresh policy:
| Type | Behavior with Policy Applied |
|---|---|
full | Creates/updates partitions per policy; refreshes incremental range; skips history |
dataOnly | Same as full but without recalculation of dependents |
automatic | Same as full but partitions use automatic processing |
calculate | Policy does not affect behavior |
clearValues | Policy does not affect behavior |
add | Policy does not affect behavior |
defragment | Policy does not affect behavior |
Set applyRefreshPolicy: false to bypass the policy and refresh all partitions manually. This is required when using commitMode: partialBatch.
MaxParallelism
Controls the number of threads used for parallel processing.
- Default:
10 - Setting to
1forces sequential processing - Higher values consume more capacity resources
- For optimal parallel processing order:
1. clearValues on all objects first 2. dataOnly on all objects 3. full or calculate on all objects
Enhanced Refresh API vs Standard Refresh
| Feature | Standard Refresh | Enhanced Refresh |
|---|---|---|
| Trigger endpoint | POST /refreshes | POST /refreshes (with body) |
| Table/partition targeting | No | Yes |
| Custom commit mode | No | Yes |
| Max parallelism control | No | Yes |
| Retry count | No | Yes |
| Per-object status (GET) | No | Yes |
| Cancel in-progress | No | Yes (DELETE /refreshes/{id}) |
| Timeout configuration | No | Yes |
| Apply/skip refresh policy | Always applies | Configurable |
| Effective date override | No | Yes |
| Requires Premium/Fabric | No (Pro supported) | Yes |
Status Values
Returned by the GET /refreshes endpoint:
| Status | Meaning |
|---|---|
Unknown | Completion state cannot be determined; may still be running |
Completed | Refresh completed successfully |
Failed | Refresh encountered an error |
Disabled | Refresh was disabled (e.g. by selective refresh) |
InProgress | Refresh is currently running |
Cancelled | Refresh was cancelled via DELETE |
The extendedStatus field provides additional detail:
| Extended Status | Meaning |
|---|---|
NotStarted | Queued but not yet started |
InProgress | Currently processing |
Completed | Successfully completed |
Failed | Error during processing |
Cancelled | Cancelled by user |
XMLA / TMSL Refresh (Advanced)
For direct XMLA endpoint access (SSMS, Tabular Editor, PowerShell), the TMSL refresh command accepts the same types:
{
"refresh": {
"type": "full",
"objects": [
{
"database": "MyModel",
"table": "FactSales",
"partition": "Sales_2024_Q1"
}
]
}
}TMSL also supports:
- Connection string overrides during refresh
- Query definition overrides for partitions
- Sequence commands for ordered multi-step operations
- The
addtype (not available via REST API)
TMSL requires a persistent XMLA connection. For long-running refreshes, prefer the REST API.
Refresh Troubleshooting
Common refresh failures, their causes, and resolutions for diagnosing refresh issues.
Credential and Authentication Errors
| Error | Cause | Resolution |
|---|---|---|
DatasourceHasNoCredentialError | Data source credentials missing or not configured | Set credentials in dataset settings (Power BI service); for cloud connections, re-authenticate via OAuth |
OAuthTokenRefreshFailedError | OAuth token expired during refresh (common with Entra ID sources like SharePoint, Dynamics) | Token expires after ~1 hour; reduce data volume per query or switch to a service principal |
| Access forbidden / 403 | Insufficient workspace permissions | Verify workspace contributor role or higher |
Credentials not carried after fab cp | Personal or gateway-bound credentials don't transfer when copying a model to a new workspace | Re-authenticate in dataset settings; only shared cloud connections carry over automatically |
Data Source and Gateway Errors
| Error | Cause | Resolution |
|---|---|---|
GatewayNotReachable | On-premises gateway offline or outdated | Install latest gateway version; check gateway status in admin portal |
| Unsupported data source for refresh | Data source type not supported for scheduled refresh | Check supported sources; consider using a gateway or switching connectors |
Web.Page connector fails | Web connector requires gateway after Nov 2016 | Configure an on-premises data gateway |
| Connection timed out or was lost | Transient network error or long-running M query | Retry; if persistent, use Table.Buffer for complex joins; check data source timeouts |
Type and Schema Errors
| Error | Cause | Resolution |
|---|---|---|
| Type mismatch | Source column type doesn't match model column dataType | Add Table.TransformColumnTypes in the partition expression; or fix the model column type to match the source |
| Column does not exist in rowset | Source column renamed, removed, or differently cased | Check source schema; add Table.RenameColumns in the partition expression |
| Duplicate value on key column | Source has duplicates on a column used on the "one" side of a relationship | Add Table.Distinct in partition expression; or fix source data; or review whether the column should be a key |
ANY type column with TRUE/FALSE | Boolean values convert to -1/0 in the service (differs from Desktop) | Set explicit data types in Power Query before publishing |
Timeout and Size Errors
| Error | Cause | Resolution |
|---|---|---|
| Scheduled refresh timeout (2h / 5h) | Model too large or complex for the refresh window (2h shared, 5h Premium) | Reduce model size; implement incremental refresh; use partitioned refresh via XMLA |
| Uncompressed data limit exceeded | Shared capacity: 10 GB uncompressed limit during refresh | Reduce data volume; filter in Power Query; move to Premium |
| Model size exceeds capacity limit | Model larger than the capacity's max size (1 GB Pro, 25 GB Trial, varies by Fabric SKU) | Enable large model storage format (Premium); reduce model size; upgrade capacity |
| Data source query timeout | Source system has its own query timeout | Override via CommandTimeout in the connection string or M expression |
| Initial incremental refresh timeout | First refresh must load all historical data | Bootstrap the initial refresh via XMLA endpoint to create partition objects without loading data |
Incremental Refresh Errors
| Error | Cause | Resolution |
|---|---|---|
| Query not folded | RangeStart/RangeEnd filter not pushed to source; engine loads all data then filters locally | Verify query folding with source profiling; ensure filter step is foldable; check RangeStart/RangeEnd are DateTime type matching the source column |
| Data type mismatch on parameters | RangeStart/RangeEnd type doesn't match the date column | Both must be DateTime; if source uses integer keys, create a conversion function |
| Partition-key conflicts | Date column values updated at source after initial partition | Refresh all affected partitions from the changed date forward via XMLA |
| Data truncated | Source returns > 64 MB compressed (Azure Data Explorer, Log Analytics) | Specify smaller refresh/store periods so each partition query stays under the limit |
| Duplicate values after date change | Transaction dates changed at source cause a row to appear in two partitions | Refresh partitions from the change point forward; avoid updating the date column used for partitioning |
Capacity and Throttling Errors
| Error | Cause | Resolution |
|---|---|---|
| Refresh throttled | Too many concurrent refreshes on the capacity | Refresh during off-peak hours; stagger schedules; check SKU concurrent refresh limits |
Capacity level limit exceeded | Capacity-wide concurrent refresh limit hit | Retry later; reduce overlapping refresh schedules |
| Memory error during refresh | Insufficient memory; refresh requires ~2x model size (original + copy for queries) | Increase Max Memory % in capacity settings; reduce model complexity; enable scale-out for refresh isolation |
Container exited unexpectedly (0x0000DEAD) | Internal service error | Disable scheduled refresh; republish the model; re-enable |
Calculated Table / Calculated Column Errors
| Error | Cause | Resolution |
|---|---|---|
| Circular dependency on refresh | SummarizeColumns inside CalculateTable introduced new dependencies (Sept 2024 change) | Add the grouped tables as explicit filters inside SummarizeColumns |
Calculated tables empty after dataOnly refresh | dataOnly clears but doesn't rebuild calculated objects | Follow with a calculate refresh to rebuild |
calculate refresh times out | Many calculation groups or large calculated tables | Refresh calculated tables individually via XMLA; increase timeout |
Debugging Workflow
1. Check the refresh history
WS_ID=$(fab get "MyWorkspace.Workspace" -q "id" | tr -d '"')
MODEL_ID=$(fab get "MyWorkspace.Workspace/MyModel.SemanticModel" -q "id" | tr -d '"')
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes?\$top=5"Look at status and serviceExceptionJson for the specific error message and which table/partition failed.
2. Isolate the failing table
Refresh tables one at a time to find which one fails:
# Refresh individual tables via the Power BI REST API
WS_ID=$(fab get "MyWorkspace.Workspace" -q "id" | tr -d '"')
MODEL_ID=$(fab get "MyWorkspace.Workspace/MyModel.SemanticModel" -q "id" | tr -d '"')
# Dimensions first
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"Full","objects":[{"table":"Customers"}]}'
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"Full","objects":[{"table":"Products"}]}'
# Then facts
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"Full","objects":[{"table":"Invoices"}]}'
# Then calculated tables
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" \
-X post -i '{"type":"Calculate"}'3. Compare source schema to model schema
Check what the source provides vs what the model expects:
# Source schema (lakehouse example)
fab table schema "MyWorkspace.Workspace/MyLakehouse.Lakehouse/Tables/invoices"
# Model column types -- inspect the TMDL definition or use fab export
fab export "MyWorkspace.Workspace/MyModel.SemanticModel" -o ./model-export -f
cat ./model-export/MyModel.SemanticModel/definition/tables/Invoices.tmdl | grep -A1 "column "If they don't match, add type conversion steps in the partition expression.
4. Verify query folding
For incremental refresh, confirm the filter is pushed to the source:
- In Power BI Desktop: right-click the filter step in Power Query; if "View Native Query" is available and shows a WHERE clause, folding works
- At the source: use SQL Profiler or query logs to verify a single filtered query per partition
5. Check capacity state
# Is the workspace on a capacity?
fab get "MyWorkspace.Workspace" -q "capacityId"
# Is the capacity active?
fab api -A powerbi "capacities" -q "value[].{name:displayName, state:state, sku:sku}"A workspace without a capacity (or on a suspended capacity) will fail all refresh operations.
Strategies for Large Models
For models that are too large or slow to refresh in a single operation:
Partition-level refresh
Refresh individual partitions instead of entire tables. Requires Premium/Fabric capacity with XMLA endpoint access:
# Refresh only recent partitions (via enhanced REST API)
fab api -A powerbi "groups/$WS_ID/datasets/$MODEL_ID/refreshes" -X post \
-i '{"type":"Full","objects":[{"table":"Invoices","partition":"2024-Q4"}]}'Incremental refresh
Automatically partition large tables by date so only recent data refreshes each cycle. Configure in Power BI Desktop with RangeStart/RangeEnd parameters. Consider:
- Store period: how much historical data to keep (e.g. 3 years)
- Refresh period: how much recent data to reload each cycle (e.g. 30 days)
- Detect data changes: skip unchanged historical partitions entirely
Aggregations
Pre-aggregate large fact tables at a coarser grain (e.g. monthly by category) into an import-mode aggregation table. Detail queries fall through to DirectQuery. Reduces both refresh time and memory.
Hybrid tables
Combine import mode (historical data) with DirectQuery (recent data) on the same table. Historical partitions import during scheduled refresh; real-time partition queries the source live. Related tables must be set to Dual storage mode.
Scale-out
Enable semantic model scale-out on Premium capacities to isolate refresh from query workloads. A read-only replica handles interactive queries while the read/write replica refreshes.
#!/usr/bin/env python3
"""
Semantic Model Refresh Script
refresh_model.py
Trigger and monitor semantic model refreshes via the Power BI Enhanced
Refresh REST API. Supports full model, individual tables, specific
partitions, and all TMSL refresh types.
AGENT USAGE GUIDE:
------------------
Use this script to programmatically refresh semantic models. It wraps the
Enhanced Refresh API with sensible defaults and clear status output.
COMMON PATTERNS:
# Full model refresh (all tables, type=full)
uv run refresh_model.py -w <workspace-id> -m <model-id>
# Automatic refresh (incremental if configured, else full)
uv run refresh_model.py -w <workspace-id> -m <model-id> -t automatic
# Refresh specific tables
uv run refresh_model.py -w <workspace-id> -m <model-id> --tables Sales,Calendar
# Refresh specific partitions
uv run refresh_model.py -w <workspace-id> -m <model-id> --partitions Sales:Sales_2024,Sales:Sales_2023
# Data-only refresh (no recalculation)
uv run refresh_model.py -w <workspace-id> -m <model-id> -t dataOnly
# Calculate only (no data reload)
uv run refresh_model.py -w <workspace-id> -m <model-id> -t calculate
# Clear values from specific table
uv run refresh_model.py -w <workspace-id> -m <model-id> -t clearValues --tables FactSales
# Partial batch commit with parallelism
uv run refresh_model.py -w <workspace-id> -m <model-id> --commit partialBatch --parallelism 4
# Skip incremental refresh policy
uv run refresh_model.py -w <workspace-id> -m <model-id> --no-policy
# Monitor only (check last N refreshes)
uv run refresh_model.py -w <workspace-id> -m <model-id> --status-only --top 5
# Cancel an in-progress refresh
uv run refresh_model.py -w <workspace-id> -m <model-id> --cancel <request-id>
PREREQUISITES:
- `fab` CLI authenticated: `fab auth login`
- Workspace contributor or higher permissions
- Premium/Fabric capacity for enhanced refresh features
TOKEN SECURITY:
Uses `fab api` for all calls (handles auth internally).
No tokens are printed or logged.
"""
import argparse
import json
import subprocess
import sys
import time
from typing import Any, Dict, List, Optional
#region Helpers
def fab_api(
endpoint: str,
method: str = "GET",
body: Optional[Dict] = None,
audience: str = "powerbi",
) -> Dict[str, Any]:
"""
Call a Power BI API endpoint via fab CLI.
Returns a dict with 'status' (int), 'data' (parsed JSON or None),
and 'error' (str or None). Auth handled internally by fab.
"""
cmd = ["fab", "api", "-A", audience, endpoint]
if method.upper() != "GET":
cmd.extend(["-X", method.lower()])
if body is not None:
cmd.extend(["-i", json.dumps(body)])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode != 0:
err = result.stderr.strip() or result.stdout.strip()
return {"status": 1, "data": None, "error": err}
stdout = result.stdout.strip()
if not stdout:
return {"status": 0, "data": None, "error": None}
raw = json.loads(stdout)
data = raw.get("text", raw)
return {"status": 0, "data": data, "error": None}
except subprocess.TimeoutExpired:
return {"status": 1, "data": None, "error": "Request timed out"}
except json.JSONDecodeError:
return {"status": 0, "data": None, "error": None}
#endregion
#region Refresh Operations
def trigger_refresh(
workspace_id: str,
model_id: str,
refresh_type: str = "full",
tables: Optional[List[str]] = None,
partitions: Optional[List[Dict[str, str]]] = None,
commit_mode: str = "transactional",
max_parallelism: int = 10,
retry_count: int = 0,
apply_policy: bool = True,
effective_date: Optional[str] = None,
timeout: Optional[str] = None,
) -> Dict[str, Any]:
"""
Trigger a semantic model refresh via the Enhanced Refresh API.
Builds the POST body from the provided parameters and sends it to
the /refreshes endpoint. Returns the API response with request ID
if successful.
Inputs:
workspace_id - Workspace GUID
model_id - Semantic model (dataset) GUID
refresh_type - One of: full, automatic, dataOnly, calculate, clearValues, defragment
tables - Optional list of table names to refresh
partitions - Optional list of {"table": ..., "partition": ...} dicts
commit_mode - transactional or partialBatch
max_parallelism - Max parallel processing threads (1-100)
retry_count - Number of retries on failure
apply_policy - Whether to apply incremental refresh policy
effective_date - Override current date for policy (YYYY-MM-DD)
timeout - Per-attempt timeout (HH:MM:SS format)
Output:
Dict with 'success' (bool), 'message' (str), and 'request_id' (str or None)
"""
endpoint = f"groups/{workspace_id}/datasets/{model_id}/refreshes"
body: Dict[str, Any] = {
"type": refresh_type,
"commitMode": commit_mode,
"maxParallelism": max_parallelism,
"retryCount": retry_count,
"applyRefreshPolicy": apply_policy,
}
if effective_date:
body["effectiveDate"] = effective_date
if timeout:
body["timeout"] = timeout
# Build objects array from tables and/or partitions
objects = []
if partitions:
for p in partitions:
objects.append({"table": p["table"], "partition": p["partition"]})
elif tables:
for t in tables:
objects.append({"table": t})
if objects:
body["objects"] = objects
resp = fab_api(endpoint, method="POST", body=body)
if resp["error"]:
return {"success": False, "message": resp["error"], "request_id": None}
# fab CLI does not expose the Location header, so retrieve the
# requestId from the most recent refresh history entry.
request_id = None
history_resp = fab_api(
f"groups/{workspace_id}/datasets/{model_id}/refreshes?$top=1"
)
if history_resp["data"]:
entries = history_resp["data"]
if isinstance(entries, dict):
entries = entries.get("value", [])
if entries:
request_id = entries[0].get("requestId")
return {
"success": True,
"message": f"Refresh triggered ({refresh_type})",
"request_id": request_id,
}
def get_refresh_history(
workspace_id: str,
model_id: str,
top: int = 5,
) -> List[Dict[str, Any]]:
"""
Retrieve recent refresh history for a semantic model.
Inputs:
workspace_id - Workspace GUID
model_id - Semantic model GUID
top - Number of recent refreshes to return
Output:
List of refresh records with status, times, and type.
"""
endpoint = f"groups/{workspace_id}/datasets/{model_id}/refreshes?$top={top}"
resp = fab_api(endpoint)
if resp["error"] or not resp["data"]:
return []
data = resp["data"]
if isinstance(data, dict):
return data.get("value", [])
if isinstance(data, list):
return data
return []
def cancel_refresh(
workspace_id: str,
model_id: str,
request_id: str,
) -> Dict[str, Any]:
"""
Cancel an in-progress enhanced refresh operation.
Inputs:
workspace_id - Workspace GUID
model_id - Semantic model GUID
request_id - The requestId of the refresh to cancel
Output:
Dict with 'success' (bool) and 'message' (str).
Only works for refreshes triggered via the Enhanced API.
"""
endpoint = f"groups/{workspace_id}/datasets/{model_id}/refreshes/{request_id}"
resp = fab_api(endpoint, method="DELETE")
if resp["error"]:
return {"success": False, "message": resp["error"]}
return {"success": True, "message": f"Cancel request sent for {request_id}"}
#endregion
#region Output
def format_refresh_history(refreshes: List[Dict]) -> str:
"""Format refresh history as readable ASCII table."""
if not refreshes:
return " No refresh history found."
lines = []
lines.append("")
lines.append(f" {'Status':<12} {'Type':<18} {'Start':<26} {'End':<26}")
lines.append(f" {'─' * 12} {'─' * 18} {'─' * 26} {'─' * 26}")
for r in refreshes:
status = r.get("status", "?")
rtype = r.get("refreshType", "?")
start = r.get("startTime", "")[:25] if r.get("startTime") else "?"
end = r.get("endTime", "")[:25] if r.get("endTime") else "..."
lines.append(f" {status:<12} {rtype:<18} {start:<26} {end:<26}")
lines.append("")
return "\n".join(lines)
def format_trigger_result(result: Dict) -> str:
"""Format the result of a refresh trigger."""
lines = []
if result["success"]:
lines.append(f" [OK] {result['message']}")
if result.get("request_id"):
lines.append(f" Request ID: {result['request_id']}")
else:
lines.append(f" [FAIL] {result['message']}")
return "\n".join(lines)
#endregion
#region CLI
def parse_partitions(partition_str: str) -> List[Dict[str, str]]:
"""
Parse partition argument string into list of table/partition dicts.
Format: "Table1:Partition1,Table2:Partition2"
"""
partitions = []
for item in partition_str.split(","):
item = item.strip()
if ":" not in item:
print(f" [WARN] Invalid partition format '{item}'; expected Table:Partition", file=sys.stderr)
continue
table, partition = item.split(":", 1)
partitions.append({"table": table.strip(), "partition": partition.strip()})
return partitions
def main():
parser = argparse.ArgumentParser(
description="Trigger and monitor semantic model refreshes via the Enhanced Refresh API.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Full refresh of entire model
%(prog)s -w <ws-id> -m <model-id>
# Refresh specific tables
%(prog)s -w <ws-id> -m <model-id> --tables Sales,Calendar
# Refresh specific partitions
%(prog)s -w <ws-id> -m <model-id> --partitions Sales:Sales_2024
# Data-only refresh (skip recalculation)
%(prog)s -w <ws-id> -m <model-id> -t dataOnly
# Check refresh status only
%(prog)s -w <ws-id> -m <model-id> --status-only
""",
)
parser.add_argument("--workspace-id", "-w", required=True, help="Workspace GUID")
parser.add_argument("--model-id", "-m", required=True, help="Semantic model (dataset) GUID")
# Refresh type
parser.add_argument(
"--type", "-t",
default="full",
choices=["full", "automatic", "dataOnly", "calculate", "clearValues", "defragment"],
help="Refresh type (default: full)",
)
# Object scope
parser.add_argument("--tables", help="Comma-separated table names to refresh")
parser.add_argument("--partitions", help="Comma-separated Table:Partition pairs to refresh")
# Enhanced options
parser.add_argument("--commit", default="transactional", choices=["transactional", "partialBatch"],
help="Commit mode (default: transactional)")
parser.add_argument("--parallelism", type=int, default=10, help="Max parallel threads (default: 10)")
parser.add_argument("--retries", type=int, default=0, help="Retry count on failure (default: 0)")
parser.add_argument("--no-policy", action="store_true", help="Skip incremental refresh policy")
parser.add_argument("--effective-date", help="Override current date for policy (YYYY-MM-DD)")
parser.add_argument("--timeout", help="Per-attempt timeout (HH:MM:SS)")
# Monitor / cancel
parser.add_argument("--status-only", action="store_true", help="Show refresh history only; do not trigger")
parser.add_argument("--top", type=int, default=5, help="Number of recent refreshes to show (default: 5)")
parser.add_argument("--cancel", metavar="REQUEST_ID", help="Cancel an in-progress refresh by request ID")
parser.add_argument("--poll", action="store_true", help="Poll refresh status after triggering until complete")
parser.add_argument("--poll-interval", type=int, default=15, help="Seconds between polls (default: 15)")
parser.add_argument("--max-wait", type=int, default=3600, help="Max seconds to poll before giving up (default: 3600)")
args = parser.parse_args()
print("=" * 72)
print(" SEMANTIC MODEL REFRESH")
print("=" * 72)
# Cancel mode
if args.cancel:
result = cancel_refresh(args.workspace_id, args.model_id, args.cancel)
print(format_trigger_result(result))
sys.exit(0 if result["success"] else 1)
# Status-only mode
if args.status_only:
refreshes = get_refresh_history(args.workspace_id, args.model_id, top=args.top)
print(format_refresh_history(refreshes))
sys.exit(0)
# Parse object scope
tables = [t.strip() for t in args.tables.split(",")] if args.tables else None
partitions = parse_partitions(args.partitions) if args.partitions else None
# Describe what will happen
scope_desc = "entire model"
if partitions:
scope_desc = ", ".join(f"{p['table']}:{p['partition']}" for p in partitions)
elif tables:
scope_desc = ", ".join(tables)
print(f"\n Type: {args.type}")
print(f" Scope: {scope_desc}")
print(f" Commit: {args.commit}")
print(f" Parallel: {args.parallelism}")
print(f" Retries: {args.retries}")
print(f" Policy: {'skip' if args.no_policy else 'apply'}")
if args.effective_date:
print(f" Eff. date: {args.effective_date}")
if args.timeout:
print(f" Timeout: {args.timeout}")
print()
# Trigger
result = trigger_refresh(
workspace_id=args.workspace_id,
model_id=args.model_id,
refresh_type=args.type,
tables=tables,
partitions=partitions,
commit_mode=args.commit,
max_parallelism=args.parallelism,
retry_count=args.retries,
apply_policy=not args.no_policy,
effective_date=args.effective_date,
timeout=args.timeout,
)
print(format_trigger_result(result))
if not result["success"]:
sys.exit(1)
# Poll if requested
if args.poll:
print(f"\n Polling for completion (max {args.max_wait}s)...")
elapsed = 0
while elapsed < args.max_wait:
time.sleep(args.poll_interval)
elapsed += args.poll_interval
refreshes = get_refresh_history(args.workspace_id, args.model_id, top=1)
if refreshes:
latest = refreshes[0]
status = latest.get("status", "Unknown")
print(f" ... {status} ({elapsed}s)")
if status in ("Completed", "Failed", "Disabled", "Cancelled"):
print(format_refresh_history([latest]))
sys.exit(0 if status == "Completed" else 1)
else:
print(f" ... unable to retrieve status ({elapsed}s)")
print(f"\n [WARN] Max wait time ({args.max_wait}s) exceeded; refresh may still be running.")
print(" Use --status-only to check later.")
# Show current history
print("\n Recent refresh history:")
refreshes = get_refresh_history(args.workspace_id, args.model_id, top=3)
print(format_refresh_history(refreshes))
if __name__ == "__main__":
main()
#endregion