DEV Community

Cover image for Why Your D365 F&O Automation Breaks: The Hidden Logic Behind TargetId and RootId
Harsh Sengar
Harsh Sengar

Posted on AI-assisted

Why Your D365 F&O Automation Breaks: The Hidden Logic Behind TargetId and RootId

Why Your D365 F&O Automation Breaks: The Hidden Logic Behind TargetId and RootId

If you've ever tried to automate, load-test, or replay actions in Dynamics 365 Finance & Operations — whether through Postman, JMeter, k6, Gatling, an RPA tool, or a custom script — you've probably hit this wall: a request that worked perfectly in one session mysteriously fails in the next, with no obvious change to your payload.

This isn't specific to any one tool. It comes down to how D365 F&O identifies UI controls internally, and it'll bite you the same way regardless of what's sending the request.

The usual suspect turns out to be two fields buried in the interaction payload: TargetId and RootId.

The Symptom

D365 F&O interaction requests look something like this:

{
"CommandName": "Click",
"RootId": "2273_0",
"TargetId": "2437_0",
"ThrottleId": "2273_0_TG"
}

These "<number>_<number>" style values look like they should be static, predictable identifiers. They aren't. Reuse one from a previous session, and some actions silently fail or get rejected — even though nothing else in your request changed.

Rejection

OR

Silent Fail

Why This Happens

TargetId and RootId aren't computed by a fixed formula. They're assigned by the server every time the form is loaded or rebuilt, and cached on the page as HTML attributes on each control's DOM element:

  • data-dyn-serverid → maps to TargetId
  • data-dyn-rootserverid → maps to RootId

Because these are re-issued per session/render, a value captured today may already be invalid tomorrow — or even after a simple page refresh.

The Fix: Resolve IDs at Runtime, Not Hardcode Them

The most reliable way to get these values is to inspect the specific element first, then query it directly by its id:

  1. Right-click the control in the browser → Inspect.
  2. Note its id attribute (e.g. LedgerJournalTransDaily_4_PostJournal). The numeric part in the middle (_4_) is session-specific and will differ next time — that's expected.
  3. In the console, query that exact element and read its attributes:

document.querySelector('#LedgerJournalTransDaily_4_PostJournal').getAttribute('data-dyn-serverid') // TargetId
document.querySelector('#LedgerJournalTransDaily_4_PostJournal').getAttribute('data-dyn-rootserverid') // RootId

You might be tempted to skip the manual inspect step and match by the control name alone (the part of the id after the session-specific number, e.g. PostJournal), since that name is stable across sessions. In practice, this is less reliable than it looks — the same control name can appear more than once in a form (for example, a duplicate rendered into an overflow menu, or a hidden/inactive clone), and a generic lookup can silently grab the wrong instance and return an id that looks valid but isn't the one you clicked. Inspecting the element directly guarantees you're reading attributes off the exact control you care about.

If you do want to automate this lookup rather than doing it manually each time, scope your selector to a container you know holds only one live instance of that control, and verify the result against what you see in the Network tab before trusting it.

Run this right before building your request, in the same session/browser context you'll be sending it from, and use the values immediately.

This inspection step is tool-agnostic by nature — it's just browser DevTools. However you plan to replay the request afterward — Postman, a load-testing tool, an RPA script, or anything else — the values need to come from this lookup each time, not from a saved or hardcoded payload.

The Nuance Most People Miss: Not All Actions Are Equally Strict

Here's the part that surprised me. I tested this across several actions sitting in the same toolbar — including Save and Post on a journal form.

  • Save tolerated a stale TargetId. Even with an outdated id, the save request still succeeded.
  • Post did not. A stale TargetId on a Post request was rejected outright.

The two buttons live in the exact same part of the UI and are dispatched through the identical client-side mechanism — same CommandName + TargetId + RootId structure, no special-casing in the client code for either one. So the difference isn't client-side at all — it's how strictly the server validates the control context before executing each type of action.

The pattern: operations that directly mutate/commit data — like Post, which triggers real business logic with lasting side effects — require the server to resolve TargetId to a currently live control before it will execute. If it can't, it rejects the request rather than guessing. Generic operations like Save don't need that same strict resolution — they just persist the form's current state, regardless of exactly which control instance triggered it.

Takeaway for Anyone Automating or Load-Testing D365 F&O

  • Never hardcode TargetId / RootId, no matter which tool is sending the request. Resolve them at runtime, every session.
  • Use data-dyn-controlname as your stable anchor, not the DOM id.
  • Be extra disciplined about refreshing these values before any data-mutating action (Post, Confirm, Invoice, etc.) — that's where stale ids will actually break your requests, not just silently get ignored.

Whether you're building a Postman collection, a JMeter/k6/Gatling load test, an RPA workflow, or a custom script against D365 F&O, baking this lookup into your setup step will save you a lot of "why did this randomly stop working" debugging later.

Top comments (0)