Generated banner for Pipeline Migration
Generated logo for Pipeline Migration

Pipeline Migration

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Workflow
  • Cloud and DevOps
  • MIT
  • No issues found

From the author

Migrates Synapse Data Factory pipelines to Fabric Data Factory, turning linked services into Fabric connections, inlining datasets into activities, converting global parameters to Variable Libraries, and mapping SynapseNotebook to TridentNotebook. Use for pipeline JSON. For Synapse Spark notebook code use synapse-migration.
— the description by Microsoft

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/microsoft/pipeline-migration/download
  2. Unzip 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.com call must carry x-ms-fabric-skill: pipeline-migration (az rest: --headers "x-ms-fabric-skill=pipeline-migration"), including every LRO poll, fabric_lro and retry. Snippets omit it — add it anyway.

CRITICAL NOTES

  1. To find workspace details (including its ID) from a workspace name: list all workspaces, then use JMESPath filtering

  2. 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

  3. Fabric does NOT have a "Dataset" item type — all dataset properties are inlined into activity typeProperties

  4. Linked Services map to Fabric Connections — in pipeline activity JSON, referenceName on the activity's linkedService block uses the Fabric connection display name (not the connection GUID); the connection GUID is used only on Fabric REST API calls

  5. Notebook activities change from SynapseNotebook to TridentNotebook and reference notebooks by GUID, not by name

  6. Synapse global parameters become a Variable Library item in Fabric, referenced as @pipeline().libraryVariables.<name>. Variable Library Number types are not consumable in pipelines — Synapse Float/Double are mapped to String for runtime compatibility

  7. The Validation activity type does not exist in Fabric — it must be rewritten as GetMetadata + IfCondition

  8. Triggers are intentionally excluded from this skill — recreate schedules manually in Fabric after migration

  9. 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 login CLI patterns, authentication recipes, pipeline run/schedule operations

  • ITEM-DEFINITIONS-CORE.md — DataPipeline and VariableLibrary item 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

TopicReference
Pre-Migration Assessment (run first)pipeline-assessment.md
Migration Orchestratorpipeline-orchestrator.md
API-Driven Migration Workflow§ API-Driven Migration Workflow
Activity Type Mappingactivity-mapping.md
Notebook Activity Migration (primary focus)notebook-activity-migration.md
Linked Services → Connectionslinked-service-to-connection.md
Dataset Inliningdataset-inlining.md
Global Parameters → Variable Libraryglobal-parameters-to-variable-library.md
Pipeline Gotchas & Parked Activitiespipeline-gotchas.md
Validation & Testingvalidation-testing.md
Migration Reportmigration-report.md
Context Loading Guide

IMPORTANT — Load only what you need. Do NOT read all resource files upfront.

WhenRead This File
User asks for an assessment, scope, or plan before migratingpipeline-assessment.md
User asks to migrate a full pipeline workspacepipeline-orchestrator.md
User asks about activity type mapping or unsupported activitiesactivity-mapping.md
User has SynapseNotebook activities (most common)notebook-activity-migration.md
User asks about linked services or connectionslinked-service-to-connection.md
User asks about Copy, Lookup, or GetMetadata with datasetsdataset-inlining.md
User has global parameters to convertglobal-parameters-to-variable-library.md
User hits SSIS, SHIR, Databricks, or other blockerspipeline-gotchas.md
Post-migration verificationvalidation-testing.md
Generating a migration summarymigration-report.md

API-Driven Migration Workflow

Authentication
TargetToken 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 tsv to acquire tokens.

Synapse Data-Plane API Reference
OperationEndpoint
List all pipelinesGET https://{ws}.dev.azuresynapse.net/pipelines?api-version=2020-12-01
Get pipeline definitionGET https://{ws}.dev.azuresynapse.net/pipelines/{name}?api-version=2020-12-01
List all datasetsGET https://{ws}.dev.azuresynapse.net/datasets?api-version=2020-12-01
Get dataset definitionGET https://{ws}.dev.azuresynapse.net/datasets/{name}?api-version=2020-12-01
List all linked servicesGET https://{ws}.dev.azuresynapse.net/linkedservices?api-version=2020-12-01
Get linked serviceGET 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
OperationEndpoint
List connectionsGET https://api.fabric.microsoft.com/v1/connections
Create connectionPOST https://api.fabric.microsoft.com/v1/connections
Create pipeline itemPOST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items
Update pipeline definitionPOST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items/{id}/updateDefinition
Get pipeline definitionPOST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items/{id}/getDefinition
Create Variable LibraryPOST https://api.fabric.microsoft.com/v1/workspaces/{wsId}/items (type: VariableLibrary)
List notebooks in workspaceGET https://api.fabric.microsoft.com/v1/workspaces/{wsId}/notebooks

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:

  1. Ask the user: "What is the name of your Synapse workspace?"

  2. Auto-discover subscription ID and resource group via az account show and az synapse workspace show

  3. Run the assessment code inline from the terminal

  4. Print the full report output directly in the chat

