---
name: build-web-resources
description: Creates, updates, debugs, and troubleshoots Dataverse web resources (JS, HTML, CSS, images) via the Dataverse MCP. Use when creating new web resources, editing existing form scripts, fixing Client API errors, adding event handlers, building ribbon/command bar buttons, or troubleshooting form script issues. Covers formContext patterns, Xrm namespace reference, namespace conventions, null-checking rules, event handler best practices, common error solutions, and MCP registration tools.
argument-hint: "[environment-url] [solution-name] [optional: --table TABLE] [optional: --form FORM]"
---

# Build Dataverse Web Resources

Create, edit, and configure web resources for model-driven apps using the Dataverse MCP tools. This skill ensures correct Client API patterns, proper metadata verification, and reliable form/ribbon integration.

## Arguments

- `$ARGUMENTS[0]` — Dataverse environment URL (e.g., `https://org209ecb48.crm.dynamics.com`)
- `$ARGUMENTS[1]` — Solution unique name to add web resources to
- `--table TABLE` — Target table logical name (for form-bound scripts)
- `--form FORM` — Target form name or ID (for event handler registration)

---

## CRITICAL RULES

Before writing ANY web resource code:

1. **ALWAYS verify metadata first** — query actual table/field/form schema before referencing fields, relationships, or option set values in code. Never assume a field exists or guess its logical name.
2. **NEVER use deprecated APIs** — `Xrm.Page` is deprecated since v9.0. Always use `formContext` from `executionContext.getFormContext()`.
3. **NEVER put executable code in global scope** — only namespace declarations. All logic must be inside registered event handler functions.
4. **ALWAYS use namespaces** — every JS web resource must use a namespace pattern to prevent function collisions.
5. **ALWAYS null-check** `getAttribute()` and `getControl()` — fields may not be on the current form.

---

## Web Resource Types

| Type | Code | Extensions | MIME Type | Use For |
|------|------|------------|-----------|---------|
| HTML | 1 | .htm, .html | text/html | Custom UI pages embedded in forms, standalone utilities |
| CSS | 2 | .css | text/css | Styling for HTML web resources |
| **JS** | **3** | **.js** | **application/x-javascript** | **Form scripts, ribbon handlers, business logic (most common)** |
| XML | 4 | .xml | text/xml | Configuration data, custom data files |
| PNG | 5 | .png | image/png | Icons, images |
| JPG | 6 | .jpg | image/jpeg | Photos, images |
| GIF | 7 | .gif | image/gif | Animated images |
| XAP | 8 | .xap | application/x-silverlight-app | DEPRECATED — do not use |
| XSL | 9 | .xsl | text/xsl | XSLT transforms |
| ICO | 10 | .ico | image/x-icon | Favicons |
| SVG | 11 | .svg | image/svg+xml | Scalable icons (preferred over raster) |
| RESX | 12 | .resx | text/xml | Localization strings (accessed via `Xrm.Utility.getResourceString()`) |

**Default choice for scripting: JS (type 3).** Use HTML (type 1) only for custom embedded pages. Use SVG (type 11) for icons.

---

## Step 1: Verify Metadata Before Coding

Before writing any web resource code that references Dataverse entities, ALWAYS query the actual schema:

### Verify table exists and get its schema name
```
dataverse_get_table(table_name: "account")
```

### Verify fields exist on the table
```
dataverse_list_columns(table_name: "account", column_filter: "fieldname")
```
Returns: logical name, display name, type, required level, option set values.

### Verify relationships
```
dataverse_list_relationships(table_name: "account")
```
Returns: relationship names, related tables, lookup field names.

### Verify the target form exists and get its ID
```
dataverse_list_forms(table_name: "account", type_filter: "main")
```
Returns: form ID (GUID), name, type, state.

### Verify fields are on the specific form
```
dataverse_get_form(form_id: "{guid}")
```
Returns: form XML with all fields, tabs, sections. Check that any field you reference in code actually exists on this form.

### Check existing web resources and events on the form
```
dataverse_list_form_events(form_id: "{guid}")
```
Returns: registered libraries and event handlers (OnLoad, OnSave, OnChange with field names).

**Why this matters:** `formContext.getAttribute("fieldname")` returns `null` if the field is not on the form. This is the #1 cause of "Cannot read property of null" runtime errors.

