Pipeline Migration
MicrosoftOfficialPublished by Microsoft
- Skill
- Workflow
- Cloud and DevOps
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured, API-driven workflow for migrating Synapse Data Factory pipelines to Microsoft Fabric. It handles the transformation of pipeline JSON, including inlining datasets, converting linked services to Fabric connections, mapping SynapseNotebook activities to TridentNotebook, and migrating global parameters to Variable Libraries. The skill includes assessment, orchestration, and validation phases to ensure a smooth transition, while identifying unsupported activities that require manual intervention.
What it does
- Assesses Synapse pipeline complexity and migration scope.
- Automates the conversion of Synapse pipeline JSON to Fabric DataPipeline format.
- Inlines dataset definitions directly into pipeline activities.
- Maps Synapse linked services to Fabric connections.
- Converts global parameters into Fabric Variable Library items.
- Updates notebook activities to reference Fabric notebook GUIDs.
When to use it
- When migrating existing Synapse Data Factory pipelines to Microsoft Fabric.
- To assess the feasibility and blockers of a pipeline migration project.
- When you need to convert Synapse global parameters to Fabric Variable Libraries.
- When you need to update notebook activity references for the Fabric environment.
Advantages
- Reduces manual effort by automating JSON transformations and API calls.
- Provides a clear, phased approach to migration with built-in assessment.
- Ensures compatibility by mapping deprecated Synapse features to Fabric equivalents.
- Reduces errors by automating GUID lookups and connection references.
Usage examples
- “Assess my Synapse workspace 'FinancePipelineWS' for migration to Fabric.”
- The agent will query the Synapse workspace, analyze the pipelines, and generate a markdown report detailing the migration scope, potential blockers, and unsupported activities.
- “Migrate all pipelines from Synapse workspace 'FinancePipelineWS' to Fabric workspace 'FinanceFabricWS'.”
- The agent will orchestrate the migration, including creating connections, converting parameters, updating notebook references, and deploying the new pipelines to Fabric.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/microsoft/pipeline-migration/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip pipeline-migration.zip -d .agents/skills
Documentation by Microsoft
Telemetry — MANDATORY. Every
api.fabric.microsoft.comcall must carryx-ms-fabric-skill: pipeline-migration(az rest:--headers "x-ms-fabric-skill=pipeline-migration"), including every LRO poll,fabric_lroand retry. Snippets omit it — add it anyway.
CRITICAL NOTES
To find workspace details (including its ID) from a workspace name: list all workspaces, then use JMESPath filtering
To find item details (including its ID) from workspace ID, item type, and item name: list all items of that type in that workspace, then use JMESPath filtering
Fabric does NOT have a "Dataset" item type — all dataset properties are inlined into activity
typePropertiesLinked Services map to Fabric Connections — in pipeline activity JSON,
referenceNameon the activity'slinkedServiceblock uses the Fabric connection display name (not the connection GUID); the connection GUID is used only on Fabric REST API callsNotebook activities change from
SynapseNotebooktoTridentNotebookand reference notebooks by GUID, not by nameSynapse global parameters become a Variable Library item in Fabric, referenced as
@pipeline().libraryVariables.<name>. Variable LibraryNumbertypes are not consumable in pipelines — SynapseFloat/Doubleare mapped toStringfor runtime compatibilityThe
Validationactivity type does not exist in Fabric — it must be rewritten asGetMetadata+IfConditionTriggers are intentionally excluded from this skill — recreate schedules manually in Fabric after migration
SSIS package execution, SHIR-exclusive connectors, and Databricks activities are parked — see pipeline-gotchas.md
Synapse Pipelines → Microsoft Fabric Data Factory Migration
Prerequisite Knowledge
These companion documents provide general Fabric REST patterns. Do NOT read them upfront — reference only when a specific phase requires a pattern not already covered in this skill's resource files:
COMMON-CORE.md — General Fabric REST API patterns, authentication & token audiences, item discovery via JMESPath
COMMON-CLI.md —
az rest/az loginCLI patterns, authentication recipes, pipeline run/schedule operationsITEM-DEFINITIONS-CORE.md —
DataPipelineandVariableLibraryitem definition structures (pipeline-content.json, variables.json)SPARK-AUTHORING-CORE.md — Fabric notebook item creation (needed when notebook items don't exist yet in Fabric)
For the notebook side of the migration (the notebooks that pipeline activities call), use the companion synapse-migration skill to migrate the notebook content itself.
Table of Contents
| Topic | Reference |
|---|---|
| Pre-Migration Assessment (run first) | pipeline-assessment.md |
| Migration Orchestrator | pipeline-orchestrator.md |
| API-Driven Migration Workflow | § API-Driven Migration Workflow |
| Activity Type Mapping | activity-mapping.md |
| Notebook Activity Migration (primary focus) | notebook-activity-migration.md |
| Linked Services → Connections | linked-service-to-connection.md |
| Dataset Inlining | dataset-inlining.md |
| Global Parameters → Variable Library | global-parameters-to-variable-library.md |
| Pipeline Gotchas & Parked Activities | pipeline-gotchas.md |
| Validation & Testing | validation-testing.md |
| Migration Report | migration-report.md |
Context Loading Guide
IMPORTANT — Load only what you need. Do NOT read all resource files upfront.
| When | Read This File |
|---|---|
| User asks for an assessment, scope, or plan before migrating | pipeline-assessment.md |
| User asks to migrate a full pipeline workspace | pipeline-orchestrator.md |
| User asks about activity type mapping or unsupported activities | activity-mapping.md |
| User has SynapseNotebook activities (most common) | notebook-activity-migration.md |
| User asks about linked services or connections | linked-service-to-connection.md |
| User asks about Copy, Lookup, or GetMetadata with datasets | dataset-inlining.md |
| User has global parameters to convert | global-parameters-to-variable-library.md |
| User hits SSIS, SHIR, Databricks, or other blockers | pipeline-gotchas.md |
| Post-migration verification | validation-testing.md |
| Generating a migration summary | migration-report.md |
API-Driven Migration Workflow
Authentication
| Target | Token Audience |
|---|---|
| Synapse Data Plane (pipelines, datasets, linked services) | https://dev.azuresynapse.net |
| Synapse ARM (global parameters, workspace properties) | https://management.azure.com |
| Fabric REST API (create pipelines, connections, Variable Libraries) | https://api.fabric.microsoft.com |
Use
az account get-access-token --resource <audience> --query accessToken -o tsvto acquire tokens.
Synapse Data-Plane API Reference
| Operation | Endpoint |
|---|---|
| List all pipelines | GET https://{ws}.dev.azuresynapse.net/pipelines?api-version=2020-12-01 |
| Get pipeline definition | GET https://{ws}.dev.azuresynapse.net/pipelines/{name}?api-version=2020-12-01 |
| List all datasets | GET https://{ws}.dev.azuresynapse.net/datasets?api-version=2020-12-01 |
| Get dataset definition | GET https://{ws}.dev.azuresynapse.net/datasets/{name}?api-version=2020-12-01 |
| List all linked services | GET https://{ws}.dev.azuresynapse.net/linkedservices?api-version=2020-12-01 |
| Get linked service | GET https://{ws}.dev.azuresynapse.net/linkedservices/{name}?api-version=2020-12-01 |
| Get workspace (global parameters) | GET https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Synapse/workspaces/{ws}?api-version=2021-06-01 |
Fabric API Reference
| Operation | Endpoint |
|---|---|
| List connections | GET https://api.fabric.microsoft.com/v1/connections |
| Create connection | POST https://api.fabric.microsoft.com/v1/connections |
| Create pipeline item | POST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items |
| Update pipeline definition | POST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items/{id}/updateDefinition |
| Get pipeline definition | POST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items/{id}/getDefinition |
| Create Variable Library | POST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items (type: VariableLibrary) |
| List notebooks in workspace | GET https://api.fabric.microsoft.com/v1/workspaces/{wsId}/notebooks |
Assessment Mode (Optional but Recommended)
Before creating any items in Fabric, run the pipeline assessment to understand scope, complexity, and blockers. The assessment is read-only — it queries Synapse APIs only and produces a markdown report.
Copilot workflow — no Python script file needed:
Ask the user: "What is the name of your Synapse workspace?"
Auto-discover subscription ID and resource group via
az account showandaz synapse workspace showRun the assessment code inline from the terminal
Print the full report output directly in the chat
| When to Use | Action |
|---|---|
| User wants to understand migration scope before committing | Ask for workspace name → load pipeline-assessment.md → run inline |
| User asks "what will and won't migrate?" | Run assessment, present the Executive Summary section |
| User asks for a migration plan or scoping document | Run assessment, print report in chat |
| User has already decided to migrate | Skip assessment — go straight to Migration Phases below |
To also save the report to disk, pass
output_path=f"pipeline-assessment-{SYNAPSE_WS}.md"togenerate_assessment_report(). ThePipelineAssessmentobjects it produces feed directly into the migration scripts in pipeline-orchestrator.md.
Migration Mode (Inline — No Script File Required)
Copilot performs the migration directly from the terminal. No Python files to save or run manually.
Ask the user for:
Synapse workspace name (reuse from assessment if already run)
Fabric workspace name
Which pipelines to migrate — specific names, or
*for allOptional name suffix to append to each pipeline in Fabric (e.g.
_migrated) — leave blank to keep the original name
Everything else is auto-discovered:
Subscription ID and resource group via
az account show+az synapse workspace showFabric workspace ID via
GET /v1/workspacesfiltered by display nameNotebook GUIDs in Fabric via
GET /v1/workspaces/{wsId}/notebooks(required forSynapseNotebook → TridentNotebook)Connection names in Fabric via
GET /v1/connections(when datasets reference linked services)
What is fully automated inline:
✅
SynapseNotebook→TridentNotebook— type rename, GUID lookup, removesparkPool/sessionConfiguration, fix timeout to 12h max✅ All compatible activity types — pass through with minor property adjustments
✅ Dataset inlining into activity
typeProperties✅ Global parameter expressions rewritten to
@pipeline().libraryVariables.<name>✅ Pipeline JSON assembly and deployment to Fabric via REST
Copilot checks before starting — will pause and report if:
A notebook referenced by a
SynapseNotebookactivity does not exist in the Fabric workspace yetA Fabric Connection is missing for a linked service referenced by dataset activities
Load pipeline-orchestrator.md for the complete inline runner.
Migration Phases (Execute in Order)
| Phase | Source | Target | Resource |
|---|---|---|---|
| Phase 0 | Synapse notebooks (referenced by pipeline activities) | Fabric Notebooks | synapse-migration skill |
| Phase 1 | Synapse global parameters | Fabric Variable Library | global-parameters-to-variable-library.md |
| Phase 2 | Synapse linked services | Fabric Connections | linked-service-to-connection.md |
| Phase 3 | Synapse datasets | Inlined into activities | dataset-inlining.md |
| Phase 4 | Synapse pipeline activities | Fabric pipeline activities | activity-mapping.md + notebook-activity-migration.md |
| Phase 5 | Assembled pipeline JSON | Fabric DataPipeline item | pipeline-orchestrator.md |
| Final | — | Validation | validation-testing.md |
Phase 0 must precede Phase 4: Fabric Notebook GUIDs are needed before TridentNotebook activities can be written. Phase 2 must precede Phase 3: Connection names must exist before they can be referenced in inlined datasets.
Activity Type Quick Reference
Full mapping table, before/after examples, and parking decisions are in activity-mapping.md.
| Synapse Activity | Fabric Equivalent | Status |
|---|---|---|
SynapseNotebook | TridentNotebook | ✅ Migrated — see notebook-activity-migration.md |
Copy | Copy | ✅ Migrated — datasets inlined |
Lookup | Lookup | ✅ Migrated — datasets inlined |
GetMetadata | GetMetadata | ✅ Migrated — datasets inlined |
Validation | GetMetadata + IfCondition | ✅ Migrated — split into 2 activities |
ForEach | ForEach | ✅ Compatible |
IfCondition | IfCondition | ✅ Compatible |
Switch | Switch | ✅ Compatible |
Until | Until | ✅ Compatible |
Wait | Wait | ✅ Compatible |
Fail | Fail | ✅ Compatible |
SetVariable | SetVariable | ✅ Compatible |
AppendVariable | AppendVariable | ✅ Compatible |
ExecutePipeline | ExecutePipeline | ✅ Compatible — add workspaceId |
WebActivity | WebActivity | ✅ Compatible |
Script | Script | ✅ Compatible — update connection refs |
Delete | Delete | ✅ Migrated — datasets inlined |
Filter | Filter | ✅ Compatible |
SparkJobDefinition (Synapse SJD) | SparkJobDefinition | ✅ Update GUID refs |
HDInsightSpark | TridentNotebook or SparkJobDefinition | ⚠️ Rewrite required |
AzureMLBatchExecution | WebActivity | ⚠️ Rewrite as REST call |
AzureFunctionActivity | WebActivity | ⚠️ Rewrite — use function URL + key |
DatabricksNotebook | ⛔ Parked | See pipeline-gotchas.md |
DatabricksSparkJar | ⛔ Parked | See pipeline-gotchas.md |
DatabricksSparkPython | ⛔ Parked | See pipeline-gotchas.md |
ExecuteSSISPackage | ⛔ Parked | See pipeline-gotchas.md |
AzureBatch | ⛔ Parked | No Fabric equivalent |
Custom | ⛔ Parked | No Fabric equivalent |
Must / Prefer / Avoid
MUST DO
Migrate notebooks before pipelines — Fabric TridentNotebook activities require notebook GUIDs, not names. Use the synapse-migration skill first
Create Fabric Connections before building pipeline JSON — linked service names in Synapse become connection references in Fabric; you need the connection names before inlining datasets
Inline all dataset definitions — Fabric Data Factory has no Dataset item type; all
inputs/outputsdataset properties must be embedded in each activityReplace
@pipeline().globalParameters.<name>with@pipeline().libraryVariables.<name>after creating the Variable LibraryReplace Validation activities with a
GetMetadata+IfConditionpair —Validationdoes not exist as an activity type in FabricRemove
sparkPoolandsessionConfigurationfrom migrated notebook activities — pool selection and session config belong in the Fabric Environment attached to the notebook
PREFER
Variable Library with Value Sets for dev/test/prod environments — use
@pipeline().libraryVariables.<name>with environment-specific Value Sets instead of pipeline-level parameters for environment promotionParameterized
notebookParametersover hardcoded values in TridentNotebook activities — mirrors Synapse parameterized notebook patternTest notebook activities individually before running the full migrated pipeline — notebook GUIDs are the most common source of failure
OneLake Lakehouse sources/sinks for Copy activities where applicable — eliminates the need for external connections for data already in OneLake
AVOID
Do not reference notebooks by name in
TridentNotebookactivities — use the GUID from the Fabric workspace notebook listDo not carry over Synapse triggers — they are not migrated by this skill; recreate schedules in Fabric after validating the pipeline
Do not attempt to migrate SSIS, Databricks, or AzureBatch activities without reading pipeline-gotchas.md first — these require manual intervention
Do not hardcode workspace/item GUIDs in pipeline JSON — use Variable Library entries or pipeline parameters so environments can be promoted without editing pipeline JSON
Do not use
@pipeline().globalParameterssyntax after migration — this expression path does not exist in Fabric; all migrated global parameters must be accessed via@pipeline().libraryVariables
Migration Gotchas — Quick Reference
Full troubleshooting guide is in pipeline-gotchas.md.
| # | Flag ID | Issue | Severity | Resolution Summary |
|---|---|---|---|---|
| PG1 | NOTEBOOK_GUID_NOT_FOUND | Notebook not yet migrated to Fabric when building TridentNotebook activity | High | Run synapse-migration skill first; get GUID from GET /v1/workspaces/{wsId}/notebooks |
| PG2 | DATASET_NOT_INLINED | Activity still references a named dataset (not valid in Fabric) | High | Apply dataset-inlining.md patterns to embed dataset properties into activity source/sink |
| PG3 | GLOBAL_PARAM_EXPRESSION | @pipeline().globalParameters.<name> expression left in migrated pipeline | High | Replace with @pipeline().libraryVariables.<name> after creating Variable Library |
| PG4 | VALIDATION_ACTIVITY_UNSUPPORTED | Validation activity type left in pipeline JSON | High | Rewrite as GetMetadata + IfCondition — see activity-mapping.md |
| PG5 | SHIR_CONNECTOR_PARKED | Activity uses a linked service backed by a Self-Hosted Integration Runtime | Medium | Must set up on-premises data gateway in Fabric; see pipeline-gotchas.md |
| PG6 | SSIS_ACTIVITY_PARKED | ExecuteSSISPackage activity cannot be migrated | High | Parked — no Fabric equivalent; see pipeline-gotchas.md for alternatives |
| PG7 | DATABRICKS_ACTIVITY_PARKED | Databricks activity type has no Fabric native equivalent | High | Parked — use Databricks REST API via WebActivity as workaround; see pipeline-gotchas.md |
| PG8 | SPARKPOOL_REF_ORPHANED | sparkPool / targetBigDataPool reference left in TridentNotebook activity | Medium | Remove sparkPool and sessionConfiguration blocks; pool config belongs in Fabric Environment |
| PG9 | EXECUTE_PIPELINE_NO_WORKSPACE | ExecutePipeline activity missing workspaceId for referenced pipeline | Medium | Add workspaceId to typeProperties; required even for same-workspace child pipelines — omitting it causes runtime failures |
| PG10 | LINKED_SERVICE_NO_CONNECTION | Linked service has no matching Fabric Connection | High | Create connection manually or via API; update connection reference in inlined dataset |
Post-Migration: What's Next
After pipeline migration, hand off to these companion skills and tools:
| Task | Skill / Tool |
|---|---|
| Migrate notebook content (mssparkutils → notebookutils, linked services) | synapse-migration skill |
| Schedule migrated pipelines | COMMON-CLI.md § Job Scheduling |
| Monitor pipeline runs | Fabric workspace → Monitor hub |
| Build new Fabric pipelines | Refer to ITEM-DEFINITIONS-CORE.md § DataPipeline |
| Explore migrated Lakehouse data post-pipeline run | spark-cli or sqldw-cli skill |
Examples
SynapseNotebook → TridentNotebook activity (before/after)
{
"name": "Run_Notebook",
"type": "SynapseNotebook",
"typeProperties": {
"notebook": {"referenceName": "MyNotebook", "type": "NotebookReference"},
"sparkPool": {"referenceName": "BigPool", "type": "BigDataPoolReference"}
}
}After migration (GUID from GET /v1/workspaces/{wsId}/notebooks):
{
"name": "Run_Notebook",
"type": "TridentNotebook",
"typeProperties": {
"notebookId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"workspaceId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
}
}Global parameter expression rewrite
Before (Synapse): @pipeline().globalParameters.batchDate
After (Fabric): @pipeline().libraryVariables.batchDateDataset inlining: Copy activity — dataset reference → connection display name
Synapse dataset InputBlobDataset (will be inlined and removed):
{
"type": "AzureBlob",
"linkedServiceName": {"referenceName": "AzureBlobLinkedService"},
"typeProperties": {"folderPath": "input/", "fileName": "data.csv"}
}Synapse Copy activity (before — references dataset by name):
{
"name": "CopyData", "type": "Copy",
"inputs": [{"referenceName": "InputBlobDataset", "type": "DatasetReference"}],
"typeProperties": {"source": {"type": "BlobSource"}, "sink": {"type": "BlobSink"}}
}After migration (connection display name for AzureBlobLinkedService is My ADLS Connection):
{
"name": "CopyData", "type": "Copy",
"typeProperties": {
"source": {"type": "BlobSource", "storeSettings": {"type": "AzureBlobStorageReadSettings"}},
"sink": {"type": "BlobSink", "storeSettings": {"type": "AzureBlobStorageWriteSettings"}}
},
"linkedService": {"referenceName": "My ADLS Connection", "type": "LinkedServiceReference"}
}See activity-mapping.md and notebook-activity-migration.md for full before/after examples.
Files
- ATTRIBUTION.mdAdded by AutoGineer881 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md21.3 KiB
- apm.yml450 B
- resources/activity-mapping.md16.2 KiB
- resources/dataset-inlining.md28.1 KiB
- resources/global-parameters-to-variable-library.md18.7 KiB
- resources/linked-service-to-connection.md16.0 KiB
- resources/migration-report.md16.3 KiB
- resources/notebook-activity-migration.md21.3 KiB
- resources/pipeline-assessment.md35.3 KiB
- resources/pipeline-gotchas.md16.3 KiB
- resources/pipeline-orchestrator.md44.7 KiB
- resources/validation-testing.md19.7 KiB
resources/pipeline-orchestrator.md
Pipeline Migration Orchestrator — Synapse → Fabric Data Factory
End-to-end workflow for migrating one or more Synapse pipelines to Fabric Data Factory.
Required Inputs
Ask the user for four things only. Discover everything else automatically.
User-provided inputs (4):
| Input | Source |
|---|---|
| Synapse workspace name | Ask the user |
| Fabric workspace name | Ask the user |
| Pipeline names to migrate | Ask the user — specific names or * for all |
| Name suffix (optional) | Ask the user — e.g. _migrated; leave blank to keep original names |
Auto-discovered (do not ask):
| Input | Source |
|---|---|
| Subscription ID | az account show --query id -o tsv |
| Resource group | az synapse workspace show --name <ws> --query resourceGroup -o tsv |
| Fabric workspace ID | GET /v1/workspaces → filter by display name |
| Notebook GUIDs | GET /v1/workspaces/{wsId}/notebooks → filter by display name |
Conditional input — only if @pipeline().globalParameters.* is used:
| Input | Source |
|---|---|
VARIABLE_LIBRARY_ID | Output of Phase 1 (global-parameters-to-variable-library.md). Run Phase 1 first, then paste the Variable Library item GUID into the runner before Phase 5 deploy. Leave as None when the source pipelines do not reference @pipeline().globalParameters.*. |
Synapse Studio URL shortcut: If the user provides a URL like
https://web.azuresynapse.net/en/home?workspace=%2Fsubscriptions%2F{subId}%2FresourceGroups%2F{rg}%2Fproviders%2FMicrosoft.Synapse%2Fworkspaces%2F{wsName}, extract all three Synapse values automatically.
Check login first: Run
az account showbefore acquiring tokens. If it fails, ask the user to runaz login.
Inline Runner — Notebook-Activity Migration (Phases 0, 4, 5 only)
Scope: This runner covers the prerequisite notebook-GUID lookup (Phase 0), activity transformation for
SynapseNotebookactivities (Phase 4), and Fabric pipeline creation (Phase 5). It does not perform:
Phase 1 — Global parameters → Variable Library (requires ARM token; run
global-parameters-to-variable-library.mdfirst)Phase 2 — Linked services → Fabric Connections (run
linked-service-to-connection.mdfirst)Phase 3 — Dataset inlining (run
dataset-inlining.mdfirst)Complete those phases manually before running this script if your pipelines depend on them. For the full orchestrated flow — all five phases in sequence — follow the Orchestration Flow section below.
Copilot executes this script inline from the terminal — no saved file needed. Fill in SYNAPSE_WS, FABRIC_WS_NAME, and PIPELINES, then run.
import requests, json, base64, subprocess, re, shutil, time, os
from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl
# ── Platform detection ─────────────────────────────────────────────────────────
_AZ = "az.cmd" if shutil.which("az.cmd") else "az"
def _az_run(*args):
return subprocess.run([_AZ, *args], capture_output=True, text=True,
check=True).stdout.strip()
# ── Config (Copilot fills from user answers) ───────────────────────────────────
SYNAPSE_WS = "<synapse-workspace-name>" # ask user
FABRIC_WS_NAME = "<fabric-workspace-name>" # ask user
PIPELINES = ["*"] # or ["Pipeline 1", "Pipeline 2"]
NAME_SUFFIX = "" # optional, e.g. "_migrated"
# Variable Library produced by Phase 1 (global-parameters-to-variable-library.md).
# Set this to the item ID printed at the end of Phase 1 to wire up
# @pipeline().libraryVariables.* expressions. Leave as None if the source
# pipelines do not reference @pipeline().globalParameters.*.
VARIABLE_LIBRARY_ID = None # e.g. "11111111-2222-3333-4444-555555555555"
# ── Auto-discover subscription and resource group ──────────────────────────────
SUBSCRIPTION = _az_run("account", "show", "--query", "id", "-o", "tsv")
RESOURCE_GROUP = _az_run("synapse", "workspace", "show",
"--name", SYNAPSE_WS, "--query", "resourceGroup", "-o", "tsv")
if not RESOURCE_GROUP:
RESOURCE_GROUP = _az_run("resource", "list", "--resource-type",
"Microsoft.Synapse/workspaces",
"--query", f"[?name=='{SYNAPSE_WS}'].resourceGroup | [0]",
"-o", "tsv")
# ── Scope note ────────────────────────────────────────────────────────────────
# This inline runner covers Phase 0 (notebook-GUID lookup), Phase 4
# (SynapseNotebook activity transformation), and Phase 5 (Fabric pipeline
# creation). Phases 1–3 (global parameters → Variable Libraries, linked
# services → connections, dataset inlining) must be completed before running.
# For the full five-phase sequence see the Orchestration Flow section below.
# ── Token acquisition ──────────────────────────────────────────────────────────
def get_token(audience):
return _az_run("account", "get-access-token", "--resource", audience,
"--query", "accessToken", "-o", "tsv")
print("Acquiring tokens...", flush=True)
synapse_token = get_token("https://dev.azuresynapse.net")
fabric_token = get_token("https://api.fabric.microsoft.com")
# ARM token not acquired here — global parameter migration (Phase 1) is a
# prerequisite step; see get_global_parameters() in the full orchestrator.
SYNAPSE_BASE = f"https://{SYNAPSE_WS}.dev.azuresynapse.net"
FABRIC_BASE = "https://api.fabric.microsoft.com/v1"
# Per-request HTTP timeout as (connect_seconds, read_seconds). Long-running
# migrations should never hang indefinitely on a transient network stall —
# a stuck connect or stuck read both surface as requests.Timeout, which the
# caller can retry or escalate. Override via env vars without editing code.
HTTP_TIMEOUT = (
float(os.environ.get("FABRIC_HTTP_CONNECT_TIMEOUT", 10)),
float(os.environ.get("FABRIC_HTTP_READ_TIMEOUT", 60)),
)
def synapse_get(path):
r = requests.get(f"{SYNAPSE_BASE}{path}",
headers={"Authorization": f"Bearer {synapse_token}"},
timeout=HTTP_TIMEOUT)
r.raise_for_status()
return r.json()
def synapse_paginate(path):
"""Collect all items from a paginated Synapse list endpoint (`nextLink`).
Required for workspaces with enough pipelines/datasets/linked services to
paginate — a bare `synapse_get('/pipelines')` returns only the first page
and silently migrates an incomplete set (especially when `PIPELINES == ['*']`)."""
items, url = [], f"{SYNAPSE_BASE}{path}"
while url:
r = requests.get(url,
headers={"Authorization": f"Bearer {synapse_token}"},
timeout=HTTP_TIMEOUT)
r.raise_for_status()
data = r.json()
items.extend(data.get("value", []))
url = data.get("nextLink")
return items
def fabric_get(path):
r = requests.get(f"{FABRIC_BASE}{path}",
headers={"Authorization": f"Bearer {fabric_token}"},
timeout=HTTP_TIMEOUT)
r.raise_for_status()
return r.json()
def _next_page_url(url: str, continuation_token: str) -> str:
"""Replace/add `continuationToken` on `url`, preserving any other query params and
URL-encoding the token value."""
parts = urlsplit(url)
query = [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True)
if k != "continuationToken"]
query.append(("continuationToken", continuation_token))
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
def fabric_paginate(path):
"""Collect all items from a paginated Fabric list endpoint (continuationToken)."""
items, url = [], f"{FABRIC_BASE}{path}"
while url:
r = requests.get(url, headers={"Authorization": f"Bearer {fabric_token}"},
timeout=HTTP_TIMEOUT)
r.raise_for_status()
data = r.json()
items.extend(data.get("value", []))
cont = data.get("continuationToken")
url = _next_page_url(url, cont) if cont else None
return items
def fabric_post(path, body):
r = requests.post(f"{FABRIC_BASE}{path}",
headers={"Authorization": f"Bearer {fabric_token}",
"Content-Type": "application/json"},
json=body, timeout=HTTP_TIMEOUT)
if r.status_code in (200, 201):
# Fabric item creation typically returns 201 Created for synchronous success;
# some endpoints may return 200 OK. Treat both as immediate success.
# On empty / non-JSON bodies the created resource identity (id) is lost.
# The downstream caller does `resp.get("id", "unknown")` and would silently
# produce broken Fabric URLs — instead, recover the id from the Location
# header (`.../items/{id}`) and refetch, or fail loudly so a partial
# success doesn't masquerade as a complete one.
if r.content:
try:
return r.json()
except ValueError:
pass
location = r.headers.get("Location") or r.headers.get("Content-Location")
if location:
# Resolve relative URLs against the Fabric base.
if location.startswith("/"):
location = f"https://api.fabric.microsoft.com{location}"
fetched = requests.get(location,
headers={"Authorization": f"Bearer {fabric_token}"},
timeout=HTTP_TIMEOUT)
fetched.raise_for_status()
return fetched.json()
raise RuntimeError(
f"Fabric returned {r.status_code} for {path!r} with an empty/non-JSON body "
f"and no Location header — cannot identify the created item. "
f"Headers: {dict(r.headers)}"
)
if r.status_code == 202:
# Long-running operation — poll via Location / Operation-Location / x-ms-operation-id
location = (
r.headers.get("Location")
or r.headers.get("Operation-Location")
or (f"https://api.fabric.microsoft.com/v1/operations/{r.headers['x-ms-operation-id']}"
if "x-ms-operation-id" in r.headers else None)
)
if not location:
raise RuntimeError("202 response missing Location, Operation-Location, or x-ms-operation-id header")
# Some Fabric endpoints return relative URLs (e.g. `/v1/operations/...`).
# Resolve against the Fabric base so `requests.get` receives a valid URL,
# matching the 200/201 path above.
if location.startswith("/"):
location = f"https://api.fabric.microsoft.com{location}"
max_polls = int(os.environ.get("FABRIC_LRO_MAX_POLLS", 150)) # default ~5 min at 2 s base
base_sleep = float(os.environ.get("FABRIC_LRO_POLL_INTERVAL", 2))
# Sleep BEFORE the first poll, honoring `Retry-After` from the initial
# 202 response when present. Polling immediately wastes a round-trip
# (typically returns transient "not ready") and ignores throttling
# guidance from busy tenants, which can trigger 429s.
initial_retry = r.headers.get("Retry-After")
time.sleep(float(initial_retry) if initial_retry else base_sleep)
for _ in range(max_polls):
poll = requests.get(location,
headers={"Authorization": f"Bearer {fabric_token}"},
timeout=HTTP_TIMEOUT)
poll.raise_for_status()
state = poll.json()
status = state.get("status")
if status == "Succeeded":
# Follow resourceLocation to fetch the created item
resource_url = (
state.get("resourceLocation")
or state.get("result", {}).get("resourceLocation")
)
if resource_url:
if resource_url.startswith("/"):
resource_url = f"https://api.fabric.microsoft.com{resource_url}"
item = requests.get(resource_url,
headers={"Authorization": f"Bearer {fabric_token}"},
timeout=HTTP_TIMEOUT)
item.raise_for_status()
return item.json()
# Fallback: some LRO responses embed the created item in result
result = state.get("result") or {}
if result.get("id"):
return result
raise RuntimeError(
f"Fabric LRO Succeeded for {path!r} but no resourceLocation "
f"or result.id found — cannot identify created item. "
f"Raw state: {state}"
)
elif status == "Failed":
raise RuntimeError(f"Fabric LRO failed: {state.get('error', state)}")
elif status == "Cancelled":
# Treat user/admin-initiated cancellation as a terminal failure —
# otherwise the loop keeps polling a terminal operation until the
# max_polls timeout fires.
raise RuntimeError(
f"Fabric LRO cancelled for {path!r}: {state.get('error', state)}"
)
retry_after = poll.headers.get("Retry-After")
sleep_s = float(retry_after) if retry_after else base_sleep
time.sleep(sleep_s)
raise TimeoutError(f"Fabric LRO timed out after {max_polls} polls: {path}")
r.raise_for_status()
return r.json() if r.content else {}
# ── Resolve Fabric workspace ID ────────────────────────────────────────────────
print(f"Resolving Fabric workspace '{FABRIC_WS_NAME}'...", flush=True)
ws_list = fabric_paginate("/workspaces")
fabric_ws = next((w for w in ws_list if w["displayName"] == FABRIC_WS_NAME), None)
if not fabric_ws:
sample = [w["displayName"] for w in ws_list[:5]]
hint = f"first {len(sample)} of {len(ws_list)}: {sample}" if len(ws_list) > 5 else str(sample)
raise ValueError(
f"Fabric workspace '{FABRIC_WS_NAME}' not found. "
f"Check the name and your access permissions ({hint})"
)
FABRIC_WS_ID = fabric_ws["id"]
print(f" Fabric workspace ID: {FABRIC_WS_ID}", flush=True)
# ── Build notebook name → Fabric GUID map ─────────────────────────────────────
notebooks_in_fabric = fabric_paginate(f"/workspaces/{FABRIC_WS_ID}/notebooks")
notebook_guid_map = {nb["displayName"]: nb["id"] for nb in notebooks_in_fabric}
_nb_count = len(notebook_guid_map)
_nb_sample = list(notebook_guid_map.keys())[:5]
_nb_suffix = f" (showing first 5 of {_nb_count})" if _nb_count > 5 else ""
print(f" Notebooks in Fabric: {_nb_count}{_nb_suffix}: {_nb_sample}", flush=True)
# ── Fetch pipelines from Synapse ───────────────────────────────────────────────
# Use paginated GET so workspaces with > 1 page of pipelines are migrated in full
# when PIPELINES == ['*'] (the bare /pipelines endpoint paginates via nextLink).
all_pipelines = synapse_paginate("/pipelines?api-version=2020-12-01")
if PIPELINES == ["*"]:
pipelines_to_migrate = all_pipelines
else:
# Validate explicit names up-front so typos, renamed pipelines, or pipelines
# outside the caller's read permissions surface as a clear error instead of
# silently shrinking the migration set (or producing a 'successful' run that
# migrated zero pipelines).
available_names = {p["name"] for p in all_pipelines}
missing = [n for n in PIPELINES if n not in available_names]
if missing:
sample = sorted(available_names)[:10]
sample_hint = (f"first 10 of {len(available_names)}: {sample}"
if len(available_names) > 10 else str(sample))
raise ValueError(
f"Requested pipeline(s) not found in Synapse workspace "
f"'{SYNAPSE_WS}': {missing}. Check spelling, that the pipelines "
f"still exist, and that the caller has read access. "
f"Available pipelines ({sample_hint})."
)
pipelines_to_migrate = [p for p in all_pipelines if p["name"] in PIPELINES]
print(f"Migrating {len(pipelines_to_migrate)} pipeline(s): "
f"{[p['name'] for p in pipelines_to_migrate]}", flush=True)
# ── Activity transformation ────────────────────────────────────────────────────
def fix_timeout(t):
"""Clamp Synapse timeouts to Fabric max of 12 hours (0.12:00:00).
Returns None when t is absent/empty so the caller can omit the key
entirely and let the platform default apply — only clamp when an
explicit timeout exceeds 12 hours.
Supports both d.hh:mm:ss and hh:mm:ss formats. Unrecognised formats
are returned unchanged so valid custom values are not silently altered.
"""
if not t:
return None
m = re.match(r'^(\d+)\.(\d+):(\d+):(\d+)$', t)
if m:
days, hours, minutes, seconds = (int(m.group(1)), int(m.group(2)),
int(m.group(3)), int(m.group(4)))
else:
m = re.match(r'^(\d+):(\d+):(\d+)$', t)
if m:
days, hours, minutes, seconds = 0, int(m.group(1)), int(m.group(2)), int(m.group(3))
else:
# Unrecognised format — pass through unchanged rather than silently clamping
print(f" ⚠️ Unrecognised timeout format '{t}' — kept as-is", flush=True)
return t
total_seconds = (((days * 24) + hours) * 60 + minutes) * 60 + seconds
if total_seconds > 12 * 60 * 60:
return "0.12:00:00"
return t
def transform_activity(activity, nb_map, ws_id):
"""Recursively transform a Synapse activity to its Fabric equivalent."""
atype = activity.get("type")
if atype == "SynapseNotebook":
tp = activity.get("typeProperties", {})
nb_name = tp.get("notebook", {}).get("referenceName", "")
nb_guid = nb_map.get(nb_name)
if not nb_guid:
# Show a bounded sample instead of the full list — real workspaces
# can have hundreds of notebooks and dumping all names floods the
# log. Mirrors the workspace-not-found hint shape above.
available = sorted(nb_map.keys())
sample = available[:5]
hint = (f"first {len(sample)} of {len(available)}: {sample}"
if len(available) > 5 else str(sample))
raise ValueError(
f"Notebook '{nb_name}' not found in Fabric workspace '{FABRIC_WS_NAME}' "
f"({hint}). "
"Migrate the notebook first using the synapse-migration skill."
)
new_tp = {"notebookId": nb_guid, "workspaceId": ws_id}
if tp.get("parameters"):
new_tp["notebookParameters"] = tp["parameters"]
policy = dict(activity.get("policy", {}))
clamped = fix_timeout(policy.get("timeout"))
if clamped is not None:
policy["timeout"] = clamped
else:
policy.pop("timeout", None) # omit key; let platform default apply
# Only emit "policy" when it has at least one meaningful field —
# an empty {} can fail schema validation on some Fabric endpoints.
result = {**activity, "type": "TridentNotebook", "typeProperties": new_tp}
if policy:
result["policy"] = policy
else:
result.pop("policy", None)
return result
# Recurse into container activities
tp = dict(activity.get("typeProperties", {}))
for key in ("activities", "ifTrueActivities", "ifFalseActivities", "defaultActivities"):
if key in tp:
tp[key] = [transform_activity(a, nb_map, ws_id) for a in tp[key]]
if "cases" in tp:
for case in tp["cases"]:
case["activities"] = [transform_activity(a, nb_map, ws_id)
for a in case.get("activities", [])]
return {**activity, "typeProperties": tp}
# ── Migrate each pipeline ──────────────────────────────────────────────────────
results = []
for pipeline in pipelines_to_migrate:
name = pipeline["name"]
print(f"\nMigrating: {name}", flush=True)
full = synapse_get(f"/pipelines/{name}?api-version=2020-12-01")
props = full.get("properties", {})
transformed = [transform_activity(a, notebook_guid_map, FABRIC_WS_ID)
for a in props.get("activities", [])]
# ── Phase 2/3 guard ────────────────────────────────────────────────────
# The inline runner only performs Phase 0/4/5 — it does NOT apply Phase 2
# (linked service → Fabric Connection name mapping) or Phase 3 (dataset
# inlining). If the source pipeline contains Copy activities with
# inputs/outputs DatasetReferences, Lookup/GetMetadata/Delete activities
# with a `dataset` reference, or any other linkedServiceName/Reference
# value still pointing at a Synapse linked service, deploying as-is would
# produce an invalid Fabric pipeline. Fail loudly instead of silently
# uploading broken JSON.
# Only flag LinkedServiceReference under Synapse-specific parent keys.
# Fabric pipeline JSON also uses `LinkedServiceReference` (e.g., under
# `linkedService` on sinks/sources, or `externalReferences` on Copy
# activities) to point at Fabric Connections, so a blanket type check
# false-positives on valid post-Phase-2 Fabric output. Synapse-only
# shapes always nest the reference under one of these parent keys:
SYNAPSE_LS_PARENTS = {"linkedServiceName", "functionLinkedService"}
def _find_synapse_only_constructs(node, path="$", parent_key=None):
problems = []
if isinstance(node, dict):
if "referenceName" in node and node.get("type") == "DatasetReference":
problems.append(f"{path} → DatasetReference to '{node['referenceName']}'")
if (
"referenceName" in node
and node.get("type") == "LinkedServiceReference"
and parent_key in SYNAPSE_LS_PARENTS
):
problems.append(f"{path} → LinkedServiceReference to '{node['referenceName']}'")
for k, v in node.items():
problems.extend(_find_synapse_only_constructs(v, f"{path}.{k}", parent_key=k))
elif isinstance(node, list):
for i, v in enumerate(node):
problems.extend(_find_synapse_only_constructs(v, f"{path}[{i}]", parent_key=parent_key))
return problems
synapse_only = _find_synapse_only_constructs({"activities": transformed})
if synapse_only:
sample = synapse_only[:5]
more = f" (and {len(synapse_only) - len(sample)} more)" if len(synapse_only) > len(sample) else ""
raise RuntimeError(
f"Pipeline '{name}' still contains Synapse-only constructs after Phase 4 "
f"transformation — the inline runner does NOT perform Phase 2 (linked "
f"service → Fabric Connection) or Phase 3 (dataset inlining). "
f"Uploading as-is would produce an invalid Fabric pipeline. Run "
f"`linked-service-to-connection.md` and `dataset-inlining.md` first, "
f"or use the full orchestrator flow. Offending references: {sample}{more}"
)
fabric_pipeline = {
"properties": {
"activities": transformed,
"parameters": props.get("parameters", {}),
"variables": props.get("variables", {}),
"annotations": props.get("annotations", []),
}
}
if props.get("folder"):
fabric_pipeline["properties"]["folder"] = props["folder"]
# Phase 4 expression rewrite: @pipeline().globalParameters → @pipeline().libraryVariables
# Walk the pipeline structure and rewrite only ADF expression strings
# (values that begin with "@"). A blind global string-replace on the
# serialized JSON could clobber unrelated text — e.g. literal descriptions,
# annotations, or embedded code snippets that happen to contain the
# substring "@pipeline().globalParameters." in prose form.
#
# Canonical implementation — keep in sync with the equivalent helpers in:
# - skills/pipeline-migration/resources/notebook-activity-migration.md
# (rewrite_global_parameters inside transform_pipeline_notebook_activities)
# - skills/pipeline-migration/resources/global-parameters-to-variable-library.md
# (rewrite_global_parameters in the Expression Rewrite section)
# If you change the rewrite rules here (e.g. additional expression prefixes
# or guarded substrings), update those copies too so the three docs don't
# drift apart.
def _rewrite_expressions(node):
if isinstance(node, dict):
# ADF/Fabric pipeline JSON places every expression inside a dict
# shaped {"value": "@...", "type": "Expression"}. Restrict the
# rewrite to that exact shape so prose in description/annotation
# fields, embedded code samples, or other non-expression text
# that happens to contain "pipeline().globalParameters." is not
# mutated (which would otherwise false-positive the VARIABLE_LIBRARY_ID
# hard-fail check downstream).
if (
node.get("type") == "Expression"
and isinstance(node.get("value"), str)
and node["value"].startswith("@")
):
return {
**node,
# Match the bare `pipeline().globalParameters.` substring so
# both `@pipeline().globalParameters.x` AND nested forms
# like `@concat('env=', pipeline().globalParameters.env)`
# get rewritten. The outer startswith("@") guard above
# still scopes this strictly to expression-shaped dicts.
"value": node["value"].replace(
"pipeline().globalParameters.",
"pipeline().libraryVariables.",
),
}
return {k: _rewrite_expressions(v) for k, v in node.items()}
if isinstance(node, list):
return [_rewrite_expressions(v) for v in node]
return node
fabric_pipeline = _rewrite_expressions(fabric_pipeline)
# Detect libraryVariables references by walking the dict for the exact
# Expression shape the rewrite produces — a raw substring scan of the
# serialized JSON would false-positive on description / annotation prose
# that happens to mention "pipeline().libraryVariables.".
def _has_library_variables_expression(node):
if isinstance(node, dict):
if (node.get("type") == "Expression"
and isinstance(node.get("value"), str)
and "pipeline().libraryVariables." in node["value"]):
return True
return any(_has_library_variables_expression(v) for v in node.values())
if isinstance(node, list):
return any(_has_library_variables_expression(v) for v in node)
return False
has_library_variables_ref = _has_library_variables_expression(fabric_pipeline)
# Hard fail if the rewrite produced libraryVariables references but no
# Variable Library was provisioned — the pipeline would deploy but its
# expressions would silently fail to resolve at runtime.
if has_library_variables_ref and not VARIABLE_LIBRARY_ID:
raise RuntimeError(
f"Pipeline '{name}' references @pipeline().libraryVariables.* but "
f"VARIABLE_LIBRARY_ID is not set. Run Phase 1 (Variable Library creation) "
f"first and pass the resulting library GUID, or remove global-parameter "
f"references from the source pipeline."
)
# Attach the libraryVariables block so the rewritten expressions resolve at runtime.
# Required whenever Phase 1 produced a Variable Library and the pipeline references it.
if VARIABLE_LIBRARY_ID and has_library_variables_ref:
fabric_pipeline["properties"]["libraryVariables"] = {
"libraryId": VARIABLE_LIBRARY_ID,
"workspaceId": FABRIC_WS_ID,
}
fabric_pipeline_json = json.dumps(fabric_pipeline)
payload_b64 = base64.b64encode(fabric_pipeline_json.encode()).decode()
resp = fabric_post(f"/workspaces/{FABRIC_WS_ID}/items", {
"displayName": name + NAME_SUFFIX,
"type": "DataPipeline",
"definition": {
"format": "DataPipeline",
"parts": [{"path": "pipeline-content.json",
"payload": payload_b64,
"payloadType": "InlineBase64"}]
}
})
pid = resp.get("id", "unknown")
url = f"https://app.fabric.microsoft.com/groups/{FABRIC_WS_ID}/datapipelines/{pid}"
print(f" [OK] Created: {name} (id: {pid})")
print(f" [link] {url}")
results.append({"name": name, "id": pid, "url": url})
print(f"\n{'='*60}")
print(f"Done — {len(results)} pipeline(s) created in Fabric workspace '{FABRIC_WS_NAME}'")
for r in results:
print(f" {r['name']}: {r['url']}")Windows:
azis called asaz.cmd. The runner detects this automatically viashutil.which("az.cmd").
Derived Values (Auto-Discovered)
| Value | Derived From |
|---|---|
| Synapse data-plane endpoint | https://{workspaceName}.dev.azuresynapse.net |
| Fabric workspace ID | GET /v1/workspaces → JMESPath filter by displayName |
| Fabric notebook GUIDs | GET /v1/workspaces/{wsId}/notebooks → filter by displayName |
| Fabric connection names | GET /v1/connections → filter by displayName |
Orchestration Flow
Pipeline Migration Orchestrator:
│
├── Step 0: Authenticate (acquire 3 tokens)
├── Step 1: Inventory Synapse workspace
│ ├── List all pipelines
│ ├── List all datasets
│ ├── List all linked services
│ └── Get workspace global parameters
│
├── Phase 0: Prerequisite — Migrate Notebooks (synapse-migration skill)
│ ├── Identify notebooks referenced by SynapseNotebook activities
│ ├── ⚠️ GATE: These notebooks must exist in Fabric before Phase 4
│ └── Output: notebookNameToGUID mapping
│
├── Phase 1: Global Parameters → Variable Library
│ ├── Execute global-parameters-to-variable-library.md
│ ├── ⏸ GATE: Confirm Variable Library created and accessible
│ └── Output: variableLibraryId
│
├── Phase 2: Linked Services → Fabric Connections
│ ├── Execute linked-service-to-connection.md for each used linked service
│ ├── ⏸ GATE: Confirm all required connections created
│ └── Output: linkedServiceToConnectionName mapping
│
├── Phase 3: Build Dataset Inline Map
│ ├── Execute dataset-inlining.md to build dataset→inline map
│ └── Output: datasetInlineMap (dataset name → inline typeProperties)
│
├── Phase 4: Transform Pipeline Activities
│ ├── Execute activity-mapping.md for each activity type
│ ├── Apply notebook-activity-migration.md for SynapseNotebook activities
│ ├── Substitute dataset references with inlined definitions (Phase 3 map)
│ ├── Substitute linked service references with connection names (Phase 2 map)
│ ├── Replace @pipeline().globalParameters → @pipeline().libraryVariables
│ ├── Flag parked activities (SSIS, Databricks, SHIR-only) — log, do not block
│ └── Output: transformedActivities JSON array
│
├── Phase 5: Assemble & Deploy Fabric Pipeline
│ ├── Build pipeline-content.json (see § Pipeline JSON Assembly)
│ ├── Base64-encode the JSON
│ ├── POST to Fabric Items API
│ └── Output: fabricPipelineId
│
└── Final: Validation
└── Execute validation-testing.mdStep 0: Authentication
Acquire three tokens:
# 1. Synapse Data Plane token (pipelines, datasets, linked services)
SYNAPSE_TOKEN=$(az account get-access-token \
--resource https://dev.azuresynapse.net \
--query accessToken -o tsv)
# 2. Synapse ARM token (workspace properties, global parameters)
ARM_TOKEN=$(az account get-access-token \
--resource https://management.azure.com \
--query accessToken -o tsv)
# 3. Fabric token
FABRIC_TOKEN=$(az account get-access-token \
--resource https://api.fabric.microsoft.com \
--query accessToken -o tsv)
SYNAPSE_WS="mysynapseworkspace"
SYNAPSE_ENDPOINT="https://${SYNAPSE_WS}.dev.azuresynapse.net"Step 1: Inventory the Synapse Workspace
List All Pipelines
# Enumerate all pipeline names
az rest --method GET \
--headers "Authorization=Bearer ${SYNAPSE_TOKEN}" \
--url "${SYNAPSE_ENDPOINT}/pipelines?api-version=2020-12-01" \
--query "value[].name" -o tsvGet a Pipeline Definition
PIPELINE_NAME="MyIngestPipeline"
az rest --method GET \
--headers "Authorization=Bearer ${SYNAPSE_TOKEN}" \
--url "${SYNAPSE_ENDPOINT}/pipelines/${PIPELINE_NAME}?api-version=2020-12-01"Response includes properties.activities, properties.parameters, properties.variables, properties.folder.
List All Datasets
az rest --method GET \
--headers "Authorization=Bearer ${SYNAPSE_TOKEN}" \
--url "${SYNAPSE_ENDPOINT}/datasets?api-version=2020-12-01" \
--query "value[].{name:name, type:properties.type, linkedService:properties.linkedServiceName.referenceName}" \
-o tableList All Linked Services
az rest --method GET \
--headers "Authorization=Bearer ${SYNAPSE_TOKEN}" \
--url "${SYNAPSE_ENDPOINT}/linkedservices?api-version=2020-12-01" \
--query "value[].{name:name, type:properties.type}" \
-o tableGet Global Parameters
SUB_ID="<subscription-id>"
RG="<resource-group>"
az rest --method GET \
--headers "Authorization=Bearer ${ARM_TOKEN}" \
--url "https://management.azure.com/subscriptions/${SUB_ID}/resourceGroups/${RG}/providers/Microsoft.Synapse/workspaces/${SYNAPSE_WS}?api-version=2021-06-01" \
--query "properties.globalParameters"Step 1b: Discover Notebook References in Pipelines
Before Phase 4 can proceed, identify which Synapse notebooks are referenced by SynapseNotebook activities so they can be pre-migrated.
import json, re
def find_notebook_references(pipeline_def):
"""Extract all unique Synapse notebook names from SynapseNotebook activities."""
notebooks = set()
for activity in pipeline_def.get("properties", {}).get("activities", []):
if activity.get("type") == "SynapseNotebook":
ref = (activity
.get("typeProperties", {})
.get("notebook", {})
.get("referenceName"))
if ref:
notebooks.add(ref)
tp = activity.get("typeProperties", {})
# Recurse into ForEach / IfCondition / Until inner activities (direct activity lists)
for inner_key in ("activities", "ifTrueActivities", "ifFalseActivities",
"defaultActivities"):
for inner in tp.get(inner_key, []):
if isinstance(inner, dict):
notebooks.update(find_notebook_references({"properties": {"activities": [inner]}}))
# Switch: cases is [{value, activities}, ...] — must unwrap each case's activity list
for case in tp.get("cases", []):
if isinstance(case, dict):
for inner in case.get("activities", []):
if isinstance(inner, dict):
notebooks.update(find_notebook_references({"properties": {"activities": [inner]}}))
return notebooks
# Usage
pipeline_json = json.loads(open("pipeline.json").read())
notebook_refs = find_notebook_references(pipeline_json)
print("Notebooks to migrate first:", notebook_refs)Build Notebook Name → Fabric GUID Map
After running the synapse-migration skill to migrate the notebooks:
FABRIC_WS_ID="<fabric-workspace-id>"
az rest --method GET \
--headers "Authorization=Bearer ${FABRIC_TOKEN}" \
--url "https://api.fabric.microsoft.com/v1/workspaces/${FABRIC_WS_ID}/notebooks" \
--query "value[].{name:displayName, id:id}" -o tableStore this mapping in a JSON file for use in Phase 4:
{
"NotebookA": "aaaaaaaa-1111-2222-3333-444444444444",
"NotebookB": "bbbbbbbb-5555-6666-7777-888888888888"
}Phase 5: Assemble Fabric Pipeline JSON
After completing Phases 0–4, assemble the final pipeline-content.json.
Pipeline JSON Structure
{
"properties": {
"description": "<pipeline description>",
"activities": [ /* transformed activities array from Phase 4 */ ],
"parameters": {
"<paramName>": {
"type": "string",
"defaultValue": "<value>"
}
},
"variables": {
"<varName>": {
"type": "String",
"defaultValue": "<value>"
}
},
"libraryVariables": {
"libraryId": "<variable-library-item-id>",
"workspaceId": "<fabric-workspace-id>"
},
"annotations": [],
"folder": {
"name": "<folder-path>"
}
}
}
libraryVariablesis only needed when the pipeline uses@pipeline().libraryVariables.<name>expressions (migrated from global parameters). SetlibraryIdto the Variable Library item GUID created in Phase 1.
folderreplicates the Synapse pipeline folder structure. This is optional but helps organize migrated pipelines.
Deploy to Fabric
import base64, json, requests
FABRIC_TOKEN = "<fabric-token>"
FABRIC_WS_ID = "<workspace-id>"
PIPELINE_NAME = "MyIngestPipeline"
pipeline_content = { /* assembled pipeline JSON */ }
payload_b64 = base64.b64encode(
json.dumps(pipeline_content).encode("utf-8")
).decode("utf-8")
headers = {
"Authorization": f"Bearer {FABRIC_TOKEN}",
"Content-Type": "application/json"
}
body = {
"displayName": PIPELINE_NAME,
"type": "DataPipeline",
"definition": {
"format": "DataPipeline",
"parts": [
{
"path": "pipeline-content.json",
"payload": payload_b64,
"payloadType": "InlineBase64"
}
]
}
}
response = requests.post(
f"https://api.fabric.microsoft.com/v1/workspaces/{FABRIC_WS_ID}/items",
headers=headers,
json=body,
timeout=60,
)
if response.status_code == 202:
# Long-running operation — poll the Location header
location = response.headers.get("Location")
print(f"Pipeline creation in progress. Poll: {location}")
elif response.status_code in (200, 201):
# Some Fabric item endpoints succeed with an empty/non-JSON body and convey
# identity via the Location header — handle both shapes so the created id
# isn't silently lost.
item_id = None
if response.content:
try:
item_id = response.json().get("id")
except ValueError:
item_id = None
if not item_id and response.headers.get("Location"):
from urllib.parse import urlsplit
segs = [s for s in urlsplit(response.headers["Location"]).path.split("/") if s]
item_id = segs[-1] if segs else None
print(f"Pipeline created: {item_id or '<id unavailable>'}")
else:
print(f"Error {response.status_code}: {response.text}")Update an Existing Pipeline Definition
If the pipeline item already exists and you need to update its definition:
PIPELINE_ITEM_ID="<fabric-pipeline-item-id>"
# payload_b64 = base64-encoded pipeline-content.json
az rest --method POST \
--headers "Authorization=Bearer ${FABRIC_TOKEN}" "Content-Type=application/json" \
--url "https://api.fabric.microsoft.com/v1/workspaces/${FABRIC_WS_ID}/items/${PIPELINE_ITEM_ID}/updateDefinition" \
--body "{
\"definition\": {
\"parts\": [{
\"path\": \"pipeline-content.json\",
\"payload\": \"${payload_b64}\",
\"payloadType\": \"InlineBase64\"
}]
}
}"Pipeline Inventory Script (Full Workspace Scan)
Use this to produce a migration manifest before starting:
import requests, json
from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl
SYNAPSE_TOKEN = "<synapse-data-plane-token>"
SYNAPSE_ENDPOINT = "https://myworkspace.dev.azuresynapse.net"
headers = {"Authorization": f"Bearer {SYNAPSE_TOKEN}"}
def paginate(url):
items = []
while url:
r = requests.get(url, headers=headers, timeout=60)
r.raise_for_status()
data = r.json()
items.extend(data.get("value", []))
url = data.get("nextLink")
return items
pipelines = paginate(f"{SYNAPSE_ENDPOINT}/pipelines?api-version=2020-12-01")
datasets = paginate(f"{SYNAPSE_ENDPOINT}/datasets?api-version=2020-12-01")
linked_services = paginate(f"{SYNAPSE_ENDPOINT}/linkedservices?api-version=2020-12-01")
manifest = {
"pipelines": [p["name"] for p in pipelines],
"datasets": [d["name"] for d in datasets],
"linked_services": [ls["name"] for ls in linked_services],
"pipeline_activity_types": {}
}
for p in pipelines:
name = p["name"]
# The list endpoint returns stubs — properties.activities is often omitted.
# Fetch each pipeline definition to get the real activity list.
r = requests.get(
f"{SYNAPSE_ENDPOINT}/pipelines/{name}?api-version=2020-12-01",
headers={"Authorization": f"Bearer {SYNAPSE_TOKEN}"},
timeout=60,
)
r.raise_for_status()
detail = r.json()
types = set()
def _collect_types(activities):
for a in activities or []:
atype = a.get("type")
if atype:
types.add(atype)
inner = a.get("typeProperties", {}) or {}
# ForEach / IfCondition / Until: direct activity lists
for key in ("activities", "ifTrueActivities", "ifFalseActivities", "defaultActivities"):
if isinstance(inner.get(key), list):
_collect_types(inner[key])
# Switch: cases is [{value, activities}, ...]
for case in inner.get("cases", []) or []:
if isinstance(case, dict):
_collect_types(case.get("activities", []))
_collect_types(detail.get("properties", {}).get("activities", []))
manifest["pipeline_activity_types"][name] = sorted(types)
# Flag pipelines with parked activity types
PARKED_TYPES = {"ExecuteSSISPackage", "DatabricksNotebook", "DatabricksSparkJar",
"DatabricksSparkPython", "AzureBatch", "Custom"}
print("=== Migration Manifest ===")
for pipeline, types in manifest["pipeline_activity_types"].items():
parked = set(types) & PARKED_TYPES
status = "⛔ PARKED" if parked else "✅ Migratable"
print(f"{status} {pipeline}: {types}")
if parked:
print(f" ↳ Parked activity types: {parked}")
print(f"\nTotal: {len(pipelines)} pipelines, {len(datasets)} datasets, {len(linked_services)} linked services")Handling Pagination
Synapse endpoints use nextLink for continuation; Fabric endpoints use continuationToken:
def paginate_synapse(url, token):
items = []
while url:
r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=60)
r.raise_for_status()
data = r.json()
items.extend(data.get("value", []))
url = data.get("nextLink")
return items
def paginate_fabric(url, token):
items = []
while url:
r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=60)
r.raise_for_status()
data = r.json()
items.extend(data.get("value", []))
# Fabric uses continuationToken, not nextLink. Preserve any other
# query-string params already present on `url` and URL-encode the token.
cont = data.get("continuationToken")
if cont:
parts = urlsplit(url)
query = [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True)
if k != "continuationToken"]
query.append(("continuationToken", cont))
url = urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
else:
url = None
return itemsFabric API uses
continuationToken(query param) rather than a fullnextLinkURL.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Microsoft
- Publisher
- Microsoft (Official)
- Source
- microsoft/skills-for-fabric · skills/pipeline-migration · commit f8fac11
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Also distributed in
- microsoft/skills-for-fabric
AutoGineer is not affiliated with or endorsed by Microsoft.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download pipeline-migration.zip