When to UseAction
User wants to understand migration scope before committingAsk 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 documentRun assessment, print report in chat
User has already decided to migrateSkip assessment — go straight to Migration Phases below

To also save the report to disk, pass output_path=f"pipeline-assessment-{SYNAPSE_WS}.md" to generate_assessment_report(). The PipelineAssessment objects 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:

  1. Synapse workspace name (reuse from assessment if already run)

  2. Fabric workspace name

  3. Which pipelines to migrate — specific names, or * for all

  4. Optional 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 show

  • Fabric workspace ID via GET /v1/workspaces filtered by display name

  • Notebook GUIDs in Fabric via GET /v1/workspaces/{wsId}/notebooks (required for SynapseNotebook → 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, remove sparkPool/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 SynapseNotebook activity does not exist in the Fabric workspace yet

  • A 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)

PhaseSourceTargetResource
Phase 0Synapse notebooks (referenced by pipeline activities)Fabric Notebookssynapse-migration skill
Phase 1Synapse global parametersFabric Variable Libraryglobal-parameters-to-variable-library.md
Phase 2Synapse linked servicesFabric Connectionslinked-service-to-connection.md
Phase 3Synapse datasetsInlined into activitiesdataset-inlining.md
Phase 4Synapse pipeline activitiesFabric pipeline activitiesactivity-mapping.md + notebook-activity-migration.md
Phase 5Assembled pipeline JSONFabric DataPipeline itempipeline-orchestrator.md
Final—Validationvalidation-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 ActivityFabric EquivalentStatus
SynapseNotebookTridentNotebook✅ Migrated — see notebook-activity-migration.md
CopyCopy✅ Migrated — datasets inlined
LookupLookup✅ Migrated — datasets inlined
GetMetadataGetMetadata✅ Migrated — datasets inlined
ValidationGetMetadata + IfCondition✅ Migrated — split into 2 activities
ForEachForEach✅ Compatible
IfConditionIfCondition✅ Compatible
SwitchSwitch✅ Compatible
UntilUntil✅ Compatible
WaitWait✅ Compatible
FailFail✅ Compatible
SetVariableSetVariable✅ Compatible
AppendVariableAppendVariable✅ Compatible
ExecutePipelineExecutePipeline✅ Compatible — add workspaceId
WebActivityWebActivity✅ Compatible
ScriptScript✅ Compatible — update connection refs
DeleteDelete✅ Migrated — datasets inlined
FilterFilter✅ Compatible
SparkJobDefinition (Synapse SJD)SparkJobDefinition✅ Update GUID refs
HDInsightSparkTridentNotebook or SparkJobDefinition⚠️ Rewrite required
AzureMLBatchExecutionWebActivity⚠️ Rewrite as REST call
AzureFunctionActivityWebActivity⚠️ Rewrite — use function URL + key
DatabricksNotebook⛔ ParkedSee pipeline-gotchas.md
DatabricksSparkJar⛔ ParkedSee pipeline-gotchas.md
DatabricksSparkPython⛔ ParkedSee pipeline-gotchas.md
ExecuteSSISPackage⛔ ParkedSee pipeline-gotchas.md
AzureBatch⛔ ParkedNo Fabric equivalent
Custom⛔ ParkedNo 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/outputs dataset properties must be embedded in each activity

  • Replace @pipeline().globalParameters.<name> with @pipeline().libraryVariables.<name> after creating the Variable Library

  • Replace Validation activities with a GetMetadata + IfCondition pair — Validation does not exist as an activity type in Fabric

  • Remove sparkPool and sessionConfiguration from 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 promotion

  • Parameterized notebookParameters over hardcoded values in TridentNotebook activities — mirrors Synapse parameterized notebook pattern

  • Test 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 TridentNotebook activities — use the GUID from the Fabric workspace notebook list

  • Do 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().globalParameters syntax 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 IDIssueSeverityResolution Summary
