Build a model-driven app script with GitHub Copilot
In this exercise, you add client-side logic to a model-driven app form using GitHub Copilot, then review and correct what it generates. You'll write a script that runs when a coordinator opens a device replacement request: it calls a custom API over the Dataverse Web API to check whether the device is eligible for replacement, shows a warning if it isn't, and shows the employee's remaining equipment allowance. You'll also add a command to the form's command bar that opens a confirmation dialog before a request is sent to the fulfillment team.
The script never writes to Dataverse — everything it does is a read, which makes it safe to run against live data as often as you like while you're learning. GitHub Copilot drafts most of the code for you, but its output varies, so part of the exercise is reviewing what it produces against a working reference and correcting any mistakes.
This exercise should take approximately 30 minutes to complete.
Prerequisites
To complete this exercise, you need:
- A developer environment with Microsoft Dataverse, with the Equipment Provisioning (base) solution installed.
- A Chromium-based browser with developer tools.
- Visual Studio Code{:target="_blank"} at
https://code.visualstudio.com/, with GitHub Copilot enabled. - A GitHub account with GitHub Copilot access (Copilot Free is sufficient for this exercise; see the setup instructions if your account can't use Copilot Free).
Confirm the environment and reset the lab data
Every exercise in this course shares the same Dataverse tables and seed data, supplied as a solution called Equipment Provisioning (base). This task confirms that solution is in place, then resets the data this exercise reads so you're starting from a known state.
-
Sign in to the Power Apps maker portal{:target="_blank"} at
https://make.powerapps.com. -
Confirm the environment selector, near the top of the page, shows the Dataverse environment you intend to use for this exercise.
-
Select Solutions, and confirm that Equipment Provisioning (base) is listed. If it isn't, follow the import steps in Set up your environment{:target="_blank"} before continuing.
-
Select Apps, and then open the Equipment Provisioning app.
-
In the Equipment provisioning area, select Requests.
-
On the command bar, select Reset lab data. Wait for the progress indicator to finish, then review the summary dialog, which lists how many rows were created, restored, or were already correct.
-
Select Refresh.
You should see the Active Requests view populated with seeded rows, including REQ-SEED-001 and REQ-SEED-002.
Draft the form-load and command logic with GitHub Copilot
The starter file already contains the requirements for both functions you need, written as comments. You'll give those comments to GitHub Copilot one at a time and review what it produces.
-
Open Visual Studio Code.
-
Open the
06-model-driven-script-copilotfolder inside the repository you cloned during setup, or download requestscript.js{:target="_blank"} fromhttps://raw.githubusercontent.com/MicrosoftLearning/AB-400-microsoft-power-platform-developer/main/Allfiles/06-model-driven-script-copilot/requestscript.js. -
Open requestscript.js. Review the two comment blocks in the file: one above the empty
onFormLoadfunction, and one above the emptyconfirmProvisioningfunction. Each one describes exactly what that function must do. -
Select the comment block and the empty
onFormLoadfunction below it, open GitHub Copilot Chat, and send the following prompt:Using the requirements in the comment above onFormLoad, implement that function. -
Review what Copilot generates. If it's missing the allowance notification, or implemented the eligibility check some other way, ask it to adjust with a follow-up prompt, for example:
The function is missing the INFO notification for the employee's remaining allowance. Add it, reading eqp_remaining from the eqp_allowance table for the request's employee. -
Select the comment block and the empty
confirmProvisioningfunction, and send Copilot this prompt:Using the requirements in the comment above confirmProvisioning, implement that function. -
Save the file.
You should now see generated code in place of both empty functions. It won't always be correct on the first try — that's expected, and you'll check it in the next task.
Review the generated code and correct it
GitHub Copilot's first draft is usually close, but not always exact. Check it against the list below, then compare it with the confirmed-working versions that follow and correct anything that's different.
| Check | What to look for |
|---|---|
| Custom API name and parameter | The operation name must be exactly eqp_IsEligibleForReplacement, with a single request parameter named DeviceId |
| Table and column names | eqp_device, eqp_issuedon, eqp_employee, eqp_allowance, eqp_remaining — Copilot sometimes guesses a plausible but wrong logical name |
| Allowance filter | The filter needs the employee's ID on the lookup's value property (_eqp_employee_value), not the lookup's display name |
| Eligible-from date math | The warning date is the issue date plus 24 calendar months, not a fixed number of days |
| Confirm command signature | confirmProvisioning must accept a single parameter — the form context, passed in by the command's Primary Control parameter |
| No writes | Neither function should call Xrm.WebApi.updateRecord or save the form |
-
Compare your
onFormLoadfunction with the confirmed-working version below, and correct any differences:function onFormLoad(executionContext) { var formContext = executionContext.getFormContext(); formContext.ui.clearFormNotification(ELIGIBILITY_NOTIFICATION_ID); formContext.ui.clearFormNotification(ALLOWANCE_NOTIFICATION_ID); var device = formContext.getAttribute("eqp_currentdevice").getValue(); if (device && device.length > 0) { var deviceId = device[0].id.replace(/[{}]/g, ""); var eligibilityRequest = new IsEligibleForReplacementRequest(deviceId); Xrm.WebApi.online.execute(eligibilityRequest).then(function (response) { return response.json(); }).then(function (result) { console.log("eqp_IsEligibleForReplacement returned IsEligible=" + result.IsEligible); if (result.IsEligible) { formContext.ui.clearFormNotification(ELIGIBILITY_NOTIFICATION_ID); return; } return Xrm.WebApi.retrieveRecord("eqp_device", deviceId, "?$select=eqp_issuedon").then(function (deviceRow) { var issuedOn = deviceRow.eqp_issuedon.substring(0, 10); var eligibleFrom = addMonthsUTC(issuedOn, 24); var message = "This device was issued on " + issuedOn + ". It can be replaced from " + eligibleFrom + ", or earlier if it's flagged as damaged beyond repair."; formContext.ui.setFormNotification(message, "WARNING", ELIGIBILITY_NOTIFICATION_ID); }); }).catch(function (error) { console.log(error.message); }); } var employee = formContext.getAttribute("eqp_employee").getValue(); if (employee && employee.length > 0) { var employeeId = employee[0].id.replace(/[{}]/g, ""); var employeeName = employee[0].name; var options = "?$select=eqp_remaining&$filter=_eqp_employee_value eq " + employeeId; Xrm.WebApi.retrieveMultipleRecords("eqp_allowance", options).then(function (result) { if (result.entities.length === 0) { formContext.ui.clearFormNotification(ALLOWANCE_NOTIFICATION_ID); return; } var remaining = result.entities[0].eqp_remaining; console.log(employeeName + " has " + remaining + " remaining in the current allowance period."); var message = employeeName + " has " + remaining + " replacement device(s) remaining in the current allowance period."; formContext.ui.setFormNotification(message, "INFO", ALLOWANCE_NOTIFICATION_ID); }).catch(function (error) { console.log(error.message); }); } }This version calls the eligibility custom API with the device's ID and only shows the WARNING notification when the response's
IsEligiblevalue isfalse. The allowance read doesn't depend on the eligibility result, so it runs whether or not the device is eligible. -
Confirm the file has a small helper near the top that builds the custom API request, similar to this. If Copilot generated something different, replace it with this version so the operation name and parameter match F2 exactly:
function IsEligibleForReplacementRequest(deviceId) { this.DeviceId = deviceId; } IsEligibleForReplacementRequest.prototype.getMetadata = function () { return { boundParameter: null, parameterTypes: { "DeviceId": { "typeName": "Edm.Guid", "structuralProperty": 1 } }, operationType: 0, operationName: "eqp_IsEligibleForReplacement" }; }; -
Confirm the file has an
addMonthsUTC(or similarly named) helper that adds calendar months, not days, and clamps to the last day of the target month. Compare it with this version:function addMonthsUTC(isoDate, months) { var parts = isoDate.substring(0, 10).split("-"); var year = Number(parts[0]); var month = Number(parts[1]) - 1; var day = Number(parts[2]); var targetIndex = month + months; var targetYear = year + Math.floor(targetIndex / 12); var targetMonth = ((targetIndex % 12) + 12) % 12; var lastDayOfTargetMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate(); var targetDay = Math.min(day, lastDayOfTargetMonth); return new Date(Date.UTC(targetYear, targetMonth, targetDay)).toISOString().substring(0, 10); } -
Compare your
confirmProvisioningfunction with the confirmed-working version below:function confirmProvisioning(primaryControl) { var formContext = primaryControl; var requestNumber = formContext.getAttribute("eqp_name").getValue(); var confirmStrings = { title: "Send to fulfillment", text: "Send " + requestNumber + " to the fulfillment team?" }; var confirmOptions = { height: 200, width: 450 }; Xrm.Navigation.openConfirmDialog(confirmStrings, confirmOptions).then(function (result) { if (result.confirmed) { console.log(requestNumber + ": coordinator confirmed sending to fulfillment."); } else { console.log(requestNumber + ": coordinator canceled."); } }); } -
Save the file.
Your file should now have two complete functions and no remaining TODO comments. You'll wire them up to the form and command next, so you can test them together.
Add the script as a web resource and register the form event handler
Now you'll create your own solution to hold this exercise's changes, upload the corrected script as a web resource, and register onFormLoad on the Request form's On Load event.
-
In the maker portal, select Solutions, then select New solution.
-
For Display name, enter
Equipment Provisioning Lab 06. Note the Name value that's generated. -
For Publisher, select the existing Equipment Provisioning Labs publisher (don't create a new one).
-
Select Save.
-
In your new, now-open solution, select New, point to More, and then select Web resource.
-
Select Choose file and select your edited
requestscript.jsfile. -
For Display name, enter
Equipment provisioning lab 06 script. For Name, enterrequestscript— the portal prefixes it witheqp_. Confirm Type is set to JavaScript (JS). -
Select Save.
-
In the same solution, select Add existing, and then select App.
-
Select the Equipment Provisioning app, and then select Add.
-
Select the Equipment Provisioning app, and then select Edit to open it in the app designer.
-
Under Pages, expand the Request table, and next to the Information form, select ..., and then select Edit.
-
In the form designer, select Form libraries, then select Add library.
-
Select the web resource you uploaded, and then select Add.
-
Select the Events tab. Under On Load, select + Event Handler.
-
For Library, select your web resource. For Function, enter
EquipmentProvisioning.Lab06.onFormLoad. -
Select Pass execution context as first parameter, and then select Done.
-
Select Save, and then select Publish.
You should see the form designer confirm the publish completed without errors. You'll trigger the handler itself once the command is in place too, in a later task.
Create a command that runs the confirm-dialog function
With the web resource in place, add a command to the Request form's command bar that calls confirmProvisioning.
-
Still in the app designer for Equipment Provisioning, next to the Request table's Pages entry, select ..., and then select Edit command bar.
-
Choose the Main form location, and then select Edit.
-
Select New, and then select Command.
-
For Label, enter
Send to fulfillment. -
For Action, select Run JavaScript.
-
Select Add library if your web resource isn't already listed, choose it, and select Add.
-
For Function name, enter
EquipmentProvisioning.Lab06.confirmProvisioning. -
Add a parameter for the function, and choose Primary Control as its type. This is what passes the form context into
confirmProvisioningas its single argument. -
Select Save and Publish.
-
Back in the app designer, select Publish all customizations.
You should see the new Send to fulfillment button listed as a command on the Request main form's command bar.
Debug with browser developer tools and prove the script
Now trigger everything you've built, and use the browser's own developer tools — not a separate network-capture tool — to see what the script is doing.
-
Play the Equipment Provisioning app, go to Requests, and open
REQ-SEED-001. -
You should see a WARNING notification naming the device and the date it becomes eligible for replacement, and an INFO notification reporting the employee's remaining allowance.
-
Open the browser's developer tools (for example, by pressing F12), and select the Console tab. Reload the form. You should see the console messages your script logged, including the
IsEligibleresult and the remaining allowance. -
Select the Sources tab, and find your web resource (use Ctrl+P to search for it by name). Set a breakpoint on the line that calls
Xrm.WebApi.online.execute. -
Reload the form. Execution should pause at your breakpoint. Inspect the
deviceIdvariable, and then resume execution. -
Select the Network tab, and reload the form again. Find the request to
eqp_IsEligibleForReplacement, and select it. Review its request payload (theDeviceIdyou passed in) and its response (theIsEligiblevalue it returned). -
On the form's command bar, select Send to fulfillment. Confirm the dialog opens with the request number in its message, then select the confirm button. Check the Console tab again to see the outcome logged.
-
Optionally, open
REQ-SEED-002instead. Its device is inside the same replacement period asLAP-1001, but it's flagged as damaged beyond repair, so the eligibility exception applies — confirm the WARNING notification doesn't appear for this request, while the INFO allowance notification still does.
You've now seen the corrected script run end to end: a warning and an allowance notification driven by live Dataverse Web API calls, visible in the Network tab, and a confirmation dialog from your new command — all without the script writing anything back.
Clean up
If you've finished exploring, remove the customizations you added in this exercise.
-
In the maker portal, select Solutions.
-
Select EquipmentProvisioningLab06, select ..., and then select Delete.
-
Confirm the deletion.
Don't delete the Equipment Provisioning (base) solution or any seed data rows — other exercises depend on them.