PathixDataverse Forensics

← Guides

Guide · Dependencies

How to find every writer of a Dataverse field, by hand

No tooling required beyond a solution export and a text editor. This is the full method, worked against a real export, along with the two places it stops working.

Updated 26 July 202612 min read

The platform will not give you this list. Dependency tracking covers the surfaces that declare their reference in metadata, and misses the ones that keep it inside compiled code, a script file, or a JSON document, which happens to be most of the things that write values. We wrote about why that gap exists separately.

You can still get the answer. It takes an export, a text editor, and a couple of hours per column on a mature environment. Everything below is worked against two real managed-solution exports, deliberately built messy for testing, so the fragments are the actual shapes rather than cleaned-up illustrations.

We will chase one column the whole way through: account.creditlimit.

Start with the export

Export the unmanaged solution containing the components you care about, or a solution you create for the purpose with everything added to it. Rename the .zip and unpack it. You get something like this:

solution.zip
customizations.xml
solution.xml
Workflows/
  Account-OnCreate-SetFields-0F3C75DE-....xaml
  Account-OnUpdate-Random-D1E0FD24-....xaml
  AccountDialog-CCD220BB-....xaml
  ActionTest-4AAFFEED-....xaml
  Account-OnCreateUpdate-665E34F8-....json
CanvasApps/
  cr1ff_creditlimittestapp_e5e04_DocumentUri.msapp
PluginAssemblies/
WebResources/

Everything you can find by hand is in there. So is the first surprise.

What the solution declares about your column

Open customizations.xml and look for the entity definitions. In both of the exports used here, the element is present and empty:

customizations.xml
<Entities></Entities>

Between them those two solutions contain five components that write account.creditlimit. The solution metadata records none of them at column level. The canvas app comes closest, and what it records is the table:

customizations.xml · CanvasApp
<CdsDependencies>{"cdsdependencies":[
  {"logicalname":"account","componenttype":1}
]}</CdsDependencies>

The modern flow does not manage even that. Its entry carries <PrimaryEntity>none</PrimaryEntity> while its definition updates the account table.

Five writers of one column, and the solution's own metadata names the column zero times.

Which is why the rest of this is a reading exercise. The references are all there. They are inside the component definitions, in four different formats, and you have to go get them.

Classic workflows, dialogs and actions

These are the .xaml files. All three are Windows Workflow Foundation documents and, usefully, all three write columns through the same activity, so one search covers the set. Look for SetEntityProperty:

Workflows/Account-OnUpdate-Random-D1E0FD24-....xaml
<mxswa:SetEntityProperty Attribute="creditlimit"
                         Entity="[CreatedEntities(&quot;primaryEntity#Temp&quot;)]"
                         EntityName="account"
                         Value="[UpdateStep1_1]">
  <mxswa:SetEntityProperty.TargetType>
    <InArgument x:TypeArguments="s:Type">
      <mxswa:ReferenceLiteral x:TypeArguments="s:Type" Value="mxs:Money" />
    </InArgument>
  </mxswa:SetEntityProperty.TargetType>
</mxswa:SetEntityProperty>

<mxswa:UpdateEntity DisplayName="UpdateStep1"
                    Entity="[CreatedEntities(&quot;primaryEntity#Temp&quot;)]"
                    EntityName="account" />

The column is in Attribute, the table is in EntityName, and the UpdateEntity activity below is what commits the change. Grepping every .xaml in the folder for Attribute="creditlimit" is a genuinely reliable way to find classic-workflow writers.

Two things that will slow you down

The value is not next to the attribute. Value="[UpdateStep1_1]" is a variable, and the literal lives in an EvaluateExpression block further up the sequence:

the same file, a few lines earlier
<mxswa:ActivityReference
    AssemblyQualifiedName="Microsoft.Crm.Workflow.Activities.EvaluateExpression, ..."
    DisplayName="EvaluateExpression">
  <mxswa:ActivityReference.Arguments>
    <InArgument x:Key="ExpressionOperator">CreateCrmType</InArgument>
    <InArgument x:Key="Parameters">[New Object() {
        Microsoft.Xrm.Sdk.Workflow.WorkflowPropertyType.Money, "5" }]</InArgument>
    <OutArgument x:Key="Result">[UpdateStep1_1]</OutArgument>
  </mxswa:ActivityReference.Arguments>
</mxswa:ActivityReference>

So finding that the column is written takes one search. Finding what gets written means tracing the variable back through the sequence, and a workflow with a dozen steps has a dozen of these.

The file also will not tell you which workflow it is. The class name is the GUID with the hyphens stripped:

x:Class="XrmWorkflow0f3c75deb8c6465784989d43f18cd5d4"

Human-readable names live back in customizations.xml, keyed by the same GUID, along with the Category that tells you what kind of thing you are reading. Category 0 is a classic workflow, 1 a dialog, 2 a business rule, 3 an action. In the test export, three different categories all wrote creditlimit through identical markup.

Modern cloud flows

Modern flows also sit in Workflows/, as .json rather than .xaml. The column reference shows up as a parameter key on a Dataverse connector action:

Workflows/Account-OnCreateUpdate-665E34F8-....json
"triggers": {
  "When_a_row_is_added,_modified_or_deleted": {
    "type": "OpenApiConnectionWebhook",
    "inputs": {
      "parameters": {
        "subscriptionRequest/message": 1,
        "subscriptionRequest/entityname": "account",
        "subscriptionRequest/scope": 4
      }
    }
  }
},
"actions": {
  "Update_a_row": {
    "type": "OpenApiConnection",
    "inputs": {
      "host": { "operationId": "UpdateOnlyRecord" },
      "parameters": {
        "entityName": "accounts",
        "recordId": "@triggerOutputs()?['body/accountid']",
        "item/creditlimit": 4,
        "item/tickersymbol": "Ticker"
      }
    }
  }
}

item/creditlimit is your search term, and the pattern generalises: item/<logicalname> in the parameters object of an OpenApiConnection action is a write.

THE NAMING TRAP

The action says entityName: "accounts", plural, because connector actions take the entity set name. The trigger three lines above says entityname: "account", singular, because it takes the logical name. Search for one spelling and you will silently miss half the references in the same file.

Two other things worth noticing here. The trigger's message and scope are numeric codes, so no search for the word “update” will find them. And this trigger has no filteringattributes parameter at all, which means it fires on every column change on the table rather than on the ones the flow cares about.

Canvas apps

The .msapp in CanvasApps/ is itself a zip. Unpack it and the readable source is under Src/ as YAML, one file per screen:

Src/Accounts screen.pa.yaml
- DataCard7:
    Control: TypedDataCard
    Properties:
      DataField: ="creditlimit"
      DisplayName: =DataSourceInfo([@Accounts],DataSourceInfo.DisplayName,'creditlimit')
      Update: =Value(DataCardValue7.Text)

Form controls bind through DataField, so that is the search. Writes that do not go through a form control show up as PowerFx instead, in which case look for Patch, Update, UpdateIf, Collect, Remove, and SubmitForm. In the test app the submit path is a plain SubmitForm(Form1), which commits every bound card at once, so the binding is the only place the column name appears.

Remember what the solution recorded for this app: the table, and no column. A canvas app that writes one column and one that writes twenty are indistinguishable from outside the .msapp.

Plugin steps

Registrations are straightforward. Open the Plugin Registration Tool, or query the sdkmessageprocessingstep table, and list every step bound to your table. That gives you which plugins run on which message, at which stage, with which filtering attributes.

Filtering attributes are worth reading closely, because a step registered with creditlimit in its filter is reacting to the column, which is not the same as writing it. Both matter for impact analysis, and they answer different questions.

Which columns the plugin actually writes is a different problem. That lives in the compiled assembly. If you have the source, search for the usual shapes:

  • entity["creditlimit"] = value and SetAttributeValue("creditlimit", value) for late-bound code
  • account.CreditLimit = value for early-bound code, which means the column name only appears in the generated typed-entity class rather than in the plugin
  • IOrganizationService.Update and Create calls, to confirm the write is committed rather than staged

Without the source, see the next section.

Form scripts and web resources

Script web resources are in WebResources/ as plain files, so this part is the easy one. Search them for the logical name and for the accessor shapes around it: getAttribute("creditlimit"), a .setValue( on the result, and any Xrm.WebApi call carrying the column in its payload.

HTML web resources need one extra pass, since the script is embedded in the markup rather than sitting in its own file.

Cross-reference what you find against the form definitions, because a script only runs where it is registered as an event handler. A web resource nobody has wired to a form is not a writer.

Where the method stops

Two gaps, and neither closes with more effort.

  • Compiled assemblies with no source. If the plugin came from an ISV or a contractor who left, the IL is in the export and the write is in the IL, but not in a form you can read or search. You know a plugin runs on the table. You cannot tell which columns it sets.
  • Late-bound writes where the column name is assembled at runtime. A plugin doing entity[configuredFieldName] = value has no column literal anywhere in the file. Nothing resolves that statically, and any tool claiming to is guessing.

The honest output of this exercise is a list plus a caveat: here are the writers I found, and here are the components I could not read. Absence of a hit is a candidate to verify, not an all-clear.

What it costs

For one column on a small solution, an hour or two. On a mature environment with a few hundred components across five formats, most of a day, and you will do it again next month because the export is a snapshot. Nothing about the method scales; it just works.

The part worth internalising is the reason it is slow. There is no single search that answers the question, because the same fact is written five different ways in five different file formats, and one of them is not text.

Why the platform will not just tell you this →

More guides →

Pathix answers this across the whole environment.

It parses the surfaces the platform stores but does not read: compiled plugin IL, classic-workflow XAML, flow definition JSON, form scripts, canvas PowerFx. Every edge records how it was resolved, and the writes it could not resolve get reported rather than quietly dropped. Self-hosted in your own Azure, read-only, metadata-only.

See it on sample data →How the scan works →
Pathix

Forensics for Dynamics 365 and the Dataverse.

See it on sample data →
USE CASES
CAPABILITIES
  • What we check
  • Dataverse MCPsoon
  • Dependency analysissoon
  • Migration impactsoon
PRODUCT
COMPANY
© 2026 Pathix · self-hosted · metadata-onlyNot affiliated with Microsoft. Dynamics 365, Dataverse, and Power Platform are trademarks of Microsoft Corporation.