
Lineage Analysis
- 36 installs
- 836 repo stars
- Updated July 29, 2026
- data-goblin/power-bi-agentic-development
Trace downstream dependencies from a Power BI semantic model to all connected reports across Fabric workspaces for impact analysis.
About
Traces relationships between semantic models and downstream reports across Fabric workspaces to map dependencies without admin permissions. A developer uses it before modifying or deleting a model to understand which reports depend on it and where they live.
- Traces model-to-report dependencies across the tenant
- Needs only workspace contributor access, no admin rights
Lineage Analysis by the numbers
- 36 all-time installs (skills.sh)
- Ranked #1,042 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 lineage-analysisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 836 |
| Last updated | July 29, 2026 |
| Repository | data-goblin/power-bi-agentic-development ↗ |
What it does
Trace downstream dependencies from a Power BI semantic model to all connected reports across Fabric workspaces for impact analysis.
Files
Lineage Analysis
Trace downstream dependencies from a semantic model to all connected reports across the tenant. No admin permissions required -- workspace contributor access is sufficient.
When to Use
- Before modifying or deleting a semantic model, to understand impact
- Auditing which reports are connected to a model and where they live
- Identifying orphaned or test reports connected to production models
- Cross-workspace dependency mapping
Downstream Reports
Run scripts/get-downstream-reports.py to find all reports bound to a semantic model.
# By workspace and model name
python3 scripts/get-downstream-reports.py "Workspace Name" "Model Name"
# By dataset GUID directly
python3 scripts/get-downstream-reports.py --dataset-id <guid>
# JSON output for further processing
python3 scripts/get-downstream-reports.py "Workspace" "Model" --jsonRequirements: azure-identity, requests (pip install azure-identity requests). Authenticated via DefaultAzureCredential (works with az login, managed identity, or environment variables).
How it works: Lists all workspaces the user can access, then queries each workspace's reports in parallel (8 workers) checking datasetId. Groups results by workspace. Typically completes in under 10 seconds for ~100 workspaces.
Permissions: Workspace contributor or higher on any workspace to be scanned. Reports in workspaces without access will not appear. For full tenant coverage, use the --dataset-id flag with a tenant admin token and the admin/reports API instead.
Limitations
Reports are not the only consumers. A semantic model can also be consumed by:
- Analyze in Excel workbooks (.xlsx live connections)
- Composite models (other semantic models chaining via DirectQuery)
- Explorations (ad-hoc visual explorations in the Power BI service)
- Fabric notebooks (connecting via Spark or sempy)
- Fabric data agents
- Paginated reports (.rdl)
- Dataflows referencing the model
- Third-party tools connecting via XMLA
The script only discovers Power BI reports. For full dependency mapping including these other item types, use the Fabric lineage APIs (fab api "admin/groups/{id}/lineage") or the lineage view in the Power BI service UI.
Not appropriate for many models at once. The script scans all accessible workspaces per invocation. Running it in a loop over dozens of semantic models will generate excessive API calls and risk throttling. For bulk inventory across many models, use the admin scan API (fab api "admin/workspaces/getInfo") when tenant-admin access is available, or the cross-workspace catalog via fab find ... -P type=SemanticModel for a quick non-admin list. Neither alternative resolves report-to-model dependency edges; fab find and the OneLake catalog do not expose lineage. For that you still need either a per-workspace scan (this script's approach) or the official Power BI lineage admin API at fab api "admin/groups/{ws-id}/lineage".
Interpreting Results
| Field | Meaning |
|---|---|
Report format PBIR | Modern format, editable as JSON |
Report format PBIRLegacy | Legacy format, needs conversion to PBIR for direct editing |
| Reports in unexpected workspaces | May indicate copies, forks, or thin reports pointing at a shared model |
| Many downstream reports | High-impact model -- changes require coordination |
Related Skills
- `semantic-model` -- Design, build, refresh, and review models (quality, memory, DAX, design)
- `refreshing-semantic-model` -- Trigger and monitor model refreshes
- `fabric-cli` (fabric-cli plugin) -- Workspace and item management via
fabCLI
#!/usr/bin/env python3
"""
Find all reports connected to a semantic model across accessible workspaces.
No tenant admin required -- uses workspace-level permissions only.
Scans all workspaces the authenticated user has access to.
Usage:
# By workspace/model name
python3 get-downstream-reports.py "Claude Code's Workspace" "SpaceParts"
# By dataset GUID directly (skips model lookup)
python3 get-downstream-reports.py --dataset-id 8f2a2a29-0738-444a-8a5a-f83d26ec1f7f
# JSON output for piping
python3 get-downstream-reports.py "ws" "model" --json
Requirements:
pip install azure-identity requests
"""
import argparse
import json
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from azure.identity import DefaultAzureCredential
# region Variables
API = "https://api.powerbi.com/v1.0/myorg"
# endregion
# region Functions
def get_token():
"""Acquire a Power BI API bearer token via DefaultAzureCredential."""
cred = DefaultAzureCredential()
tok = cred.get_token("https://analysis.windows.net/powerbi/api/.default")
return tok.token
def get_dataset_id(headers, workspace_name, model_name):
"""
Resolve a semantic model name to its dataset GUID.
Searches the named workspace for a matching dataset by displayName.
Returns the dataset ID string or exits with error.
"""
resp = requests.get(f"{API}/groups", headers=headers, timeout=15)
resp.raise_for_status()
workspaces = resp.json().get("value", [])
ws = next((w for w in workspaces if w["name"] == workspace_name), None)
if not ws:
print(f"Workspace '{workspace_name}' not found", file=sys.stderr)
sys.exit(1)
resp = requests.get(
f"{API}/groups/{ws['id']}/datasets", headers=headers, timeout=15
)
resp.raise_for_status()
datasets = resp.json().get("value", [])
ds = next((d for d in datasets if d["name"] == model_name), None)
if not ds:
print(f"Model '{model_name}' not found in '{workspace_name}'", file=sys.stderr)
print(f"Available: {[d['name'] for d in datasets]}", file=sys.stderr)
sys.exit(1)
return ds["id"], ws["id"], ws["name"]
def scan_workspace(ws_id, ws_name, dataset_id, headers):
"""
Scan a single workspace for reports bound to the target dataset.
Returns a list of matched report dicts with workspace context.
"""
try:
resp = requests.get(
f"{API}/groups/{ws_id}/reports", headers=headers, timeout=10
)
if resp.status_code != 200:
return []
reports = resp.json().get("value", [])
matched = []
for r in reports:
if r.get("datasetId") == dataset_id:
matched.append({
"report": r["name"],
"reportId": r["id"],
"workspace": ws_name,
"workspaceId": ws_id,
"format": r.get("format", "?"),
"webUrl": r.get("webUrl", ""),
})
return matched
except Exception:
return []
def main():
parser = argparse.ArgumentParser(
description="Find all reports connected to a semantic model"
)
parser.add_argument("workspace", nargs="?", help="Workspace name")
parser.add_argument("model", nargs="?", help="Semantic model name")
parser.add_argument("--dataset-id", help="Dataset GUID (skip name lookup)")
parser.add_argument("--json", action="store_true", help="JSON output")
parser.add_argument(
"--workers", type=int, default=8, help="Parallel workers (default: 8)"
)
args = parser.parse_args()
if not args.dataset_id and not (args.workspace and args.model):
parser.error("Provide workspace + model name, or --dataset-id")
token = get_token()
headers = {"Authorization": f"Bearer {token}"}
# Resolve dataset ID
if args.dataset_id:
dataset_id = args.dataset_id
source_ws = "?"
else:
dataset_id, source_ws_id, source_ws = get_dataset_id(
headers, args.workspace, args.model
)
if not args.json:
print(f"Dataset: {dataset_id}")
print(f"Scanning workspaces...", end="", flush=True)
# Get all accessible workspaces
resp = requests.get(f"{API}/groups", headers=headers, timeout=15)
resp.raise_for_status()
workspaces = resp.json().get("value", [])
# Scan in parallel
start = time.time()
all_matched = []
with ThreadPoolExecutor(max_workers=args.workers) as pool:
futures = {
pool.submit(scan_workspace, ws["id"], ws["name"], dataset_id, headers): ws
for ws in workspaces
}
for f in as_completed(futures):
all_matched.extend(f.result())
elapsed = time.time() - start
# Sort: source workspace first, then alphabetical
all_matched.sort(key=lambda r: (r["workspace"] != source_ws, r["workspace"], r["report"]))
if args.json:
print(json.dumps(all_matched, indent=2))
return
print(f" {len(workspaces)} workspaces in {elapsed:.1f}s\n")
if not all_matched:
print("No downstream reports found.")
return
# Group by workspace
by_ws = {}
for r in all_matched:
by_ws.setdefault(r["workspace"], []).append(r)
print(f"Downstream reports ({len(all_matched)}):\n")
for ws_name, reports in by_ws.items():
print(f" {ws_name}/")
for r in reports:
print(f" {r['report']}.Report ({r['format']})")
print(f"\n{len(all_matched)} reports across {len(by_ws)} workspaces")
# endregion
if __name__ == "__main__":
main()