
Dataflows Save As Authoring Cli
- 89 installs
- 934 repo stars
- Updated July 30, 2026
- microsoft/skills-for-fabric
dataflows-save-as-authoring-cli is an agent skill for Gen1 to Gen2.1 Dataflow save-as readiness assessment and CLI migration via Fabric and Power BI APIs.
About
The dataflows-save-as-authoring-cli skill assesses, plans, and executes Power BI Gen1 to Fabric Gen2.1 dataflow save-as operations via az rest and curl against Power BI and Fabric REST APIs. It scans workspaces or entire tenants for Gen1 dataflows, evaluates save-as readiness with seven risk signals including incremental refresh, BYOSA storage, Power Automate triggers, pipeline dependencies, linked entities, DirectQuery, and caller-not-owner, produces a Save-As Readiness Snapshot in markdown and JSON, and invokes saveAsNativeArtifact to create upgraded Gen2.1 copies while preserving originals. Two API audiences require Fabric api.fabric.microsoft.com for Gen2 items and analysis.windows.net powerbi api for Gen1 discovery and save-as. Workflows cover tenant scan, per-dataflow risk assessment, guarded execution with user confirmation, post-save-as integrity validation, and residual Gen1 reference detection. Cannot perform in-place migration, only save-as copy upgrade. Use when discovering Gen1 dataflows, assessing migration readiness, upgrading to Gen2.1, or validating post-save-as integrity.
- Gen1 to Gen2.1 save-as workflow with seven risk signal readiness assessment.
- Dual API audience auth for Fabric and Power BI REST endpoints.
- Tenant and workspace scan with Save-As Readiness Snapshot output.
- saveAsNativeArtifact execution preserving original Gen1 dataflow.
- Post-save-as integrity validation and residual Gen1 reference detection.
Dataflows Save As Authoring Cli by the numbers
- 89 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #592 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
dataflows-save-as-authoring-cli capabilities & compatibility
- Capabilities
- gen1 tenant and workspace scanning · seven risk signal readiness evaluation · dual api audience authentication · saveasnativeartifact guarded execution · post migration integrity validation
- Works with
- azure · power bi
- Use cases
- devops · data analysis
What dataflows-save-as-authoring-cli says it does
We currently cannot perform an in-place migration of your dataflow.
evaluate save-as readiness with seven risk signals
npx skills add https://github.com/microsoft/skills-for-fabric --skill dataflows-save-as-authoring-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 89 |
|---|---|
| repo stars | ★ 934 |
| Last updated | July 30, 2026 |
| Repository | microsoft/skills-for-fabric ↗ |
How do I assess and upgrade Power BI Gen1 dataflows to Fabric Gen2.1 copies safely?
Assess and execute Gen1 to Gen2.1 Dataflow save-as migrations with readiness risk signals via CLI.
Who is it for?
Data platform teams migrating Gen1 dataflows to Fabric Gen2.1 with CI/CD readiness gates.
Skip if: Skip for read-only dataflow inspection, new Gen2 authoring, or in-place migration which is unsupported.
When should I use this skill?
User asks to save Gen1 dataflow, convert to Gen2, assess dataflow readiness, or saveAsNativeArtifact migration.
What you get
Readiness snapshot with risk signals and upgraded Gen2.1 dataflow copies via saveAsNativeArtifact.
Files
Update Check — ONCE PER SESSION (mandatory)
The first time this skill is used in a session, run the check-updates skill before proceeding.
- GitHub Copilot CLI / VS Code: invoke the check-updates skill.- Claude Code / Cowork / Cursor / Windsurf / Codex: compare local vs remote package.json version.
- Skip if the check was already performed earlier in this session.
CRITICAL NOTES
1. To find the workspace details (including its ID) from workspace name: list all workspaces and, then, use JMESPath filtering
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 filtering
dataflows-save-as-authoring-cli — Dataflow Save-As Gen1 → Gen2.1 CI/CD via CLI
A save-as companion for creating upgraded Gen2.1 copies from Power BI Gen1 dataflows using readiness assessment and guarded execution.
We currently cannot perform an in-place migration of your dataflow. We can use save-as to create an upgraded Gen2.1 copy while preserving the original Gen1 dataflow.
Table of Contents
| Task | Reference | Notes |
|---|---|---|
| Finding Workspaces and Items in Fabric | COMMON-CLI.md § Finding Workspaces and Items | Mandatory — READ link first |
| Fabric Topology & Key Concepts | COMMON-CORE.md § Fabric Topology | |
| Environment URLs | COMMON-CORE.md § Environment URLs | |
| Authentication & Token Acquisition | COMMON-CORE.md § Authentication | Wrong audience = 401 |
| Core Control-Plane REST APIs | COMMON-CORE.md § Core REST APIs | Pagination, LRO polling, rate limits |
| Tool Selection Rationale | COMMON-CLI.md § Tool Selection | |
| Authentication Recipes | COMMON-CLI.md § Auth Recipes | az login flows and token acquisition |
Fabric Control-Plane API via az rest | COMMON-CLI.md § az rest | Always pass `--resource` |
| Gotchas & Troubleshooting (CLI) | COMMON-CLI.md § Gotchas | az rest audience, shell escaping |
| Quick Reference | COMMON-CLI.md § Quick Ref | Token audience / tool matrix |
| Dataflow Definition Structure | DATAFLOWS-AUTHORING-CORE.md § Definition | 3-part format for Gen2 CI/CD |
| Consumption Capability Matrix | DATAFLOWS-CONSUMPTION-CORE.md § Capabilities | Read-only discovery patterns |
| Upgrade CLI Quick Reference | upgrade-cli-quickref.md | All az rest one-liners for scanning & save-as |
| Risk Assessment Guide | risk-assessment-guide.md | Risk signal detection logic & API calls |
---
Tool Stack
| Tool | Role | Install |
|---|---|---|
az CLI | Primary: Auth (az login), REST API calls (az rest) against both Fabric and Power BI APIs. | Pre-installed in most dev environments |
jq | Parse and filter JSON responses (dataflow lists, risk signal extraction). | Pre-installed or trivial |
base64 | Decode dataflow definitions for inspection. | Built into bash / [Convert]::ToBase64String() in PowerShell |
Agent check — verify before first operation:
```bash
az --version 2>/dev/null || echo "INSTALL: https://aka.ms/install-azure-cli"
jq --version 2>/dev/null || echo "INSTALL: apt-get install jq OR brew install jq"
```
---
Authentication & API Audiences
This skill uses two distinct API audiences. Using the wrong audience returns 401.
| API | Audience (--resource) | Use For |
|---|---|---|
| Fabric Items API | https://api.fabric.microsoft.com | List Gen2 dataflows (Fabric-native), workspace discovery |
| Power BI REST API | https://analysis.windows.net/powerbi/api | Gen1 dataflow discovery, saveAsNativeArtifact, data sources, upstream dataflows, Admin API scanning |
# Fabric Items API — list Gen2 dataflows in a workspace
az rest --method get \
--resource "https://api.fabric.microsoft.com" \
--url "https://api.fabric.microsoft.com/v1/workspaces/$WS_ID/dataflows"
# Power BI REST API — list all dataflows (Gen1 + Gen2) in a workspace
az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows"
# Power BI Admin API — list all dataflows tenant-wide (requires admin role)
az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/admin/dataflows"---
Phase 1 — Awareness & Readiness
Goal: "Should I use save-as, and what will happen when I create a Gen2.1 copy?"
Agentic Workflow: Discover → Assess → Classify → Report
Follow this sequence for every save-as assessment:
1. Discover — Scan workspace(s) to inventory all dataflows, identifying Gen1 vs Gen2 2. Assess — For each Gen1 dataflow, check seven risk signals 3. Classify — Assign a readiness category: ✅ Safe / ⚠️ Manual followups / ❌ Blocked 4. Report — Output a Save-As Readiness Snapshot (markdown table + JSON)
Step 1: Discover — Identify Gen1 Dataflows
The Power BI REST API returns a generation property (value 1 or 2) on each dataflow. This is the preferred detection method — a single API call per workspace.
Non-Admin Path (per workspace)
WS_ID="<workspaceId>"
RESOURCE_PBI="https://analysis.windows.net/powerbi/api"
# List all dataflows — the `generation` property distinguishes Gen1 from Gen2
ALL_DATAFLOWS=$(az rest --method get \
--resource "$RESOURCE_PBI" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows" \
--query "value[].{id:objectId, name:name, generation:generation, modelUrl:modelUrl, configuredBy:configuredBy}" -o json)
# Filter Gen1 dataflows
echo "$ALL_DATAFLOWS" | jq '[.[] | select(.generation == 1)]'
# Filter Gen2 dataflows
echo "$ALL_DATAFLOWS" | jq '[.[] | select(.generation == 2)]'Tip: AmodelUrlpointing todfs.core.windows.netadditionally indicates BYOSA (customer-managed storage) — a save-as blocker.
Admin Path (tenant-wide)
Requires Fabric administrator role or service principal with Tenant.Read.All scope. Rate limited to 200 requests/hour.
RESOURCE_PBI="https://analysis.windows.net/powerbi/api"
# List ALL dataflows in the tenant
ADMIN_DATAFLOWS=$(az rest --method get \
--resource "$RESOURCE_PBI" \
--url "https://api.powerbi.com/v1.0/myorg/admin/dataflows" \
--query "value[].{id:objectId, name:name, workspaceId:workspaceId, modelUrl:modelUrl, configuredBy:configuredBy}" \
-o json)
# Filter Gen1 dataflows — those with a modelUrl indicate CDM/Gen1 storage
# Note: Admin API may not expose the `generation` property; use modelUrl as fallback
echo "$ADMIN_DATAFLOWS" | jq '[.[] | select(.modelUrl != null and .modelUrl != "")]'Note: The Admin API supports$filter,$top, and$skipfor pagination on large tenants.
Step 2: Assess — Check Risk Signals
For each Gen1 dataflow found, evaluate seven risk signals. See risk-assessment-guide.md for detailed API calls.
| # | Risk Signal | Detection Method | Impact |
|---|---|---|---|
| 1 | Incremental refresh | Check dataflow definition for incremental refresh policy configuration | ⚠️ Schedule migrates in disabled state; must re-enable and validate |
| 2 | BYOSA / Custom ADLS Gen2 storage | Check modelUrl — if points to customer storage account (not Power BI managed) | ❌ Data stays in old storage; Gen2 CI/CD uses Fabric-managed storage |
| 3 | Power Automate / API triggers | Check for external orchestration referencing the Gen1 dataflow ID | ⚠️ All integrations must update to new Gen2 artifact ID |
| 4 | Downstream pipeline dependencies | Check Fabric pipelines for dataflow activity references | ⚠️ Pipeline activities reference dataflow by ID; must re-bind |
| 5 | Linked / computed entities | Inspect dataflow definition for entity references to other dataflows | ⚠️/❌ Cross-dataflow references may break if source dataflows are not saved first |
| 6 | DirectQuery connections | Inspect data source types in definition | ❌ DirectQuery not supported in Gen2 CI/CD dataflows |
| 7 | Caller is not owner / insufficient role | Compare configuredBy against az account show --query user.name -o tsv — or attempt call and catch DataflowUnauthorizedError | ❌ saveAsNativeArtifact requires the caller to be the dataflow owner or have Contributor/Admin in the source workspace; Viewer/Member without ownership cannot execute save-as |
Step 3: Classify — Readiness Categories
| Category | Criteria | Action |
|---|---|---|
| ✅ Safe | No risk signals detected | Create a Gen2.1 save-as copy with saveAsNativeArtifact |
| ⚠️ Manual followups | Risk signals 1, 3, 4, or 5 (non-blocking) | Execute save-as, then remediate flagged issues |
| ❌ Blocked | Risk signals 2, 6, or 7 (blocking) | Cannot execute save-as until blocker is resolved |
Tip — detect ownership before save-as: TheconfiguredByfield in the dataflow list response contains the owner's email. Compare it against the currently logged-in user (az account show --query user.name -o tsv). If they don't match and your workspace role is below Contributor, flag the dataflow as ❌ Blocked (signal 7) and escalate to the owner.
Step 4: Report — Save-As Readiness Snapshot
Markdown Output (terminal)
## Save-As Readiness Snapshot
| Workspace | Dataflow | Type | Readiness | Risk Signals | Recommendation |
|---|---|---|---|---|---|
| Sales Analytics | SalesETL | Gen1 | ✅ Safe | None | Save as Gen2.1 copy now |
| Sales Analytics | CustomerLoad | Gen1 | ⚠️ Manual | Incremental refresh, Pipeline dep | Save as Gen2.1 copy, then re-enable schedule & update pipeline |
| Finance | FinanceDaily | Gen1 | ❌ Blocked | BYOSA storage | Resolve storage dependency first |JSON Output (automation)
{
"snapshotDate": "2025-04-13T10:00:00Z",
"summary": { "total": 3, "safe": 1, "manual": 1, "blocked": 1 },
"dataflows": [
{
"workspaceName": "Sales Analytics",
"workspaceId": "...",
"dataflowName": "SalesETL",
"dataflowId": "...",
"type": "Gen1",
"readiness": "safe",
"riskSignals": [],
"recommendation": "Save as Gen2.1 copy now",
"saveAsPath": "saveAsNativeArtifact"
}
]
}Save JSON to file: pipe to jq '.' > readiness-snapshot.json---
Execute with Guardrails
Goal: Invoke save-as and capture outcomes safely.
Gen1 → Gen2.1 CI/CD: saveAsNativeArtifact API
POST https://api.powerbi.com/v1.0/myorg/groups/{groupId}/dataflows/{gen1DataflowId}/saveAsNativeArtifactThis is a Preview API. It creates a new Gen2.1 CI/CD artifact copy while preserving the original Gen1 dataflow.
WS_ID="<workspaceId>"
GEN1_ID="<gen1DataflowId>"
# Write body to a temp file — az rest wraps inline --body in an envelope
# on some platforms, causing "saveAsRequest is a required parameter" errors.
cat > /tmp/save-as-body.json <<'EOF'
{
"displayName": "MyDataflow_Gen2CICD",
"description": "Saved as Gen2.1 copy from Gen1",
"includeSchedule": true,
"targetWorkspaceId": "<targetWorkspaceId>"
}
EOF
az rest --method post \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$GEN1_ID/saveAsNativeArtifact" \
--headers "Content-Type=application/json" \
--body @/tmp/save-as-body.jsonGotcha — inline body: Passing JSON inline via--body '{...}'can causeaz restto wrap the payload in an extra envelope, resulting in"saveAsRequest is a required parameter"errors. Always use file-based body (--body @file.json) for this endpoint.
Gotcha — Windows `az.cmd`: On Windows, omit-o jsonfromsaveAsNativeArtifactcalls — the flag produces"A value that is not valid (json) was specified for the outputFormat parameter"when routed throughaz.cmd. Capture output without-o jsonand parse withConvertFrom-Jsonin PowerShell, or pipe tojqin bash.
Gotcha — not idempotent (duplicate artifacts on retry):saveAsNativeArtifactcreates a new artifact every time it is called. If a batch is interrupted and re-run, you will end up with multiple copies in the target workspace. To make retries safe: (1) check whether a Gen2 artifact with the intended name already exists before calling, or (2) include a timestamp indisplayNameand treat each run as a distinct artifact.
Gotcha — owner permissions: You must be the dataflow owner or have Contributor/Admin in the source workspace to callsaveAsNativeArtifact. If you are only a Viewer or Workspace Member who does not own the dataflow, the API returnsDataflowUnauthorizedError. Ask the dataflow owner or a workspace admin to run the save-as operation for those dataflows.
Request parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
displayName | string (max 200) | No | Name for new artifact. Auto-generated with _copy1 suffix if omitted |
description | string (max 4000) | No | Description. Copied from source if omitted |
includeSchedule | boolean | No | Copy refresh schedule in disabled state |
targetWorkspaceId | string (uuid) | No | Target workspace. Same workspace if omitted |
Response: 200 OK with SaveAsNativeDataflowResponse:
artifactMetadata— full metadata of the new Gen2 CI/CD artifact (includingobjectId,provisionState)errors[]— non-fatal warning codes (save-as succeeds even if these occur):FailedToCopySchedule— schedule could not be copiedSetDataflowOriginFailed— origin tracking not setConnectionsUpdateFailed— connection strings could not be updated to Fabric format
Gen2 → Gen2 CI/CD: In-Place Upgrade
NOT YET AVAILABLE — This API is not available in the current public surface. This skill will be updated when the endpoint is published. Do not attempt to call a non-existent endpoint.
Post-Save-As Validation Checklist
Run these checks after save-as before any Gen1 cleanup:
- Confirm
artifactMetadata.provisionStatereachesActive. - Review
errors[]inSaveAsNativeDataflowResponseand create follow-up tasks for each warning. - Confirm the new artifact exists in the target workspace and has expected name/description.
- Verify dependent orchestration (pipelines, flows, API callers) is updated to the new artifact ID.
- Only trigger refresh when the user explicitly approves.
---
Must/Prefer/Avoid
MUST DO
- Always pass `--resource` to
az rest— use the correct audience per the API table above. Wrong audience = silent 401. - Always include `--headers "Content-Type=application/json"` on POST calls to the Power BI REST API.
- Use file-based body for `saveAsNativeArtifact` — pass
--body @file.jsoninstead of inline JSON. Inline--body '{...}'can causeaz restto wrap the payload in an extra envelope, producing"saveAsRequest is a required parameter"errors. - On Windows, omit `-o json` on `saveAsNativeArtifact` calls — use
ConvertFrom-Jsonin PowerShell or pipe tojqinstead. The-o jsonflag fails with"A value that is not valid (json)"error when routed throughaz.cmd. - Verify you are the dataflow owner or Contributor before save-as —
saveAsNativeArtifactreturnsDataflowUnauthorizedErrorfor non-owners who are only Workspace Members or Viewers. - Check for existing Gen2 artifacts before retrying —
saveAsNativeArtifactis not idempotent; interrupted batch runs create duplicate copies on retry. Either verify the target name is absent before calling, or use a unique timestampeddisplayNameper run. - Scan before save-as — always run the readiness scan before execution.
- Never refresh without explicit user consent — the Gen2 CI/CD artifact schedule is created in disabled state for safety.
- Check `errors[]` in saveAsNativeArtifact response — save-as may succeed with non-fatal warnings.
- Verify `provisionState` is `Active` after save-as — poll the artifact metadata until terminal state.
- Preserve the original Gen1 dataflow —
saveAsNativeArtifactleaves the Gen1 intact. Do not delete it until post-save-as validation passes.
PREFER
- Admin API for tenant-wide scanning — more efficient than workspace-by-workspace for large tenants.
- JSON output for automation — markdown is for human review, JSON for scripting and CI/CD integration.
- Topological save-as order — save upstream dataflows (with linked entities) before downstream consumers.
- Readiness-first execution — do not skip readiness assessment before save-as.
- `generation` property for Gen1 detection — the Power BI REST API returns
generation: 1orgeneration: 2on each dataflow. Use this single-API-call approach over cross-API comparison. - `modelUrl` check as secondary signal — dataflows with a
modelUrlpointing todfs.core.windows.netare Gen1 using customer storage (BYOSA blocker).
AVOID
- Saving blocked (❌) dataflows — resolve BYOSA storage and DirectQuery issues first.
- Batch save-as without review — always review the readiness snapshot before saving multiple dataflows.
- Hardcoded workspace/dataflow IDs — resolve dynamically via REST API.
- Ignoring non-fatal errors —
FailedToCopySchedule,ConnectionsUpdateFailed, etc. require manual follow-up. - Calling the Gen2→Gen2 CI/CD in-place save-as API — it does not exist yet. Do not fabricate endpoints.
- Deleting Gen1 artifacts without explicit user consent — never delete Gen1 dataflows unless the user explicitly approves deletion, even after post-save-as validation confirms data integrity.
- Using `-o json` with `saveAsNativeArtifact` on Windows — omit the flag; capture raw output and parse it explicitly.
- Re-running batch save-as scripts without idempotency checks — always verify the target artifact doesn't already exist to avoid duplicate copies in the workspace.
---
Examples
Example 1: Full Phase 1 — Readiness Scan for a Single Workspace
Prompt: "Scan my Sales workspace for Gen1 dataflows and show me a readiness snapshot"
Agent response:
# Step 1 — Resolve workspace ID
WS_ID=$(az rest --method get \
--resource "https://api.fabric.microsoft.com" \
--url "https://api.fabric.microsoft.com/v1/workspaces" \
--query "value[?displayName=='Sales'].id" -o tsv)
# Step 2 — List all dataflows with generation property
ALL_DFS=$(az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows" -o json)
# Step 3 — Filter Gen1 dataflows by generation property
GEN1_DFS=$(echo "$ALL_DFS" | jq '[.value[] | select(.generation == 1) |
{id: .objectId, name: .name, generation: .generation, modelUrl: .modelUrl}]')
echo "Gen1 dataflows found: $(echo $GEN1_DFS | jq length)"
echo "$GEN1_DFS" | jq '.'Example 2: Save-As a Gen1 Dataflow to Gen2.1
Prompt: "Migrate or upgrade the SalesETL Gen1 dataflow in my Sales workspace"
Agent response:
WS_ID="<workspaceId>"
GEN1_ID="<gen1DataflowId>"
# Write body to file (avoids az rest envelope wrapping)
cat > /tmp/save-as-body.json <<'EOF'
{"displayName": "SalesETL_Gen2CICD", "includeSchedule": true}
EOF
# Execute save-as
RESPONSE=$(az rest --method post \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$GEN1_ID/saveAsNativeArtifact" \
--headers "Content-Type=application/json" \
--body @/tmp/save-as-body.json)
# Check result
echo "$RESPONSE" | jq '{
newId: .artifactMetadata.objectId,
state: .artifactMetadata.provisionState,
errors: .errors
}'Example 3: Tenant-Wide Admin Scan
Prompt: "Show me all Gen1 dataflows across the entire tenant"
Agent response:
# Requires Fabric administrator role
ALL_DFS=$(az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/admin/dataflows" -o json)
# Gen1 dataflows have a modelUrl
echo "$ALL_DFS" | jq '[.value[] | select(.modelUrl != null and .modelUrl != "") |
{id: .objectId, name: .name, workspace: .workspaceId,
owner: .configuredBy, storage: .modelUrl}]'Risk Assessment Guide — Dataflow Save-As (Gen2.1 Copy)
Detailed detection logic, API calls, and classification criteria for each of the seven risk signals evaluated during Phase 1 (Readiness Assessment).
This guide assumes saveAsNativeArtifact behavior: no in-place migration is available; save-as creates an upgraded Gen2.1 copy while preserving the original Gen1 dataflow.
---
Risk Signal Overview
| # | Signal | Severity | Detection API | Key Indicator |
|---|---|---|---|---|
| 1 | Incremental refresh | ⚠️ Warning | Power BI Dataflows — Get Dataflow | entities[].ppiConfig present |
| 2 | BYOSA / Custom ADLS Gen2 storage | ❌ Blocker | Power BI Dataflows — Get Dataflows / Admin API | modelUrl → customer dfs.core.windows.net |
| 3 | Power Automate / API triggers | ⚠️ Warning | Manual / Power Automate inventory | External refresh triggers referencing Gen1 ID |
| 4 | Downstream pipeline dependencies | ⚠️ Warning | Fabric Pipelines API | Pipeline activities referencing dataflow ID |
| 5 | Linked / computed entities | ⚠️/❌ | Power BI Dataflows — Get Upstream Dataflows | upstreamDataflows[] non-empty |
| 6 | DirectQuery connections | ❌ Blocker | Power BI Dataflows — Get Dataflow (definition) | entities[].partitions[].mode == "directQuery" |
| 7 | Caller is not owner / insufficient role | ❌ Blocker | Power BI Dataflows — List Dataflows (configuredBy) + workspace role check | configuredBy differs from current user AND workspace role < Contributor |
---
Signal 1: Incremental Refresh (PPI)
What It Is
Gen1 dataflows can be configured with incremental refresh policies (PPI — Power Platform Incremental). This policy defines how much historical data to retain and how to refresh incrementally. The PPI configuration may need reconfiguration or verification after save-as to ensure it works the same way in Gen2 CI/CD format.
Note: Schedule copying (disabled state) is universal to all save-as operations via the includeSchedule parameter — this is not specific to PPI. The risk signal here is about the PPI policy configuration itself, not the schedule state.Detection
WS_ID="<workspaceId>"
DF_ID="<gen1DataflowId>"
# Export the full definition and check for PPI config
DEFINITION=$(az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$DF_ID" -o json)
# Check for incremental refresh policies (ppiConfig)
echo "$DEFINITION" | jq '[.entities[]? | select(.ppiConfig != null) |
{entity: .name, incrementalRefreshPolicy: .ppiConfig}]'Classification
- Empty result → No incremental refresh policy → safe from this signal
- Entities with `ppiConfig` → ⚠️ Warning: PPI configuration detected; verify policy behavior and performance after save-as
Remediation
1. After save-as, verify the new Gen2 CI/CD dataflow's incremental refresh policy is working as expected 2. Test a refresh cycle to confirm the incremental window (retention + new data) is correct 3. Adjust retention periods or incremental logic if needed for Gen2 format
---
Signal 2: BYOSA / Custom ADLS Gen2 Storage
What It Is
Gen1 dataflows can use Bring Your Own Storage Account (BYOSA) — data is stored in the customer's ADLS Gen2 account instead of Power BI-managed storage. Gen2 CI/CD dataflows use Fabric-managed storage exclusively.
Detection
# From List Dataflows response — check modelUrl
az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows" \
--query "value[].{id:objectId, name:name, modelUrl:modelUrl}" -o json
# BYOSA indicator: modelUrl points to a customer storage account
# Pattern: https://<customAccount>.dfs.core.windows.net/powerbi/...
# Non-BYOSA: modelUrl is null, empty, or points to Power BI-managed storage# Using Admin API — tenant-wide BYOSA detection
az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/admin/dataflows" \
--query "value[?modelUrl!=null].{id:objectId, name:name, ws:workspaceId, storage:modelUrl}" -o jsonClassification
- `modelUrl` is null or Power BI-managed → No BYOSA → safe from this signal
- `modelUrl` points to `<customer>.dfs.core.windows.net` → ❌ Blocker: data lives in external storage; save-as will not move data
Remediation
1. Evaluate whether the BYOSA data can be moved to Fabric-managed storage 2. If data must stay in customer storage, save-as cannot proceed — keep as Gen1 or redesign the dataflow 3. Consider creating a new Gen2 dataflow from scratch that reads from the same ADLS source
---
Signal 3: Power Automate / API Triggers
What It Is
External systems (Power Automate flows, custom APIs, Logic Apps) may trigger refreshes on the Gen1 dataflow using its ID. After save-as, the Gen2 CI/CD artifact has a new ID — all triggers must be updated.
Detection
This signal cannot be fully detected via REST API alone. The agent should:
1. Ask the user: "Are there Power Automate flows or external APIs that trigger refresh on this dataflow?" 2. Check refresh history for API-initiated refreshes:
# Check transaction history for externally triggered refreshes
az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$DF_ID/transactions" \
--query "value[?refreshType=='OnDemand' || refreshType=='ViaApi']" -o jsonClassification
- No external triggers confirmed → safe from this signal
- External triggers exist → ⚠️ Warning: all triggers must be updated to use the new Gen2 CI/CD artifact ID
Remediation
1. Note the new artifact ID from the saveAsNativeArtifact response 2. Update all Power Automate flows to reference the new ID 3. Update any custom API integrations 4. Test each trigger against the new artifact
---
Signal 4: Downstream Pipeline Dependencies
What It Is
Fabric Data Factory pipelines may have dataflow activities that reference the Gen1 dataflow by ID. After save-as, pipeline activities must be re-bound to the new Gen2 CI/CD artifact.
Detection
# List pipelines in the workspace
PIPELINES=$(az rest --method get \
--resource "https://api.fabric.microsoft.com" \
--url "https://api.fabric.microsoft.com/v1/workspaces/$WS_ID/dataPipelines" \
--query "value[].{id:id, name:displayName}" -o json)
# For each pipeline, get its definition and search for the dataflow ID
# (Pipeline definitions reference dataflow activities by artifact ID)Note: Full pipeline definition inspection requires the getDefinition API on each pipeline. For large workspaces, this may be time-consuming.Classification
- No pipelines reference this dataflow → safe from this signal
- Pipelines reference this dataflow → ⚠️ Warning: pipeline activities must be updated to the new Gen2 CI/CD artifact ID
Remediation
1. After save-as, update each referencing pipeline's dataflow activity to point to the new artifact ID 2. Test pipeline execution to verify the updated reference works
---
Signal 5: Linked / Computed Entities
What It Is
Gen1 dataflows can reference entities from other dataflows (linked entities) or compute entities based on other entities in the same or different dataflows. If the source dataflow isn't saved first, linked entities in the saved dataflow may break.
Detection
# Check upstream dataflow dependencies
UPSTREAM=$(az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$DF_ID/upstreamDataflows" -o json)
echo "$UPSTREAM" | jq '.'# Also check the definition for entity references
DEFINITION=$(az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$DF_ID" -o json)
# Look for linked entity references
echo "$DEFINITION" | jq '[.entities[]? | select(.linkedEntityRef != null) |
{entity: .name, linkedTo: .linkedEntityRef}]'Classification
- No upstream dataflows and no linked entity refs → safe from this signal
- Upstream dataflows exist, all already Gen2 → ⚠️ Warning: verify references after save-as
- Upstream dataflows exist, some still Gen1 → ❌ Blocker: must save upstream dataflows first
Remediation
1. Build a dependency graph of all linked dataflows 2. Save in topological order: upstream first, then downstream 3. After each save-as, verify linked entity references still resolve
---
Signal 6: DirectQuery Connections
What It Is
Gen1 dataflows can use DirectQuery mode for certain data sources. Gen2 CI/CD dataflows do not support DirectQuery — they only support Import mode with optional staging (Fast Copy).
Detection
# Export the definition and check partition modes
DEFINITION=$(az rest --method get \
--resource "https://analysis.windows.net/powerbi/api" \
--url "https://api.powerbi.com/v1.0/myorg/groups/$WS_ID/dataflows/$DF_ID" -o json)
# Check for DirectQuery partitions
echo "$DEFINITION" | jq '[.entities[]? | .partitions[]? |
select(.mode == "directQuery") | {entity: .name, mode: .mode}]'Classification
- No DirectQuery partitions → safe from this signal
- DirectQuery partitions found → ❌ Blocker: DirectQuery not supported in Gen2 CI/CD
Remediation
1. Convert DirectQuery entities to Import mode before save-as 2. Alternatively, redesign the dataflow to use Import with scheduled refresh 3. If DirectQuery is required, the dataflow cannot be saved — keep as Gen1
---
Composite Risk Assessment
After checking all seven signals, combine results into a final readiness classification:
| Rule | Result |
|---|---|
| Any ❌ Blocker present | ❌ Blocked — resolve blockers before save-as |
| Only ⚠️ Warnings present | ⚠️ Manual followups — can save, but remediation needed |
| No signals detected | ✅ Safe — save immediately |
Assessment Script Pattern
# Initialize
READINESS="safe"
SIGNALS="[]"
# Check each signal and accumulate
# (See individual signal sections above for API calls)
# Signal 2: BYOSA check
MODEL_URL=$(echo "$PBI_DATAFLOW" | jq -r '.modelUrl // empty')
if [ -n "$MODEL_URL" ]; then
READINESS="blocked"
SIGNALS=$(echo "$SIGNALS" | jq '. + [{"signal":"BYOSA storage","severity":"blocker","detail":"modelUrl: '$MODEL_URL'"}]')
fi
# Signal 6: DirectQuery check
DQ_COUNT=$(echo "$DEFINITION" | jq '[.entities[]?.partitions[]? | select(.mode == "directQuery")] | length')
if [ "$DQ_COUNT" -gt 0 ]; then
READINESS="blocked"
SIGNALS=$(echo "$SIGNALS" | jq '. + [{"signal":"DirectQuery","severity":"blocker","detail":"'$DQ_COUNT' DirectQuery partitions found"}]')
fi
# Signal 5: Linked entities check
UPSTREAM_COUNT=$(echo "$UPSTREAM" | jq '. | length')
if [ "$UPSTREAM_COUNT" -gt 0 ]; then
if [ "$READINESS" != "blocked" ]; then READINESS="manual"; fi
SIGNALS=$(echo "$SIGNALS" | jq '. + [{"signal":"Linked entities","severity":"warning","detail":"'$UPSTREAM_COUNT' upstream dataflow dependencies"}]')
fi
# ... (repeat for signals 1, 3, 4)
echo "Readiness: $READINESS"
echo "Signals: $SIGNALS" | jq '.'Dataflow Save-As (Gen2.1 Copy) — CLI Quick Reference
One-liner az rest patterns for scanning, assessment, and save-as operations.
---
Authentication Setup
# Set API audiences
RESOURCE_FABRIC="https://api.fabric.microsoft.com"
RESOURCE_PBI="https://analysis.windows.net/powerbi/api"
API_FABRIC="https://api.fabric.microsoft.com/v1"
API_PBI="https://api.powerbi.com/v1.0/myorg"---
Workspace Discovery
# List all workspaces — resolve ID by name
az rest --method get --resource "$RESOURCE_FABRIC" \
--url "$API_FABRIC/workspaces" \
--query "value[?displayName=='MyWorkspace'].id" -o tsv
# List all workspaces (Admin API — tenant-wide, requires admin role)
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/admin/groups?\$top=5000" -o json---
Gen1 Dataflow Detection
The Power BI REST API returns a generation property (1 or 2) on each dataflow. This is the preferred detection method.
Per-Workspace Detection (Non-Admin)
WS_ID="<workspaceId>"
# List all dataflows — use `generation` property to distinguish Gen1 from Gen2
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows" \
--query "value[].{id:objectId, name:name, generation:generation, modelUrl:modelUrl}" -o json
# Filter Gen1 only
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows" \
--query "value[?generation==\`1\`].{id:objectId, name:name, modelUrl:modelUrl}" -o json
# Filter Gen2 only
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows" \
--query "value[?generation==\`2\`].{id:objectId, name:name}" -o jsonTenant-Wide Detection (Admin)
# All dataflows in tenant — Admin API may not expose `generation`; use modelUrl as fallback
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/admin/dataflows" \
--query "value[?modelUrl!=null].{id:objectId, name:name, ws:workspaceId, owner:configuredBy, storage:modelUrl}" \
-o json
# Paginate large tenants
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/admin/dataflows?\$top=200&\$skip=0" -o json---
Risk Signal Detection
Data Sources (per dataflow)
DF_ID="<dataflowId>"
# Get data sources for a dataflow
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows/$DF_ID/datasources" -o jsonUpstream Dataflow Dependencies
# Check for linked/computed entities (upstream dataflows)
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows/$DF_ID/upstreamDataflows" -o jsonDataflow Transactions (recent refresh status)
# Get recent transactions — check for incremental refresh patterns
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows/$DF_ID/transactions" -o jsonDataflow Definition (full JSON export)
# Export dataflow definition — inspect for DirectQuery, incremental refresh config
az rest --method get --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows/$DF_ID" -o json > dataflow-definition.json
# Check for incremental refresh policies in the definition
cat dataflow-definition.json | jq '.entities[]? | select(.ppiConfig != null) | {name: .name, incrementalRefresh: .ppiConfig}'
# Check for DirectQuery data source types
cat dataflow-definition.json | jq '.entities[]?.partitions[]? | select(.mode == "directQuery") | {entity: .name, mode: .mode}'---
Save-As Execution
saveAsNativeArtifact (Gen1 → Gen2.1 CI/CD)
saveAsNativeArtifact does not do an in-place migration. It creates an upgraded Gen2.1 copy and keeps the original Gen1 dataflow.
Gotcha — inline body: Pass the request body via file (--body @file.json), not inline. Inline--body '{...}'can causeaz restto wrap the payload in an extra envelope, producing"saveAsRequest is a required parameter"errors.
Gotcha — Windows `az.cmd`: Omit-o jsonfrom this call on Windows. The flag produces"A value that is not valid (json) was specified for the outputFormat parameter"when routed throughaz.cmd. Capture output without-o jsonand parse withConvertFrom-Json(PowerShell) orjq(bash).
Gotcha — not idempotent: Each call creates a new artifact. If a batch run is interrupted and retried, duplicate artifacts appear in the target workspace. Before calling, verify no Gen2 artifact with the intended name already exists, or use a unique timestamped displayName per run.Gotcha — owner permissions: Requires you to be the dataflow owner or have Contributor/Admin in the source workspace. Non-owners receive DataflowUnauthorizedError.GEN1_ID="<gen1DataflowId>"
# Basic — same workspace, auto-generated name
echo '{}' > /tmp/save-as-body.json
az rest --method post --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows/$GEN1_ID/saveAsNativeArtifact" \
--headers "Content-Type=application/json" \
--body @/tmp/save-as-body.json
# Full options — custom name, target workspace, include schedule
# Use a unique displayName to avoid duplicates on retry (e.g., append a timestamp)
TS=$(date -u +%Y%m%d%H%M%S)
cat > /tmp/save-as-body.json <<EOF
{
"displayName": "MyDataflow_Gen2CICD_${TS}",
"description": "Saved as Gen2.1 copy from Gen1",
"includeSchedule": true,
"targetWorkspaceId": "<targetWorkspaceId>"
}
EOF
az rest --method post --resource "$RESOURCE_PBI" \
--url "$API_PBI/groups/$WS_ID/dataflows/$GEN1_ID/saveAsNativeArtifact" \
--headers "Content-Type=application/json" \
--body @/tmp/save-as-body.json
# PowerShell — parse output without -o json (Windows az.cmd compatible)
$body = @{ displayName = "MyDataflow_Gen2CICD"; includeSchedule = $true; targetWorkspaceId = "<targetWorkspaceId>" }
$tmp = [System.IO.Path]::GetTempFileName()
$body | ConvertTo-Json | Set-Content $tmp
$response = az rest --method post --resource $RESOURCE_PBI `
--url "$API_PBI/groups/$WS_ID/dataflows/$GEN1_ID/saveAsNativeArtifact" `
--headers "Content-Type=application/json" `
--body "@$tmp" | ConvertFrom-Json
$response.artifactMetadata.objectId # new Gen2 CI/CD artifact ID
# Parse response (bash/jq)
# .artifactMetadata.objectId — new Gen2 CI/CD artifact ID
# .artifactMetadata.provisionState — should be "Active"
# .errors[] — non-fatal warnings (FailedToCopySchedule, ConnectionsUpdateFailed, etc.)---
Post-Save-As Verification
NEW_ID="<newGen2CicdArtifactId>"
# Verify the new artifact exists in Fabric API
az rest --method get --resource "$RESOURCE_FABRIC" \
--url "$API_FABRIC/workspaces/$WS_ID/dataflows/$NEW_ID" -o json
# Get the new artifact's definition (Gen2 CI/CD — 3-part format)
az rest --method post --resource "$RESOURCE_FABRIC" \
--url "$API_FABRIC/workspaces/$WS_ID/dataflows/$NEW_ID/getDefinition" \
--headers "Content-Type=application/json" -o json---
Readiness Snapshot JSON Schema
{
"$schema": "readiness-snapshot",
"snapshotDate": "ISO-8601 datetime",
"scanScope": "workspace | tenant",
"scanTarget": "workspace name or 'tenant-wide'",
"summary": {
"total": 0,
"safe": 0,
"manual": 0,
"blocked": 0
},
"dataflows": [
{
"workspaceName": "string",
"workspaceId": "uuid",
"dataflowName": "string",
"dataflowId": "uuid",
"type": "Gen1",
"owner": "email",
"modelUrl": "URL or null",
"readiness": "safe | manual | blocked",
"riskSignals": [
{
"signal": "string",
"severity": "warning | blocker",
"detail": "string"
}
],
"recommendation": "string",
"saveAsPath": "saveAsNativeArtifact"
}
]
}---
Agent Integration Notes
GitHub Copilot CLI / VS Code
- Use
az restfor all API calls — no extra dependencies needed. - Pipe JSON output through
jqfor filtering and formatting. - Store readiness snapshot to file with
> readiness-snapshot.json.
Claude Code / Cursor / Windsurf / Codex
- Same
az restpatterns apply. - For large tenant scans, implement pagination with
$topand$skip. - The Admin API is rate-limited to 200 req/hour — batch assessment calls.
PowerShell Alternative
# Fabric API — list workspaces
$headers = @{ Authorization = "Bearer $(az account get-access-token --resource 'https://api.fabric.microsoft.com' --query accessToken -o tsv)" }
$workspaces = Invoke-RestMethod -Uri "https://api.fabric.microsoft.com/v1/workspaces" -Headers $headers
# Power BI API — list dataflows
$pbiHeaders = @{ Authorization = "Bearer $(az account get-access-token --resource 'https://analysis.windows.net/powerbi/api' --query accessToken -o tsv)" }
$dataflows = Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/groups/$wsId/dataflows" -Headers $pbiHeadersRelated skills
FAQ
What does dataflows-save-as-authoring-cli produce?
Readiness snapshot with seven risk signals and upgraded Gen2.1 copies via saveAsNativeArtifact API.
When should I use dataflows-save-as-authoring-cli?
When discovering Gen1 dataflows, assessing save-as readiness, or executing Gen1 to Gen2.1 upgrade.
Is dataflows-save-as-authoring-cli safe to install?
Review the Security Audits panel on this page before installing in production.