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.
OR
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 toTargetId -
data-dyn-rootserverid→ maps toRootId
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:
- Right-click the control in the browser → Inspect.
- Note its
idattribute (e.g.LedgerJournalTransDaily_4_PostJournal). The numeric part in the middle (_4_) is session-specific and will differ next time — that's expected. - 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.
-
Savetolerated a staleTargetId. Even with an outdated id, the save request still succeeded. -
Postdid not. A staleTargetIdon 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-controlnameas your stable anchor, not the DOMid. - 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 (2)
I really liked how this explains a failure mode that can otherwise feel completely random.
The key insight is that TargetId and RootId are not stable identifiers at all. They belong to the current server-rendered control state, so reusing them across sessions or renders is fundamentally unreliable.
The most interesting part for me was the difference between actions like Save and Post. Even though they travel through the same client-side interaction mechanism, the server can validate their control context differently depending on the operation being performed.
That makes the issue much easier to reason about: the automation is not “randomly breaking”, it is replaying identifiers that no longer belong to the live UI state.
I also liked that the solution stays tool-agnostic. Whether the request eventually comes from Postman, JMeter, k6, RPA, or custom code, the important part is resolving the control metadata from the active session instead of treating captured values as permanent.
Congratulations on making it into the Hall of Fame, very well deserved. 🎉 And if you ever feel like stopping by my corner of DEV, you're always welc
Harsh, the Save/Post comparison is the detail that makes this useful. A replay working for one action could easily convince someone that the captured IDs are reusable, when your test shows that conclusion doesn't hold for the next action.
The warning about duplicate or hidden controls matters too: finding an ID isn't enough unless it belongs to the live control you intended. For a regression check, I'd want to reload the form, resolve the IDs again, and verify the resulting business state rather than just the request status. Have you found form rebuilds within the same session to be a common source of stale IDs as well?
I write about uncovering hidden logic in enterprise data systems, mainly SAP HANA and Snowflake, on my DEV profile. Different stack, but a related interest in explaining why systems behave the way they do. You're welcome to stop by and share your perspective.