Skip to content
Back to guides

Flows That Don’t Break Prod

Duplicates, loops, “works in Dev but fails in Prod,” and the dreaded lookup/choice errors — this is the Power Automate survival guide for Dataverse. You’ll get a 5-minute “stop the bleeding” checklist plus three copy/paste recipes that make flows re-run safe and production-proof.

February 16, 20267
Flows That Don’t Break Prod

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.

  1. Grab one example record
    Pick a single record that duplicated or failed (get the GUID if you can).
  2. 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
  3. 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)
  4. 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
  5. 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

  1. 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
  2. If found → update or do nothing
  3. 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

  1. Compute the value you intend to set
  2. Compare it to the current value
  3. 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

  1. Add a boolean or date field, e.g.:
    • new_processed (Yes/No), or
    • new_processedon (Date/Time)
  2. Trigger condition: only run when NOT processed
  3. At the end, set processed = true (or processedon = 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
#power-automate#tips#troubleshooting