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 reference implementations that follow and correct anything that's different. Test the result on the form in the final task.
| Check | What to look for |
|---|---|
| Custom API name and parameter | Use Xrm.WebApi.online.execute with operation name eqp_IsEligibleForReplacement and one DeviceId parameter. The reference request uses { guid: deviceId }, verified in the tested client |
| Authentication and errors | Use the signed-in model-driven app's Web API context. Check the HTTP status and Boolean IsEligible response, and report failures rather than treating them as an eligibility result |
| 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 reference implementation 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, ""); checkEligibility(deviceId).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.error(error.message); formContext.ui.setFormNotification( "The eligibility check couldn't be completed. Check the browser console for details.", "ERROR", ELIGIBILITY_NOTIFICATION_ID ); }); } 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 even if the eligibility call fails. -
Confirm the file has a small helper near the top that calls the custom API, 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 = { guid: deviceId }; } IsEligibleForReplacementRequest.prototype.getMetadata = function () { return { boundParameter: null, parameterTypes: { "DeviceId": { "typeName": "Edm.Guid", "structuralProperty": 1 } }, operationType: 0, operationName: "eqp_IsEligibleForReplacement" }; }; function checkEligibility(deviceId) { return Xrm.WebApi.online.execute(new IsEligibleForReplacementRequest(deviceId)).then(function (response) { if (response.ok) { return response.json(); } return response.text().then(function (body) { throw new Error("Eligibility check failed (" + response.status + "): " + body); }); }).then(function (result) { if (!result || typeof result.IsEligible !== "boolean") { throw new Error("Eligibility check returned an invalid IsEligible value."); } return result; }); }The request uses the signed-in app's Web API context, without an API key or a separately acquired token. The GUID wrapper produced the correct string
DeviceIdin the tested client's outgoing JSON; it isn't a general requirement for every GUID parameter or SDK. An unsuccessful HTTP response or an invalidIsEligiblevalue reaches the handler's error notification instead of becoming an eligibility decision. -
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 reference implementation 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, select App, and then select Model-driven 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, select the Request form page for the Requests table, then select Edit (the pencil icon) next to the form name to open the Information form.
-
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 publish, and select Back.
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, select Requests view under Pages so its ... menu appears. 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. -
Select Add a parameter, and choose Primary Control as its type. This is what passes the form context into
confirmProvisioningas its single argument. -
Select Save and Publish, and select Back.
-
Select Back in the app designer to return to the solution, then select Publish all customizations.
When you run the Equipment Provisioning app and open a request, you should see Send to fulfillment 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). Confirm the loaded script contains your
IsEligibleForReplacementRequestimplementation, and set a breakpoint on the line that callsXrm.WebApi.online.execute. If the loaded code differs from your saved file, verify the uploaded web resource, form library, and published handler before testing the logic. -
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.
Important: This exercise creates an unmanaged solution. Deleting that solution removes only its container, not the command, form event handler, or web resource. Remove those lab-owned customizations first. Don't delete the shared app, the Information form, or any unrelated commands or event handlers.
-
In the maker portal, open the solution you created, Equipment Provisioning Lab 06, and edit the Equipment Provisioning app. Open the Request table's command bar editor and choose Main form.
-
Select the Send to fulfillment command you created in this exercise, delete it, and select Save and Publish. Return to the app designer and publish the app.
-
Open the Request Information form in the form designer. Under Events > On Load, remove only the handler whose function is
EquipmentProvisioning.Lab06.onFormLoad. -
Under Form libraries, remove Equipment provisioning lab 06 script (
eqp_requestscript) from this form. Save and publish the form. -
Return to the lab solution's component list and select the Equipment provisioning lab 06 script web resource. Select Delete from this environment, not Remove from this solution, and confirm.
If dependencies prevent deletion, first check that the lab command, handler, and library references are removed and published. Don't delete unrelated components. A deleted modern command can leave an App Action dependency. Only for that case:
- Open the dependency details and identify the remaining App Action's ID. Keep those details available for comparison. If you can't identify the exact dependency, stop rather than guessing.
- Download remove-lab-command.js{:target="_blank"} from
https://raw.githubusercontent.com/MicrosoftLearning/AB-400-microsoft-power-platform-developer/main/Allfiles/06-model-driven-script-copilot/remove-lab-command.js, and open it in Visual Studio Code. This optional cleanup helper uses the signed-in app's Web API to inspect the remaining action and delete it only after ownership checks and confirmation. - Review the helper. It requires exactly one unmanaged action linked to the Equipment Provisioning app and
eqp_requestscript, with the lab's function name and asendtofulfillmentunique name. It uses an ETag to reject a concurrent change. It never deletes the app, form, or web resource. - In the Equipment Provisioning app, open browser developer tools and select Console. Use the context where
Xrmis available. After reviewing the helper, paste its contents into the console and run it. Don't disable browser security controls to allow pasting. - Compare the displayed action ID with the dependency details. Confirm deletion only if they match and you created that command in this exercise. Otherwise cancel. If any ownership check fails, leave the component intact and investigate; don't weaken the checks.
- Confirm the console reports that deletion and absence verification succeeded. Return to the maker portal and retry deletion of the lab web resource. This helper uses the documented App Action DELETE operation{:target="_blank"} at
https://learn.microsoft.com/power-apps/developer/data-platform/reference/entities/appaction.
-
In the lab solution, remove any remaining shared app or table entries using Remove from this solution, never Delete from this environment. Return to Solutions, select the lab solution using the Name value you noted when you created it, and delete only its container.
-
Reopen a request in Equipment Provisioning. Confirm the lab's Send to fulfillment command and its eligibility/allowance notifications are no longer present, and the form loads without a missing-library error.
Don't delete the Equipment Provisioning (base) solution or any seed data rows — other exercises depend on them.