Your pipeline works perfectly — when you click “Run.” But clicking “Run” every morning at 6 AM is not a production strategy. Neither is clicking “Run” every time a vendor drops a file. And definitely not clicking “Run” 180 times to backfill six months of historical data. Triggers, scheduling, and orchestration are what turn a working pipeline into a production platform — one that runs itself, handles failures, processes data in parallel, and alerts you when something goes wrong.
Our Data Factory post covered pipeline basics — activities, control flow, and building your first pipeline. This post goes deep on WHEN and HOW things run: schedule triggers (run at specific times), event-based triggers (run when data arrives), tumbling window triggers (run for specific time slices with guaranteed processing), notebook scheduling, and five advanced orchestration patterns that every production data platform needs.
Real-life analogy: Think of your data platform like a train network. Schedule triggers are the train timetable — the 6 AM express runs every morning whether anyone is waiting or not. Event-based triggers are on-demand shuttles — a bus only departs when enough passengers arrive at the station. Tumbling window triggers are mail delivery routes — every house (time window) gets visited exactly once, and if a house was missed yesterday, it gets delivered today. Notebook scheduling is a single train car running alone. Pipeline orchestration is the central station coordinator that ensures the passenger train waits for the cargo train, the local connects to the express, and if the 6 AM is canceled, the 7 AM still runs.
Table of Contents
- Trigger Types Overview
- Schedule Triggers
- Setting Up a Schedule Trigger
- Cron-Based Scheduling
- Multiple Schedules on One Pipeline
- Time Zone Configuration
- Event-Based Triggers
- File Arrival Triggers (Storage Event)
- How Debounce Works
- Table Change Triggers
- When to Use Event Triggers vs Schedule Triggers
- Tumbling Window Triggers
- How Tumbling Windows Work
- Backfill Scenario
- Window Dependencies
- Retry and Concurrency
- Tumbling Window vs Schedule — When to Use Which
- Notebook Scheduling
- Schedule a Notebook Directly
- Schedule via Pipeline (Recommended)
- Why Pipeline Scheduling is Better
- Advanced Orchestration Patterns
- Pattern 1: Master-Child Pipeline
- Pattern 2: Conditional Execution (Skip When No Data)
- Pattern 3: Retry with Exponential Backoff
- Pattern 4: Fan-Out Fan-In (Parallel Then Merge)
- Pattern 5: Cross-Pipeline Dependency Chain
- Dynamic Expressions for Scheduling
- Monitoring Scheduled Runs
- Monitoring Hub
- Proactive Alerting
- Common Mistakes
- Interview Questions
- Wrapping Up
Trigger Types Overview
| Trigger Type | Fires When | Tracks State? | Backfill? | Best For |
|---|---|---|---|---|
| Schedule | Fixed time/interval (daily, hourly, cron) | No | No | Regular ETL runs at known times |
| Event-Based | File arrives in storage / table changes | No | No | Reactive processing when data lands |
| Tumbling Window | Per time window (each hour, each day) | Yes | Yes | Historical backfill, exactly-once per window |
| Manual | You click “Trigger Now” or call REST API | No | No | Ad-hoc runs, testing, debugging |
The key distinction is state tracking. Schedule and event triggers are stateless — they fire and forget. If a schedule trigger fires at 6 AM and the pipeline fails, the trigger does not know or care. Tomorrow at 6 AM, it fires again without retrying yesterday. Tumbling window triggers are stateful — they track which windows succeeded and which failed, and they retry failed windows automatically.
Schedule Triggers
Schedule triggers fire at a fixed time or interval — the simplest and most common trigger type. Use them when you know WHEN your data pipeline should run and do not need guaranteed processing of every time window.
Setting Up a Schedule Trigger
To schedule a pipeline in Fabric:
- Open your pipeline in Fabric Data Factory
- Click Schedule in the toolbar (or Home tab → Schedule)
- Toggle the schedule On
- Configure the frequency: Repeat: Every 1 Day, Time: 06:00 AM
- Set the Start date (when the schedule begins) and optionally an End date
- Select the Time zone (critical — see below)
- Click Apply
The schedule is now active. The pipeline runs automatically at 6 AM every day without any manual intervention.
Cron-Based Scheduling
For complex schedules beyond “every X hours,” Fabric supports cron-like expressions. These give you precise control over exactly when a pipeline runs:
Schedule Examples:
Every day at 6 AM: Repeat: Every 1 Day, Time: 06:00
Every hour: Repeat: Every 1 Hour
Every 15 minutes: Repeat: Every 15 Minutes
Weekdays at 6 AM: Repeat: Every 1 Day, Days: Mon-Fri, Time: 06:00
First of every month at 2 AM: Repeat: Monthly, Day 1, Time: 02:00
Every 6 hours: Repeat: Every 6 Hours
Multiple times per day:
Time 1: 06:00 AM (before business hours)
Time 2: 12:00 PM (midday refresh)
Time 3: 06:00 PM (after business hours)
ADF cron syntax (for reference — same concept):
0 6 * * * = Every day at 6 AM
0 * * * * = Every hour
*/15 * * * * = Every 15 minutes
0 6 * * 1-5 = Weekdays at 6 AM
0 2 1 * * = First of every month at 2 AM
Multiple Schedules on One Pipeline
A single pipeline can have multiple schedule triggers. This is useful when the same pipeline serves different purposes at different times:
Pipeline: PL_Customer_ETL
Schedule 1: Daily at 6 AM — full load (parameter: loadType = "FULL")
Schedule 2: Every 2 hours, 8 AM to 6 PM weekdays — incremental (loadType = "INCREMENTAL")
The pipeline checks the loadType parameter and behaves differently:
FULL → truncate target, copy all rows
INCREMENTAL → copy only rows modified since last run
Real-life analogy: Multiple schedules are like a bus route that runs differently on weekdays vs weekends. The 6 AM bus is the express (full load) — it takes everyone. The hourly bus during the day is the local (incremental) — it picks up only new passengers.
Time Zone Configuration
Schedule triggers default to UTC. If you schedule a pipeline for “6 AM” without changing the time zone, it runs at 6 AM UTC — which is 1 AM Eastern (EST) or 2 AM Eastern (EDT). This catches many teams off guard.
Always set the time zone explicitly. Select (UTC-05:00) Eastern Time for Ontario/Toronto. Fabric handles daylight saving time automatically — 6 AM Eastern is 6 AM Eastern regardless of whether DST is active. However, verify after DST transitions (March and November) by checking the Monitor tab to confirm pipelines fired at the expected time.
Event-Based Triggers
Event-based triggers fire reactively — when something happens in your data environment. Instead of running on a fixed schedule (“every day at 6 AM”), they run in response to events (“a new file just arrived”).
Real-life analogy: A schedule trigger is like a newspaper delivery — it arrives at 6 AM every morning whether you read yesterday’s paper or not. An event trigger is like a doorbell — it rings only when someone is actually at the door. Event triggers are more efficient (no wasted runs when there is no new data) but require the source system to produce detectable events.
File Arrival Triggers (Storage Event)
Storage event triggers monitor an ADLS Gen2 or OneLake path for new files. When a file is created (or deleted) that matches your filter criteria, the trigger fires and starts the pipeline. The file path and name are passed as parameters to the pipeline, so you can process the specific file that triggered the run.
Configuration:
Trigger type: Storage event
Storage: OneLake or ADLS Gen2 connection
Container/Path: Files/incoming/
File filter: *.csv (or customers_*.parquet, or specific prefix)
Event: File created
Debounce: 5 minutes (wait before firing — see below)
What happens:
1. Vendor uploads orders_20260708.csv to Files/incoming/
2. Storage fires a "blob created" event
3. Trigger detects the event, checks the file filter (*.csv → match)
4. Trigger waits 5 minutes (debounce — ensures file is fully uploaded)
5. Pipeline starts with parameters:
@trigger().outputs.body.fileName = "orders_20260708.csv"
@trigger().outputs.body.folderPath = "Files/incoming/"
6. Pipeline reads the specific file, processes it, moves to Files/archive/
How Debounce Works
Debounce prevents the trigger from firing while a file is still being uploaded. Large files take minutes to upload — without debounce, the trigger fires on the first byte, and the pipeline reads a partial (corrupted) file.
Debounce works by waiting a specified period after the last detected change before firing. If the file continues growing (more bytes arrive), the timer resets. Once the file has been stable for the debounce period (no new changes), the trigger fires.
Recommended settings: 5 minutes for small files (< 100 MB), 10-15 minutes for large files (GB+). If your vendor uploads a completion marker file (like _SUCCESS or done.flag), trigger on the marker file instead of the data file — the marker only appears after all data files are fully uploaded.
Table Change Triggers
In some scenarios, you want to trigger a pipeline when a Delta table changes — not when a file lands. For example, a mirrored database continuously replicates data into a bronze Lakehouse table. When the bronze table gets new rows, you want to automatically run the bronze-to-silver transformation.
How it works:
bronze.raw_customers receives new rows (from mirroring or another pipeline)
→ Delta table version increments (new files in the _delta_log)
→ Event trigger detects the version change
→ Pipeline runs: Bronze → Silver transformation
→ Silver table updated
This is the Delta-native equivalent of file arrival triggers — instead of monitoring
a folder for new files, you monitor a Delta table for new commits.
When to Use Event Triggers vs Schedule Triggers
| Scenario | Use | Why |
|---|---|---|
| Vendor drops files at unpredictable times | Event trigger | Process immediately when data arrives — no wasted runs |
| Data must be ready by 8 AM every morning | Schedule trigger | Predictable timing, SLA-driven |
| Source updates continuously (CDC, mirroring) | Event trigger | React to each batch of changes |
| Multiple source systems, one pipeline | Schedule trigger | One trigger orchestrates all sources at a fixed time |
| Need to guarantee every hour is processed | Tumbling window | State tracking ensures no window is missed |
Tumbling Window Triggers
How Tumbling Windows Work
A tumbling window trigger divides time into fixed, non-overlapping windows (hourly, daily) and fires once for each window. The critical difference from a schedule trigger: each window is tracked independently. The trigger knows which windows succeeded, which failed, and which have not been processed yet.
Real-life analogy: A tumbling window is like a mail carrier’s route. The carrier has a list of 365 houses (one per day of the year). Each house must be visited exactly once. If the carrier is sick on Day 50, Day 50 is not skipped — it gets visited the next time the carrier works. Days 51-55 are not affected by Day 50’s failure. And if the postal service hires a new carrier starting Day 200, the new carrier can go back and deliver to houses 1-199 (backfill). A schedule trigger, by contrast, is like a TV broadcast — if you miss the 6 PM news, it is gone. There is no replay.
Tumbling Window: Daily, starting 2026-01-01
Window 1: 2026-01-01 00:00 → 2026-01-02 00:00 → processes Jan 1 data
Window 2: 2026-01-02 00:00 → 2026-01-03 00:00 → processes Jan 2 data
Window 3: 2026-01-03 00:00 → 2026-01-04 00:00 → processes Jan 3 data
...
Window 189: 2026-07-08 00:00 → 2026-07-09 00:00 → processes today's data
Pipeline receives window boundaries as parameters:
@trigger().outputs.windowStartTime = "2026-07-08T00:00:00Z"
@trigger().outputs.windowEndTime = "2026-07-09T00:00:00Z"
Your pipeline uses these to filter source data:
WHERE modified_date >= '@{trigger().outputs.windowStartTime}'
AND modified_date < '@{trigger().outputs.windowEndTime}'
Backfill Scenario
Backfill is the killer feature of tumbling window triggers. When you create a tumbling window with a start date in the past, it automatically creates windows for every interval between the start date and now — and processes them all. Each window is independent, so multiple windows can run in parallel.
Backfill Example:
Today: 2026-07-08
Create tumbling window with start date: 2026-01-01 (6 months ago)
Interval: Daily
Max concurrency: 5
The trigger creates 189 windows (Jan 1 to Jul 8)
It processes 5 windows simultaneously (max concurrency = 5)
Batch 1: Jan 1, Jan 2, Jan 3, Jan 4, Jan 5 (parallel)
Batch 2: Jan 6, Jan 7, Jan 8, Jan 9, Jan 10 (parallel)
...until all 189 windows are processed
If Window 50 (Feb 19) fails:
→ Windows 51-55 continue processing (independent)
→ Window 50 is retried automatically (up to configured retry count)
→ No manual intervention needed
Without tumbling windows, you would need to:
→ Write a loop script to process each day
→ Track which days succeeded and which failed
→ Manually retry failed days
→ Handle concurrent execution yourself
Window Dependencies
Tumbling window triggers support dependencies between windows — a window does not fire until its dependency is met. The most common dependency: the current window waits for the previous window to complete successfully. This ensures sequential processing when order matters (e.g., running balance calculations where today depends on yesterday’s closing balance).
Dependency: Self (previous window must complete first)
Window 1 (Jan 1): Runs immediately → SUCCESS
Window 2 (Jan 2): Waits for Window 1 → Window 1 succeeded → Runs → SUCCESS
Window 3 (Jan 3): Waits for Window 2 → Window 2 succeeded → Runs → SUCCESS
Window 4 (Jan 4): Waits for Window 3 → Window 3 FAILED → Window 4 WAITS
(Window 3 is retried → succeeds → Window 4 runs)
Cross-pipeline dependency:
Pipeline A (tumbling window): Ingest raw data per day
Pipeline B (tumbling window): Transform data per day — depends on Pipeline A's window
Pipeline B's Jan 5 window only runs after Pipeline A's Jan 5 window succeeds
Retry and Concurrency
Retry: Configure how many times a failed window is retried (e.g., 3 retries). If all retries fail, the window is marked as failed and can be manually rerun later. Concurrency: Controls how many windows run simultaneously. Set to 1 for sequential processing (when order matters), 5-10 for parallel backfill (when windows are independent), or higher for lightweight operations. Higher concurrency = faster backfill but more CU consumption.
Tumbling Window vs Schedule — When to Use Which
| Need | Schedule Trigger | Tumbling Window |
|---|---|---|
| Simple daily run | ✅ Simpler to configure | Works but overkill |
| Guaranteed every window is processed | ❌ Missed = lost | ✅ Tracks and retries |
| Backfill historical data | ❌ No built-in support | ✅ Auto-creates past windows |
| Window-to-window dependencies | ❌ Not supported | ✅ Built-in dependency chains |
| Pass window boundaries to pipeline | ❌ Only trigger time | ✅ windowStartTime, windowEndTime |
| Concurrent processing of windows | ❌ One run per trigger | ✅ Configurable concurrency |
Rule of thumb: Use schedule triggers for simple, recurring ETL where a missed run is not catastrophic. Use tumbling windows whenever you need exactly-once processing per time period, backfill capability, or window-to-window dependencies.
Notebook Scheduling
Schedule a Notebook Directly
Fabric notebooks have a built-in schedule feature — you can schedule a notebook to run at a fixed interval without creating a pipeline. Open the notebook → click Schedule in the toolbar → configure frequency and time → Apply. The notebook runs on schedule with its default parameters.
This is convenient for simple, standalone tasks — a daily data quality check, a weekly report generation, or an ad-hoc cleanup script.
Schedule via Pipeline (Recommended)
Pipeline: PL_Daily_ETL
├── Notebook Activity: NB_Bronze_to_Silver
│ Parameters: table_name = "customers", load_date = @utcNow()
│ Timeout: 30 minutes
│ Retry: 2
├── Notebook Activity: NB_Silver_to_Gold
│ Depends on: NB_Bronze_to_Silver (success path)
├── Semantic Model Refresh Activity
│ Depends on: NB_Silver_to_Gold (success path)
└── Teams Activity: "Daily ETL Complete"
Depends on: Semantic Model Refresh (success path)
Failure path (any activity fails):
└── Teams Activity: "⚠ Daily ETL FAILED — check Monitor tab"
Pipeline schedule trigger: Daily at 6 AM Eastern
Why Pipeline Scheduling is Better
| Capability | Notebook Schedule | Pipeline Schedule |
|---|---|---|
| Run a notebook on a schedule | ✅ | ✅ |
| Pass parameters dynamically | ❌ Uses defaults only | ✅ Pass any expression |
| Chain multiple notebooks in sequence | ❌ | ✅ |
| Error handling (retry, timeout) | ❌ | ✅ Per activity |
| Conditional logic (run only if data exists) | ❌ | ✅ If Condition activity |
| Failure alerts (Teams, email) | ❌ | ✅ Teams/Outlook activity on failure path |
| Mix with Copy, Dataflow, SP activities | ❌ | ✅ Full activity palette |
| Monitoring and run history | Basic | Full (Monitor hub, per-activity details) |
Rule: Use direct notebook scheduling only for standalone tasks that do not need error handling, alerts, or sequencing. For anything in production, wrap the notebook in a pipeline.
Advanced Orchestration Patterns
Pattern 1: Master-Child Pipeline
The master-child pattern splits a large pipeline into smaller, independently testable and reusable child pipelines. The master pipeline orchestrates the execution order and handles cross-pipeline error handling.
Real-life analogy: A general contractor (master pipeline) hires specialized subcontractors (child pipelines) — one for plumbing, one for electrical, one for painting. The GC coordinates the order (plumbing before drywall, drywall before painting) and handles issues (if plumbing is delayed, painting waits). Each subcontractor can also be hired independently for other projects (reusability).
Master Pipeline: PL_Master_Daily_ETL
├── Execute Pipeline: PL_Ingest_Customers (wait for completion: ✅)
├── Execute Pipeline: PL_Ingest_Orders (wait for completion: ✅)
├── Execute Pipeline: PL_Ingest_Products (wait for completion: ✅)
│ ← ALL three ingestion pipelines must succeed before transform starts
├── Execute Pipeline: PL_Transform_Silver (depends on all three above)
├── Execute Pipeline: PL_Build_Gold (depends on silver)
└── Execute Pipeline: PL_Refresh_Reports (depends on gold)
Benefits:
✅ Each child is independently testable (run PL_Ingest_Customers alone)
✅ Each child is reusable (other master pipelines can call PL_Ingest_Customers)
✅ Clear separation of concerns (ingestion, transformation, serving)
✅ Master handles sequencing — children do not need to know about each other
Pattern 2: Conditional Execution (Skip When No Data)
Not every scheduled run has work to do. On weekends, the source system may not produce new data. Running a full ETL pipeline on no data wastes CU and clutters the Monitor tab. Conditional execution checks for new data first and skips processing if there is nothing to process.
Pipeline: PL_Smart_ETL
Step 1: Lookup Activity — "Check for new data"
Query: SELECT COUNT(*) AS new_rows FROM source.customers
WHERE modified_date > '@{pipeline().parameters.lastLoadDate}'
Step 2: If Condition
Expression: @greater(activity('Check_New_Data').output.firstRow.new_rows, 0)
TRUE branch (new data exists):
→ Copy Activity: Ingest new rows
→ Notebook: Transform
→ Update watermark
FALSE branch (no new data):
→ Web Activity: Log "No new data — skipping"
→ (Pipeline completes in 5 seconds instead of 30 minutes)
This saves CU on days with no data and keeps the Monitor tab clean.
Pattern 3: Retry with Exponential Backoff
Transient errors — network timeouts, database connection drops, API rate limits — are common in data pipelines. Instead of failing immediately, configure retries with increasing wait times between attempts. This gives the source system time to recover.
Copy Activity: Copy_API_Orders
Retry: 3
Retry interval: 30 seconds
Timeline:
6:00:00 AM → Attempt 1 → FAILS (API timeout)
6:00:30 AM → Attempt 2 → FAILS (API still recovering)
6:01:00 AM → Attempt 3 → SUCCEEDS ✅
Without retry: pipeline fails at 6:00 AM, you get an alert,
you investigate, you rerun manually at 6:15 AM. Wasted 15 minutes.
With retry: pipeline recovers automatically in 60 seconds.
For exponential backoff (not built-in, but achievable):
Use a Until loop with a Wait activity:
Attempt 1 → fail → Wait 30s
Attempt 2 → fail → Wait 60s
Attempt 3 → fail → Wait 120s
Attempt 4 → succeed ✅
Pattern 4: Fan-Out Fan-In (Parallel Then Merge)
Fan-Out Fan-In is one of the most important orchestration patterns in data engineering. Fan-Out means splitting work into multiple parallel streams. Fan-In means waiting for ALL parallel streams to complete before running the next step. Together, they dramatically reduce pipeline runtime.
Real-life analogy: A restaurant kitchen during dinner rush. The head chef (pipeline) receives an order for a table: steak, salad, soup, dessert. Instead of cooking each dish one at a time (sequential = 40 minutes), the chef assigns each dish to a different station simultaneously (fan-out). The steak cook, salad prep, soup station, and pastry chef all work in parallel. The dishes arrive at the pass at different times, but the waiter waits until ALL four dishes are ready (fan-in) before serving the table. Total time = the slowest dish (steak, 15 minutes), not the sum of all dishes (40 minutes).
FAN-OUT FAN-IN ARCHITECTURE:
┌── Copy: customers ──┐
│ │
Pipeline Start ── ForEach┼── Copy: orders ──────┤
(parallel) ├── ALL COMPLETE ── Notebook: Build Gold
├── Copy: products ────┤ (fan-in) (merge all tables)
│ │
└── Copy: inventory ───┘
←── FAN-OUT ──→ ←── FAN-IN ──→
Sequential: customers(5m) → orders(8m) → products(3m) → inventory(4m) = 20 minutes
Fan-Out: All 4 run simultaneously → slowest is orders(8m) → notebook(3m) = 11 minutes
Savings: 45% faster
ForEach configuration for fan-out:
- Sequential: UNCHECKED — this enables parallel execution
- Batch count: 5 — max items to process simultaneously (1 = sequential, 50 = max in Fabric)
- Items:
@activity('Lookup_Tables').output.value
Fan-in is automatic — any activity placed after the ForEach (connected by a green arrow) waits until ALL iterations complete. No synchronization code needed.
Error handling in Fan-Out:
If one parallel item fails, the others CONTINUE running.
ForEach overall status: FAILED (at least one item failed).
Fan-in activity: does NOT run by default.
To handle gracefully:
Inside ForEach, add success AND failure paths per item:
Copy Activity
├── (Success) → Log_Success
└── (Failure) → Log_Failure
After ForEach, add both paths:
ForEach
├── (Success) → Build Gold (all tables loaded)
└── (Failure) → Send alert + optionally still build Gold with available tables
Real-World Fan-Out Fan-In Example:
Step 1 (Sequential): Lookup metadata → 20 tables
Step 2 (Fan-Out): ForEach (batch=5, parallel)
→ 20 tables, 5 at a time → 4 batches → ~25 min
Step 3 (Fan-In): Notebook builds Silver layer
Step 4 (Fan-Out again): ForEach over 5 Gold tables (batch=5)
Step 5 (Fan-In): Semantic Model Refresh
Total: ~25 minutes (vs ~90 minutes sequential)
Pattern 5: Cross-Pipeline Dependency Chain
When multiple teams own different pipeline stages, use Execute Pipeline activities to chain them with explicit success/failure paths. Each team maintains their own pipeline independently — the master pipeline coordinates the sequence.
Team A owns: PL_Ingest (runs at 6:00 AM)
Team B owns: PL_Transform (depends on PL_Ingest)
Team C owns: PL_Serve (depends on PL_Transform)
Master Pipeline: PL_Orchestrator (scheduled at 6:00 AM)
├── Execute Pipeline: PL_Ingest
│ ├── (Success) → Execute Pipeline: PL_Transform
│ │ ├── (Success) → Execute Pipeline: PL_Serve
│ │ │ ├── (Success) → Semantic Model Refresh → Teams: "✅ All complete"
│ │ │ └── (Failure) → Teams: "❌ PL_Serve failed"
│ │ └── (Failure) → Teams: "❌ PL_Transform failed"
│ └── (Failure) → Teams: "❌ PL_Ingest failed — downstream skipped"
Each team can independently test and deploy their pipeline.
The master pipeline is the single point of orchestration and monitoring.
Dynamic Expressions for Scheduling
Dynamic expressions make your pipelines date-aware — generating file paths, filter conditions, and parameters based on the current date and time at runtime.
# Today's date
@formatDateTime(utcNow(), 'yyyy-MM-dd') → "2026-07-08"
# Yesterday (for incremental loads: "give me yesterday's data")
@formatDateTime(addDays(utcNow(), -1), 'yyyy-MM-dd') → "2026-07-07"
# First day of current month
@formatDateTime(startOfMonth(utcNow()), 'yyyy-MM-dd') → "2026-07-01"
# Last day of previous month
@formatDateTime(addDays(startOfMonth(utcNow()), -1), 'yyyy-MM-dd') → "2026-06-30"
# Dynamic file path with date partitioning
@concat('Files/incoming/', formatDateTime(utcNow(), 'yyyy/MM/dd'), '/')
→ "Files/incoming/2026/07/08/"
# Conditional: full load on Monday, incremental other days
@if(equals(dayOfWeek(utcNow()), 1), 'FULL', 'INCREMENTAL')
→ "FULL" on Monday, "INCREMENTAL" every other day
# Tumbling window boundaries (inside tumbling window trigger)
@trigger().outputs.windowStartTime → "2026-07-08T00:00:00Z"
@trigger().outputs.windowEndTime → "2026-07-09T00:00:00Z"
Monitoring Scheduled Runs
Monitoring Hub
The Monitoring Hub (left sidebar → Monitor) shows all pipeline runs, notebook runs, Dataflow Gen2 runs, and Spark job runs. Filter by status (Succeeded, Failed, In Progress, Cancelled), date range, and item type. For each run, drill into activity-level details: duration per activity, rows read/written, error messages, and Spark UI for notebook activities.
Key things to check daily:
- Did all scheduled pipelines run? (Check for “No runs” — the schedule may have been paused)
- Did any runs fail? (Red status — click to see which activity failed and the error message)
- How long did each run take? (Compare to the baseline — a 5-minute pipeline that took 45 minutes signals a problem)
- Were any runs queued? (Orange/yellow status — indicates CU throttling, too many concurrent pipelines)
Proactive Alerting
Do not rely on checking the Monitor tab manually. Add alerting activities to every production pipeline:
Every production pipeline should end with:
├── (Success path) → Teams Activity: "✅ PL_Daily_ETL succeeded — 150K rows loaded in 12m"
└── (Failure path) → Teams Activity: "❌ PL_Daily_ETL FAILED at [activity name] — [error]"
→ Outlook Activity: Email DE team with error details
Include in the alert message:
Pipeline name: @pipeline().Pipeline
Run ID: @pipeline().RunId
Failed activity: @activity('Copy_Customers').error.message (on failure path)
Duration: @activity('Copy_Customers').output.copyDuration
Rows: @activity('Copy_Customers').output.rowsCopied
For advanced alerting, use Data Activator to monitor pipeline audit tables
and trigger alerts on patterns (3 consecutive failures, SLA breach, row count anomaly).
Common Mistakes
- Scheduling all pipelines at the same time — if 10 pipelines all trigger at 6:00 AM, they compete for CU capacity, causing throttling and slow runs. Stagger pipelines by 10-15 minutes: 6:00 AM, 6:10 AM, 6:20 AM. Or use master-child pattern where one pipeline orchestrates the sequence.
- Not setting a time zone on schedule triggers — default is UTC. A pipeline scheduled for “6 AM” in UTC runs at 1 AM Eastern (or 2 AM during DST). Always select your local time zone explicitly.
- Using schedule triggers when event triggers are appropriate — polling a folder every 5 minutes (“is there a new file?”) wastes 288 pipeline runs per day when the file arrives once. Use event triggers for file arrival — they fire only when data actually lands.
- Not configuring debounce on file event triggers — the trigger fires while the file is still being uploaded. The pipeline reads a partial (corrupted) file. Set debounce to 5+ minutes to wait for the file to finish uploading.
- Not setting retry on Copy and Web activities — transient network errors are common. A single failed attempt at 6:00 AM triggers an alert and manual investigation. With retry = 3, the pipeline recovers automatically in 60 seconds.
- Scheduling notebooks directly instead of via pipeline — no error handling, no failure alerts, no sequencing, no parameterization. Always wrap production notebooks in a pipeline.
- Using schedule triggers for historical backfill — running a pipeline 180 times manually (one per day for 6 months) is tedious and error-prone. Use a tumbling window trigger with a past start date — it automatically creates and processes all 180 windows.
- Ignoring the Monitor tab after deployment — a pipeline can fail silently for days if nobody checks. Add Teams/email alerts on the failure path of every production pipeline. Check the Monitor tab daily during the first week of any new schedule.
Interview Questions
Q: What trigger types are available in Fabric Data Factory? A: Schedule triggers (time-based — daily, hourly, cron), event-based triggers (fire when a file arrives in storage or a table changes), tumbling window triggers (process data in non-overlapping time windows with state tracking), and manual triggers (Trigger Now or REST API). A pipeline can have multiple triggers simultaneously.
Q: What is the difference between a schedule trigger and a tumbling window trigger? A: A schedule trigger fires at a fixed time and is stateless — if the pipeline fails, the trigger does not retry or track the missed run. A tumbling window trigger is stateful — it tracks which windows succeeded and which failed, retries failed windows, supports backfill by creating past windows, supports window-to-window dependencies, and passes window boundaries (start/end time) as parameters to the pipeline.
Q: When would you use an event-based trigger instead of a schedule trigger? A: When data arrives at unpredictable times (vendor file drops), when you want to process data immediately upon arrival (low latency), or when running on a fixed schedule would waste resources (the source only produces data twice a week but you would schedule daily). Event triggers are reactive; schedule triggers are proactive.
Q: What is the Fan-Out Fan-In pattern and how does it work in Fabric? A: Fan-Out splits work into parallel streams using a ForEach activity with Sequential unchecked and a batch count (e.g., 5). Fan-In happens automatically — any activity after the ForEach waits for ALL parallel iterations to complete. This reduces pipeline time dramatically: loading 20 tables sequentially takes ~90 minutes, but with fan-out (batch=5), it takes ~25 minutes because 5 tables load simultaneously per batch.
Q: What happens when one item fails in a parallel ForEach? A: Other items continue running. ForEach overall status is FAILED because at least one item failed. The next activity (fan-in) does NOT run by default. To handle gracefully, add success and failure paths inside ForEach for per-item logging, and a failure path after ForEach for alerting.
Q: How do you backfill historical data in Fabric? A: Create a tumbling window trigger with a start date in the past. The trigger automatically creates one window for each interval between the start date and today, and processes them with configurable concurrency. Each window is independent — if one fails, others continue. This is dramatically easier than writing a loop to process each date manually.
Q: Why should you schedule notebooks via pipelines instead of directly? A: Pipelines provide error handling (retry, timeout), failure alerts (Teams, email), sequencing (chain notebooks in order), parameterization (pass dynamic values), conditional logic (skip when no data), and detailed monitoring (per-activity run history). Direct notebook scheduling has none of these — it runs the notebook and provides minimal monitoring. For anything in production, wrap the notebook in a pipeline.
Wrapping Up
Triggers and orchestration determine WHEN your data platform runs and HOW it handles complexity. Schedule triggers for predictable cadence, event triggers for reactive processing, tumbling windows for stateful processing with backfill. Wrap these in orchestration patterns — master-child for modularity, conditional execution to skip empty runs, fan-out for parallelism, and dependency chains for cross-team coordination. Add monitoring and alerting to every production pipeline so failures are caught in minutes, not hours.
For the DP-700 exam, know the key differences: schedule triggers are stateless (missed = lost), tumbling windows are stateful (missed = retried). Event triggers react to file arrival, not polling. Fan-Out uses ForEach with Sequential unchecked. Fan-In is automatic. And always schedule notebooks via pipelines, not directly.
Related posts:
- Fabric Data Factory & Pipelines
- Data Factory Expression Language
- Fabric Notebooks Deep Dive
- Fabric Monitoring & Troubleshooting
- Data Activator
Naveen Vuppula is a Senior Data Engineering Consultant and app developer based in Ontario, Canada. He writes about Python, SQL, AWS, Azure, and everything data engineering at DriveDataScience.com.