PG1NOTEBOOK_GUID_NOT_FOUNDNotebook not yet migrated to Fabric when building TridentNotebook activityHighRun synapse-migration skill first; get GUID from GET /v1/workspaces/{wsId}/notebooks
PG2DATASET_NOT_INLINEDActivity still references a named dataset (not valid in Fabric)HighApply dataset-inlining.md patterns to embed dataset properties into activity source/sink
PG3GLOBAL_PARAM_EXPRESSION@pipeline().globalParameters.<name> expression left in migrated pipelineHighReplace with @pipeline().libraryVariables.<name> after creating Variable Library
PG4VALIDATION_ACTIVITY_UNSUPPORTEDValidation activity type left in pipeline JSONHighRewrite as GetMetadata + IfCondition — see activity-mapping.md
PG5SHIR_CONNECTOR_PARKEDActivity uses a linked service backed by a Self-Hosted Integration RuntimeMediumMust set up on-premises data gateway in Fabric; see pipeline-gotchas.md
PG6SSIS_ACTIVITY_PARKEDExecuteSSISPackage activity cannot be migratedHighParked — no Fabric equivalent; see pipeline-gotchas.md for alternatives
PG7DATABRICKS_ACTIVITY_PARKEDDatabricks activity type has no Fabric native equivalentHighParked — use Databricks REST API via WebActivity as workaround; see pipeline-gotchas.md
PG8SPARKPOOL_REF_ORPHANEDsparkPool / targetBigDataPool reference left in TridentNotebook activityMediumRemove sparkPool and sessionConfiguration blocks; pool config belongs in Fabric Environment
PG9EXECUTE_PIPELINE_NO_WORKSPACEExecutePipeline activity missing workspaceId for referenced pipelineMediumAdd workspaceId to typeProperties; required even for same-workspace child pipelines — omitting it causes runtime failures
PG10LINKED_SERVICE_NO_CONNECTIONLinked service has no matching Fabric ConnectionHighCreate 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:

TaskSkill / Tool
Migrate notebook content (mssparkutils → notebookutils, linked services)synapse-migration skill
Schedule migrated pipelinesCOMMON-CLI.md § Job Scheduling
Monitor pipeline runsFabric workspace → Monitor hub
Build new Fabric pipelinesRefer to ITEM-DEFINITIONS-CORE.md § DataPipeline
Explore migrated Lakehouse data post-pipeline runspark-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.batchDate

Dataset 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

resources/validation-testing.md

Validation & Testing — Post-Migration Verification

Step-by-step checks to confirm each migrated pipeline is structurally valid and functionally correct in Fabric before handing off to production.


Validation Checklist

CheckDescriptionPass Criteria
V1Pipeline definition round-tripsCall getDefinition (POST LRO) and verify the returned definition contains all activities
V2Pipeline runs without errorStatus = Completed (job-instance terminal status per Fabric pipeline REST API)
V3TridentNotebook activities succeed end-to-endNotebook completes; output readable
V4Variable Library values accessible@pipeline().libraryVariables.* expressions resolve
V5Copy activity with inlined dataset succeedsRow count > 0 or expected; no connector errors
V6Expression references validNo null values from libraryVariables expressions; no remaining @pipeline().globalParameters references

V1 — Pipeline Definition Round-Trip

After deploying a pipeline, retrieve its definition and verify the activity structure is intact.

FABRIC_TOKEN="<fabric-token>"
FABRIC_WS_ID="<workspace-id>"
PIPELINE_ITEM_ID="<pipeline-item-id>"

az rest --method POST \
  --headers "Authorization=Bearer ${FABRIC_TOKEN}" \
  --url "https://api.fabric.microsoft.com/v1/workspaces/${FABRIC_WS_ID}/items/${PIPELINE_ITEM_ID}/getDefinition"

The response is a long-running operation (LRO). Poll until complete:

import requests, time, base64, json, os

