A PATCH to a row’s ID will create it, or quietly update whatever is already there. One header decides which.
In the app I work on, categories keep the same row ID in every environment. I recently needed to add a new category across three Dataverse environments, and it had to follow that convention.
My familiar option was the Configuration Migration Tool. Claude proposed another approach: use PowerShell to call the Dataverse Web API and create the row with a specific GUID. For one row, that was more direct, with less setup than the migration workflow I would otherwise have used.
I hadn’t considered PowerShell for this kind of Dataverse work. Before running the script, I asked Claude to walk me through what each line meant, and I asked about security and whether the approach was appropriate for the task. I had already created the category in Dev through the Dataverse interface, which assigned its GUID. The script’s job was to create the same row, with that same GUID, in Staging and Production.
A PATCH to an ID is an upsert
The script authenticated through Azure CLI, created the row at that GUID, and retrieved it afterward. To create the row, it didn’t send a POST. It sent a PATCH to a URL that already contained the ID:
PATCH [Organization URI]/api/data/v9.2/<entity set>(<GUID from Dev>)
In the Dataverse Web API, that request is an upsert. If no row exists with that ID, Dataverse creates one. If a row already exists, Dataverse updates it with the values in the request, and the response looks the same either way.[1]
That second case was the one worth understanding. If the GUID in the script were wrong, say, copied from an existing category by mistake, the request meant to add a new category would instead overwrite that one, and still report success.
The header that makes it create-only
Inside the script’s request headers was this entry:
'If-None-Match' = '*'
This header restricts the request to creation. If a row already exists at that ID, Dataverse rejects the request with 412 Precondition Failed rather than updating it.[2] Its counterpart, If-Match: *, does the reverse: it updates an existing row but never creates one.[2]
That addressed a specific concern, but I didn’t treat it as my only safeguard. Before running the script, I confirmed that no row with that GUID existed in Staging or Production, so I knew the request would take the create path. The header also didn’t establish that the environment or supplied values were correct, so I checked both before each run.
After working through the script, I asked how to enter it in PowerShell and ran it myself in Staging and then Production. Afterward, I compared the row the script retrieved with what I intended, and confirmed the new category appeared in the app.
When to reach for it
Microsoft recommends letting the system assign GUIDs in normal cases, because system-generated keys are optimized for the index; upsert is the way to create a row with a specific ID when you have a reason to.[1] Here the reason was a convention: the same category, with the same ID, in every environment.
I came away with the category added and a clearer sense of where PowerShell fits in my Dataverse work. The next time I see a PATCH to a specific ID, I’ll check which of those headers it carries before I run it.
References
[1] Microsoft Learn: Update and delete table rows using the Web API — Upsert a table row.
[2] Microsoft Learn: Perform conditional operations using the Web API — Limit upsert operations.