Form scripting with the Client API
How to write JavaScript form handlers in Dynamics 365 — the Client API, event handlers, common patterns, and the rules for maintainable code.
For Dynamics 365 model-driven apps, complex client-side logic — beyond what business rules can express — lives in JavaScript running on the form, using the Client API. Used well, scripting handles validation, dynamic UI behaviour, calls to other tables, and integration with external services. Used badly, it produces unmaintainable code that breaks across platform updates.
The Client API. Microsoft's Xrm global object provides the JavaScript API for interacting with the form:
formContext— the current form, with methods to get / set field values, control visibility, raise notifications, save the form.Xrm.WebApi— make calls to Dataverse's Web API to read / write records.Xrm.Navigation— open records, dialogs, alerts, confirmations.Xrm.Utility— utility methods (open quick create, show progress indicator).Xrm.Page(deprecated) — older equivalent of formContext; new code should not use it.
The Client API is the only supported way to interact with the form from JavaScript. Direct DOM manipulation is unsupported and will break.
Event handlers. JavaScript runs in response to form events:
- OnLoad — when the form opens.
- OnSave — when the user saves.
- OnChange — when a specific field changes.
- TabStateChange — when a tab is collapsed / expanded.
- OnReadyStateComplete — when the form is fully rendered.
- PreSearch — before a lookup search executes (to apply additional filters).
Handlers are registered through the form designer, pointing to a function name in a registered web resource.
Web resources. JavaScript files are uploaded as web resources to the solution — versioned, deployable, source-controllable. Best practice:
- One JavaScript file per major table or feature.
- Wrap code in a namespace object to avoid global pollution.
- Use TypeScript for type safety, compile to JavaScript at build time.
Common patterns.
- Conditional required fields. OnChange of one field, set required level of another field via
setRequiredLevel(). - Dynamic visibility. Show / hide sections, tabs, or fields based on field values.
- Validation beyond business rules. Complex multi-field validation; raise a notification with
formContext.ui.setFormNotification(). - Cross-table fetch. OnLoad, query a related table via
Xrm.WebApi.retrieveMultipleRecords(), populate a UI element. - Cascade defaults. OnChange of Account lookup, fetch the Account's default Payment Terms and populate them on the current record.
- External API calls. OnSave, validate against an external service (address verification, tax-ID validation).
- Lookup filtering. PreSearch on a lookup, apply a custom filter via
addCustomFilter()so users only pick valid related records.
Performance.
- OnLoad weight matters — heavy initialisation slows form opening. Defer non-critical work to OnChange or async load.
- Limit OnChange handlers — handlers on every field with cascading logic produce slow user experiences.
- Async API calls — use
awaitand promises; never block the UI thread. - Cache repeated lookups — fetching the same related data multiple times wastes API calls.
Maintenance and platform updates.
- API versioning — the Client API has versioned methods; use the recommended current versions.
- Deprecation tracking — Microsoft deprecates older API methods; review release notes per wave for affected code.
- Avoid undocumented behaviour — relying on DOM structure or non-API internals is the express route to broken code after a platform update.
Debugging.
- Browser developer tools — Sources tab to step through scripts.
console.log()— for trace logging during development.- Forms debug mode — query string parameter that enables verbose logging.
- Test in unmanaged dev solution; deploy as managed.
Common pitfalls.
- Calling Web API in OnLoad without await — race conditions where UI renders before data arrives. Use async/await properly.
- Manipulating the DOM directly — works today, breaks tomorrow when Microsoft changes the rendered structure.
- Hard-coded GUIDs — magic GUIDs for security roles, business units, etc. break when solutions deploy across environments.
- No error handling — uncaught exceptions in handlers produce silent failures users notice as "weird behaviour".
When not to use Client API JavaScript.
- Simple field-level logic → business rules.
- Server-side validation (cannot be bypassed) → plug-ins or workflow.
- Cross-system integration → Power Automate flows or plug-ins.
- Multi-step business processes → business process flows.
Operational discipline. Source-control JavaScript like any code. Code review changes. Version solutions. Maintain a catalogue of handlers per table.
Related guides
- Dataverse Organization Service vs Web APIHow the two Dataverse SDK surfaces differ — SOAP-era IOrganizationService vs the OData v4 Web API — and when each is the right choice for code that touches Dataverse.
- Account hierarchies in Dynamics 365How Dynamics 365 models corporate parent-subsidiary relationships — account hierarchy field, hierarchy charts, security, and reporting roll-up.
- App designer for model-driven appsHow to build a model-driven Power App — site map, tables, forms, views, business process flows, dashboards, and the app-experience layer.
- Async jobs in DataverseHow Dataverse runs background work — system jobs, async plug-ins, workflow runs, and how to monitor, troubleshoot, and prevent the async backlog from getting out of hand.
- Bulk delete jobs in DataverseHow Dataverse's bulk delete handles mass record cleanup — scheduling, filters, retention policies, and the operational discipline around storage management.