def get_pipeline_definition(workspace_id, pipeline_item_id, fabric_token,
                            max_polls=None, base_poll_interval=None):
    headers = {"Authorization": f"Bearer {fabric_token}"}
    # Per-request HTTP timeout as (connect, read). Long-running migrations
    # must not hang indefinitely on a transient network stall.
    http_timeout = (
        float(os.environ.get("FABRIC_HTTP_CONNECT_TIMEOUT", 10)),
        float(os.environ.get("FABRIC_HTTP_READ_TIMEOUT", 60)),
    )
    # Polling cadence: getDefinition on large pipelines can exceed the
    # naive 30 * 2 s = 60 s budget. Make max_polls and base_poll_interval
    # configurable (env vars or call args), and honor Retry-After when the
    # service supplies a polling hint — matches the LRO pattern in
    # pipeline-orchestrator.md fabric_post.
    if max_polls is None:
        max_polls = int(os.environ.get("FABRIC_LRO_MAX_POLLS", 150))
    if base_poll_interval is None:
        base_poll_interval = float(os.environ.get("FABRIC_LRO_POLL_INTERVAL", 2))

    # Start getDefinition
    r = requests.post(
        f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items/{pipeline_item_id}/getDefinition",
        headers=headers, timeout=http_timeout,
    )

    if r.status_code == 200:
        # Empty / non-JSON bodies on 200 are rare for getDefinition but possible
        # for other Fabric endpoints -- guard so the success path can't raise
        # a bare ValueError from r.json() on an empty or malformed body.
        if not r.content:
            raise RuntimeError(
                f"getDefinition returned HTTP 200 with an empty body for pipeline "
                f"{pipeline_item_id!r} -- Fabric definition payload missing."
            )
        try:
            return r.json()
        except ValueError as exc:
            raise RuntimeError(
                f"getDefinition returned HTTP 200 with a non-JSON body for "
                f"pipeline {pipeline_item_id!r}: {exc}. "
                f"Body excerpt: {r.text[:500]!r}"
            ) from exc
    
    # Handle LRO — prefer Location/Operation-Location header; fall back to x-ms-operation-id
    op_url = r.headers.get("Location") or r.headers.get("Operation-Location")
    operation_id = r.headers.get("x-ms-operation-id")
    if not op_url and operation_id:
        op_url = f"https://api.fabric.microsoft.com/v1/operations/{operation_id}"
    if not op_url:
        # Anything that isn't 200 and didn't surface a poll URL is an error.
        # raise_for_status() only fires for >=400, so a 202 with no headers
        # (or any other non-error status missing the LRO handles) would
        # otherwise fall through to op_url.startswith(...) and AttributeError.
        r.raise_for_status()
        raise RuntimeError(
            f"getDefinition returned HTTP {r.status_code} with no Location, "
            f"Operation-Location, or x-ms-operation-id header — cannot poll. "
            f"Response body: {r.text[:500]}"
        )
    # Some Fabric endpoints return a relative Location/Operation-Location
    # (e.g. "/v1/operations/..."). Resolve against the Fabric base before
    # polling so requests.get() receives a valid absolute URL.
    if op_url.startswith("/"):
        op_url = f"https://api.fabric.microsoft.com{op_url}"

    # Honor the original Retry-After hint, if any, for the first sleep.
    last_response = r
    for _ in range(max_polls):
        retry_after = last_response.headers.get("Retry-After")
        sleep_s = float(retry_after) if retry_after else base_poll_interval
        time.sleep(sleep_s)
        poll = requests.get(op_url, headers=headers, timeout=http_timeout)
        poll.raise_for_status()
        last_response = poll
        state = poll.json()
        status = state.get("status")
        if status == "Succeeded":
            result_url = state.get("resourceLocation") or state.get("result", {}).get("resourceLocation")
            if result_url:
                if result_url.startswith("/"):
                    result_url = f"https://api.fabric.microsoft.com{result_url}"
                # Fetch and validate the resourceLocation payload defensively:
                # raise on 4xx/5xx, and guard r.json() on an empty 200 body so
                # the success path can't fail with a confusing ValueError.
                result_resp = requests.get(result_url, headers=headers, timeout=http_timeout)
                result_resp.raise_for_status()
                if not result_resp.content:
                    raise RuntimeError(
                        f"getDefinition resourceLocation returned HTTP "
                        f"{result_resp.status_code} with an empty body "
                        f"({result_url}) -- Fabric definition payload missing."
                    )
                try:
                    return result_resp.json()
                except ValueError as exc:
                    raise RuntimeError(
                        f"getDefinition resourceLocation returned HTTP "
                        f"{result_resp.status_code} with a non-JSON body "
                        f"({result_url}): {exc}. "
                        f"Body excerpt: {result_resp.text[:500]!r}"
                    ) from exc
            return state
        elif status == "Failed":
            raise RuntimeError(f"getDefinition failed: {state}")
        elif status == "Cancelled":
            raise RuntimeError(f"getDefinition cancelled: {state}")
    
    raise TimeoutError(
        f"getDefinition LRO timed out after {max_polls} polls "
        f"(base interval {base_poll_interval}s). Increase FABRIC_LRO_MAX_POLLS "
        f"or FABRIC_LRO_POLL_INTERVAL for large pipeline definitions."
    )


