Sam's Dynamics, Power Platform & AI Blog

Monday, 14 September 2026

Getting Started with JavaScript in Dynamics 365 CRM Forms: A Step-by-Step Guide

If you've spent any time around the Dynamics 365 / Power Platform world lately, it's all Copilot, agents, and AI everywhere. But underneath most of that AI layer, the forms your users actually work in still run on plain old client-side JavaScript. It's still the most reliable way to control field behavior, run validations, and react to what a user is doing in real time — and it's a skill every CRM developer should be solid on. This is the first post in a short series on form scripting, working up from the basics to where it starts overlapping with Copilot Studio agents.

Why JavaScript Still Matters

Business Rules and Power Automate cover a lot of ground now, but there are still things only JavaScript does well: instant field-level reactions as a user types, custom validation logic that's too complex for a Business Rule, and dynamic UI changes (hiding sections, changing option sets, enabling/disabling controls) based on more than one condition at once. If you're building anything non-trivial on a form, you'll end up here eventually.

What You'll Need

  • A Dynamics 365 (model-driven app) environment with System Customizer or System Administrator access
  • A basic understanding of JavaScript (variables, functions, conditionals)
  • Browser developer tools (F12) for testing and debugging

Step 1: Create the JavaScript Web Resource

In the Power Apps maker portal, go to your solution and select New → More → Web resource. Choose type Script (JScript), upload or paste your .js file, and give it a clear naming convention, e.g. new_/forms/account_form.js. Save and publish.

Step 2: Add the Library to the Form

Open the form in the form editor, go to the Form Libraries section, and add the web resource you just created. This makes its functions available to the form's event handlers.

Step 3: Understand formContext Basics

Every modern form script receives an executionContext, from which you get the formContext — your entry point to everything on the form. Avoid the old Xrm.Page pattern; it's deprecated. Here's the core API you'll use constantly:

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
    
        // Get an attribute (field)
            var statusAttr = formContext.getAttribute("statuscode");
            
                // Get a control (the UI element for that field)
                    var reasonControl = formContext.getControl("new_reasoncode");
                    
                        // Read and write values
                            var currentValue = statusAttr.getValue();
                                statusAttr.setValue(1);
                                }

Step 4: Register the OnLoad and OnChange Events

In the form editor, select the form's properties (or a specific field's event handlers) and register your function against the OnLoad or OnChange event. Here's a working example that shows/hides and requires a reason field based on status:

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
        var statusAttr = formContext.getAttribute("statuscode");
            if (statusAttr) {
                    statusAttr.addOnChange(onStatusChange);
                            onStatusChange(executionContext); // run once on load too
                                }
                                }
                                
                                function onStatusChange(executionContext) {
                                    var formContext = executionContext.getFormContext();
                                        var status = formContext.getAttribute("statuscode").getValue();
                                            var reasonControl = formContext.getControl("new_reasoncode");
                                                var reasonAttr = formContext.getAttribute("new_reasoncode");
                                                
                                                    if (status === 2) {
                                                            reasonControl.setVisible(true);
                                                                    reasonAttr.setRequiredLevel("required");
                                                                        } else {
                                                                                reasonControl.setVisible(false);
                                                                                        reasonAttr.setRequiredLevel("none");
                                                                                            }
                                                                                            }

Step 5: Add Save Validation

Register a function on the form's OnSave event to block a save when something's wrong — useful for checks that are too specific for a Business Rule:

function onSave(executionContext) {
    var formContext = executionContext.getFormContext();
        var emailAttr = formContext.getAttribute("emailaddress1");
            var email = emailAttr ? emailAttr.getValue() : null;
            
                if (email && email.indexOf("@") === -1) {
                        executionContext.getEventArgs().preventDefault();
                                formContext.ui.setFormNotification(
                                            "Please enter a valid email address before saving.",
                                                        "ERROR",
                                                                    "email_validation"
                                                                            );
                                                                                } else {
                                                                                        formContext.ui.clearFormNotification("email_validation");
                                                                                            }
                                                                                            }

Common Pitfalls

  • Forgetting to pass executionContext. When registering an event handler in the form editor, make sure "Pass execution context as first parameter" is checked.
  • Library load order. If one script depends on functions in another, the dependent library needs to be added after it in the Form Libraries list.
  • Null reference errors. Always check an attribute or control exists before calling methods on it — fields aren't always on every form.
  • Testing only in the maker portal. Always test in the actual app the field workers use; form customizations can differ across apps.

What's Next in This Series

This is post one of five on form scripting. Coming up:

  • Field-level logic in depth: show/hide, enable/disable, required levels, and when to use a Business Rule instead
  • Working with lookups, option sets, and subgrids from JavaScript
  • Calling the Dataverse Web API from form scripts to read and update related records
  • From script to agent: wiring a Copilot Studio agent action into a JavaScript-driven ribbon button

Form scripting isn't going away just because agents are getting smarter — if anything, knowing both is what makes a CRM developer valuable right now. Next post picks up with field-level logic in more depth.

What your Copilot agent can actually see: Dataverse security when the agent is asking

What your Copilot agent can actually see

Nobody worried much about oversharing in CRM, because advanced find was tedious and nobody went looking. An agent removes that. Everything a user technically has access to is now one plain-English question away.


Your security model didn't change. What changed is that it's finally being exercised.

Identity is the whole design

Two options, and they behave completely differently.