---

## Step 2: Write the JavaScript Code

### Required Pattern: Namespace + formContext

Every JS web resource MUST follow this structure:

```javascript
// Namespace declaration (ONLY thing at global scope)
var PublisherPrefix_TableName = window.PublisherPrefix_TableName || {};

(function () {
    "use strict";

    /**
     * Form OnLoad handler
     * @param {Object} executionContext - Execution context (auto-passed when registered)
     */
    this.formOnLoad = function (executionContext) {
        var formContext = executionContext.getFormContext();

        // Always null-check before accessing fields
        var nameAttr = formContext.getAttribute("name");
        if (nameAttr) {
            var value = nameAttr.getValue();
            // ... logic here
        }
    };

    /**
     * Field OnChange handler
     * @param {Object} executionContext - Execution context
     */
    this.fieldOnChange = function (executionContext) {
        var formContext = executionContext.getFormContext();
        var changedAttribute = executionContext.getEventSource();
        // ... logic here
    };

    /**
     * Form OnSave handler
     * @param {Object} executionContext - Execution context
     */
    this.formOnSave = function (executionContext) {
        var formContext = executionContext.getFormContext();
        var saveMode = executionContext.getEventArgs().getSaveMode();
        // To cancel save: executionContext.getEventArgs().preventDefault();
    };

    /**
     * Ribbon command handler
     * @param {Object} primaryControl - The formContext (passed via CrmParameter)
     */
    this.ribbonAction = function (primaryControl) {
        var formContext = primaryControl; // In ribbon commands, primaryControl IS formContext
        // ... logic here
    };

}).call(PublisherPrefix_TableName);
```

### Naming Convention

Web resource names MUST include the publisher prefix and use `/` for folder structure:
```
{prefix}_/scripts/{table}_{purpose}.js
```
Examples:
- `cr1a2_/scripts/account_form.js`
- `cr1a2_/scripts/contact_ribbon.js`
- `cr1a2_/pages/custom_dashboard.html`
- `cr1a2_/styles/form_overrides.css`
- `cr1a2_/images/custom_icon.svg`

---

## Step 3: Client API Reference

### Getting formContext (CORRECT ways only)

| Context | How to Get formContext |
|---------|----------------------|
| Form OnLoad / OnChange / OnSave | `executionContext.getFormContext()` |
| Ribbon command (form) | `primaryControl` parameter (via `CrmParameter Value="PrimaryControl"`) |
| Ribbon command (grid) | `selectedControl` parameter (via `CrmParameter Value="SelectedControl"`) |
| HTML web resource on form | `parent.Xrm.Page` (legacy) — prefer messaging API |
| Custom pages | NOT available — use PCF/Canvas patterns |

### Xrm Namespace Reference

| Namespace | Key Methods | Notes |
|-----------|-------------|-------|
| `Xrm.Navigation` | `openForm()`, `openAlertDialog()`, `openConfirmDialog()`, `openWebResource()`, `navigateTo()` | All return Promises |
| `Xrm.WebApi` | `createRecord()`, `retrieveRecord()`, `retrieveMultipleRecords()`, `updateRecord()`, `deleteRecord()` | CRUD operations, always use `$select` |
| `Xrm.Utility` | `getGlobalContext()`, `getPageContext()`, `getEntityMetadata()`, `showProgressIndicator()`, `lookupObjects()` | Replaces deprecated `Xrm.Page.context` |
| `Xrm.App` | `addGlobalNotification()`, `clearGlobalNotification()` | App-level banners only — NO `getCurrentPage()` method |
| `Xrm.Device` | `captureImage()`, `getBarcodeValue()`, `getCurrentPosition()`, `captureAudio()`, `captureVideo()` | Mobile/device capabilities |
| `Xrm.Encoding` | `xmlAttributeEncode()`, `xmlEncode()`, `htmlAttributeEncode()`, `htmlDecode()`, `htmlEncode()` | String encoding |
| `Xrm.Panel` | `loadPanel()` | Side panel display |

### DEPRECATED APIs — NEVER USE

