Deep linking in Canvas Apps with screen and record parameters
Beginner-friendly implementation guidance for opening a Canvas App on a requested screen, loading a record by URL parameter, and restricting access to contributor users.
#Issue
The app must open from a URL and take the user to the requested screen with the requested record loaded. For example, a URL may contain scr=details and recid=<record-guid>. The app should open the details screen and display the Dataverse record identified by recid.
Before allowing the user to continue, the app must also check whether the signed-in user belongs to the approved contributor security group.
#What deep linking means
A deep link is a URL that opens a Canvas App and passes query parameters to it. Power Apps reads those parameters with the Param function.
Example URL:
https://apps.powerapps.com/play/<APP-ID>?scr=details&recid=<RECORD-GUID>
The parameter names in this example are scr and recid. Use specific, documented names instead of common names such as screen and id. Parameter names and values should use a documented, consistent format.
#URL parameter best practices
- Use
scrfor the requested screen andrecidfor the record identifier. - Keep parameter names lowercase and use the same casing in every generated URL.
- Keep the names stable because links may be shared in email, Teams, and bookmarks.
- Pass a stable record GUID or identifier, not a display name.
- Never put passwords, access tokens, or sensitive personal information in a URL.
- Do not use URL parameters as an authorization mechanism. Always check group membership and Dataverse permissions.
- Encode values that may contain spaces or reserved characters with
EncodeUrl. - Use only an allowlisted screen value such as
homeordetails; never execute arbitrary screen or formula text from a URL. - Generate links with the published app ID and correct environment, and test them in both a browser and Power Apps Mobile.
- Provide a safe fallback when
scrorrecidis missing, malformed, or no longer valid.
Recommended URL shapes:
https://apps.powerapps.com/play/<APP-ID>?scr=home
https://apps.powerapps.com/play/<APP-ID>?scr=details&recid=<RECORD-GUID>
#Recommended flow
Use this startup sequence:
App opens
↓
Read screen and ID parameters
↓
Check contributor-group membership
↓
Validate the requested screen and record ID
↓
Load the record from Dataverse
↓
Open the requested screen, or show a safe error screen
Do not trust a URL parameter as proof that a user is allowed to view or edit a record. The app must still enforce Dataverse permissions and app-level authorization.
#Step 1: Prepare the app
Before writing formulas, identify:
- The Dataverse table that contains the requested record.
- The primary key column for that table.
- The screen names that may be opened from a link.
- The Entra ID or Microsoft 365 security group used for contributors.
- The app ID and published environment.
Use a small allowlist of screens. Do not allow a URL to provide an arbitrary control or screen name.
#Step 2: Add the required data sources
Add the following to the app as appropriate:
- The Dataverse table that contains the records.
- The Office 365 Groups connector if the app checks membership through a group.
- Any approved user or security service used by the organization.
The first time the app is run, the user may be asked to authorize a connector. Test this with a normal contributor account and a non-contributor account.
#Step 3: Read and validate the URL parameters
Use Param to read the values. A good place to initialize them is App > OnStart:
Set(varRequestedScreen, Lower(Trim(Param("scr"))));
Set(varRequestedIdText, Trim(Param("recid")));
Set(varRequestedId, IfError(Guid(varRequestedIdText), Blank()));
Set(varHasValidScreen, varRequestedScreen in ["home", "details"]);
Set(varHasValidId, !IsBlank(varRequestedId));
The Guid conversion prevents the app from using an invalid ID as though it were a valid record identifier. If your primary key is not a GUID, validate it using the correct type and format.
Avoid placing unvalidated URL values directly into a Dataverse formula or using them to construct arbitrary navigation behavior.
#Step 4: Check contributor-group membership
Create a group membership check during startup or on a dedicated loading screen. The exact formula depends on the connector action exposed in your environment. Use the formula Power Apps generates for the Office 365 Groups membership action.
A typical implementation pattern is:
Set(varContributorGroupId, "<CONTRIBUTOR-GROUP-ID>");
Set(
varIsContributor,
IfError(
Office365Groups.IsMemberOfGroup(
varContributorGroupId,
User().Email
).value,
false
)
);
If the connector expects the user's object ID instead of email, pass the user ID required by the generated action. Do not guess the parameter order; select the action from IntelliSense and confirm its signature.
Keep the group ID in an environment variable or configuration table rather than repeating it throughout the app. Do not use a group display name as the security key because names can change.
For stronger enforcement, protect the Dataverse table with security roles or teams as well. A Canvas App visibility check improves the user experience but is not a replacement for server-side Dataverse security.
#Step 5: Load the requested record safely
After the user is authorized and the ID is valid, retrieve the record:
If(
varIsContributor &&
varRequestedScreen = "details" &&
varHasValidId,
Set(
varDeepLinkedRecord,
LookUp(
Requests,
RequestId = varRequestedId
)
)
)
Replace Requests and RequestId with the actual table and primary key. Check for a blank result before opening the details screen:
Set(
varCanOpenDeepLink,
varIsContributor &&
varHasValidScreen &&
(
varRequestedScreen = "home" ||
(
varRequestedScreen = "details" &&
varHasValidId &&
!IsBlank(varDeepLinkedRecord)
)
)
)
If the record does not exist or the user cannot read it, do not display a blank edit form that appears valid. Show a clear message such as “The requested record could not be found or you do not have access.”
#Step 6: Choose the startup screen
Select the App object and set its StartScreen property. StartScreen must return a screen; it is not the place to call Navigate.
If(
!varIsContributor,
AccessDeniedScreen,
!varHasValidScreen,
HomeScreen,
varRequestedScreen = "details" &&
varHasValidId &&
!IsBlank(varDeepLinkedRecord),
DetailsScreen,
HomeScreen
)
If the app uses a loading screen while the membership check and Dataverse lookup are running, return LoadingScreen until varStartupComplete is true:
If(
!varStartupComplete,
LoadingScreen,
!varIsContributor,
AccessDeniedScreen,
varRequestedScreen = "details" &&
!IsBlank(varDeepLinkedRecord),
DetailsScreen,
HomeScreen
)
Use OnVisible on DetailsScreen to bind a form's Item property to varDeepLinkedRecord, or set the form's Item property directly:
varDeepLinkedRecord
#Step 7: Handle links created inside the app
Create links with EncodeUrl when values may contain spaces or special characters. For a GUID, the value normally does not require encoding, but encoding the complete URL is still good practice:
Set(
varDeepLink,
"https://apps.powerapps.com/play/<APP-ID>?scr=details&recid=" &
Text(ThisItem.RequestId)
)
If using Launch, test the behavior on both desktop and Power Apps Mobile. The app must already be shared with the intended users, and they must have access to the environment and data.
#Beginner implementation checklist
- Create or identify
HomeScreen,DetailsScreen,LoadingScreen, andAccessDeniedScreen. - Add the Dataverse table to the app.
- Add the approved group-membership connector.
- Add
scrandrecidparameters to a test URL. - Read the parameters with
Paramin App > OnStart. - Validate the screen against an allowlist.
- Convert and validate the record ID.
- Check contributor-group membership.
- Look up the record only after the user passes the access check.
- Set the app's
StartScreenformula. - Bind the details form to the loaded record.
- Publish the app before testing the URL.
- Test with a contributor and a non-contributor.
- Test a valid record, an invalid
recid, a missingrecid, an unknownscr, and a record the user cannot access.
#Test URLs
Valid details link:
https://apps.powerapps.com/play/<APP-ID>?scr=details&recid=<VALID-RECORD-GUID>
Home link:
https://apps.powerapps.com/play/<APP-ID>?scr=home
Invalid or incomplete links to test:
https://apps.powerapps.com/play/<APP-ID>
https://apps.powerapps.com/play/<APP-ID>?scr=unknown
https://apps.powerapps.com/play/<APP-ID>?scr=details
https://apps.powerapps.com/play/<APP-ID>?scr=details&recid=not-a-guid
#Validation checklist
| Test | Expected result |
|---|---|
| Contributor opens a valid details link | Details screen opens with the requested record |
| Contributor opens a home link | Home screen opens |
| Non-contributor opens any link | Access denied screen opens and no protected record is displayed |
Missing scr parameter |
Home screen or safe default opens |
Unknown scr value |
Safe default opens; arbitrary navigation does not occur |
Missing or invalid recid |
Details screen does not open |
| Valid ID for a deleted record | User sees a not-found message |
| Valid ID without Dataverse permission | User sees a not-found or access message |
| App opened from Power Apps Mobile | Same routing behavior as browser testing |
| Published app tested | The latest published version handles the parameters |
#Common mistakes
- Calling
NavigatefromApp.StartScreeninstead of returning a screen. - Comparing a URL parameter to a GUID without converting it with
Guid. - Using the
scrparameter directly without an allowlist. - Loading the record before checking group membership.
- Treating group membership as a replacement for Dataverse table permissions.
- Hard-coding a group display name instead of using a stable group ID.
- Testing only in Studio and not testing the published app.
- Forgetting that connector permissions and user consent can affect the group check.
- Displaying a record from a local variable without confirming that the Dataverse lookup succeeded.