Power Automate + Dataverse — practical patterns for flows that don’t duplicate, don’t loop, and don’t mysteriously die in Prod.
TL;DR
If your flow can’t be safely re-run, it’s not automation — it’s a one-time event.
You’re here because… (symptoms)
Pick your pain:
- Duplicates: “It created the same related record 12 times.”
- Loops: “Why are there 300 runs for one record?”
- Dev works, Prod fails: “Same flow, same data… different outcomes.”
- Lookups/Choices break updates: “Invalid reference link” / “Bad Request” / “it says it can’t find the record.”
- Backfills feel dangerous: “Can we run this on 40,000 records?” (😐)
Fast path: stop the bleeding (5 minutes)
Do this before you “optimize” anything.
- Grab one example record
Pick a single record that duplicated or failed (get the GUID if you can). - Open the most recent run for that record
Find the first action that writes data:- Create a new row
- Update a row
- Anything that changes Dataverse
- Answer these 3 questions
- Did the flow run when it shouldn’t have? (trigger too broad)
- Did it create something that already existed? (no idempotency)
- Did it update the trigger record / trigger fields? (loop)
- Apply one emergency patch
- Add a trigger condition to block noise
- Add an exists check before create
- Add update-only-when-changed before update
- Add a processed flag if you need a quick “do this once” gate
- Re-test on ONE record
Test small first. Always.
The 5 rules of “safe” Dataverse flows
These are the rules that keep Prod boring (a compliment).
Rule 1 — Triggers should be picky
If it fires on everything, it will eventually fire on something you didn’t mean.
- Use Change type intentionally
- Use Trigger columns (only the fields that matter)
- Use Trigger conditions to block runs early
Rule 2 — Every create needs an “already exists” plan
Duplicate prevention is not optional.
Rule 3 — Every update needs “only if changed”
Updating the same value repeatedly causes loops and wasted runs.
Rule 4 — Flows must be re-run safe
If you re-run a flow on the same record and it creates a mess, it isn’t production-ready.
Rule 5 — Nothing environment-specific is hardcoded
URLs, IDs, Teams/Channels, mailbox IDs, “magic GUIDs” → environment variables.
Consultant law #12: If you can’t safely re-run it, you can’t safely support it.
Mini recipe 1: Create-if-missing
Use this when your flow creates related records (tasks, child rows, invoices, etc.).
What this solves
- Duplicate related records
- “It worked once, then multiplied”
Pattern
- List rows (search for the related record using a stable unique key)
Examples of stable keys:- a true External ID
- parent record + type (e.g., “Case + ‘Initial Review’ task”)
- a composite of fields that should only exist once
- If found → update or do nothing
- If not found → create
Copy/paste logic
Condition to check if any rows exist:
@greater(length(outputs('List_rows')?['body/value']), 0)
Example filter query idea (replace placeholders):
<parentLookupField> eq @{triggerBody()?['<parentRowIdField>']} and <typeField> eq '<TypeValue>'
If your uniqueness isn’t clear, stop and define it first — otherwise you’re “creating duplicates with confidence.”
Mini recipe 2: Update-only-when-changed
Use this when you sync fields, copy values, or set statuses.
What this solves
- Trigger loops
- Wasted updates (runs where nothing actually changed)
- “Why is this flow running constantly?”
Pattern
- Compute the value you intend to set
- Compare it to the current value
- Only update when different
Copy/paste logic
Condition before an Update row:
@not(equals(triggerBody()?['<fieldToSet>'], variables('<newValue>')))
If you’re building the new value inline (no variable), compare against the expression directly.
Do not be a hero: “I’ll just update it every time” becomes your next incident.
Mini recipe 3: Processed flag + trigger condition
Use this when something should happen once per record (or once per stage).
What this solves
- “This should only run one time”
- Backfill safety (you can mark what’s already processed)
- Loop prevention (when a flow updates the trigger row)
Pattern
- Add a boolean or date field, e.g.:
new_processed(Yes/No), ornew_processedon(Date/Time)
- Trigger condition: only run when NOT processed
- At the end, set
processed = true(orprocessedon = utcNow)
Copy/paste trigger condition (generic)
@and( equals(triggerBody()?['<statusField>'], <StatusNumericValue>), not(equals(triggerBody()?['<processedFlagField>'], true)) )
Gotcha: If you forget to set the processed flag at the end… the flow will politely keep “processing” forever. Ask me how I know.
Choices + lookups cheat sheet
These two cause 80% of Dataverse flow misery.
Choices (option sets)
- Compare using numeric values, not labels.
- Labels change. Values don’t.
If your condition says:
- ✅
equals(Status, 923170002) - ❌
equals(Status, "Approved")
Lookups
- Use the Row ID (GUID) for updates/relationships.
- Don’t accidentally use the display name (it looks right, then fails hard).
If you see errors like:
- “Invalid reference link”
- “Expecting a reference link of the form /entityset(key)”
…it’s usually a formatting problem, not a missing record.
Common gotchas (ask me how I know)
- Trigger is too broad (runs for changes you don’t care about)
- Create row with no exists check (duplicates on re-run)
- Update row without “only if changed” (loop storms)
- Choice fields compared by label, not numeric value
- Lookup value is the display name, not the GUID
- Prod connection tied to a person’s account (PTO = outage)
- Hardcoded GUIDs/URLs (Dev works, Prod cries)
Definition of Done
Your flow is “Prod-safe” when:
- ☐ Re-run safe: reprocessing the same record doesn’t create duplicates
- ☐ No loops: it won’t retrigger itself endlessly
- ☐ Trigger scoped: it fires only for meaningful changes
- ☐ Writes are conditional: create/update only when needed
- ☐ Errors are actionable: failures include record ID + step name
- ☐ Deployable: solution + connection references + environment vars
- ☐ Tested realistically: enough data to hit pagination/concurrency reality
Future You will thank you.
Printable checklist
Copy/paste this into your notes, PR description, or UAT script.
Flow Pre-Flight: “Don’t Break Prod”
Trigger
- ☐ Correct change type (Added/Modified/Deleted)
- ☐ Trigger columns set (only what matters)
- ☐ Trigger conditions added (block noise early)
Safety
- ☐ Create row steps have an exists check or upsert strategy
- ☐ Update row steps run only when the value changes
- ☐ Loop prevention plan exists (filter columns OR processed flag)
Data types
- ☐ Choices compared using numeric values (not labels)
- ☐ Lookups use GUIDs (not display names)
- ☐ Null handling is explicit in filters/conditions
Operations
- ☐ Concurrency set intentionally (start with 1 if shared records)
- ☐ Pagination enabled for List Rows if volume is high
- ☐ Errors handled (Try/Catch scope or at least clear notifications)
- ☐ Run output includes record IDs (debuggable)
ALM
- ☐ Flow is in a solution
- ☐ Connection references used
- ☐ Environment variables used for URLs/IDs/magic values
- ☐ Prod connection not tied to a single human login