User authentication. The signed-in user's identity flows through to Dataverse, which applies your security roles and record-level permissions as normal. The same prompt returns different answers for different people, which is exactly right.

Agent author authentication. A fixed credential does the work regardless of who asked. Microsoft scopes this to implicit or low-risk access — a weather lookup, a public phone number. If you reach for it to make something work, you've just built a bypass around your own security model.

Default to user auth. Fall back only for a genuine service-account case, like reading a shared mailbox.

Cumulative permissions get expensive

Dataverse adds up every grant from every role and every team, then checks. There's no deny.

That was survivable when access was theoretical. Now an agent will happily aggregate across everything the user can reach and summarise it in one answer. The stray owner team somebody created three years ago to solve a visibility request stops being clutter and starts being a disclosure.

Before you switch anything on: pull the role and team membership for a sample of users and check what they can actually see, not what they're supposed to see.


What the model can't protect

  • Column security profiles — salary, ID numbers, bank details. If the field is readable, the agent can read it and put it in a sentence.
  • Record sharing — invisible at design time, unauditable in bulk, and it grants exactly the access the agent will use.
  • Notes and attachments — usually wide open, frequently containing things people would never put in a proper field.

The paths that aren't in the security model at all

This is where agent projects leak. Most failures aren't a misconfigured table privilege:

  • Which connection the agent's actions run under, and whose permissions that carries
  • Whether a flow behind the agent runs as a service account with more rights than the user
  • App sharing and DLP policy governing which connectors the agent can reach
  • Any autonomous agent, which has no signed-in user at all — that identity is the ceiling on everything it can do

Give service identities a purpose-built role scoped to the specific tables they need. Not System Administrator because it was faster.

Treat retrieved content as untrusted

Records and documents are input, and input can carry instructions. A case description written by an external customer is not a safe place to take direction from.

The design rule: retrieved content informs an answer, it never authorises an action. Anything privileged runs on validated parameters through a flow or plugin, where the rules are enforced server-side and a model can't talk its way past them.

Where to start

  • Confirm every action is using user authentication unless there's a written reason not to
  • Audit team memberships and delete the ones nobody can explain
  • Column-secure the fields that would be a problem in a chat window
  • Check which identity your flows and connections actually run as
  • Test the agent as three different users, not as yourself

None of this is AI work. It's the security model you already had, finally being read out loud.

Related: Copilot in Dynamics 365 — what it actually does

Thanks for reading this article. Hope this Article will help you. Cheers!!!

#Dynamics365 #PowerPlatform #MicrosoftCopilot #DynamicsCRM #CopilotStudio #Dataverse #AIDeveloper #DynamicsAIEngineer #HireAIEngineer #AIEngineer #D365Consultant #CRMDeveloper #AIAgents #MSDyn365

Copilot Studio agent ALM: getting from dev to production


Copilot Studio gets you from idea to working agent very quickly. That's the whole appeal. It also means an agent can be in front of real users before anyone's thought about how to change it safely.

Here's what I'd have in place before that happens.

Work inside a solution from the start

Agents are proper solution components, so the agent, its topics, knowledge configuration, flows, environment variables and connection references all move together as one package.

  • Create a custom publisher with your own prefix before you build anything — changing it later means recreating components
  • Keep one solution unless you genuinely need to deploy parts independently
  • Build unmanaged in dev, export managed to test and production
  • Push changes one direction only: fix in dev and redeploy, never patch production

Make anything environment-specific a variable

In dev it all just works, which is exactly why nobody notices it's hardcoded.

  • SharePoint site URLs used as knowledge sources
  • External API endpoints and base URLs
  • Notification and system email addresses
  • API keys and client secrets — use the Secret type so the value lives in Azure Key Vault, not your solution

There's a trap here worth knowing. A default value you set in dev travels with the solution. If nobody sets a proper value in the target, it quietly falls back to the dev one. That's how a production agent ends up reading your dev SharePoint site while looking perfectly healthy.

Use connection references, not connections

Credentials then bind per environment and you can run as a different account in prod. Which means deciding who the agent actually runs as — a dedicated service account or application user, with roles scoped to what it genuinely needs. Not a maker's personal account. You'll find out why the week they leave.

Promote through Pipelines

The useful part is what it checks before letting you deploy:

  • Every environment variable has a value in the target
  • Every connection reference resolves

When it fails on missing dependencies, it's usually the three dots next to the agent → Advanced → Add required objects, then try again.

The bit no tooling fixes

You can diff a plugin. You can read a pull request. You can't meaningfully diff an agent, because the change lives in instructions and topic logic — so "what changed since Tuesday" has no honest answer.

The only workaround I've found:

  • Keep twenty or so questions with known good answers
  • Cover your main topics plus whatever broke before
  • Run the lot by hand after every deploy

It's crude. It's also the only regression test you've got.

None of this is exciting work. It's just the difference between shipping version 2 and being scared to touch version 1.

Related: Copilot in Dynamics 365 — what it actually does


Thanks for reading this article. Hope this Article will help you. Cheers!!!

#Dynamics365 #PowerPlatform #MicrosoftCopilot #DynamicsCRM #CopilotStudio #Dataverse #AIDeveloper #DynamicsAIEngineer #HireAIEngineer #AIEngineer #D365Consultant #CRMDeveloper #AIAgents #MSDyn365

Packaging and Deploying Web Resources: A Practical ALM Workflow for Dynamics 365 JavaScript

You've written the script. You've unit-tested the logic and stepped through it in dev tools. It works perfectly on your form. Now co...