Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Integrating Power Automate with Microsoft Dataverse: Querying, Creating, and Updating Records in Business Process Flows

Integrating Power Automate with Microsoft Dataverse: Querying, Creating, and Updating Records in Business Process Flows

Power Automate⚡ Practitioner21 min readJul 18, 2026Updated Jul 18, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • Understanding the Dataverse Connector's Architecture
  • Querying Records with List Rows
  • Setting Up Your Filter
  • Selecting Columns
  • Expanding Related Records
  • Sorting and Limiting
  • Handling the Results
  • Creating Records
  • Basic Field Mapping
  • Setting Lookup Fields
  • Setting Choice (Option Set) Fields
  • Setting Status
  • Updating Records
  • Finding the Row ID
  • Partial Updates and Unchanged Fields
  • The Upsert Pattern
  • Building the Complete Business Process Flow
  • Concurrency, Pagination, and Performance Considerations
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • "The request failed: Table not found"
  • Filter Returns Zero Rows Unexpectedly
  • "The property 'fieldname' does not exist on type"
  • Lookup Field Updates Fail with Relationship Error
  • Apply to Each Runs But Records Aren't Updated
  • "SecLib::AccessCheckEx failed" Errors
  • Flow Runs Slowly on Large Datasets
  • Summary & Next Steps
  • Integrating Power Automate with Microsoft Dataverse: Querying, Creating, and Updating Records in Business Process Flows

    Introduction

    Your sales team closes a deal in Dynamics 365. Within seconds, a new project record should appear in your project management table, a welcome email should fire off to the client, and the opportunity status should update to "Won — Contracted." None of that happens automatically unless something orchestrates it — and that something is Power Automate working directly against Microsoft Dataverse.

    Dataverse is the backbone of the Power Platform. It's where Dynamics 365 stores its data, where Power Apps model-driven apps live, and increasingly where enterprise automation state gets tracked. If you're building flows that touch any of these systems and you're still routing everything through HTTP connectors or manual exports, you're leaving performance, reliability, and governance on the table. The native Dataverse connector in Power Automate gives you direct, server-side access to your tables with full support for OData filtering, relationship expansion, and transactional operations — no middleware required.

    By the end of this lesson, you'll be able to build production-grade flows that read, create, and update Dataverse records as part of real business processes. You'll understand how filtering and querying actually work under the hood, how to handle related records and lookups, and where flows commonly break in Dataverse integrations — and how to fix them.

    What you'll learn:

    • How to use the Dataverse connector's List rows action with OData filters, column selection, and relationship expansion
    • How to create new records with proper lookup and choice column handling
    • How to update existing records efficiently, including upsert patterns
    • How to wire these operations into a coherent business process flow with branching logic
    • How to debug Dataverse connector errors and avoid the most expensive performance mistakes

    Prerequisites

    This lesson assumes you're comfortable with Power Automate fundamentals — you've built flows before, you understand triggers, actions, and dynamic content, and you've worked with at least one connector beyond email. You should also have a Dataverse environment available (a developer environment from make.powerapps.com works fine) and either a Dynamics 365 license or access to a model-driven app with custom tables. Familiarity with basic OData syntax is helpful but not required — we'll build it from scratch.


    Understanding the Dataverse Connector's Architecture

    Before touching a single action, it's worth spending two minutes on how the Dataverse connector actually communicates with the platform. Unlike connectors that call REST APIs through a gateway or a third-party SaaS endpoint, the Dataverse connector talks directly to the Dataverse Web API — the same OData v4 endpoint that Dynamics 365 itself uses. This matters for a few reasons.

    First, it respects your security model completely. When your flow runs, it runs under the identity of the connection owner — typically a service account or the flow creator. That identity's Dataverse security roles govern exactly what records are visible and what operations are permitted. If your service account doesn't have read access to the cr7b4_projects table, the List rows action will return zero rows, not an error. Keep this in mind during debugging.

    Second, the Dataverse connector supports server-side filtering. When you use OData filter expressions in the List rows action, the filter executes on the Dataverse server before any data is sent to your flow. This is dramatically different from connectors where you fetch all records and filter client-side. For tables with tens of thousands of rows, this distinction is the difference between a 2-second flow and a 45-second flow that hits API limits.

    Third, Dataverse has its own column naming conventions. Columns you created have a publisher prefix (like cr7b4_ or new_), and system columns use camelCase (like createdon, modifiedon, statecode). The connector exposes both through logical names, not the friendly display names you see in Power Apps. You'll spend time early on looking these up — build the habit of checking the table definition in make.powerapps.com when something doesn't match what you expect.


    Querying Records with List Rows

    The List rows action is your primary read mechanism. Open a new flow, add the Dataverse action, and select List rows. You'll see a deceptively simple form with a Table name dropdown and several optional fields. Let's work through each one using a realistic scenario.

    Scenario: You're building a flow that runs nightly to find all open service cases (in the incident table in Dynamics 365) that have been open for more than 7 days and have no activity logged in the last 48 hours. You need to notify the case owner and escalate the priority.

    Setting Up Your Filter

    In the Filter rows field, you write OData filter expressions. This is standard OData, but the Dataverse connector has a few quirks worth knowing.

    For our scenario, the filter looks like this:

    statecode eq 0 and createdon le @{addDays(utcNow(), -7)} and modifiedon le @{addDays(utcNow(), -2)}
    

    Let's unpack this:

    • statecode eq 0 means the case is active (0 = Active, 1 = Resolved, 2 = Cancelled in the incident table)
    • createdon le @{addDays(utcNow(), -7)} — the le operator means "less than or equal to," so we want cases created on or before 7 days ago
    • The @{...} syntax is Power Automate's expression syntax, letting us inject dynamic values directly into the OData string

    Warning: OData datetime values must be ISO 8601 format. The utcNow() and addDays() functions produce this automatically, so using them in filters is reliable. Don't try to format dates manually here — you'll introduce timezone bugs.

    For string comparisons, the syntax looks like:

    cr7b4_status eq 'Pending Review' and contains(cr7b4_description, 'urgent')
    

    For lookup fields (foreign keys), you filter on the GUID directly:

    _ownerid_value eq '8f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c'
    

    Notice the underscore-wrapped _ownerid_value pattern — that's how Dataverse exposes lookup IDs in OData. The leading underscore and trailing _value are added by the system.

    Selecting Columns

    The Select columns field takes a comma-separated list of logical column names. Always fill this in.

    incidentid,title,createdon,modifiedon,prioritycode,_ownerid_value,ticketnumber
    

    If you leave Select columns blank, Dataverse returns every column on every record — including rich text fields, large memo fields, and columns you'll never use. For a table like incident with 200+ columns, this makes your flow slow and your dynamic content panel unusable. Column selection is free — use it.

    Expanding Related Records

    The Expand Query field lets you pull in related record data in a single call using OData $expand. This is hugely valuable when you need data from a parent record.

    For example, to expand the owning user's information when querying cases:

    ownerid($select=fullname,internalemailaddress)
    

    This tells Dataverse: when you return each case, also fetch the related ownerid record and include only the fullname and internalemailaddress fields from it.

    After this expansion, in your flow's dynamic content you'll find ownerid fullname and ownerid internalemailaddress available for each row in your Apply to Each loop — no secondary query needed.

    Tip: You can expand multiple relationships by comma-separating them: ownerid($select=fullname,internalemailaddress),customerid_account($select=name,telephone1). Expanding more than two or three relationships starts to impact response time, so be selective.

    Sorting and Limiting

    Sort By takes an OData orderby expression:

    createdon asc
    

    Or for multiple columns:

    prioritycode desc, createdon asc
    

    Row count limits the number of records returned. If you're processing records in batches or just need the most recent N items, set this. The Dataverse connector pages results automatically for large datasets when you leave Row count blank, but pagination adds round trips and runtime. If your business logic only needs the top 50 records, set Row count to 50 and save yourself the overhead.

    Handling the Results

    List rows returns an array in the value property of the response. In most flows, you'll immediately follow it with an Apply to each action that iterates over value. Inside that loop, every column you selected is available as dynamic content.

    One common source of confusion: lookup fields come back as GUIDs (the _fieldname_value pattern), but the friendly name of the related record comes through the expanded navigation property if you used $expand. If you need a lookup's display name without expanding, you'll need a secondary Get row action — which is less efficient. Expand when you can.


    Creating Records

    The Add a new row action is the create operation. It's straightforward for simple fields but has some real gotchas for lookups, choices, and relationships that will bite you if you don't know about them.

    Scenario extension: Your nightly escalation flow has identified high-priority cases. Now you want to create a follow-up task record (in the task table) linked to each case, assigned to a dedicated escalation team member.

    Basic Field Mapping

    After selecting the task table in Add a new row, you'll see the common fields. Some fields appear immediately; others are in the "Show advanced options" section. Fill in the obvious ones first:

    • Subject: Escalation Follow-up — followed by the ticketnumber from your List rows dynamic content
    • Description: A templated string with case details
    • Due Date: @{addDays(utcNow(), 1)} — tomorrow

    Setting Lookup Fields

    This is where people get stuck. Lookup fields in the Add a new row action don't show a friendly dropdown at runtime — you provide the GUID of the related record.

    For the regardingobjectid field on tasks (which links the task to its parent — in this case, the incident), you need to provide the incident's GUID. That GUID came back from your List rows call as incidentid. But you can't just drop it in the field — you also need to tell Dataverse what type of record it points to, because regardingobjectid is a polymorphic lookup (it can point to an account, contact, opportunity, case, etc.).

    The connector handles this through a paired field pattern. You'll see:

    • Regarding (Tasks) — where you provide the GUID
    • Regarding Type (Tasks) — where you select the entity type

    Set Regarding to the incidentid value from dynamic content, and set Regarding Type to incidents.

    For non-polymorphic lookups (a lookup that only ever points to one table), you just need the GUID:

    ownerid: 8f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
    

    Warning: Never hardcode GUIDs for user lookups in production flows. Use a "Get a row" action to look up the user by email first, then reference that record's systemuserid. Hardcoded GUIDs break when environments are copied, users are disabled, or you migrate from sandbox to production.

    Setting Choice (Option Set) Fields

    Choice columns store integer codes, not display labels. When you set prioritycode to escalate the task, you're not setting it to "High" — you're setting it to 2 (in the standard Dynamics 365 task priority scheme where 0=Low, 1=Normal, 2=High).

    The connector renders choice fields as dropdowns in the action card when you configure them statically, but when you're setting them dynamically (based on logic in your flow), you need to provide the integer value. You can find the code values by checking the column definition in make.powerapps.com under Tables > your table > Columns > the choice column > Edit Choices.

    For our escalation flow, we'd set priority to 2 to mark the task as high priority.

    Setting Status

    statecode and statuscode work together and have a dependency: statuscode must be a valid status reason for the given statecode. You generally don't set these on record creation — let them default. If you need to create a record in a specific status, set both consistently. Creating a task with statecode = 1 (Completed) and leaving statuscode at its default will fail or produce unexpected results.


    Updating Records

    The Update a row action modifies an existing record. You provide the table name and the row's unique identifier (the primary key GUID), then set only the fields you want to change.

    Scenario extension: After creating the follow-up task, we want to update the original case record to stamp it with a new field value — let's say cr7b4_lastescalateddate — and change its priority to High.

    Finding the Row ID

    The Update a row action requires the row's GUID in the Row ID field. For our case update, this is incidentid from the List rows dynamic content. Simple enough.

    Partial Updates and Unchanged Fields

    A key behavior to understand: Update a row only sends the fields you explicitly set. It does not overwrite fields you leave blank in the action card. This is a PATCH operation, not a PUT — partial updates are the default. This means you can safely update one field without touching anything else on the record.

    Set cr7b4_lastescalateddate to @{utcNow()} and prioritycode to 2. Everything else on the case remains untouched.

    The Upsert Pattern

    Sometimes you don't know whether a record exists and you want to create it if it doesn't, or update it if it does. This is the upsert pattern, and Dataverse supports it natively.

    In the Dataverse connector, if you use Add a new row and provide a value for the primary key field (the GUID), Dataverse will perform an upsert — create if the GUID doesn't exist, update if it does.

    Here's where this gets practical. Imagine you're syncing project data from an external system into Dataverse. Each project has an external ID. Your flow can:

    1. Compose a deterministic GUID from the external ID using the guid() function with a seed — or, more practically, use an alternate key.

    In Dataverse, alternate keys let you identify records by a business key rather than a GUID. If you've set up an alternate key on cr7b4_externalid for your projects table, the Add a new row action exposes a "Use alternate key" option. You provide the alternate key value, and Dataverse handles the upsert logic server-side. This is the cleanest pattern for integration scenarios.

    Tip: Alternate key upserts are significantly more efficient than the query-then-branch pattern (List rows to check existence, condition action, then Add or Update). Fewer API calls means faster flows and lower API consumption — which matters when you're processing thousands of records.


    Building the Complete Business Process Flow

    Now let's wire all of these concepts into the full escalation flow. Here's the complete architecture:

    Trigger: Recurrence — every day at 6:00 AM UTC

    Step 1: List rows — Get escalation candidates

    • Table: Cases (incident)
    • Filter: statecode eq 0 and prioritycode ne 2 and createdon le @{addDays(utcNow(), -7)} and modifiedon le @{addDays(utcNow(), -2)}
    • Select columns: incidentid,title,createdon,ticketnumber,prioritycode,_ownerid_value
    • Expand query: ownerid($select=fullname,internalemailaddress)
    • Sort by: createdon asc

    Step 2: Condition — Check if any results exist

    Wrap this in a condition checking @{empty(outputs('List_rows')?['body/value'])} is equal to true. If true, send a "nothing to escalate" notification and end. If false, continue.

    Tip: Always check for empty results before entering an Apply to each. An empty array doesn't cause an error in Apply to each, but any actions inside that depend on array items having certain properties can fail unpredictably on edge cases. Explicit checking also makes your flow easier to read.

    Step 3: Apply to each — Process each case

    Iterate over value from List rows.

    Inside the loop:

    Step 3a: Add a new row — Create follow-up task

    • Table: Tasks
    • Subject: Escalation: @{items('Apply_to_each')?['ticketnumber']}
    • Description: Case "@{items('Apply_to_each')?['title']}" has been open since @{items('Apply_to_each')?['createdon']} with no recent activity. Immediate follow-up required.
    • Due Date: @{addDays(utcNow(), 1)}
    • Regarding: @{items('Apply_to_each')?['incidentid']}
    • Regarding Type: incidents
    • Priority: 2

    Step 3b: Update a row — Stamp the case

    • Table: Cases
    • Row ID: @{items('Apply_to_each')?['incidentid']}
    • cr7b4_lastescalateddate: @{utcNow()}
    • Priority: 2

    Step 3c: Send email — Notify case owner

    • To: @{items('Apply_to_each')?['ownerid']?['internalemailaddress']}
    • Subject: Action Required: Case @{items('Apply_to_each')?['ticketnumber']} Escalated
    • Body: Templated email with case details and a link to the record

    Note the dot-notation path for the expanded ownerid data: items('Apply_to_each')?['ownerid']?['internalemailaddress']. The ? operator prevents null reference errors if the expansion returned no data for a particular record.

    Step 4: Post a summary to Teams

    After the loop, compose a summary message counting how many cases were processed:

    @{length(outputs('List_rows')?['body/value'])} cases escalated at @{utcNow('yyyy-MM-dd HH:mm')} UTC
    

    Post this to your operations channel.


    Concurrency, Pagination, and Performance Considerations

    Running operations in an Apply to each loop sequentially is safe but slow. If you have 200 cases to process, and each iteration takes 2 seconds (three Dataverse operations plus an email), you're looking at 6+ minutes. Power Automate supports parallel Apply to each execution.

    In the Apply to each settings (the three-dot menu in the action header), you can enable Concurrency Control and set a degree of parallelism up to 50. For Dataverse operations, a parallelism of 10–20 is usually safe. Watch out: parallel execution means your loop iterations no longer have a predictable order, and any logic that depends on the sequence of operations (like incrementing a counter variable) will produce race conditions. Dataverse record operations themselves are thread-safe — two concurrent updates to different records won't interfere with each other.

    For large datasets, the List rows action automatically handles pagination when you leave Row count blank. The connector fetches pages of 5,000 records and continues until all results are retrieved. This works correctly but means your flow can't start processing until all pages are fetched. For tables with 50,000+ records, consider adding more specific filters, using the Row count limit with a scheduled cursor pattern (filter by createdon gt lastRunTime), or breaking your flow into smaller scoped executions.

    Warning: The Dataverse connector counts each API call against your Power Platform API limits. As of current licensing, most environments get a base entitlement of 100,000 API calls per day per tenant, with additional calls allocated by user license type. A flow that processes 1,000 records with 3 Dataverse operations each consumes 3,000 calls. Monitor this in the Power Platform admin center if you're building high-volume automation.


    Hands-On Exercise

    Build the following flow from scratch in your Dataverse developer environment. This exercise will take approximately 45–60 minutes.

    Scenario: You work for a company that tracks vendor contracts in a custom Dataverse table called cr_contracts. Each contract has:

    • cr_contractname (text)
    • cr_expirationdate (date)
    • cr_status (choice: Active=1, Expiring Soon=2, Expired=3)
    • cr_ownerid (lookup to systemuser)
    • cr_renewalnoticesentdate (date, nullable)

    Your task: Build a daily flow that:

    1. Queries all active contracts expiring within the next 30 days where cr_renewalnoticesentdate is null (no notice sent yet)
    2. For each contract found, sends a notification email to the contract owner
    3. Updates the contract's cr_status to 2 (Expiring Soon) and sets cr_renewalnoticesentdate to today
    4. If no contracts are found, posts a log entry to a SharePoint list confirming the flow ran with zero results

    Stretch goal: Add a second branch that finds contracts where cr_expirationdate is in the past and cr_status is not 3, and updates them to Expired. Run both branches in parallel using the Parallel Branch control action.

    Verification: Run your flow manually, then inspect the flow run history. Check the inputs and outputs of the List rows action to verify your OData filter returned what you expected. Open one of the updated records in a model-driven app or the Dataverse table view to confirm the field values were written correctly.


    Common Mistakes & Troubleshooting

    "The request failed: Table not found"

    This almost always means you have the wrong table logical name. The display name shown in the connector dropdown might be "Projects," but the logical name in your filter or column select needs to be cr7b4_project (singular, with prefix). Check the table's logical name in make.powerapps.com under Tables > your table > Properties.

    Filter Returns Zero Rows Unexpectedly

    First, verify the connection identity has access to the table and records (security roles). Then, test your OData filter string in isolation using the Dataverse Web API directly. In your browser (while logged into the correct environment), navigate to:

    https://yourorg.crm.dynamics.com/api/data/v9.2/incidents?$filter=statecode eq 0&$top=5
    

    If you get results in the browser but not in your flow, the connection account's security role is the likely culprit.

    "The property 'fieldname' does not exist on type"

    You've referenced a column that doesn't exist or used the wrong logical name. Common causes: using the display name instead of the logical name, forgetting the publisher prefix, or a typo. Look up the exact logical name in the column definition in make.powerapps.com.

    Lookup Field Updates Fail with Relationship Error

    When updating a lookup field, you need to provide the GUID as a plain string — not wrapped in any extra formatting. Some flows accidentally pass an object or include extra whitespace. Use trim() around any dynamically constructed GUIDs. Also confirm you're using the _fieldname_value syntax only for reading — when writing, use the fieldname without underscores and without the _value suffix.

    Apply to Each Runs But Records Aren't Updated

    If your Update a row action runs without error but you don't see changes in Dataverse, check whether a Business Rule or Plugin on the table is overwriting your values on save. Dataverse plugins run after the API write, and a synchronous plugin can reset field values. Check with your Dataverse administrator whether there are any pre-configured business rules on the table you're targeting.

    "SecLib::AccessCheckEx failed" Errors

    This is a permission error. The connection account doesn't have the required privilege for the operation on that table. Review the Dataverse security role assigned to the service account running the flow. You need at minimum: Read privilege for List rows, Create privilege for Add a new row, Write privilege for Update a row — all scoped appropriately (Organization, Business Unit, or User level depending on which records you need to touch).

    Flow Runs Slowly on Large Datasets

    Profile your flow run time in the flow run history — click on any step to see its start time, end time, and duration. If List rows is fast but the Apply to each loop is slow, enable concurrency. If List rows itself is slow, your filter isn't selective enough and Dataverse is scanning too many rows. Add more specific filter conditions or ensure the columns you're filtering on have Dataverse indexes (custom columns on large tables sometimes benefit from a Relevance Search indexing configuration).


    Summary & Next Steps

    You've built a complete pattern for integrating Power Automate with Dataverse that goes well beyond "drag in an action and hope for the best." You understand how the connector communicates with the Dataverse Web API, how OData filtering keeps your flows performant by pushing work to the server, and how to handle the real-world complexity of lookups, choice fields, and status transitions. You've seen how to structure a multi-step business process flow with conditional logic, parallel processing, and proper error hygiene.

    The patterns here — query with filtered List rows, process with Apply to each, create or update with proper field mapping — are the foundation of virtually every Dataverse-integrated flow you'll build. The specific tables and fields change; the architecture doesn't.

    Where to go from here:

    • Error handling and retry logic: Add a Configure Run After condition on your update actions so that if an individual record update fails, the loop continues processing remaining records and logs failures to a separate table. This is essential for production-grade flows.
    • Child flows and flow chaining: When your loop body grows beyond 8–10 actions, extract it into a child flow called via the "Run a Child Flow" action. This keeps your flows maintainable and enables reuse across multiple parent flows.
    • Dataverse change tracking and delta queries: Rather than querying all active records daily, use the @odata.deltaLink returned by List rows to fetch only records that changed since the last run. This can reduce your API consumption by 90%+ on stable datasets.
    • Power Automate Desktop and Dataverse: For scenarios requiring UI automation alongside Dataverse operations, Power Automate Desktop flows can call cloud flows via HTTP triggers, bridging legacy application data into your Dataverse records.
    • Custom connectors and the Dataverse Web API directly: For advanced operations the standard connector doesn't expose — like bulk delete, ExecuteMultiple requests, or custom API endpoints — the HTTP with Azure AD connector pointed at your Dataverse endpoint gives you full API access with the same security model.

    The data is in Dataverse. The business rules are defined. Your job is building the automation layer that makes the system respond intelligently to real-world events — and now you have the tools to do it properly.

    Learning Path: Flow Automation Basics

    Previous

    Organizing and Reusing Logic with Power Automate Environments, Connections, and Flow Templates

    Related Articles

    Power Automate🌱 Foundation

    Organizing and Reusing Logic with Power Automate Environments, Connections, and Flow Templates

    18 min
    Power Automate🔥 Expert

    Deploying and Managing Power Automate Solutions Across Environments: ALM Pipelines, Solution-Aware Flows, and Environment Variables for Enterprise-Scale Delivery

    30 min
    Power Automate⚡ Practitioner

    Handling Pagination and Throttling When Querying Large Datasets in Power Automate

    21 min

    On this page

    • Introduction
    • Prerequisites
    • Understanding the Dataverse Connector's Architecture
    • Querying Records with List Rows
    • Setting Up Your Filter
    • Selecting Columns
    • Expanding Related Records
    • Sorting and Limiting
    • Handling the Results
    • Creating Records
    • Basic Field Mapping
    • Setting Lookup Fields
    • Setting Choice (Option Set) Fields
    • Setting Status
    • Updating Records
    • Finding the Row ID
    • Partial Updates and Unchanged Fields
    • The Upsert Pattern
    • Building the Complete Business Process Flow
    • Concurrency, Pagination, and Performance Considerations
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • "The request failed: Table not found"
    • Filter Returns Zero Rows Unexpectedly
    • "The property 'fieldname' does not exist on type"
    • Lookup Field Updates Fail with Relationship Error
    • Apply to Each Runs But Records Aren't Updated
    • "SecLib::AccessCheckEx failed" Errors
    • Flow Runs Slowly on Large Datasets
    • Summary & Next Steps