def decode_pipeline_content(definition_response: dict) -> dict:
    """Decode base64 pipeline-content.json from getDefinition response."""
    parts = definition_response.get("definition", {}).get("parts", [])
    for part in parts:
        if part.get("path") != "pipeline-content.json":
            continue
        payload = part.get("payload")
        if not payload:
            raise ValueError(
                "pipeline-content.json part has no 'payload' field — "
                f"got keys {sorted(part.keys())}. Cannot decode."
            )
        payload_type = part.get("payloadType")
        if payload_type and payload_type != "InlineBase64":
            # We only know how to decode InlineBase64. Surface unexpected
            # payload types clearly so the error is actionable instead of a
            # cryptic base64 decode failure.
            raise ValueError(
                f"Unsupported payloadType '{payload_type}' for "
                f"pipeline-content.json — only 'InlineBase64' is supported."
            )
        return json.loads(base64.b64decode(payload).decode())
    raise KeyError("pipeline-content.json not found in definition")


def verify_activities(fabric_pipeline: dict, expected_names: list[str]) -> list[str]:
    """Return list of expected activity names NOT found in the pipeline."""
    activities = fabric_pipeline.get("properties", {}).get("activities", [])
    # Defensive: pipeline JSON is third-party content, so an activity entry
    # could be malformed (non-dict, missing 'name'). Filter rather than let
    # the set comprehension KeyError and abort the whole validation run.
    actual_names = {
        a.get("name")
        for a in activities
        if isinstance(a, dict) and a.get("name")
    }
    return [n for n in expected_names if n not in actual_names]


# Example
pipeline_def = get_pipeline_definition(FABRIC_WS_ID, PIPELINE_ITEM_ID, FABRIC_TOKEN)
pipeline_content = decode_pipeline_content(pipeline_def)
missing = verify_activities(pipeline_content, ["Run ETL Notebook", "Copy Orders", "Get Source File Metadata"])
if missing:
    print(f"⚠️  Missing activities: {missing}")
else:
    print("✅ V1: All expected activities present")

V2 — Pipeline Run

Trigger a pipeline run and wait for it to complete.

The same Fabric pipeline GUID is used as both PIPELINE_ITEM_ID (under the generic /v1/workspaces/{wsId}/items/{itemId}/... Item-management surface) and as the path segment under the Data Factory job surface /v1/workspaces/{wsId}/datapipelines/{pipelineItemId}/jobs/.... The parameter name is kept consistent (pipeline_item_id) across both surfaces below to avoid ambiguity.

import requests, time, os

# Per-request HTTP timeout (connect, read). Mirrors the pattern in
# pipeline-orchestrator.md / get_pipeline_definition so validation runs
# never hang indefinitely on a transient network stall.
_HTTP_TIMEOUT = (
    float(os.environ.get("FABRIC_HTTP_CONNECT_TIMEOUT", 10)),
    float(os.environ.get("FABRIC_HTTP_READ_TIMEOUT", 60)),
)

