7 min readRishi

Get a Row by ID: Missing Fails the Run, Empty List Rows Does Not

In this series: Power Automate gotchas

Get a Row by ID: Missing Fails the Run, Empty List Rows Does Not

The warehouse posted customer US-001. The flow took first() of yesterday's List rows, stuffed that GUID into Get a row by ID, and turned red. The account was never in this environment. Get a row did what it says: it asked Dataverse for one primary key and the key was not there. The "create the account, then the order" branch never ran, because a failed action stops the path. List rows with zero matches would have succeeded with an empty value array.

That is the whole trap. Get a row is not find. It is a primary-key read that must hit.

Two actions, two meanings of "nothing"

Get a row by ID needs a table and a Row ID. Row ID is the GUID (accountid, contactid, the primary key). An account number, an email, or a name is the wrong field. If that GUID does not exist, Dataverse answers not found and the connector marks the action Failed. The run is failed unless you attached a run-after path that expected failure.

List rows is a query. Filter rows is OData evaluated by Dataverse, the same idea as Filter rows on the trigger. No matches is still a successful call. The body is an array named value with length zero.

Get a row by ID
  Table name  Accounts
  Row ID      00000000-0000-0000-0000-000000000001
→ Failed. There is no row with that id.

List rows
  Table name   Accounts
  Filter rows  accountnumber eq 'US-001'
→ Succeeded. body/value is [].

empty(outputs('List_rows')?['body/value']) is true in the second case. A Condition can take If yes and create. Get a row gives you nothing to Condition on: the action already failed.

first() does not save you. first of an empty array is empty. Feed that empty value into Get a row and you fail one step later, with a worse message, because now the Row ID was blank rather than a GUID you could search for.

SharePoint is the same shape. Get item wants the list item id. Get items with a Filter Query that matches nothing succeeds with an empty array. Do not copy a title into Get item's Id field.

Look up with List rows, then branch

When the caller has a business key — account number, email, external id — List rows is the lookup. You already have the row in first(outputs('List_rows')?['body/value']) when the array has one element. A second Get a row is a round trip you do not need unless you must change Select columns or Expand Query after you know the id.

List rows
  Table name   Accounts
  Filter rows  accountnumber eq '@{triggerBody()?['accountNumber']}'
  Row count    2

Condition: empty(outputs('List_rows')?['body/value'])
  If yes: Add a new row  (Accounts)
          then use that accountid
  If no:  Condition: length(outputs('List_rows')?['body/value']) is 1
            If yes: use first(... )?['accountid']
            If no:  Terminate  Status = Failed
                    Message  = duplicate accountnumber

Row count 2 is deliberate. You need to know whether the business key is unique. first() of two rows is an arbitrary row. If account number is supposed to be unique, make it an alternate key and fail closed when List rows returns more than one.

Filter rows is an OData string, not a workflow expression. statuscode eq 1 is right. @equals(item()?['statuscode'], 1) is Filter array syntax and does not belong in this box. Interpolate the incoming value into the string, as in the example. If that value can contain a quote, sanitize it before you concatenate; a broken OData filter is a failed List rows, which is a different failure than empty.

When Get a row is the right call

You already hold a GUID from this run: the trigger body's accountid, an outputs('Add_a_new_row')?['body/accountid'], a lookup column's _parentid_value. Primary key, one row, Get a row. It is cheaper than List rows and it does not pretend the row might be missing.

Use Select columns on that Get. It is a payload trim — logical names, comma-separated — not the trigger setting that decides whether the flow starts. Expand Query belongs here when you need a related contact in the same call. If the GUID came from List rows, set Expand Query on List rows instead and skip Get.

A lookup column that is empty is not a GUID. triggerBody()?['_parentid_value'] can be empty. Passing it to Get a row fails the run. Check empty first, then Get.

Condition: empty(triggerBody()?['_parentid_value'])
  If yes: the parent was not set; do not Get
  If no:  Get a row by ID  Row ID = triggerBody()?['_parentid_value']

Failed Get is not a clean "else"

You can put Add a new row after Get a row with Configure run after = has failed. That looks like find-or-create. It is not. Failed also means throttled, forbidden, a deleted table name, a bad GUID format, or a transient 5xx. Your create path then runs because the read was unauthorized, and you invent a duplicate under a caller who should have been rejected.

List rows plus empty is a successful query with a boolean. Keep Get a row's failed path for unexpected errors, inside a scope, not as the "row does not exist" branch.

Retries do not create the row. A not-found is not a blip. Raising the retry count on Get a row only delays the red run. Waiting for another process to insert the row is a Do until on List rows (or a trigger on that table), not retry settings. Those retries are for transient connector failures.

Empty list versus a null list

List rows that ran and matched nothing: value is []. Apply to each over that array succeeds with zero iterations. That can hide a missing customer if the next action after the loop is "send confirmation" — the same class of quiet success as a Do until that hit count. Test empty before the loop when zero rows is a business problem.

List rows that never ran — skipped Condition, failed previous action — has no value. Apply to each over a null input fails. coalesce(outputs('List_rows')?['body/value'], json('[]')) makes the loop see an array. It does not decide whether zero rows is acceptable. That is still your Condition.

? on the path keeps a missing property from exploding the expression. It does not turn a failed Get a row into a blank. The action status is already Failed. Safe navigation is for optional fields on a payload you have, not for a primary-key miss.

What to put in the Row ID box

  • A GUID this run already owns: trigger, a create output, a lookup _…_value you checked is not empty.
  • Not an account number, email, or name. Those go in List rows Filter rows, or in an alternate-key upsert.
  • Not first(...) of a list you have not proven has length one.
  • Not a GUID copied from a maker environment into production. Ids are per organization.

Get a row by ID is the right tool when the primary key is known and the row is supposed to exist. Missing then should fail the run. When the caller has a business key, and missing is a normal branch, List rows is the lookup: success with an empty array, a Condition on empty, and a create or a terminate you chose on purpose.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

  • No comments yet. Be the first.