| Deprecated | Replacement |
|-----------|-------------|
| `Xrm.Page.getAttribute()` | `formContext.getAttribute()` |
| `Xrm.Page.getControl()` | `formContext.getControl()` |
| `Xrm.Page.ui` | `formContext.ui` |
| `Xrm.Page.data` | `formContext.data` |
| `Xrm.Page.context` | `Xrm.Utility.getGlobalContext()` |
| `Xrm.Utility.alertDialog()` | `Xrm.Navigation.openAlertDialog()` |
| `Xrm.Utility.confirmDialog()` | `Xrm.Navigation.openConfirmDialog()` |
| `Xrm.Utility.openEntityForm()` | `Xrm.Navigation.openForm()` |
| `Xrm.Utility.openQuickCreate()` | `Xrm.Navigation.openForm()` (with `useQuickCreateForm: true`) |
| `Xrm.Utility.openWebResource()` | `Xrm.Navigation.openWebResource()` |
| `Xrm.App.getCurrentPage()` | Does NOT exist. Use `Xrm.Utility.getPageContext()` |

### formContext.getAttribute() vs formContext.getControl()

These are DIFFERENT objects — do not confuse them:

**Attribute** (data layer) — `formContext.getAttribute("name")`:
- `.getValue()` / `.setValue(value)` — get/set field value
- `.getIsDirty()` — check if changed
- `.setRequiredLevel("none" | "required" | "recommended")`
- `.setSubmitMode("always" | "never" | "dirty")`
- `.addOnChange(handler)` / `.removeOnChange(handler)`
- `.controls` — collection of UI controls bound to this attribute
- `.getAttributeType()` — returns "boolean", "datetime", "decimal", "double", "integer", "lookup", "memo", "money", "multiselectoptionset", "optionset", "string"

**Control** (UI layer) — `formContext.getControl("name")`:
- `.setVisible(bool)` / `.getVisible()`
- `.setDisabled(bool)` / `.getDisabled()`
- `.setLabel(string)` / `.getLabel()`
- `.setFocus()`
- `.getAttribute()` — returns the attribute this control is bound to
- `.setNotification(message, uniqueId)` / `.clearNotification(uniqueId)` — field-level notifications
- For lookups: `.addPreSearch(handler)`, `.addCustomFilter(filter)`, `.addCustomView()`

### Xrm.WebApi CRUD Patterns

```javascript
// CREATE
Xrm.WebApi.createRecord("account", {
    name: "Contoso",
    revenue: 5000000,
    "primarycontactid@odata.bind": "/contacts(guid-here)"  // lookup binding
}).then(function(result) {
    console.log("Created: " + result.id);
});

// RETRIEVE single — ALWAYS use $select
Xrm.WebApi.retrieveRecord("account", recordId, "?$select=name,revenue")
    .then(function(result) { /* result.name, result.revenue */ });

// RETRIEVE multiple — ALWAYS use $select and $filter
Xrm.WebApi.retrieveMultipleRecords("account",
    "?$select=name,revenue&$filter=revenue gt 1000000&$top=50")
    .then(function(result) {
        result.entities.forEach(function(e) { /* process */ });
    });

// UPDATE
Xrm.WebApi.updateRecord("account", recordId, { name: "Updated" })
    .then(function(result) { /* result.id */ });

// DELETE
Xrm.WebApi.deleteRecord("account", recordId)
    .then(function(result) { /* result.id */ });
```

### Option Set Values

Option set (choice) fields use INTEGER values in code, not display labels:
```javascript
// CORRECT — use integer value
formContext.getAttribute("statuscode").setValue(100000001);

// WRONG — this does NOT work
formContext.getAttribute("statuscode").setValue("Active");
```

To get option set metadata for correct values:
```
dataverse_get_column(table_name: "account", column_name: "statuscode")
```
This returns the option set values with their integer codes. Always query these before hardcoding option values in scripts.

---

## Step 4: Create the Web Resource via MCP

### Create the JS web resource
```
dataverse_create_web_resource(
    name: "cr1a2_/scripts/account_form.js",
    display_name: "Account Form Script",
    type: "JS",
    content: "<the JavaScript code as plain text>",
    solution_name: "MySolution",
    description: "OnLoad, OnChange, OnSave handlers for Account main form"
)
```

**Content handling:** For text types (JS, HTML, CSS, XML, SVG, RESX), pass readable source code — the tool auto-encodes to base64. For binary types (PNG, JPG, GIF, ICO), pass base64-encoded content.