def run_pipeline(workspace_id, pipeline_item_id, fabric_token, parameters=None):
    headers = {
        "Authorization": f"Bearer {fabric_token}",
        "Content-Type": "application/json"
    }
    body = {}
    if parameters:
        body["executionData"] = {"parameters": parameters}
    
    r = requests.post(
        f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/datapipelines/{pipeline_item_id}/jobs/instances?jobType=Pipeline",
        headers=headers,
        json=body,
        timeout=_HTTP_TIMEOUT,
    )
    r.raise_for_status()
    
    # Get job instance ID from Location header
    location = r.headers.get("Location", "")
    if location:
        from urllib.parse import urlparse
        path_segments = [s for s in urlparse(location).path.split("/") if s]
        job_instance_id = path_segments[-1] if path_segments else r.json().get("id")
    else:
        job_instance_id = r.json().get("id")
    return job_instance_id


def wait_for_pipeline_run(workspace_id, pipeline_item_id, job_instance_id, fabric_token, timeout_seconds=3600):
    headers = {"Authorization": f"Bearer {fabric_token}"}
    deadline = time.time() + timeout_seconds
    
    while time.time() < deadline:
        r = requests.get(
            f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/datapipelines/{pipeline_item_id}/jobs/instances/{job_instance_id}",
            headers=headers,
            timeout=_HTTP_TIMEOUT,
        )
        r.raise_for_status()
        status_data = r.json()
        status = status_data.get("status")
        
        # Fabric Job Scheduler getJobInstance returns "Completed" as the
        # terminal success state on a job instance (not "Succeeded" --
        # "Succeeded" is what the per-activity queryactivityruns response
        # carries at the activity level). See the pipeline REST API docs:
        # learn.microsoft.com/fabric/data-factory/pipeline-rest-api
        if status == "Completed":
            print(f"✅ V2: Pipeline run completed successfully")
            return status_data
        elif status in ("Failed", "Cancelled"):
            failure_reason = status_data.get("failureReason", {})
            raise RuntimeError(
                f"Pipeline run {status}: {failure_reason.get('message', 'Unknown error')}"
            )
        
        time.sleep(15)  # Poll every 15 seconds
    
    raise TimeoutError(f"Pipeline run did not complete within {timeout_seconds} seconds")


# Example
job_id = run_pipeline(FABRIC_WS_ID, PIPELINE_ITEM_ID, FABRIC_TOKEN, 
                      parameters={"runDate": "2024-01-01"})
result = wait_for_pipeline_run(FABRIC_WS_ID, PIPELINE_ITEM_ID, job_id, FABRIC_TOKEN)

V3 — TridentNotebook Activity Verification

After a pipeline run that includes TridentNotebook activities, confirm the notebook ran successfully and produced expected output.

def verify_notebook_activity_output(run_result: dict, activity_name: str, expected_key: str = None):
    """
    Check that a TridentNotebook activity in the run result succeeded.
    
    Args:
        run_result: The completed job instance response
        activity_name: Name of the TridentNotebook activity
        expected_key: Optional key to check in runOutput JSON
    """
    # Note: Fabric run result may need activity-level status via separate API
    # This checks the top-level run status as a proxy. Terminal success on
    # a job instance is "Completed" per the pipeline REST API.
    status = run_result.get("status")
    if status != "Completed":
        raise AssertionError(f"Run did not complete. Status: {status}")
    
    print(f"✅ V3: Pipeline containing notebook activity '{activity_name}' succeeded")
    print(f"   Run ID: {run_result.get('id')}")
    print(f"   Start: {run_result.get('startTimeUtc')}")
    print(f"   End: {run_result.get('endTimeUtc')}")

Common TridentNotebook failure reasons:

Failure MessageRoot CauseFix
"Item not found"notebookId GUID is wrong or notebook was deletedRe-run notebook GUID lookup and update pipeline
"Session could not be started"No Fabric capacity available; Spark session failedCheck workspace capacity; try again during off-peak
"Notebook execution failed"Code error inside the notebookCheck notebook run history in Fabric portal
"Parameter type mismatch"Notebook expects int but pipeline passed stringAlign parameter types in notebookParameters
"Timeout"Notebook ran longer than policy.timeoutIncrease timeout (max 12h) or split notebook

V4 — Variable Library Accessibility

Confirm that @pipeline().libraryVariables.* expressions resolve correctly during a run.

Test pipeline structure (minimal pipeline that writes a library variable to a pipeline variable for inspection):

