Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Microsoft Fabric

Implementing End-to-End Pipeline Error Handling in Microsoft Fabric: If Condition Activities, Failed Dependencies, and Email Alerts

Build production-grade error handling in Microsoft Fabric pipelines that actually catches failures, routes them intelligently, and sends actionable email alerts — across Copy, Dataflow Gen2, and Notebook activities. Learn the dependency condition model, activity output expressions, and centralized error handler patterns that keep your medallion pipelines resilient.

🔥 Expert26 min readSep 22, 2026Updated Sep 23, 2026
Implementing End-to-End Pipeline Error Handling in Microsoft Fabric: If Condition Activities, Failed Dependencies, and Email Alerts
On this page
  • Introduction
  • Prerequisites
  • Understanding Dependency Conditions: The Foundation of Everything
  • The Activity Output Object: Your Diagnostic Goldmine
  • Designing a Failure Routing Architecture
  • Capturing Failure Details with Set Variable Activities
  • Handling Dataflow Gen2 Failures: The Special Case
  • Handling Notebook Failures: Business Logic vs. Technical Errors
  • Building the If Condition Activity
  • Wiring Up the Dependency Graph: Step by Step
  • Advanced Pattern: The ForEach-Based Multi-Source Error Handler
  • Writing the Error Report Notebook
  • Configuring Activity-Level Retries: Your First Line of Defense
  • Email Alert Content Design: Making Alerts Actionable
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Implementing End-to-End Pipeline Error Handling in Microsoft Fabric: Using If Condition Activities, Failed Dependencies, and Email Alerts to Catch and Route Failures Across Copy, Dataflow, and Notebook Activities

    Introduction

    Picture this: it's Monday morning, your sales leadership is waiting for the weekly revenue dashboard to reflect the weekend's transactions, and your pipeline silently failed at 2 AM because the source REST API returned a 503. Nobody got an alert. Nobody knows. Power BI is showing stale data from Friday. The pipeline logs are sitting in the Monitoring Hub waiting to be discovered, but nobody thought to check them because the assumption was everything worked fine.

    This is the scenario that end-to-end error handling exists to prevent. A pipeline that runs unattended without proper failure routing is not a production pipeline — it's a time bomb. In Microsoft Fabric, you have all the primitives you need to build genuinely resilient orchestration: dependency conditions that route execution based on upstream outcomes, If Condition activities that evaluate dynamic expressions, Office 365 Outlook activities that fire email alerts, and a rich @activity() expression language that lets you interrogate exactly what failed and why. The challenge is knowing how these pieces fit together across the heterogeneous mix of Copy, Dataflow Gen2, and Notebook activities that make up a real-world Fabric pipeline.

    By the end of this lesson, you will know how to architect error handling that actually works in production — not just the happy path. You'll understand how dependency conditions chain, why Dataflow Gen2 activities require special handling, how to extract failure metadata from the @activity() object and embed it in alert emails, and how to use If Condition activities to branch execution based on business logic rather than just technical failures.

    What you'll learn:

    • How Fabric pipeline dependency conditions work — Success, Failure, Completion, and Skipped — and when each one is appropriate
    • How to wire Failed dependency paths from Copy, Dataflow Gen2, and Notebook activities into a centralized error handler
    • How to use the If Condition activity with @activity() expressions to evaluate outcomes and route execution dynamically
    • How to configure the Office 365 Outlook activity to send rich, context-aware email alerts with failure details embedded
    • How to design a reusable error handling pattern that scales across multi-stage medallion pipelines without becoming a spaghetti of dependencies

    Prerequisites

    This lesson assumes you are comfortable with the following:

    • Navigating the Microsoft Fabric portal and working within a workspace with at least Contributor access. If you need to set up your environment, review Fabric Capacities and Workspaces: F SKUs, Trials, and Setting Up Your First Workspace.
    • Building and configuring basic Fabric data pipelines, including Copy activities and scheduling. The lesson Orchestrating Loads with Fabric Data Pipelines: Copy Activities, Parameters, and Schedules covers everything you need.
    • A working understanding of Dataflow Gen2 and how it sits inside a pipeline. See Scheduling Dataflow Gen2 Refreshes and Chaining Them into a Fabric Data Pipeline for End-to-End Orchestration.
    • Basic familiarity with PySpark notebooks in Fabric. If you need a refresher, see Transforming Data with Spark Notebooks in Microsoft Fabric: PySpark for Lakehouse Tables.
    • An Office 365 account connected to your Fabric tenant (needed for the email alert configuration).

    Understanding Dependency Conditions: The Foundation of Everything

    Before you can handle failures, you need to understand how Fabric pipelines model execution flow. Every arrow you draw between two activities in the pipeline canvas is not just a sequence — it's a dependency with a condition. That condition determines under which outcome of the upstream activity the downstream activity is allowed to run.

    Fabric exposes four dependency conditions:

    Success — the downstream activity runs only if the upstream activity completed without error. This is the default when you draw a connection between activities.

    Failure — the downstream activity runs only if the upstream activity failed. This is your primary tool for error routing.

    Completion — the downstream activity runs regardless of whether the upstream activity succeeded or failed. This is useful for cleanup operations, audit logging, or setting status flags that need to happen no matter what.

    Skipped — the downstream activity runs if the upstream activity was skipped due to an If Condition branch that wasn't taken, or if the upstream activity itself was dependent on a Skipped predecessor.

    Here is the critical subtlety that trips people up: an activity can have multiple incoming dependency conditions simultaneously. If Activity B depends on Activity A with both a Success and a Failure condition, then B runs after A regardless of A's outcome — equivalent to Completion. This matters when you're building centralized error handlers that need to catch failures from multiple upstream activities.

    Warning

    The Completion condition and the combination of Success + Failure conditions are not identical in all scenarios. Completion triggers even if an activity is explicitly cancelled. Success + Failure will not run if the activity was cancelled. For monitoring pipelines that run unattended overnight, this distinction matters — a cancellation mid-run should behave differently than a clean failure.

    Another important nuance: dependency conditions are evaluated per activity pair, not per pipeline run globally. If you have Activity A → Activity B (Success) → Activity C (Success), and Activity A fails, then B is never triggered, and C's dependency on B is evaluated against B's status — which is "Skipped," not "Failed." If C only has a Success dependency on B, C also gets Skipped. This cascading Skipped behavior is the most common source of error handling bugs in Fabric pipelines.


    The Activity Output Object: Your Diagnostic Goldmine

    Before we start wiring up failure paths, you need to understand what information is available to you when an activity fails. The @activity() expression function is how you access it.

    Every completed activity — regardless of success or failure — exposes a rich output object. The general structure is:

    {
      "status": "Failed",
      "output": {
        "errors": [
          {
            "Code": "UserErrorInvalidParameter",
            "Message": "The connection to the source could not be established.",
            "Details": "..."
          }
        ]
      },
      "error": {
        "errorCode": "UserErrorInvalidParameter",
        "message": "The connection to the source could not be established.",
        "failureType": "UserError",
        "target": "Copy_SalesTransactions"
      }
    }
    

    In pipeline expressions, you access this with:

    @activity('Copy_SalesTransactions').output.errors[0].Message
    @activity('Copy_SalesTransactions').error.message
    @activity('Copy_SalesTransactions').error.errorCode
    @activity('Copy_SalesTransactions').error.failureType
    @activity('Copy_SalesTransactions').status
    

    The failureType property is particularly valuable for routing logic. Fabric distinguishes between:

    • UserError — a configuration or data issue (wrong connection string, schema mismatch, invalid parameter). Retrying will not fix this.
    • SystemError — a transient infrastructure problem (service unavailable, timeout). Retrying might fix this.

    This distinction lets you build smarter error handling: for SystemError, you might trigger a retry loop or a compensating action; for UserError, you escalate immediately to a human.

    Tip

    The @activity() function only works in activities that depend on the target activity, directly or transitively. You cannot call @activity('CopyActivity1') from an activity that has no dependency path back to CopyActivity1. Plan your graph topology with this in mind.

    For Notebook activities specifically, you have an additional escape hatch. Within the notebook itself, you can set the exit value:

    # In your PySpark notebook
    mssparkutils.notebook.exit("VALIDATION_FAILED: Row count is 0 for sales_transactions_2024_01")
    

    This exit value surfaces in the pipeline as @activity('Transform_SilverLayer').output.result.exitValue. You can evaluate it in a downstream If Condition activity to make decisions based on semantic business outcomes, not just technical pass/fail status.


    Designing a Failure Routing Architecture

    Theory is useful, but what does a well-architected error handling pattern actually look like in a real Fabric pipeline? Let's build one based on a concrete scenario.

    The scenario: You're running a nightly medallion pipeline that:

    1. Copies raw sales data from an Azure SQL source into the Bronze lakehouse (Copy activity)
    2. Transforms it from Bronze to Silver using a Dataflow Gen2 that handles deduplication and type casting
    3. Runs a PySpark notebook to compute aggregations and write the Gold layer tables
    4. Sends a success notification to the analytics team

    You want failures at any stage to: (a) stop downstream processing immediately, (b) capture the specific error details, (c) email the data engineering team with actionable information, and (d) ideally distinguish between retriable infrastructure failures and non-retriable configuration failures.

    Here's the high-level graph topology you're aiming for:

    [Copy_Bronze] ──(Success)──► [Dataflow_Silver] ──(Success)──► [Notebook_Gold] ──(Success)──► [Email_Success]
          │                              │                              │
          └──(Failure)──►                └──(Failure)──►               └──(Failure)──►
                        │                              │                              │
                        └──────────────────────────────┴──────────────────────────────┘
                                                       │
                                              [If_IsRetriable]
                                              /               \
                                   (True branch)         (False branch)
                                [Email_RetryAlert]    [Email_EscalationAlert]
    

    The key design decision here is the centralized error handler pattern: all three failure paths converge on a single If_IsRetriable activity, rather than each activity having its own disconnected error handler. This keeps the pipeline graph readable and ensures consistent error handling behavior regardless of which stage fails.

    Key insight

    The centralized error handler pattern has one important limitation: because @activity() expressions require a direct dependency path, you can't call @activity('Copy_Bronze') from within the If Condition if the If Condition was triggered by a Dataflow failure. The solution is to use pipeline variables as error capture points — each failure path sets pipeline variables before converging on the handler.


    Capturing Failure Details with Set Variable Activities

    Since we can't interrogate all upstream activities from a single centralized handler, we use pipeline variables as the intermediary. Add the following variables to your pipeline (Pipeline settings → Variables):

    Variable Name Type Default Value
    ErrorMessage String (empty)
    ErrorCode String (empty)
    ErrorSource String (empty)
    FailureType String (empty)
    PipelineRunId String (empty)

    Each failure path gets a Set Variable activity that captures the relevant information before passing control to the centralized handler.

    For the Copy activity failure path, add a Set Variable activity named SetError_Copy with these assignments:

    ErrorMessage:

    @concat('Copy activity failed: ', activity('Copy_Bronze').error.message)
    

    ErrorCode:

    @activity('Copy_Bronze').error.errorCode
    

    ErrorSource:

    Copy_Bronze
    

    FailureType:

    @activity('Copy_Bronze').error.failureType
    

    PipelineRunId:

    @pipeline().RunId
    

    For the Dataflow Gen2 failure path, the expression structure is similar but there's an important difference: Dataflow Gen2 activities surface their errors differently.


    Handling Dataflow Gen2 Failures: The Special Case

    Dataflow Gen2 activities are notoriously less transparent in their error reporting than Copy activities. When a Dataflow Gen2 fails, the error structure often looks like this:

    {
      "error": {
        "errorCode": "DFExecutionFailed",
        "message": "Dataflow refresh failed.",
        "failureType": "UserError"
      }
    }
    

    That message — "Dataflow refresh failed" — is spectacularly unhelpful. The actual root cause is buried in the Dataflow refresh history within the Fabric portal, not surfaced cleanly in the pipeline activity output.

    This has two practical implications for your error handling design:

    First, your error message for Dataflow failures should direct the on-call engineer to look in the right place:

    @concat(
      'Dataflow Gen2 [Silver_Dedup] failed at ',
      formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm:ss'),
      ' UTC. Pipeline Run ID: ',
      pipeline().RunId,
      '. Review the Dataflow refresh history in the Fabric portal for root cause details.'
    )
    

    Second, because Dataflow errors are almost always classified as UserError by the pipeline engine regardless of whether the actual cause is transient or not, you should implement a more conservative retry policy for Dataflows at the activity level (via the Settings tab on the Dataflow activity) rather than relying on your FailureType routing logic.

    Warning

    The failureType field for Dataflow Gen2 activities is unreliable as a routing signal for retriable vs. non-retriable errors. A transient Power Query engine timeout will appear as UserError: DFExecutionFailed exactly the same as a genuine M query syntax error. Always configure activity-level retries on Dataflow Gen2 activities (2-3 retries with a 30-second wait) and treat all Dataflow failures that reach your pipeline-level error handler as requiring human investigation.

    For the Dataflow failure path, your Set Variable activity looks like:

    ErrorMessage:

    @concat(
      'Dataflow Gen2 [Silver_Dedup] failed. ',
      'Error: ', activity('Dataflow_Silver').error.message,
      ' | Run ID: ', pipeline().RunId,
      ' | Check Fabric portal > Dataflow refresh history for details.'
    )
    

    FailureType:

    @if(
      equals(activity('Dataflow_Silver').error.errorCode, 'DFExecutionFailed'),
      'RequiresInvestigation',
      activity('Dataflow_Silver').error.failureType
    )
    

    Notice how we're overriding the FailureType for the DFExecutionFailed case with a custom value RequiresInvestigation — this tells our downstream If Condition that Dataflow failures always need a human, bypassing the auto-retry routing.


    Handling Notebook Failures: Business Logic vs. Technical Errors

    Notebooks are the richest source of failure information, but they require you to write that information yourself. A notebook that crashes with an unhandled exception surfaces in the pipeline with a generic error. A notebook that uses mssparkutils.notebook.exit() strategically can communicate semantic failure reasons.

    Here's a pattern for a Gold layer aggregation notebook that distinguishes between different failure modes:

    from pyspark.sql import SparkSession
    import traceback
    
    spark = SparkSession.builder.getActiveSession()
    
    # Read from Silver layer
    try:
        df_silver = spark.read.format("delta").load(
            "abfss://sales-lakehouse@onelake.dfs.fabric.microsoft.com/Silver/sales_transactions"
        )
    except Exception as e:
        mssparkutils.notebook.exit(f"TECHNICAL_ERROR: Could not read Silver table. {str(e)}")
    
    # Business validation
    row_count = df_silver.count()
    if row_count == 0:
        mssparkutils.notebook.exit(
            "BUSINESS_ERROR: Silver table is empty. Possible upstream Dataflow failure. "
            f"Expected >0 rows for date partition {processing_date}."
        )
    
    # Run aggregations
    try:
        df_gold = df_silver.groupBy("region", "product_category", "sale_date") \
            .agg(
                {"revenue": "sum", "transaction_id": "count", "quantity": "sum"}
            ) \
            .withColumnRenamed("sum(revenue)", "total_revenue") \
            .withColumnRenamed("count(transaction_id)", "transaction_count") \
            .withColumnRenamed("sum(quantity)", "total_quantity")
    
        df_gold.write.format("delta").mode("overwrite").option("overwriteSchema", "true").save(
            "abfss://sales-lakehouse@onelake.dfs.fabric.microsoft.com/Gold/sales_aggregations"
        )
    except Exception as e:
        mssparkutils.notebook.exit(f"TECHNICAL_ERROR: Gold write failed. {str(e)[:500]}")
    
    # Success
    mssparkutils.notebook.exit("SUCCESS")
    

    In the pipeline, after the Notebook activity, you add a Set Variable activity in the failure path that captures:

    ErrorMessage:

    @concat(
      'Notebook [Transform_Gold] failed. ',
      'Exit value: ', activity('Notebook_Gold').output.result.exitValue,
      ' | Spark error: ', activity('Notebook_Gold').error.message
    )
    

    FailureType:

    @if(
      startsWith(activity('Notebook_Gold').output.result.exitValue, 'BUSINESS_ERROR'),
      'UserError',
      'SystemError'
    )
    

    This is powerful: you're using the notebook's own exit value to classify the error type at the pipeline level, enabling genuinely intelligent routing. A BUSINESS_ERROR (empty table) should immediately escalate to the data team. A TECHNICAL_ERROR from a Spark executor crash might be worth an automatic retry.

    Tip

    Keep notebook exit values short and structured. The @activity().output.result.exitValue field has a size limit, and if your error message is a full Python stack trace, it will be truncated. Prefix exit values with a short category token (SUCCESS, BUSINESS_ERROR, TECHNICAL_ERROR) that the pipeline can parse with startsWith() expressions, and log full details to a Delta table or lakehouse file separately.


    Building the If Condition Activity

    Now we converge all three failure paths — each having set our pipeline variables — onto the If_IsRetriable If Condition activity.

    The If Condition activity evaluates a boolean expression and routes to either a True activities block or a False activities block. Each block is essentially a mini-pipeline canvas where you can add any activities.

    The routing expression:

    @equals(variables('FailureType'), 'SystemError')
    

    This evaluates to true for infrastructure failures (potentially retriable) and false for everything else.

    True branch (SystemError — potentially retriable):

    Add a single Office 365 Outlook activity named Email_RetryAlert with subject:

    @concat('[AUTO-RETRY CANDIDATE] Fabric Pipeline Failure — ', pipeline().Pipeline, ' at ', formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm'))
    

    And body (HTML):

    @concat(
      '<h2>Pipeline Failure — Potentially Retriable</h2>',
      '<table>',
      '<tr><td><b>Pipeline:</b></td><td>', pipeline().Pipeline, '</td></tr>',
      '<tr><td><b>Run ID:</b></td><td>', pipeline().RunId, '</td></tr>',
      '<tr><td><b>Failed Activity:</b></td><td>', variables('ErrorSource'), '</td></tr>',
      '<tr><td><b>Error Code:</b></td><td>', variables('ErrorCode'), '</td></tr>',
      '<tr><td><b>Failure Type:</b></td><td>', variables('FailureType'), '</td></tr>',
      '<tr><td><b>Message:</b></td><td>', variables('ErrorMessage'), '</td></tr>',
      '</table>',
      '<p>This failure type is classified as a system/infrastructure error and may resolve on retry. ',
      'Review the <a href="https://app.fabric.microsoft.com">Monitoring Hub</a> before manually re-triggering.</p>'
    )
    

    False branch (UserError / RequiresInvestigation — escalate immediately):

    Add a single Office 365 Outlook activity named Email_EscalationAlert with subject:

    @concat('[ACTION REQUIRED] Fabric Pipeline Failure — ', pipeline().Pipeline, ' at ', formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm'))
    

    And body with higher urgency language that includes the same error details plus an explicit call to action. Set the importance to High.

    Note

    The Office 365 Outlook activity requires a connection configured using an organizational account. That account must have an active Exchange Online mailbox. If you're using a service principal or a shared mailbox, test this connection explicitly before deploying to production — authentication silently fails in some tenant configurations and the activity will show as succeeded while no email was sent.


    Wiring Up the Dependency Graph: Step by Step

    Let me walk you through the exact wiring process in the Fabric pipeline canvas. The goal is to implement the topology we described earlier without creating ambiguous or circular dependencies.

    Step 1: Create the main success path

    Add your three main activities in sequence with Success dependencies:

    • Copy_Bronze → Dataflow_Silver (Success)
    • Dataflow_Silver → Notebook_Gold (Success)
    • Notebook_Gold → Email_Success (Success)

    Step 2: Add Set Variable activities for each failure path

    Add three Set Variable activities (one per stage) to the canvas outside the main flow:

    • SetError_Copy
    • SetError_Dataflow
    • SetError_Notebook

    Connect each one with a Failure dependency from its corresponding activity:

    • Copy_Bronze → SetError_Copy (Failure)
    • Dataflow_Silver → SetError_Dataflow (Failure)
    • Notebook_Gold → SetError_Notebook (Failure)

    Step 3: Add the If Condition activity

    Add If_IsRetriable to the canvas. Connect all three Set Variable activities to it with Success dependencies:

    • SetError_Copy → If_IsRetriable (Success)
    • SetError_Dataflow → If_IsRetriable (Success)
    • SetError_Notebook → If_IsRetriable (Success)

    This means If_IsRetriable will only run after one of the Set Variable activities has successfully captured the error context.

    Step 4: Wire the If Condition branches

    Open the If Condition activity. In the True Activities canvas, add Email_RetryAlert. In the False Activities canvas, add Email_EscalationAlert.

    Step 5: Verify the graph

    At this point, your complete pipeline has two distinct flows:

    1. The happy path: Copy → Dataflow → Notebook → Email Success
    2. The error path (any stage): Stage Failure → SetError_[Stage] → If Condition → Email Retry OR Email Escalation

    The Email_Success activity has no failure path — if the success notification itself fails, you probably don't want that to trigger another alert spiral. Accept that as a tolerable gap.

    Warning

    A common mistake is connecting the Set Variable activities with Completion (rather than Failure) dependencies from the main activities. This would cause the Set Variable activities to run on both success and failure, meaning the If Condition would always fire — and you'd always get a failure alert even on successful runs. Use Failure-only dependencies for the error capture Set Variable activities.


    Advanced Pattern: The ForEach-Based Multi-Source Error Handler

    The flat topology above works well for sequential pipelines with three to five stages. But what if you're implementing a medallion architecture with twelve source tables being copied in parallel, each potentially failing independently?

    In this scenario, the single-activity error handler pattern produces an unreadable tangle of dependency lines. The better approach is a ForEach-based parallel ingestion pattern with a centralized post-run assessment.

    Here's the architecture:

    [Lookup_SourceTableList]
             │ (Success)
    [ForEach_Tables]
      ├── [Copy_Table] ──(Failure)──► [SetVar_IncrementFailCount]
      └── (Success) ──► (nothing additional — ForEach continues)
             │ (Completion)
    [If_AnyFailures] ── True ──► [Notebook_ErrorReport] ──► [Email_FailureSummary]
             │
             └── False ──► [Email_Success]
    

    The key technique here is using a pipeline variable of type Integer named FailureCount (default 0). Inside the ForEach, on each Copy activity failure, a Set Variable activity increments the count:

    @add(variables('FailureCount'), 1)
    

    After the ForEach completes (using a Completion dependency so it runs regardless of inner failures), the If Condition evaluates:

    @greater(variables('FailureCount'), 0)
    

    If any tables failed, a Notebook activity runs to query the pipeline's own run results using the Fabric REST API and generate a structured failure report, which is then attached to the email.

    Key insight

    The ForEach activity's isSequential setting interacts importantly with error handling. When isSequential is false (parallel execution), inner activity failures do not stop the ForEach — other iterations continue. When isSequential is true, a failure in one iteration stops all subsequent iterations. For bulk ingestion where you want maximum throughput and accept partial failures, use parallel execution. For ordered dependencies where step N+1 depends on step N, use sequential.


    Writing the Error Report Notebook

    For the multi-source pattern, you can write a notebook that queries the Monitoring Hub API to generate a structured failure report. This demonstrates how notebook activities can serve dual purposes — not just transforming data, but participating in the pipeline's operational intelligence.

    import requests
    import json
    from pyspark.sql import SparkSession
    import pandas as pd
    from datetime import datetime
    
    # Parameters passed from pipeline
    pipeline_run_id = getArgument("pipeline_run_id")  # @pipeline().RunId
    workspace_id = getArgument("workspace_id")        # @pipeline().DataFactory
    failure_count = int(getArgument("failure_count")) # @string(variables('FailureCount'))
    
    # Build error report
    error_report = {
        "pipeline_run_id": pipeline_run_id,
        "workspace_id": workspace_id,
        "failure_count": failure_count,
        "report_generated_at": datetime.utcnow().isoformat(),
        "status": "PARTIAL_FAILURE" if failure_count > 0 else "SUCCESS"
    }
    
    # Write report to lakehouse for audit trail
    report_df = spark.createDataFrame([error_report])
    report_df.write.format("delta") \
        .mode("append") \
        .save("abfss://ops-lakehouse@onelake.dfs.fabric.microsoft.com/pipeline_run_audit")
    
    # Return structured summary for email
    mssparkutils.notebook.exit(
        json.dumps({
            "status": "PARTIAL_FAILURE",
            "failure_count": failure_count,
            "run_id": pipeline_run_id,
            "report_saved": True
        })
    )
    

    This pattern turns your error handling into an audit trail. Every pipeline run — success or failure — writes a record to a Delta table in an operations lakehouse. You can query this table to track failure rates, identify chronically failing sources, and build SLA monitoring reports directly in Power BI. For teams implementing monitoring of Fabric capacity and pipeline activity, this complements the built-in Monitoring Hub with business-contextual failure data.


    Configuring Activity-Level Retries: Your First Line of Defense

    Before your pipeline-level error handler even fires, Fabric gives you activity-level retry configuration. This is your first line of defense and it's criminally underused.

    For every Copy, Dataflow, and Notebook activity, the Settings tab exposes:

    • Retry count: How many times to retry before declaring failure (0-10)
    • Retry interval (seconds): How long to wait between retries (30-86400)
    • Secure output / Secure input: For sensitive parameters

    Here's how to configure retry policies by activity type:

    Copy activities connecting to external APIs:

    • Retry count: 3
    • Retry interval: 60 seconds
    • This handles transient 429 (rate limit) and 503 (service unavailable) responses. If the source is an internal Azure SQL database with high reliability, 1 retry at 30 seconds is sufficient.

    Dataflow Gen2 activities:

    • Retry count: 2
    • Retry interval: 30 seconds
    • Dataflow Gen2 occasionally fails on first attempt due to engine warmup time. Two retries with short intervals catch most transient failures without adding significant latency.

    Notebook activities:

    • Retry count: 0 or 1
    • Retry interval: 60 seconds
    • Notebooks are stateful. Retrying a notebook that partially wrote Delta tables without idempotency logic can cause data corruption. Only add retries if your notebook is explicitly written to be idempotent (using MERGE or overwrite semantics, never append-only). For patterns like writing data using append, overwrite, and merge patterns with PySpark, overwrite and merge are safe to retry; pure append is not.

    Warning

    Activity-level retries consume capacity units for each attempt. On shared F-SKU capacities during peak hours, a Dataflow Gen2 with 3 retries at 30-second intervals during a transient failure period can throttle other workloads for 90+ seconds. Calibrate retry counts against your capacity tier and the acceptable latency budget for the pipeline's SLA.


    Email Alert Content Design: Making Alerts Actionable

    The difference between a useful alert email and one that gets ignored after the first week is specificity. Here are the principles for designing alert content that engineers will actually respond to:

    1. Lead with what failed, not that something failed

    Bad subject: Pipeline Run Failed Good subject: [ACTION REQUIRED] Copy_Bronze failed — SalesAPI returned 503 | Pipeline: Nightly_Sales_Medallion | 2024-01-15 02:17 UTC

    2. Tell them where to look

    Include a direct link to the Monitoring Hub run:

    @concat(
      'https://app.fabric.microsoft.com/groups/',
      pipeline().DataFactory,
      '/monitoring'
    )
    

    Note: As of current Fabric releases, deep-linking directly to a specific pipeline run ID is not yet supported in the portal URL scheme. Linking to the workspace monitoring page is the closest available option.

    3. Include the RunId prominently

    The RunId is the single most useful piece of information for correlating the email alert with the Monitoring Hub log. Make it copy-pasteable.

    4. Distinguish severity

    Use HTML formatting in the email body to visually distinguish CRITICAL (complete data loss risk) from WARNING (partial failure, downstream impact) from INFO (non-blocking anomaly). Fabric's Office 365 Outlook activity supports HTML bodies, so use color-coded headers:

    <h2 style="color: #cc0000;">🔴 CRITICAL: Pipeline Failure Requires Immediate Action</h2>
    

    5. Include the blast radius

    For partial failures in a multi-source pipeline, tell the reader which downstream datasets or reports will be stale:

    @concat(
      variables('FailureCount'),
      ' of ', variables('TotalSourceCount'),
      ' source tables failed to load. ',
      'The following Power BI datasets may show stale data: Sales_Dashboard, Regional_Performance.'
    )
    

    Hands-On Exercise

    Build the complete error handling pipeline described in this lesson using the following scenario:

    Scenario: You have a nightly pipeline that copies order data from an Azure SQL Database to a Bronze lakehouse table, runs a Dataflow Gen2 to clean and deduplicate it into Silver, and runs a notebook to compute daily KPIs into a Gold table. Implement end-to-end error handling.

    Step 1: Set up pipeline variables Create the five pipeline variables: ErrorMessage, ErrorCode, ErrorSource, FailureType, and PipelineRunId.

    Step 2: Build the happy path Add Copy, Dataflow, Notebook, and Email Success activities connected with Success dependencies.

    Step 3: Add failure capture for the Copy activity

    • Add a Set Variable activity connected to Copy with a Failure dependency
    • Set ErrorSource to the string literal "Copy_Bronze"
    • Set ErrorMessage to @activity('Copy_Bronze').error.message
    • Set FailureType to @activity('Copy_Bronze').error.failureType

    Step 4: Add failure capture for the Dataflow activity

    • Add a Set Variable activity connected to Dataflow with a Failure dependency
    • Set ErrorSource to "Dataflow_Silver"
    • Set FailureType to "RequiresInvestigation" (literal string, overriding Dataflow's unreliable classification)

    Step 5: Add failure capture for the Notebook activity

    • Write the notebook with a mssparkutils.notebook.exit() pattern that uses BUSINESS_ERROR: or TECHNICAL_ERROR: prefixes
    • Add a Set Variable activity that captures the exit value and routes FailureType accordingly

    Step 6: Wire the If Condition

    • Add the If Condition activity with expression @equals(variables('FailureType'), 'SystemError')
    • In the True branch, add an Outlook email activity for retry candidates
    • In the False branch, add an Outlook email activity for escalation

    Step 7: Test each failure path

    • Force a Copy failure by temporarily changing the source connection string to an invalid value. Verify the escalation email fires with the correct error message.
    • Force a Notebook BUSINESS_ERROR by commenting out the data write and calling mssparkutils.notebook.exit("BUSINESS_ERROR: Test failure"). Verify the False branch fires.
    • Restore the pipeline and verify a clean run sends the Success notification.

    Common Mistakes & Troubleshooting

    Mistake 1: Forgetting that Skipped is contagious

    If your Copy activity fails and you have Copy → Dataflow (Success only), the Dataflow gets Skipped. If your error handler depends on the Dataflow with a Failure dependency, it will not fire for a Copy failure. Always design your failure paths to originate directly from the activity that can fail, not transitively through a downstream activity.

    Mistake 2: Using Set Variable in parallel without understanding race conditions

    Inside a ForEach activity, multiple iterations can attempt to set the same pipeline variable simultaneously. Variables are pipeline-scoped, and concurrent writes can result in non-deterministic values. Use the FailureCount increment pattern carefully — the @add(variables('FailureCount'), 1) expression is not atomic in a parallel ForEach. For parallel scenarios, append to a String variable with a delimiter instead of incrementing an Integer, and parse the string afterward.

    Mistake 3: Assuming activity().error exists on success

    If you reference @activity('Copy_Bronze').error.message in an activity that runs on both Success and Failure paths (using Completion dependency), the expression will fail at evaluation when the activity succeeded because the error property doesn't exist. Use @if(equals(activity('Copy_Bronze').status, 'Failed'), activity('Copy_Bronze').error.message, 'No error') to safely access error properties conditionally.

    Mistake 4: Email activity succeeding but no email received

    The Office 365 Outlook activity's connection uses delegated permissions — the email is sent as the connected user account. If that account's mailbox is full, disabled, or the account has been removed from Exchange Online, the activity will still show as Succeeded in the pipeline (because the API call was accepted) but the email will bounce or be silently dropped. Monitor the connected account's mailbox health separately.

    Mistake 5: The If Condition's expression returning a non-boolean

    The If Condition expression must evaluate to exactly true or false (boolean). If your expression returns a string "true" instead of boolean true, the If Condition will either error or always take the False branch. Always use the built-in comparison functions (equals(), greater(), startsWith(), etc.) which return proper booleans, rather than string comparisons.

    Troubleshooting: Error path fires on every run

    Open the pipeline's run history in the Monitoring Hub and expand the activity tree. Look for which activities have a Completion dependency where they should have Failure-only. A single misclick on the dependency condition dropdown during canvas editing is the most common root cause.

    Troubleshooting: FailureType variable is empty when If Condition evaluates

    This happens when the dependency graph has a gap — the Set Variable activity ran but its downstream connection to the If Condition is missing, or the wrong dependency type was used. In the Monitoring Hub, click into the specific activity instance and verify its input/output. If the Set Variable shows as Succeeded and the variable value is populated, but the If Condition shows an empty value, check whether you have multiple If Condition activities and the wrong one is connected.


    Summary & Next Steps

    You've now built a production-grade error handling system for Microsoft Fabric pipelines. The patterns covered here — dependency condition routing, centralized error capture via Set Variable, dynamic If Condition branching on FailureType, and actionable email alerts via Office 365 Outlook — form a complete defensive layer around your data pipelines.

    The key conceptual takeaways:

    • Dependency conditions are not just sequence arrows — they're decision gates. Model your graph topology deliberately, with awareness of how Skipped cascades.
    • Dataflow Gen2 and Notebook activities require special handling — Dataflow error messages are opaque, and Notebook exit values are your primary channel for communicating semantic business failures.
    • Centralized error handlers reduce maintenance burden — converging multiple failure paths through Set Variable activities onto a single If Condition keeps your graph readable and your alert logic consistent.
    • Activity-level retries are your first line of defense — configure them by activity type with awareness of idempotency, and let your pipeline-level handler catch only the failures that survive all retries.

    Where to go next:

    For teams running incremental loads with watermark patterns, error handling takes on additional complexity: when a watermark-based pipeline fails mid-run, you need to decide whether to roll back the watermark or accept partial progress. Combine the patterns in this lesson with Delta Lake time travel to build truly recoverable incremental pipelines.

    If your pipelines are parameterized and dynamically generated, explore using notebook variables and parameters for dynamic lakehouse ingestion — the same parameter passing mechanism that enables dynamic ingestion also enables you to pass rich context (including failure metadata) into diagnostic notebooks.

    Finally, for teams who need to promote their hardened error handling pipelines across Dev/Test/Prod environments, Fabric Git Integration and Deployment Pipelines shows how to version-control your pipeline definitions and deploy them systematically — so your error handling logic ships with the same rigor as your transformation logic.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Microsoft Fabric Fundamentals

    Previous

    Deduplicating and Cleansing Lakehouse Delta Tables with PySpark: Drop Duplicates, Fill Nulls, and Enforce Data Quality Rules Across Medallion Layers

    Next

    Implementing Table Partitioning in a Fabric Lakehouse: Choosing Partition Keys, Writing Partitioned Delta Tables with PySpark, and Pruning Partitions for Faster SQL and Spark Queries

    Related Insights

    Microsoft FabricExpert

    Building a Reusable Fabric Pipeline Template with Metadata-Driven Orchestration: Driving Copy Activities, Notebook Runs, and Dataflow Refreshes from a Configuration Table

    28 min
    Microsoft FabricPractitioner

    Using Fabric Data Activator to Trigger Alerts and Pipeline Actions from Power BI Reports and Eventstreams

    24 min
    Microsoft FabricExpert

    Implementing Cross-Workspace Data Sharing in Microsoft Fabric with OneLake Shortcuts: Exposing Lakehouse and Warehouse Tables to Downstream Workspaces Without Copying Data

    30 min

    On this page

    • Introduction
    • Prerequisites
    • Understanding Dependency Conditions: The Foundation of Everything
    • The Activity Output Object: Your Diagnostic Goldmine
    • Designing a Failure Routing Architecture
    • Capturing Failure Details with Set Variable Activities
    • Handling Dataflow Gen2 Failures: The Special Case
    • Handling Notebook Failures: Business Logic vs. Technical Errors
    • Building the If Condition Activity
    • Wiring Up the Dependency Graph: Step by Step
    • Advanced Pattern: The ForEach-Based Multi-Source Error Handler
    • Writing the Error Report Notebook
    • Configuring Activity-Level Retries: Your First Line of Defense
    • Email Alert Content Design: Making Alerts Actionable
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Summary & Next Steps