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
Power Automate

Implementing Dataverse Change Tracking and Delta Queries in Power Automate: Building Incremental Sync Pipelines for High-Volume, Low-Latency Data Integration

Stop syncing entire tables when you only need what changed. This expert-level lesson teaches you how Dataverse delta queries work at the OData protocol level, how to persist delta tokens reliably, and how to build an incremental sync pipeline that handles token expiration, pagination, and concurrent execution without breaking production.

🔥 Expert28 min readSep 6, 2026Updated Sep 27, 2026
Implementing Dataverse Change Tracking and Delta Queries in Power Automate: Building Incremental Sync Pipelines for High-Volume, Low-Latency Data Integration
On this page
  • Introduction
  • Prerequisites
  • How Change Tracking Actually Works in Dataverse
  • The OData Delta Link Protocol
  • Enabling Change Tracking on a Table
  • Designing the Pipeline Architecture
  • The Four Responsibilities
  • Where to Store the Delta Token
  • Flow Topology
  • Building the Delta Query HTTP Request
  • Authentication Setup
  • The Initial Delta Query (No Existing Token)
The Subsequent Delta Query (With Existing Token)
  • Handling Pagination in Delta Responses
  • Processing the Change Set
  • Identifying Deleted Records
  • Building the Processing Logic
  • Persisting Delta Tokens in Azure Table Storage
  • Token Table Schema
  • Reading the Current Token
  • Writing the New Token
  • Handling Token Expiration and Full Resync
  • Detecting Token Expiration
  • The Full Resync Flow
  • Concurrency Control and Idempotency
  • Preventing Concurrent Runs
  • Making Change Processing Idempotent
  • Monitoring and Alerting
  • Run-Level Metrics
  • Alerting on Anomalies
  • Performance Tuning and Scaling Considerations
  • Page Size Selection
  • Parallelizing Change Processing
  • Chunking Large Change Sets
  • Hands-On Exercise
  • Common Mistakes and Troubleshooting
  • "My delta query always returns zero records even though I know data changed."
  • "I'm getting 401 Unauthorized on my delta calls."
  • "I'm missing some changes — records that I know were updated aren't appearing."
  • "My flow is timing out on large change sets."
  • "I'm getting duplicate records in my target system."
  • "The 410 error is happening more frequently than it should."
  • Summary and Next Steps
  • Implementing Dataverse Change Tracking and Delta Queries in Power Automate: Building Incremental Sync Pipelines for High-Volume, Low-Latency Data Integration

    Introduction

    Picture this: your organization runs a Dynamics 365 Sales environment with 400,000 Account records. Every night, a scheduled Power Automate flow queries the entire Accounts table to push updates into your enterprise data warehouse. On a good night it takes 90 minutes. On a bad night—when someone ran a bulk import during business hours or the API starts throttling—it fails at minute 47 with no clear recovery path. The next morning, your analytics team is working with yesterday's data, your integration team is firefighting, and your Power Platform admin is staring at a retry queue full of 429 errors.

    This is the problem that Dataverse Change Tracking and delta queries were built to solve. Instead of asking "give me everything," you ask "give me what changed since the last time I checked." The API returns only the records that were created, updated, or deleted in that window—potentially reducing your payload from 400,000 rows to 200 rows. Your flow finishes in seconds instead of hours, your throttling exposure collapses, and your downstream systems receive genuinely fresh data.

    By the end of this lesson, you'll understand how change tracking works at the Dataverse API level, how to implement a production-grade incremental sync pipeline in Power Automate using HTTP actions and delta tokens, and how to handle the hard edge cases—token expiration, full resync recovery, concurrent execution, and table schema changes—that separate a proof-of-concept from a system you can trust in production.

    What you'll learn:

    • How Dataverse change tracking works at the OData protocol level and why delta tokens are the mechanism you actually need
    • How to enable change tracking on Dataverse tables and craft delta query HTTP requests in Power Automate
    • How to persist and retrieve delta tokens across flow runs using Dataverse itself or Azure Table Storage
    • How to build a complete incremental sync pipeline with proper pagination, error handling, and full-resync recovery logic
    • How to monitor, alert on, and tune a high-volume delta sync pipeline for throughput and reliability

    Prerequisites

    Before diving in, you should be comfortable with:

    • Power Automate HTTP actions and OData query syntax. If you need a refresher on building custom HTTP requests in flows, see Advanced Power Automate: Custom Connectors and HTTP Actions for Production Integration.
    • Basic Dataverse connector usage — List rows, Create a row, Update a row.
    • Error handling patterns — particularly scope-based error catching. The patterns in Master Error Handling and Retry Patterns in Power Automate for Bulletproof Flows apply directly here.
    • Pagination concepts — You should understand how @odata.nextLink works when querying large datasets. If not, read Handling Pagination and Throttling When Querying Large Datasets in Power Automate first.
    • A Dataverse environment with a table you want to sync, and an Azure AD app registration (or a service account) with appropriate Dataverse API permissions.

    How Change Tracking Actually Works in Dataverse

    Before writing a single action in the designer, you need to understand what's happening under the hood. This isn't just academic — the design decisions you'll make in your pipeline flow directly from how the Dataverse change tracking mechanism works.

    The OData Delta Link Protocol

    Dataverse change tracking is built on the OData v4 delta query specification. The core idea is simple but elegant: when you make an initial query, the API returns your records and a special @odata.deltaLink token embedded in the response. That token is an opaque cursor that encodes the state of the dataset at the time of your query.

    On your next run, instead of calling the original endpoint again, you call the @odata.deltaLink URL. The API compares current state to the state at your token's timestamp and returns only the records that have changed. Each response again includes a new @odata.deltaLink, which you save for the next run.

    The Dataverse-specific implementation has a few important characteristics:

    Deleted records are returned as "tombstones" — objects with the record's ID and a special @odata.context annotation indicating deletion. The response body looks like:

    {
      "@odata.context": "https://yourorg.crm.dynamics.com/api/data/v9.2/$metadata#accounts/$deletedEntity",
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "reason": "deleted"
    }
    

    This is critical: if you're only looking at the value array in your response and processing each item identically, you'll miss deletes entirely, or worse, you'll try to update a record in your target system with an ID that no longer has any field data.

    Updated records return only the entity ID and the fields that changed if you're using selective change tracking — but in practice, Dataverse returns the full row for changed records by default. This is actually useful because you don't need to merge partial updates.

    The token expiration window is configurable but defaults to 3 days in most Dataverse environments. If your flow doesn't run for longer than this window, the delta token becomes invalid and you must perform a full resync. This is the single most important operational constraint to design for.

    Enabling Change Tracking on a Table

    Change tracking is not enabled by default on custom tables. You need to turn it on explicitly before any delta tokens will work.

    In the Power Apps maker portal (make.powerapps.com), navigate to your environment and open the table you want to track. In the table settings (the gear icon in the table editor), look for the Properties panel. You'll find a checkbox labeled "Track changes." Enable it and save.

    For standard Dataverse tables like Account, Contact, Lead, and Opportunity, change tracking is already enabled. For custom tables created in your solutions, you'll need to enable it manually.

    Warning

    Enabling change tracking on a large, heavily-used table has a mild performance cost on the Dataverse write path because the platform needs to maintain additional metadata per record. For tables with very high write volume (thousands of records per minute), test the impact in a non-production environment first.

    You can also enable change tracking programmatically via the Dataverse Web API or using the XRM SDK. In a solution-aware deployment, this setting lives in the table's metadata and travels with your solution across environments. This is worth doing — you don't want to deploy to production and discover you forgot to flip the switch.


    Designing the Pipeline Architecture

    Before building anything in Power Automate, sketch the architecture on paper. Incremental sync pipelines have four distinct responsibilities, and conflating them in a single flow causes fragility.

    The Four Responsibilities

    1. Token Management — Reading the current delta token from storage, and writing the new token after a successful sync.

    2. Delta Query Execution — Calling the Dataverse API with the current token, handling pagination (because a delta response can span multiple pages), and assembling the full change set.

    3. Change Processing — Differentiating creates, updates, and deletes, then writing each to the target system with appropriate operations.

    4. Error Recovery — Detecting token expiration, triggering full resync when necessary, and alerting on failures.

    Where to Store the Delta Token

    You have three realistic options:

    Option A: A Dataverse "Sync State" table — Create a custom table with columns for TableName, DeltaToken, LastSuccessfulSync, and SyncStatus. This keeps everything within the Microsoft ecosystem and is queryable. The downside is that if Dataverse itself is having issues, your token store is unavailable at the same time you most need it.

    Option B: Azure Table Storage — Cheap, fast, and completely independent of Dataverse. A single row per table being synced, updated atomically using the ETag concurrency mechanism. This is the option we'll use in this lesson because it's the most operationally robust.

    Option C: SharePoint List — Convenient but slower and subject to SharePoint throttling, which you absolutely don't want your sync pipeline to be sensitive to. Avoid this for high-frequency pipelines.

    Flow Topology

    We'll build two flows:

    Flow 1: Incremental Sync Orchestrator (Scheduled) — Runs on a schedule (every 5 minutes for near-real-time, every hour for batch). Reads the token, executes the delta query, processes changes, and writes the new token. This is the primary happy-path flow.

    Flow 2: Full Resync Recovery (Manually triggered or error-triggered) — Performs a full table sync, generates a fresh initial delta token, and writes it to storage. You trigger this flow on initial setup, after a token expiration, or after a schema change.

    This separation means your recovery logic doesn't pollute your hot path. It also means you can trigger a full resync without modifying the incremental flow.

    Key insight

    A common anti-pattern is building the full resync into the incremental flow as an else branch. This sounds elegant but means your scheduled flow occasionally runs for hours when it hits token expiration — which causes it to overlap with the next scheduled run, which causes concurrency issues. Keep the paths separate.


    Building the Delta Query HTTP Request

    Let's get concrete. Here's how to call the Dataverse delta query endpoint from a Power Automate HTTP action.

    Authentication Setup

    For production use, you should use an Azure AD app registration with the Dataverse API permission. Store the client secret in Azure Key Vault — see Integrating Power Automate with Azure Key Vault and Managed Identities: Complete Guide to Secrets Management and Zero-Trust Authentication for the complete pattern.

    For our flow, assume you have:

    • tenantId: Your Azure AD tenant ID
    • clientId: The app registration's client ID
    • clientSecret: Retrieved from Key Vault at flow start

    First, obtain a bearer token with an HTTP action:

    Method: POST
    URI: https://login.microsoftonline.com/@{variables('tenantId')}/oauth2/v2.0/token
    Headers:
      Content-Type: application/x-www-form-urlencoded
    Body:
      grant_type=client_credentials
      &client_id=@{variables('clientId')}
      &client_secret=@{variables('clientSecret')}
      &scope=https://yourorg.crm.dynamics.com/.default
    

    Parse the response with a Parse JSON action to extract access_token.

    The Initial Delta Query (No Existing Token)

    When you have no delta token stored yet, you make an ordinary query with a special Prefer header:

    Method: GET
    URI: https://yourorg.crm.dynamics.com/api/data/v9.2/accounts
         ?$select=accountid,name,emailaddress1,telephone1,modifiedon,statecode
         &$filter=statecode eq 0
         &trackChanges=true
    
    Headers:
      Authorization: Bearer @{body('Parse_Bearer_Token')?['access_token']}
      OData-MaxVersion: 4.0
      OData-Version: 4.0
      Accept: application/json
      Prefer: odata.track-changes, odata.maxpagesize=5000
    

    The trackChanges=true query parameter activates delta tracking. The Prefer: odata.track-changes header tells the API to include the @odata.deltaLink in the response. Both are required.

    Note

    The $filter parameter in your initial query becomes baked into the delta link. Future delta calls will automatically apply the same filter — you don't need to repeat it. This means your filter is set at initialization time. If you need to change the filter later (for example, adding a new column), you'll need to regenerate the delta token via a full resync.

    The Subsequent Delta Query (With Existing Token)

    Once you have a delta link stored, calling it is much simpler:

    Method: GET
    URI: @{variables('CurrentDeltaToken')}
    
    Headers:
      Authorization: Bearer @{body('Parse_Bearer_Token')?['access_token']}
      OData-MaxVersion: 4.0
      OData-Version: 4.0
      Accept: application/json
      Prefer: odata.maxpagesize=5000
    

    Notice you don't need the Prefer: odata.track-changes header here — the delta link already encodes that behavior. The URI is the complete delta link string, which looks something like:

    https://yourorg.crm.dynamics.com/api/data/v9.2/accounts?$deltatoken=05671b15-6df7-4b2e-a33a-b56f5f7234bc_1700000000000
    

    Handling Pagination in Delta Responses

    A delta response can itself be paginated. When the change set is large, the API returns @odata.nextLink for intermediate pages and only @odata.deltaLink on the final page. Your pipeline must follow all the nextLink pages before it can get the new delta token.

    Here's the pagination loop structure in Power Automate:

    1. Initialize a variable NextLink to the delta query URI.
    2. Initialize an array variable AllChangedRecords as an empty array.
    3. Use a Do until loop with the condition: NextLink is equal to "DONE".
    4. Inside the loop, execute the HTTP request to NextLink.
    5. Append the value array from the response to AllChangedRecords using union() or a compose + set variable pattern.
    6. Check if @odata.nextLink exists in the response body. If yes, set NextLink to that value. If no, check if @odata.deltaLink exists — set it to a variable NewDeltaToken and set NextLink to "DONE" to break the loop.

    The expression to check for the presence of @odata.deltaLink:

    if(
      contains(body('HTTP_Delta_Query'), '@odata.deltaLink'),
      body('HTTP_Delta_Query')['@odata.deltaLink'],
      null
    )
    

    Warning

    Do not save the new delta token until after all pages have been processed AND all changes have been successfully written to your target system. If you save the token mid-process and then fail, you'll have a new token that skips over changes you never applied. Token writes are your "commit" operation — treat them with transaction-like discipline.

    For a deeper look at how to structure and optimize these loops, the patterns in Implementing Parallel Branching and Concurrency Control in Power Automate to Maximize Throughput and Prevent Race Conditions are directly applicable to the parallelization opportunities in your change processing stage.


    Processing the Change Set

    Once you've assembled the full AllChangedRecords array across all pages, you need to process each record correctly. The tricky part is distinguishing creates/updates from deletes.

    Identifying Deleted Records

    Deleted records in a delta response don't have normal entity properties. They have an @odata.context property that ends with /$deletedEntity and an id property instead of the entity's primary key column name (accountid in our example).

    In your processing loop, check for deletion before doing anything else:

    // Expression to check if record is a delete tombstone:
    contains(
      string(items('Apply_to_each_Changed_Record')),
      '$deletedEntity'
    )
    

    Alternatively, check for the id property specifically:

    if(
      contains(items('Apply_to_each_Changed_Record'), 'id'),
      true,   // This is a delete
      false   // This is a create or update
    )
    

    This works because regular Dataverse entities use accountid (or whatever the primary key column is), not id. The deleted entity tombstone uses the generic id property.

    Building the Processing Logic

    Structure your change processing with a Condition action at the top:

    If delete:

    • Extract the id from the tombstone: items('Apply_to_each_Changed_Record')?['id']
    • Issue a DELETE or a soft-delete UPDATE to your target system for this ID

    If create or update:

    • Extract all field values normally
    • Determine whether to INSERT or UPSERT based on whether the ID already exists in your target

    For most warehouse-style targets, an UPSERT by the Dataverse record ID is the right pattern — you don't need to distinguish creates from updates. The record either exists in your target (update it) or it doesn't (insert it).

    Here's what the processing section looks like for an Azure SQL target using the SQL connector:

    // Inside your Apply to each (AllChangedRecords):
    
    [Condition: Is Delete?]
      Yes branch:
        - SQL: Execute a stored procedure
          Procedure: usp_DeleteSyncedAccount
          Parameters: 
            @AccountId: items('Apply_to_each_Changed_Record')?['id']
    
      No branch:
        - SQL: Execute a stored procedure  
          Procedure: usp_UpsertAccount
          Parameters:
            @AccountId: items('Apply_to_each_Changed_Record')?['accountid']
            @Name: items('Apply_to_each_Changed_Record')?['name']
            @Email: items('Apply_to_each_Changed_Record')?['emailaddress1']
            @Phone: items('Apply_to_each_Changed_Record')?['telephone1']
            @ModifiedOn: items('Apply_to_each_Changed_Record')?['modifiedon']
            @StateCode: items('Apply_to_each_Changed_Record')?['statecode']
    

    Using stored procedures is deliberately preferred here over raw SQL INSERT/UPDATE statements — it keeps your SQL logic out of your flow, makes schema changes easier to manage, and gives you transaction control at the database level.

    Tip

    If your change sets are large (hundreds of records per run), serialize the entire AllChangedRecords array as JSON and pass it to a single stored procedure that uses OPENJSON to process the batch in one database round trip. This is dramatically faster than one stored procedure call per record and avoids throttling on the SQL connector, which counts API calls against your Power Automate plan limits.


    Persisting Delta Tokens in Azure Table Storage

    Now let's implement the token store. Azure Table Storage is accessed via the HTTP connector in Power Automate — there's no dedicated connector, which actually works in our favor because it gives us precise control over the request.

    Token Table Schema

    Your Azure Storage account needs a table (let's call it SyncState) with these logical columns:

    • PartitionKey: "Dataverse" (constant)
    • RowKey: The table logical name (e.g., "account")
    • DeltaToken: The full delta link URL
    • LastSyncAt: ISO 8601 timestamp of last successful sync
    • SyncStatus: "Active", "Resyncing", or "Failed"

    Reading the Current Token

    Use an HTTP action with the Azure Storage Shared Key or SAS token for auth:

    Method: GET
    URI: https://yourstorage.table.core.windows.net/SyncState(PartitionKey='Dataverse',RowKey='account')
    Headers:
      Accept: application/json;odata=nometadata
      x-ms-date: @{utcNow('R')}
      Authorization: SharedKeyLite yourstorage:@{base64(hmac-sha256-signature)}
    

    Note

    Computing the SharedKeyLite HMAC-SHA256 signature in a Power Automate expression is painful. In practice, use a SAS token scoped to the specific table with read/write permissions and a reasonable expiry. Store the SAS token in Azure Key Vault and retrieve it at flow start, just like your Dataverse credentials.

    When no token exists (new setup), the response will be a 404. Handle this in a condition: if the status code is 404, set CurrentDeltaToken to null and proceed to the initial delta query path. If 200, parse the JSON body and set CurrentDeltaToken to the DeltaToken property.

    Writing the New Token

    After all changes are successfully processed, write the token back:

    Method: MERGE
    URI: https://yourstorage.table.core.windows.net/SyncState(PartitionKey='Dataverse',RowKey='account')
    Headers:
      Content-Type: application/json
      Accept: application/json;odata=nometadata
      If-Match: *
    Body:
    {
      "DeltaToken": "@{variables('NewDeltaToken')}",
      "LastSyncAt": "@{utcNow()}",
      "SyncStatus": "Active"
    }
    

    The MERGE method (Azure Table Storage's equivalent of a partial update) means you only need to provide the columns you're changing — it won't overwrite PartitionKey and RowKey.

    The If-Match: * header means "overwrite regardless of current ETag." For higher concurrency safety, you can store the ETag from the read operation and use it here to implement optimistic locking — which prevents two concurrent flow runs from both writing their tokens and causing a split-brain state.


    Handling Token Expiration and Full Resync

    This is where most tutorial implementations fall apart. Handling token expiration gracefully is what separates a production pipeline from a fragile demo.

    Detecting Token Expiration

    When you call a delta link with an expired token, the Dataverse API returns an HTTP 410 Gone response with a body like:

    {
      "error": {
        "code": "0x80060888",
        "message": "The delta token has expired. Perform a full query to get the latest data."
      }
    }
    

    In your HTTP action, configure it to not fail on non-2xx responses. Do this by expanding the action settings and setting "On failure" to continue. Then check the status code in a Condition action:

    Expression: outputs('HTTP_Delta_Query')['statusCode']
    Condition: is equal to 410
    

    If 410: trigger the full resync flow via "Run a Child Flow" action, update SyncStatus in your token store to "Resyncing", and terminate the current run with "Succeeded" status (so it doesn't spam error alerts for an expected condition).

    If another non-200 code: that's a genuine error. Handle it with your standard error pattern — log to an error table, send an alert, terminate with "Failed."

    Warning

    After a 410 token expiration, do not immediately start processing records from the full resync in the same flow run. Full resyncs can involve millions of records and take hours. Trigger the resync as a separate flow and let the incremental sync flow exit cleanly. The scheduled incremental flow will do nothing useful until the resync completes and writes a fresh token, which is exactly the right behavior.

    The Full Resync Flow

    The full resync flow follows this pattern:

    1. Set SyncStatus to "Resyncing" in token store.
    2. Delete any existing delta token for this table.
    3. Execute the initial delta query (with trackChanges=true and Prefer: odata.track-changes).
    4. Follow all @odata.nextLink pages, writing each batch to the target system. This is the bulk load phase.
    5. On the final page, capture @odata.deltaLink as the new fresh token.
    6. Write the new token and set SyncStatus to "Active."

    During the bulk load phase, performance optimization is critical. Use the patterns from Orchestrating Child Flows and Scoped Execution in Power Automate for Scalable, Reusable Automation Architecture to fan out page processing across multiple child flows in parallel, so you're not processing 80 pages of 5,000 records sequentially.

    One important subtlety: during the full resync, your target system is in a partially-updated state. If the incremental sync flow fires during this window (which it will, since it's scheduled), it should detect SyncStatus = "Resyncing" and exit early without doing anything. Add this check at the very beginning of your incremental sync flow.


    Concurrency Control and Idempotency

    Preventing Concurrent Runs

    Your incremental sync flow should never have two instances running simultaneously against the same table. Configure the flow's concurrency settings:

    In the flow trigger settings (click the three dots on the trigger in the designer), find "Concurrency Control." Enable it and set "Degree of Parallelism" to 1. This ensures if a previous run is still executing when the schedule fires again, the new run is queued (or rejected, depending on your setting) rather than starting a parallel execution.

    This matters because two concurrent runs could both read the same delta token, both process the same changes, both write to your target system (duplicating work), and then both write different "new" tokens — and the loser's write will silently overwrite the winner's, causing a gap in your sync.

    Making Change Processing Idempotent

    Even with concurrency control, you may encounter reprocessing due to flow reruns after partial failures. Your target-side write operations must be idempotent — running them twice on the same change should produce the same result as running them once.

    For UPSERT operations, this is natural: inserting a record that already exists just updates it. For DELETE operations, a delete on an already-deleted record should return success (or at least not throw an error your flow will catch).

    In your SQL stored procedures, handle this explicitly:

    -- usp_DeleteSyncedAccount
    CREATE PROCEDURE usp_DeleteSyncedAccount @AccountId UNIQUEIDENTIFIER
    AS
    BEGIN
      DELETE FROM dbo.Accounts WHERE DataverseId = @AccountId;
      -- No error if not found -- that's fine, it was already deleted
    END
    
    -- usp_UpsertAccount  
    CREATE PROCEDURE usp_UpsertAccount 
      @AccountId UNIQUEIDENTIFIER,
      @Name NVARCHAR(160),
      @Email NVARCHAR(100),
      @Phone NVARCHAR(50),
      @ModifiedOn DATETIME2,
      @StateCode INT
    AS
    BEGIN
      MERGE dbo.Accounts AS target
      USING (VALUES (@AccountId, @Name, @Email, @Phone, @ModifiedOn, @StateCode)) 
        AS source (DataverseId, Name, Email, Phone, ModifiedOn, StateCode)
      ON target.DataverseId = source.DataverseId
      WHEN MATCHED AND target.ModifiedOn < source.ModifiedOn THEN
        UPDATE SET Name = source.Name, Email = source.Email, 
                   Phone = source.Phone, ModifiedOn = source.ModifiedOn, StateCode = source.StateCode
      WHEN NOT MATCHED THEN
        INSERT (DataverseId, Name, Email, Phone, ModifiedOn, StateCode)
        VALUES (source.DataverseId, source.Name, source.Email, source.Phone, source.ModifiedOn, source.StateCode);
    END
    

    Notice the AND target.ModifiedOn < source.ModifiedOn condition on the MATCH branch. This is a "last write wins" guard — if a record appears in two consecutive delta runs (possible if it was updated very quickly), you only apply the update if the incoming data is actually newer. This prevents a stale reprocessed record from overwriting a more recent version.


    Monitoring and Alerting

    A sync pipeline you can't observe is a liability. At minimum, you need:

    Run-Level Metrics

    Log the following to your Dataverse or Azure SQL monitoring table at the end of every successful run:

    • Timestamp of run start and end
    • Number of records in the change set (total, creates/updates, deletes)
    • Duration of delta query phase
    • Duration of processing phase
    • Whether pagination occurred and how many pages
    // Expression to count deletes in your change set:
    length(
      filter(
        variables('AllChangedRecords'),
        item() => contains(string(item()), '$deletedEntity')
      )
    )
    

    Alerting on Anomalies

    Two conditions should trigger immediate alerts:

    Change set size spike — If your delta query returns more than, say, 10,000 records when the average is 50, something unusual happened (bulk import, data migration, misconfigured automation). This isn't necessarily a problem, but you want to know.

    Processing lag growth — If successive runs consistently return large change sets without them shrinking, your processing pipeline may be slower than the rate of changes. You're falling behind, and eventually your delta token will expire before you ever catch up. Alert on this and consider switching to a higher-frequency schedule or parallelizing your write operations.

    Tip

    Power BI can consume your monitoring log table directly and give you a live dashboard showing sync lag, change volume trends, and error rates over time. This is far more useful than watching flow run history in Power Automate. Connect the dashboard to your Dataverse sync monitoring table and schedule an auto-refresh. See Building Real-Time Power BI Dataset Refresh Pipelines with Power Automate: Triggering, Monitoring, and Alerting for the refresh pipeline pattern.


    Performance Tuning and Scaling Considerations

    Page Size Selection

    The odata.maxpagesize preference controls how many records Dataverse returns per page. The maximum is 5,000 for change tracking queries (versus 1,000 for standard queries). Use 5,000 in production — each HTTP round trip has fixed overhead, and fewer round trips means lower total latency.

    However, page size affects memory usage in your flow. Very large pages (5,000 records × many fields × many columns) can cause your flow to hit the 100MB expression evaluation limit. If you're syncing wide tables with many columns, consider selecting only the columns you actually need in your $select clause, and reduce page size to 2,000 or 3,000 if you start seeing expression size errors.

    Parallelizing Change Processing

    For change sets with hundreds or thousands of records, sequential processing in an Apply to each loop is too slow. Power Automate allows configuring concurrency on apply-to-each loops — set "Degree of Parallelism" to 10–15 for SQL upsert operations.

    Warning

    Parallel SQL writes against the same rows from the same change set can cause deadlocks. Design your stored procedures with proper transaction isolation, and add retry logic on the SQL connector for transient deadlock errors (SQL error code 1205). The Master Error Handling and Retry Patterns in Power Automate for Bulletproof Flows article covers retry-on-specific-error patterns you can apply here.

    Chunking Large Change Sets

    If a single delta run returns more than ~50,000 records (possible after a bulk operation), processing all of them in a single flow run risks hitting Power Automate's 30-day run history size limit or the flow timeout limit.

    Implement a chunking strategy: after paginating through the delta response, if AllChangedRecords exceeds a threshold (say 25,000 records), write the partial batch to a staging queue in Azure Service Bus or a Dataverse staging table, then trigger a batch processor flow. This pattern is described in detail in Implementing Event-Driven Automation with Power Automate and Azure Service Bus: Queue-Based Decoupling for High-Volume, Resilient Enterprise Workflows.

    The key insight here is that your sync orchestrator flow's job is to collect changes reliably and enqueue them — not necessarily to process them in the same execution context. Separating collection from processing gives you independent scaling and retry for each concern.


    Hands-On Exercise

    Build an incremental sync pipeline that keeps a SQL Server table synchronized with Dataverse Account records. Use this specification:

    Target table: dbo.AccountSync in Azure SQL

    Columns: DataverseId (UNIQUEIDENTIFIER, PK), AccountName (NVARCHAR(160)), PrimaryEmail (NVARCHAR(100)), SyncedAt (DATETIME2), IsDeleted (BIT)

    Step 1: Enable change tracking on the Account table in your Dataverse environment via the maker portal.

    Step 2: Create the token store. Create an Azure Storage account and add a SyncState table. Generate a SAS token with Table Read + Write + Update permissions.

    Step 3: Build Flow 1 — Full Resync. This flow is manually triggered. It calls the Accounts endpoint with trackChanges=true, paginates through all results, upserts each to dbo.AccountSync, and on the final page, writes @odata.deltaLink to your SyncState table.

    Step 4: Build Flow 2 — Incremental Sync. This flow runs every 10 minutes. It reads the delta token from SyncState, calls the delta URL, processes changes (distinguishing tombstones from regular records), upserts or soft-deletes records in dbo.AccountSync, then writes the new delta token.

    Step 5: Test your pipeline. Run Flow 1 to establish the initial state. Then in Dataverse, update 3 Account records and delete 1. Run Flow 2 manually and verify that exactly 4 records appear in your change set, that the 3 updates are reflected in dbo.AccountSync, and that the deleted record has IsDeleted = 1.

    Step 6: Test token expiration recovery. Manually corrupt the delta token in your SyncState table (change a few characters in the token string). Run Flow 2 and verify it receives a 410 response and handles it without crashing — ideally triggering a notification.

    Stretch goal: Add monitoring. After each successful Flow 2 run, insert a row to a SyncAuditLog table with the run timestamp, change count, and run duration. Build a simple Power Automate flow that queries SyncAuditLog and sends a Teams alert if the change count exceeds 5,000 in a single run.


    Common Mistakes and Troubleshooting

    "My delta query always returns zero records even though I know data changed."

    Check two things: first, is change tracking enabled on the table? Second, are you calling the delta link from a previous run? If you're regenerating the delta link on every run from a fresh initial query, you'll always see zero changes because you're comparing current state to current state (no time has passed). The delta link must be stored persistently between runs.

    "I'm getting 401 Unauthorized on my delta calls."

    The delta link URL includes your Dataverse environment URL. If you're refreshing your bearer token each run (which you should be), make sure you're passing the fresh token — not a cached one — to the delta query. Also verify that the service principal used for authentication has the prvReadAccount privilege in Dataverse. Missing read privilege on the entity produces a 401, not a 403, which is confusing.

    "I'm missing some changes — records that I know were updated aren't appearing."

    This is usually a $select clause issue from your initial query. If you selected name but not modifiedon in your initial query, changes that only affect modifiedon may not surface. More commonly, if you added a $filter to your initial query (like statecode eq 0), records that were previously active but became inactive won't appear in subsequent deltas — they're filtered out of the change set. This is a fundamental constraint of Dataverse delta queries: filter scope is fixed at initialization.

    "My flow is timing out on large change sets."

    Power Automate cloud flows have a maximum duration of 30 days per run, but practically you'll hit performance limits much sooner with very large sequential Apply to each loops. Enable parallelism on the loop, switch to batch SQL processing (passing the entire JSON array to a stored procedure), and consider splitting your change processing into a child flow that receives a chunk of records and processes them independently.

    "I'm getting duplicate records in my target system."

    Two likely causes: either concurrency control is not set on your flow trigger (two instances ran simultaneously and both processed the same delta), or your UPSERT logic isn't keying on DataverseId correctly. Also check whether your full resync flow and your incremental sync flow could have overlapped — the SyncStatus = "Resyncing" guard in the incremental flow is specifically designed to prevent this, but verify it's implemented correctly.

    "The 410 error is happening more frequently than it should."

    Your delta token window is shorter than you think, or your flow isn't running frequently enough. Check your Dataverse environment's change tracking retention setting (in Power Platform admin center under Environment > Settings). You can increase it up to 90 days in some configurations, though the default is 3 days. Also verify your scheduled flow isn't being throttled or silently skipping runs — check Using Power Automate Run History and Flow Checker to Debug and Fix Failing Flows for how to audit your run history for gaps.


    Summary and Next Steps

    You've built a production-grade incremental sync pipeline grounded in how Dataverse change tracking actually works at the protocol level. Let's recap what you've implemented:

    • Change tracking at the API level: OData delta links that return only true differences rather than full table scans, with tombstones for deleted records.
    • Durable token persistence: Azure Table Storage as a lightweight, independent state store that survives Dataverse hiccups.
    • Robust pagination: A Do until loop that follows @odata.nextLink through multi-page change sets before capturing the new @odata.deltaLink.
    • Correct change discrimination: Distinguishing deleted-record tombstones from create/update records before writing to the target.
    • Idempotent writes: SQL MERGE-based upserts with ModifiedOn guards that are safe to rerun on the same data.
    • Graceful recovery: 410 detection, status flagging, and separation of resync logic from the hot path.
    • Concurrency protection: Single-instance trigger configuration and optimistic locking on token writes.

    Where to go next:

    For deeper data transformation skills — particularly for mapping and reshaping the Dataverse field structures before writing to your target — the patterns in Transforming and Mapping Data with Power Automate's Compose, Select, and Filter Array Actions will give you a complete vocabulary for JSON transformation inside the flow.

    If you're deploying this pipeline across multiple environments (dev, test, production), the ALM considerations in Deploying and Managing Power Automate Solutions Across Environments: ALM Pipelines, Solution-Aware Flows, and Environment Variables for Enterprise-Scale Delivery show you how to parameterize environment-specific settings (Dataverse URLs, storage account names, SAS tokens) so your flow package is truly portable.

    Finally, consider where this pipeline fits in a broader event-driven architecture. A delta sync that runs every 5 minutes is near-real-time, but it's still polling. If you need sub-minute latency for specific high-priority record changes, a Service Bus–based trigger architecture (where Dataverse plugin steps push messages directly to a queue) can complement your delta sync for the events that truly can't wait.

    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

    Enterprise Cloud Flows

    Previous

    Auditing and Governing Power Automate at Scale: Flow Ownership Policies, Usage Analytics, and Automated Compliance Reporting with CoE Toolkit

    Next

    Building a Power Automate Monitoring and Alerting System: Detecting Failed Flows, Notifying Owners, and Logging Telemetry to Azure Application Insights

    Related Insights

    Power AutomatePractitioner

    Automating Recurring Team Status Reports with Power Automate: Forms, SharePoint, and Teams

    22 min
    Power AutomateFoundation

    Establishing Power Automate Maker Onboarding Controls: Configuring Default Environment Restrictions, First-Party Connector Policies, and Trial License Boundaries

    20 min
    Power AutomateFoundation

    Scheduling Desktop Flow Runs with Windows Task Scheduler and the Power Automate Machine Runtime

    19 min

    On this page

    • Introduction
    • Prerequisites
    • How Change Tracking Actually Works in Dataverse
    • The OData Delta Link Protocol
    • Enabling Change Tracking on a Table
    • Designing the Pipeline Architecture
    • The Four Responsibilities
    • Where to Store the Delta Token
    • Flow Topology
    • Building the Delta Query HTTP Request
    • Authentication Setup
    • The Initial Delta Query (No Existing Token)
    • The Subsequent Delta Query (With Existing Token)
    • Handling Pagination in Delta Responses
    • Processing the Change Set
    • Identifying Deleted Records
    • Building the Processing Logic
    • Persisting Delta Tokens in Azure Table Storage
    • Token Table Schema
    • Reading the Current Token
    • Writing the New Token
    • Handling Token Expiration and Full Resync
    • Detecting Token Expiration
    • The Full Resync Flow
    • Concurrency Control and Idempotency
    • Preventing Concurrent Runs
    • Making Change Processing Idempotent
    • Monitoring and Alerting
    • Run-Level Metrics
    • Alerting on Anomalies
    • Performance Tuning and Scaling Considerations
    • Page Size Selection
    • Parallelizing Change Processing
    • Chunking Large Change Sets
    • Hands-On Exercise
    • Common Mistakes and Troubleshooting
    • "My delta query always returns zero records even though I know data changed."
    • "I'm getting 401 Unauthorized on my delta calls."
    • "I'm missing some changes — records that I know were updated aren't appearing."
    • "My flow is timing out on large change sets."
    • "I'm getting duplicate records in my target system."
    • "The 410 error is happening more frequently than it should."
    • Summary and Next Steps