Build a Dataverse equipment request code app
In this exercise, you build a Power Apps code app - a React and TypeScript application that calls Dataverse directly through strongly typed, generated services instead of a low-code canvas. You'll connect it to three Dataverse tables, call a Dataverse custom API to check a business rule, and read an environment variable at run time. Together, these are the three ways a code app uses Dataverse beyond simple data storage.
The app you build lets an employee ask for a replacement for a device they currently hold. It reads the employee's own record and their devices from Dataverse, calls a supplied eligibility check before letting the request through, and shows a fulfilment contact address that's read live from an environment variable rather than hardcoded.
This exercise should take approximately 35 minutes to complete.
Note: Code apps and the Power Apps CLI are being actively developed, so command names, flags, and generated file names can change between releases. If a command or a generated file name in this exercise doesn't quite match what you see, check the current Power Apps CLI command reference{:target="_blank"} at
https://learn.microsoft.com/power-apps/developer/code-apps/reference/cli.
Confirm the base solution and reset lab data
This and every other Dataverse exercise in this course share one base solution, with the same four tables, sample data, a custom API, and an environment variable. Confirm it's installed, then reset the sample data to a known starting point.
- Sign in to the Power Apps maker portal{:target="_blank"} at
https://make.powerapps.com, and select the environment you're using for these exercises. - Select Solutions, and confirm that Equipment Provisioning (base) is listed. If it isn't, follow the import steps in the setup guide, then come back here.
- In the left navigation, select Apps, and open Equipment Provisioning.
- In the app, select Requests under Equipment provisioning.
- On the command bar, select Reset lab data.
- Wait for the progress indicator, review the Lab data reset summary, and close the dialog.
- Select Refresh.
You should see the Active Requests view populated with sample requests, including open requests for employee EMP-001 - that's your own employee row for the rest of this exercise.
Note: Reset lab data only writes values that differ from the lab's starting data, and it never deletes a request you create later in this exercise. You can run it as many times as you like, on this or any other exercise.
Get the starter code app and connect it to your environment
The starter files for this exercise are a React and TypeScript project, scaffolded from the Power Apps CLI's Vite template and already set up to work with code apps. It has a request form with nothing connected to Dataverse yet - you'll wire that up over the next three tasks.
-
Open a terminal, and go to the
Allfiles/04-dataverse-code-appfolder in the lab files you cloned in the setup. If you didn't clone the repository, download the files from the 04-dataverse-code-app folder{:target="_blank"} athttps://github.com/MicrosoftLearning/AB-400-microsoft-power-platform-developer/tree/main/Allfiles/04-dataverse-code-app. -
Open the folder in Visual Studio Code.
-
In the integrated terminal, install the project's dependencies:
npm install -
Confirm the Power Apps CLI version:
pa --versionUse version 1.3.0 or later. If your installed version is older, update it before you initialize the app:
npm install --global @microsoft/power-apps-cli@latestThis avoids generating the service models with an older CLI.
-
Initialize the code app for your environment:
pa app initWhen prompted, sign in using your credentials, select the environment you're using for these exercises, and create a code app named
AB400 Lab 04 - Equipment Request- or select it if you already created it on an earlier run of this exercise. -
Start the local host for the app:
pa app run -
Open the URL labeled Local Play in the same browser profile you use for your Power Platform tenant.
You should see the Equipment request form, with a status message reading "Dataverse tables are not connected yet." That's expected - you connect the form to Dataverse in the next task.
Note: Leave
pa app runrunning in this terminal for the rest of the exercise; it picks up most code changes automatically, so you usually only need to refresh the Local Play page. Open a second terminal in the same folder for thepacommands in the tasks that follow.
Add Dataverse tables as data sources
Connect the form to the three Dataverse tables it needs: Request, Employee, and Device. The Power Apps CLI generates a strongly typed model and a service for each one.
-
In a second terminal, in the same folder, add each table as a data source by its logical name:
pa app add data-source --connector dataverse --table eqp_request pa app add data-source --connector dataverse --table eqp_employee pa app add data-source --connector dataverse --table eqp_deviceNote: If a command fails with
fetch failedor a connection timeout, run it again — see Troubleshooting in the setup guide. It's a transient connectivity issue, not a sign that something's set up wrong. -
Open the
src/generated/servicesfolder and note the three service file names the CLI generated — one per table. The CLI names them from each table's logical name, with only the first letter capitalized (for exampleEqp_requestsService.ts,Eqp_employeesService.ts,Eqp_devicesService.ts); these names are case-sensitive, so confirm the exact names here before the next step.Note: Every table in this solution also has an alternate key (for example
eqp_key_employeenumberon Employee), used by the Reset lab data fixture you ran earlier in this exercise. Code apps don't support alternate keys today, so - like every code app - this exercise finds rows by filtering on a plain column instead, such aseqp_employeenumber. -
Open
src/components/RequestForm.tsx. At the top of the file, under the existing imports, add imports for the three generated services (adjust the names if yours differ from the step above):import { Eqp_employeesService } from '../generated/services/Eqp_employeesService'; import { Eqp_devicesService } from '../generated/services/Eqp_devicesService'; import { Eqp_requestsService } from '../generated/services/Eqp_requestsService'; -
Find the comment Load the employee and device data sources and replace the entire
loadEmployeeAndDevicesfunction with the following code:async function loadEmployeeAndDevices() { // Load the employee and device data sources const employeeResult = await Eqp_employeesService.getAll({ select: ['eqp_employeeid'], filter: "eqp_employeenumber eq 'EMP-001'", top: 1, }); const myEmployee = employeeResult.data?.[0]; if (!myEmployee) { setStatus('Could not find your Employee row (EMP-001). Run Reset lab data and try again.'); return; } setEmployeeId(myEmployee.eqp_employeeid); const deviceResult = await Eqp_devicesService.getAll({ select: ['eqp_deviceid', 'eqp_name', 'eqp_devicetype'], filter: `_eqp_assignedto_value eq ${myEmployee.eqp_employeeid}`, }); setDevices( (deviceResult.data ?? []).map((device) => ({ eqp_deviceid: device.eqp_deviceid, eqp_name: device.eqp_name ?? '', eqp_devicetype: Number(device.eqp_devicetype ?? 0), })), ); setStatus(null); }This finds your own Employee row by its employee number (a plain column, not the alternate key), then retrieves only the devices whose
eqp_assignedtolookup points at that row - using the lookup's plain_eqp_assignedto_valuecolumn, the same pattern Dataverse uses for any lookup filter. The.mapstep copies just the fields this form needs, so the form doesn't depend on exactly which columns the generated model marks as optional. -
Save the file, and refresh the Local Play page.
You should see the device dropdown populated with LAP-1001 and PHN-1002 - the two devices seeded for EMP-001 - and the status message is gone.
Tip: If you use GitHub Copilot, this is a good point to ask it to explain the generated service file for one of your tables, or to suggest the
select/filteroptions for a query you haven't written yet.
Call the eligibility custom API and gate the request form
Before the form creates a request, it calls the supplied eqp_IsEligibleForReplacement custom API to check whether the selected device is due for replacement, and only continues if the answer is yes.
-
In your terminal, search for the custom API:
pa app find-dataverse-api --search "eqp_IsEligibleForReplacement"Note: A custom API that was imported recently can take a few minutes before
pa app find-dataverse-apidiscovers it - this is a known timing issue after import (tracked as microsoft/PowerAppsCodeApps#433{:target="_blank"},https://github.com/microsoft/PowerAppsCodeApps/issues/433). If the search returns nothing, wait a few minutes and try again - it isn't something you did wrong. -
Add it to the app:
pa app add dataverse-api --api-name eqp_IsEligibleForReplacement -
Open
src/generated/servicesagain, and confirm the name of the generated service file foreqp_IsEligibleForReplacement— it keeps the custom API's own name, with a capital first letter (Eqp_IsEligibleForReplacementService.ts). Import it at the top ofsrc/components/RequestForm.tsx:import { Eqp_IsEligibleForReplacementService } from '../generated/services/Eqp_IsEligibleForReplacementService'; -
In the same file, find the comment Call the eligibility custom API. Replace it, the
let isEligible = false;line above it, and the explanatory comment lines below it, with the following code:// Call the eligibility custom API const eligibility = await Eqp_IsEligibleForReplacementService.eqp_IsEligibleForReplacement( selectedDeviceId, ); const isEligible = Boolean(eligibility.data?.IsEligible);This passes the selected device's ID as the
DeviceIdparameter, and reads theIsEligiblevalue from the response. -
Find the comment Create the replacement request. Replace it, the explanatory comment lines below it, and the
setStatus('Request creation is not connected yet.');placeholder, with the following code:// Create the replacement request const requestedDeviceType = selectedDevice.eqp_devicetype; if ( requestedDeviceType !== 100000000 && requestedDeviceType !== 100000001 && requestedDeviceType !== 100000002 && requestedDeviceType !== 100000003 ) { setStatus('The selected device has an unsupported device type. Check its Device type in Dataverse.'); return; } const newRequest: Parameters<typeof Eqp_requestsService.create>[0] = { eqp_justification: justification, eqp_requesteddevicetype: requestedDeviceType, statecode: 0 as const, 'eqp_Employee@odata.bind': `/eqp_employees(${employeeId})`, 'eqp_CurrentDevice@odata.bind': `/eqp_devices(${selectedDeviceId})`, }; const result = await Eqp_requestsService.create(newRequest); if (result.data) { setStatus(`Request ${result.data.eqp_name ?? ''} submitted for ${selectedDevice.eqp_name}.`); setJustification(''); setSelectedDeviceId(''); } else { setStatus('Failed to create the request.'); }Parameters<typeof Eqp_requestsService.create>[0]uses the generated service's input type to check the payload before the call. The@odata.bindproperty names match that model exactly, including capitalization (eqp_Employee,eqp_CurrentDevice). They differ from the lowercase lookup logical names.The device-type check narrows the value to the four numeric options supported by the generated choice type. Don't convert it to a string. Set
statecodeto the number0(Active).as constpreserves the literal0type. Dataverse fills in the rest, including the autonumbered request number (eqp_name) this code reads back from the response. -
Save the file, refresh the Local Play page, select
LAP-1001, enter a justification, and select Submit request.
You should see the message "LAP-1001 isn't eligible for replacement yet." - LAP-1001 was issued 8 months ago, inside the 24-month replacement window. You'll submit an eligible request, and prove this gate properly, in a later task.
Note: This gate runs entirely in the code app. Dataverse itself still allows the request to be created either way - the eligibility rule is enforced server-side only when a coordinator approves a request, not when it's created - so don't rely on this check as a security boundary; it's a convenience for the person filling in the form.
Tip: If a call to the custom API fails with an error you don't recognize, ask GitHub Copilot to help interpret the error message and check the request payload against the custom API's parameters.
Read an environment variable from the code app
The app also shows a fulfilment contact address, read at run time from the eqp_FulfillmentContact environment variable, so it can change per environment without a code change.
-
List the environment variables available to your code app:
pa app list-environment-variablesThis lists variable names only - including
eqp_FulfillmentContact- not their values. You read a value by calling a Dataverse function, next. -
Find the function that reads a variable's value:
pa app find-dataverse-api --search "RetrieveEnvironmentVariableValue"Note: You might see environment variable references written as
@envvar:schemanameelsewhere - that shorthand only works for a connector data source such as SharePoint, and doesn't apply to Dataverse tables. This task calls the platform function directly instead, which works the same way for any Dataverse environment variable. -
Add it to the app:
pa app add dataverse-api --api-name RetrieveEnvironmentVariableValue -
Open
src/generated/services, confirm the generated service file's name, and import it at the top ofsrc/components/FulfilmentContact.tsx(adjust the name if yours differs):import { RetrieveEnvironmentVariableValueService } from '../generated/services/RetrieveEnvironmentVariableValueService'; -
Find the comment Read the fulfilment contact environment variable. Replace it, the explanatory comment lines below it, and the
setContact(null);placeholder, with the following code:// Read the fulfilment contact environment variable const result = await RetrieveEnvironmentVariableValueService.RetrieveEnvironmentVariableValue( 'eqp_FulfillmentContact', ); const value = result.data?.Value; setContact(typeof value === 'string' ? value : null);This always re-reads the live value - never assume you're seeing the published default, because a previous run of this exercise, or another learner in the same environment, may already have set a current value. The generated service returns the response as an untyped record, so the
typeofcheck is what lets this value be assigned to astring | nullfield. -
Save the file, and refresh the Local Play page.
You should see a contact address under "Questions about fulfilment?" - this is whatever value the environment currently holds, default or not.
- In the Power Apps maker portal{:target="_blank"} at
https://make.powerapps.com, select Solutions, and open Equipment Provisioning (base). - Filter the components list to Environment variables, and select Fulfillment contact.
- Under Current Value, enter a new value, for example
lab04-fulfilment@example.com, then select Save. - Refresh the Local Play page again.
You should now see the new value you just set, confirming the app reads the environment variable live, not a value baked in at build time.
Submit a request and prove the eligibility rule
You've connected all three building blocks - Dataverse tables, a custom API, and an environment variable. Now prove the rule they implement actually holds, in both directions.
- In the running app, select
PHN-1002, enter a justification such asDevice is past its replacement period, and select Submit request.
You should see a message confirming a new request number was submitted for PHN-1002 - PHN-1002 was issued 26 months ago, past the 24-month window, so the eligibility check passed and the app created the row.
- In the Power Apps maker portal{:target="_blank"} at
https://make.powerapps.com, open the Equipment Provisioning app, select Requests, and confirm your new request appears in the Active Requests view, with status Submitted and Device to replace set toPHN-1002. - Back in the app, select
LAP-1001, enter a justification, and select Submit request again.
You should see the same "isn't eligible for replacement yet" message as before, and no new row for LAP-1001 in the Requests view - the same rule, holding for the same employee, for two devices with different issue dates.
Clean up
If you set a current value on the eqp_FulfillmentContact environment variable in this exercise, you can remove it so a later run of this exercise - or the resilient cloud flow exercise, which reads the same variable - sees the published default again.
- In the Power Apps maker portal{:target="_blank"} at
https://make.powerapps.com, select Solutions, and open Equipment Provisioning (base). - Filter the components list to Environment variables, and select Fulfillment contact.
- Under Current Value, delete the value you entered, and select Save.
The base solution, its tables, and the seed data are shared by every exercise in this course, so this exercise's clean-up never removes or changes them. You also don't need to delete the code app itself - leaving it doesn't affect any other exercise.