The tool auto-publishes after creation.

### Update existing web resource
```
dataverse_update_web_resource(
    name: "cr1a2_/scripts/account_form.js",
    content: "<updated JavaScript code>"
)
```

Always `dataverse_get_web_resource` first to read current content before modifying.

---

## Step 5: Register on Form

### Add as form library + event handlers
```
dataverse_add_form_event(
    form_id: "{form-guid}",
    event_type: "OnLoad",
    library_name: "cr1a2_/scripts/account_form.js",
    function_name: "Cr1a2_Account.formOnLoad",
    pass_execution_context: true
)
```

```
dataverse_add_form_event(
    form_id: "{form-guid}",
    event_type: "OnChange",
    library_name: "cr1a2_/scripts/account_form.js",
    function_name: "Cr1a2_Account.nameOnChange",
    field_name: "name",
    pass_execution_context: true
)
```

```
dataverse_add_form_event(
    form_id: "{form-guid}",
    event_type: "OnSave",
    library_name: "cr1a2_/scripts/account_form.js",
    function_name: "Cr1a2_Account.formOnSave",
    pass_execution_context: true
)
```

**IMPORTANT:**
- `library_name` = the web resource `name` (e.g., `cr1a2_/scripts/account_form.js`)
- `function_name` = the full namespaced function (e.g., `Cr1a2_Account.formOnLoad`)
- `pass_execution_context` should almost always be `true`
- `field_name` is REQUIRED for OnChange events — must be the field's logical name
- The tool automatically adds the library to the form's `<formLibraries>` if not already present

### Embed HTML web resource on a form
```
dataverse_add_form_web_resource(
    form_id: "{form-guid}",
    web_resource_name: "cr1a2_/pages/dashboard.html",
    tab_name: "General",
    section_name: "Dashboard",
    label: "Dashboard View",
    height: 400,
    pass_parameters: true
)
```

### Register on ribbon/command bar
```
dataverse_create_ribbon_command(
    app_id: "{app-guid}",
    table_name: "account",
    label: "Run Custom Action",
    location: "Form",
    js_library_name: "cr1a2_/scripts/account_ribbon.js",
    js_function_name: "Cr1a2_Account.ribbonAction",
    icon: "Play",
    tooltip: "Executes the custom processing action"
)
```

In ribbon commands, `primaryControl` (the formContext) is automatically passed as the first parameter.

---

## Step 6: Verify the Registration

After registering events, verify:

```
dataverse_list_form_events(form_id: "{form-guid}")
```

Check that:
- The library appears in `libraries` array
- Each handler appears in `events` array with correct `event_type`, `function_name`, `field_name`
- `pass_execution_context` is `true`
- `enabled` is `true`

---

## Common Patterns

### Show/Hide fields based on a choice value

```javascript
var MyNamespace = window.MyNamespace || {};
(function () {
    "use strict";

    this.formOnLoad = function (executionContext) {
        var formContext = executionContext.getFormContext();
        toggleFields(formContext);
    };

    this.typeOnChange = function (executionContext) {
        var formContext = executionContext.getFormContext();
        toggleFields(formContext);
    };

    function toggleFields(formContext) {
        var typeAttr = formContext.getAttribute("cr1a2_type");
        if (!typeAttr) return;

        var typeValue = typeAttr.getValue();
        var detailControl = formContext.getControl("cr1a2_details");
        if (detailControl) {
            detailControl.setVisible(typeValue === 100000001); // Show only for specific option
        }
    }
}).call(MyNamespace);
```

### Set field required based on another field

```javascript
this.statusOnChange = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var status = formContext.getAttribute("statuscode");
    var reason = formContext.getAttribute("cr1a2_reason");
    if (!status || !reason) return;

    if (status.getValue() === 100000002) { // e.g., "Rejected"
        reason.setRequiredLevel("required");
    } else {
        reason.setRequiredLevel("none");
    }
};
```

### Filter a lookup field

```javascript
this.formOnLoad = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var lookupControl = formContext.getControl("cr1a2_parentaccountid");
    if (lookupControl) {
        lookupControl.addPreSearch(function () {
            var filter = '<filter><condition attribute="statecode" operator="eq" value="0" /></filter>';
            lookupControl.addCustomFilter(filter, "account");
        });
    }
};
```