{
  "properties": {
    "activities": [
      {
        "name": "Read Library Variable",
        "type": "SetVariable",
        "typeProperties": {
          "variableName": "capturedEnv",
          "value": {
            "value": "@pipeline().libraryVariables.environmentName",
            "type": "Expression"
          }
        }
      },
      {
        "name": "Assert Variable Set",
        "type": "IfCondition",
        "dependsOn": [{ "activity": "Read Library Variable", "dependencyConditions": ["Succeeded"] }],
        "typeProperties": {
          "expression": {
            "value": "@not(empty(variables('capturedEnv')))",
            "type": "Expression"
          },
          "ifFalseActivities": [
            {
              "name": "Fail - Variable Empty",
              "type": "Fail",
              "typeProperties": {
                "message": "Variable Library value was null or empty",
                "errorCode": "LibraryVariableMissing"
              }
            }
          ]
        }
      }
    ],
    "variables": {
      "capturedEnv": { "type": "String" }
    },
    "libraryVariables": {
      "libraryId": "<variable-library-id>",
      "workspaceId": "<workspace-id>"
    }
  }
}

If the Fail activity is triggered, the Variable Library is not correctly attached to the pipeline.


V5 — Copy Activity with Inlined Dataset

For Copy activities with inlined datasets, run the pipeline with a small data sample and verify:

  1. Job-instance status = Completed (terminal success on the pipeline job instance)

  2. Rows written > 0 (or expected count)

  3. No connector errors in the run output

Completed vs Succeeded: The Fabric pipeline job-instance polling endpoint returns "status": "Completed" on success. Succeeded is the per-activity value returned by queryactivityruns (and is also the right token for dependencyConditions between activities) — not the job-instance terminal status.

def check_copy_activity_success(run_result: dict) -> bool:
    """
    Check that the pipeline run succeeded as a proxy for Copy activity success.
    Detailed per-activity output requires the Fabric monitoring API
    (queryactivityruns), which returns "Succeeded" at the activity level.
    """
    return run_result.get("status") == "Completed"

Common Copy activity failures after inlining:

FailureRoot CauseFix
"Connection not found"Fabric connection name doesn't match linkedServiceNameCreate connection with matching display name or update reference
"Access denied to storage"Workspace identity lacks RBAC on storageGrant Storage Blob Data Contributor to workspace managed identity
"File not found"Inlined path expression is incorrectCheck folderPath / fileName values after inlining
"Schema mismatch"Column mapping not preserved during inliningRe-add translator block from original Synapse Copy activity

V6 — Generate Validation Summary

After running all checks, generate a summary for each pipeline:

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class PipelineValidationResult:
    pipeline_name: str
    pipeline_item_id: str
    v1_definition_ok: bool = False
    v2_run_status: Optional[str] = None
    v3_notebook_ok: Optional[bool] = None
    v4_library_vars_ok: Optional[bool] = None
    v5_copy_ok: Optional[bool] = None
    errors: list[str] = field(default_factory=list)

    def passed(self) -> bool:
        critical = [self.v1_definition_ok, self.v2_run_status == "Completed"]
        return all(critical) and not self.errors

    def summary_row(self) -> dict:
        return {
            "Pipeline": self.pipeline_name,
            "V1 Definition": "✅" if self.v1_definition_ok else "❌",
            "V2 Run": "✅" if self.v2_run_status == "Completed" else f"❌ {self.v2_run_status}",
            "V3 Notebook": "✅" if self.v3_notebook_ok else ("N/A" if self.v3_notebook_ok is None else "❌"),
            "V4 Library Vars": "✅" if self.v4_library_vars_ok else ("N/A" if self.v4_library_vars_ok is None else "❌"),
            "V5 Copy": "✅" if self.v5_copy_ok else ("N/A" if self.v5_copy_ok is None else "❌"),
            "Errors": "; ".join(self.errors) if self.errors else ""
        }


def print_validation_report(results: list[PipelineValidationResult]):
    passed = sum(1 for r in results if r.passed())
    print(f"\n{'='*60}")
    print(f"Pipeline Validation Report — {passed}/{len(results)} passed")
    print(f"{'='*60}")
    for r in results:
        row = r.summary_row()
        status = "✅ PASS" if r.passed() else "❌ FAIL"
        print(f"\n{status} | {row['Pipeline']}")
        for k, v in row.items():
            if k != "Pipeline":
                print(f"  {k}: {v}")

Results feed into migration-report.md for the final handoff document.

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