### Show form notification

```javascript
this.formOnLoad = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var formType = formContext.ui.getFormType();

    if (formType === 1) { // 1 = Create
        formContext.ui.setFormNotification(
            "Please fill in all required fields before saving.",
            "INFO",     // INFO, WARNING, or ERROR
            "createMsg"
        );
    }
};
```

### Async Web API call in OnLoad (with null-safety)

```javascript
this.formOnLoad = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var accountId = formContext.data.entity.getId().replace(/[{}]/g, "");

    // Capture values BEFORE any async call
    var nameControl = formContext.getControl("cr1a2_relatedcount");

    Xrm.WebApi.retrieveMultipleRecords("cr1a2_relatedrecord",
        "?$select=cr1a2_relatedrecordid&$filter=_cr1a2_accountid_value eq " + accountId + "&$count=true"
    ).then(function (result) {
        // formContext may be stale here — use captured references carefully
        if (nameControl) {
            nameControl.setLabel("Related Records (" + result.entities.length + ")");
        }
    }).catch(function (error) {
        console.error("Error loading related records: " + error.message);
    });
};
```

---

## Common Errors and Solutions

| Error | Cause | Fix |
|-------|-------|-----|
| `TypeError: X is not a function` | Wrong API (e.g., `Xrm.App.getCurrentPage()`) | Check API reference above — use correct method |
| `TypeError: Cannot read property 'getValue' of null` | Field not on form or typo in logical name | Verify field exists on form via `dataverse_get_form`, null-check `getAttribute()` |
| `TypeError: Cannot read property 'getFormContext' of undefined` | "Pass execution context" not checked | Set `pass_execution_context: true` in `add_form_event` |
| `Function not found` | Function name mismatch or library not on form | Verify `function_name` includes namespace, library is in form libraries |
| `Xrm is not defined` | Code running outside model-driven app context | Only use Xrm API inside registered event handlers, not in standalone pages |
| Option set value not setting | Using string label instead of integer code | Query `dataverse_get_column` for actual integer values |
| Lookup filter not working | Filter XML syntax error or wrong entity name | Validate FetchXML filter, use correct logical name in `addCustomFilter` |
| Save loop / "Saving in Progress" | Calling `formContext.data.entity.save()` inside OnSave | Never call save inside OnSave — use business rules or flags instead |
| Intermittent null errors | Executable code in global scope | Move ALL logic inside registered handler functions |
| stale formContext after await | Async code using formContext after user navigated | Capture needed values before await, check form still exists after |

---

## Code Quality Rules

### NEVER store formContext in a module-scope variable

```javascript
// BAD — if two forms are open in the same session, one overwrites the other
var formContext = null;
this.formOnLoad = function (executionContext) {
    formContext = executionContext.getFormContext(); // module-scope!
};

// GOOD — pass formContext to helper functions, or get it fresh each time
this.formOnLoad = function (executionContext) {
    var formContext = executionContext.getFormContext(); // local scope
    toggleFields(formContext);
};
```

### Remove OnChange handlers before adding them in OnLoad

If the form reinitializes (e.g., user navigates back), OnLoad fires again. Without removing old handlers first, they stack and fire multiple times:

```javascript
this.formOnLoad = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var statusAttr = formContext.getAttribute("statuscode");
    if (statusAttr) {
        statusAttr.removeOnChange(onStatusChange); // prevent stacking
        statusAttr.addOnChange(onStatusChange);
    }
};
```

### Do NOT call addPreSearch inside async callbacks

`addPreSearch` adds a NEW handler each time it's called. If you put it inside a `.then()` or async callback that fires on every change, filters will stack:

```javascript
// BAD — adds a new preSearch handler every time the lookup changes
this.parentOnChange = function (executionContext) {
    var formContext = executionContext.getFormContext();
    Xrm.WebApi.retrieveRecord("account", parentId, "?$select=name").then(function () {
        formContext.getControl("cr1a2_childid").addPreSearch(filterChild); // stacks!
    });
};

// GOOD — register addPreSearch once in OnLoad, have the callback read current form state
this.formOnLoad = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var childControl = formContext.getControl("cr1a2_childid");
    if (childControl) {
        childControl.addPreSearch(function () {
            filterChild(formContext); // reads current values at execution time
        });
    }
};
```

### Prefer async/await over deeply nested .then() chains

For Xrm.WebApi calls, `async/await` is cleaner and easier to maintain than nested `.then()` callbacks. Use `.then()` for simple single-call patterns, but switch to async/await when chaining 2+ calls:

```javascript
// Cleaner with async/await
this.formOnLoad = async function (executionContext) {
    var formContext = executionContext.getFormContext();
    try {
        var account = await Xrm.WebApi.retrieveRecord("account", accountId, "?$select=name");
        var contacts = await Xrm.WebApi.retrieveMultipleRecords("contact",
            "?$select=fullname&$filter=_parentcustomerid_value eq " + accountId);
        // process results...
    } catch (error) {
        console.error("[MyFeature] Error: " + error.message);
    }
};
```

### Compare values before calling setValue()

Avoid setting a field to the same value it already has — this unnecessarily marks the form as dirty and can trigger OnChange handlers or auto-save:

```javascript
function setFieldIfChanged(formContext, fieldName, newValue) {
    var attr = formContext.getAttribute(fieldName);
    if (!attr) return;
    var current = attr.getValue();
    if (current !== newValue) {
        attr.setValue(newValue);
    }
}
```

### Define option set constants as named objects

Instead of scattering magic integers through your code, define constants at the top of each file:

```javascript
var STATUS = {
    ACTIVE: 100000000,
    INACTIVE: 100000001,
    PENDING_APPROVAL: 100000002
};

// Usage: formContext.getAttribute("statuscode").setValue(STATUS.PENDING_APPROVAL);
```

---

## Execution Workflow

Complete workflow for creating a form script:

```
1. VERIFY METADATA
   a. dataverse_get_table(table_name)           → confirm table exists
   b. dataverse_list_columns(table_name)         → get field logical names + types
   c. dataverse_get_column(table_name, col)      → get option set values if needed
   d. dataverse_list_forms(table_name, "main")   → get form ID
   e. dataverse_get_form(form_id)                → confirm fields are on the form
   f. dataverse_list_form_events(form_id)        → check existing handlers

2. WRITE CODE
   a. Use namespace pattern with formContext
   b. Null-check every getAttribute/getControl call
   c. Use correct integer values for option sets
   d. Use correct logical names (verified in step 1)

3. CREATE WEB RESOURCE
   a. dataverse_create_web_resource(name, display_name, type: "JS", content, solution_name)

4. REGISTER ON FORM
   a. dataverse_add_form_event(form_id, "OnLoad", library, function, pass_execution_context: true)
   b. dataverse_add_form_event(form_id, "OnChange", library, function, field_name, ...)
   c. dataverse_add_form_event(form_id, "OnSave", library, function, ...)

5. VERIFY
   a. dataverse_list_form_events(form_id)        → confirm all handlers registered
   b. dataverse_get_web_resource(name)            → confirm content is correct
```

---

## References

- [Client API form context](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/clientapi-form-context)
- [Client API Xrm object](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/clientapi-xrm)
- [Events in forms and grids](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/events-forms-grids)
- [JavaScript web resources](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/script-jscript-web-resources)
- [Walkthrough: Write your first client script](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/walkthrough-write-your-first-client-script)
- [Xrm.Navigation reference](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-navigation)
- [Xrm.WebApi reference](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-webapi)
- [Xrm.Utility reference](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-utility)
- [Important changes (deprecations)](https://learn.microsoft.com/en-us/power-platform/important-changes-coming)
- [Troubleshoot form issues](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/troubleshoot-forms)

## Validation

After creating or updating web resources and registering event handlers:
- Verify the web resource exists in the solution: `dataverse_list_web_resources` filtered by solution
- Verify event registration on the form: `dataverse_list_form_events` for the target form
- Open the form in the browser and check the browser console (F12) for:
  - The `[Namespace] init` or `[Namespace] onLoad` log message (confirms the script loaded)
  - Any "Cannot read property of null" errors (indicates a field referenced in code is not on the form)
  - Any 401/403 errors (indicates permission issues with Web API calls)
- If errors found, inspect the DOM and console for details — consider `/validate-ui` for a comprehensive visual check
- Report: web resource name, form it's registered on, events attached (OnLoad, OnSave, OnChange), any console errors found
