
Keep the record at the center of the work—and put a guided canvas experience beside it.
Model-driven apps give teams forms, views, security and business process flows. Canvas apps offer more freedom for an intake wizard, a call script or a focused workspace. A small HTML web resource can bring a standalone canvas app onto a record form while keeping it independently testable.
I use this pattern when a reusable standalone app is the priority. The trade-off is explicit: the host owns the launch contract, environment configuration and lifecycle. It does not provide the built-in canvas control’s form integration.
New to embedding? Start with Canvas apps on model-driven forms: the quick guide for the built-in options and when to choose this host.
01 / Choose the right integration
| Approach | Best fit | What to account for |
|---|---|---|
| HTML host + standalone canvas app | Reuse the same app on forms and through direct links. | Launch parameters, separate app sharing, player startup and manual lifecycle handling. |
| Built-in embedded canvas control | Use the platform’s ModelDrivenFormIntegration contract. | Its specific data, refresh and hosting constraints. |
| Custom page | Design a canvas-style experience within a model-driven app. | Navigation, supported controls and custom-page limitations. |
Start with the built-in integration when it meets the requirement. Choose a custom host because independent reuse matters, and budget for the integration code it creates.
02 / Define the launch contract
The flow is simple: record form → HTML web resource → canvas player → Dataverse. The form passes saved-record context to the host. The host builds the player URL. The canvas app reads parameters and retrieves data through its configured connection.
recordId: the saved row GUID, normalized without braces.appSource: the table logical name, such ascontact.clientUrl: the organization origin, if the app needs to build links.
These values identify context; they do not authorize access. Keep sensitive values out of URLs. App sharing, licensing, connection identity and Dataverse permissions still apply. The app does not automatically receive unsaved form edits.
03 / Build the HTML host
Use the copied app ID from each environment. The sample uses explicit organization origins and documented player URLs. Replace all placeholders, add test environments, and confirm the actual IDs after deployment; do not assume IDs always change or always remain the same.
The host exposes an initialization function instead of reaching into parent.Xrm.Page. URL construction preserves existing query parameters and encodes values through URL.searchParams. Repeated initialization with the same URL leaves the player running.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Canvas workspace host</title>
<style>
html,body { height:100%; margin:0; font-family:"Segoe UI",sans-serif; }
iframe { width:100%; height:100%; border:0; display:block; }
[hidden] { display:none !important; }
#status { padding:16px; }
</style>
<script>
// Replace every placeholder before importing this web resource.
// Exact organization origins avoid accidental substring matches.
const apps = new Map([
["https://YOUR-DEV-ORG.crm.dynamics.com",
"https://apps.powerapps.com/play/YOUR-DEV-APP-ID"],
["https://YOUR-PROD-ORG.crm.dynamics.com",
"https://apps.powerapps.com/play/YOUR-PROD-APP-ID"]
]);
let loadedUrl = "";
window.initializeCanvasHost = function (form, clientUrl) {
const frame = document.getElementById("canvas");
const status = document.getElementById("status");
const recordId = form.data.entity.getId().replace(/[{}]/g, "");
const appUrl = apps.get(new URL(clientUrl).origin);
if (!recordId || !appUrl) {
frame.hidden = true;
frame.removeAttribute("src");
loadedUrl = "";
status.hidden = false;
status.textContent = !recordId
? "Save the record, then reopen this panel."
: "No canvas app is configured for this environment.";
return;
}
const url = new URL(appUrl);
url.searchParams.set("source", "iframe");
url.searchParams.set("recordId", recordId);
url.searchParams.set("appSource", form.data.entity.getEntityName());
url.searchParams.set("clientUrl", new URL(clientUrl).origin);
if (loadedUrl === url.href) return;
status.hidden = false;
status.textContent = "Opening workspace. Sign in if prompted.";
frame.hidden = false;
frame.onload = function () {
// Frame load does not prove the app has finished loading or signed in.
status.textContent = "Workspace opened. Complete sign-in below if required.";
};
loadedUrl = url.href;
frame.src = loadedUrl;
};
</script>
</head>
<body>
<p id="status" role="status">Waiting for the form to initialize the workspace.</p>
<iframe id="canvas" title="Record workspace" hidden></iframe>
</body>
</html>
This host starts without delegated camera, microphone or location access. Add only the browser permissions your app requires. The frame’s load event indicates a document loaded; it does not confirm successful authentication or app readiness.
For a larger estate, move app configuration into a Dataverse environment variable and resolve it through an authorized configuration layer. That removes the need to edit the HTML per environment. The downloadable sample deliberately keeps that lookup outside its scope.
04 / Read context in Power Fx
Put the launch values in App.Formulas. The following contact example uses English-locale separators and assumes the Contacts data source and its primary key display name are available. Adjust names to match your app.
// App.Formulas — English-locale separators; Contact data source required.
fxRecordId = Param("recordId");
fxAppSource = Param("appSource");
fxIsEmbedded = !IsBlank(fxRecordId);
fxContactId = IfError(GUID(fxRecordId), Blank());
fxContact = If(
fxAppSource = "contact" && !IsBlank(fxContactId),
LookUp(Contacts, Contact = fxContactId)
);
A malformed GUID becomes blank rather than failing the lookup. Named formulas are evaluated as needed; they are not a guarantee of one network request or permanently fresh data. Provide loading, access-denied and missing-record states on the workspace screen.
// App.StartScreen — create these two screens first.
If(
fxAppSource = "contact" && !IsBlank(fxContactId),
scrContactWorkspace,
scrNoRecord
)
Create scrContactWorkspace and scrNoRecord before using this formula. A valid GUID does not establish that the user can read the record. Test with launch parameters in the published player; keep any Studio fallback record strictly in development and remove it before release.
05 / Add the host to the model-driven form
- Add the host HTML and form JavaScript as web resources in your solution. Include the canvas app and its dependencies.
- Place the HTML web resource in a generous single-column section. Use a meaningful accessible label and enough height for the workflow.
- Replace the control name in the script with its form control name. Register
TuneXCanvas.onLoadon form OnLoad and enable passing the execution context. - Publish the components and share the canvas app with the intended users or group. Form access alone does not grant canvas app access.
// Register TuneXCanvas.onLoad on form OnLoad; pass execution context.
// Replace WebResource_CanvasHost with the control NAME on the form.
var TuneXCanvas = {
onLoad: async function (executionContext) {
const form = executionContext.getFormContext();
const control = form.getControl("WebResource_CanvasHost");
if (!control) return;
try {
const host = await control.getContentWindow();
host.initializeCanvasHost(form, Xrm.Utility.getGlobalContext().getClientUrl());
} catch (error) {
form.ui.setFormNotification(
"The embedded workspace could not be initialized.",
"WARNING", "canvas-host");
console.error("Canvas host initialization failed", error);
}
}
};
getContentWindow() is the documented Unified Interface entry point for calling a web resource function. Microsoft notes that tab navigation can destroy and reinitialize the control. Where required, invoke the same initializer from the relevant tab’s TabStateChange handler. Register each event once.
On a new record the host asks the user to save first. This minimal example requires reopening or reinitializing the panel after that save. A production implementation can use a carefully managed PostSave handler. Decide when reloading is acceptable: relaunching the canvas player can discard in-progress work.
06 / Understand the messaging boundary
The original design adds a canvas PCF control that calls window.parent.postMessage. That is not included as ready-to-use code here. Microsoft’s PCF FAQ explicitly excludes Window access, and the player’s internal frame nesting is not a stable integration contract. A message may reach a different frame than expected.
If you investigate a bridge as a prototype, validate the browser topology first. Use an exact target origin, verify both the sender origin and expected source window, validate a versioned payload schema, allow-list operations and catch malformed JSON. Never use an unrestricted message to open arbitrary records or trigger privileged work. A refresh request must also account for dirty form data.
For production form actions, assess the built-in embedded control’s documented integration functions or a custom page. Browser support for postMessage does not establish support for a particular Power Apps integration.
07 / Check before release
- Record context: test saved and new records, invalid IDs, inaccessible records and switching between records.
- Identity: confirm app sharing, required licenses, connector configuration, Dataverse access and authentication in supported browsers.
- Lifecycle: test collapsed tabs, tab changes, navigation, form saves and repeated initialization. Do not promise lazy loading without measuring it.
- Experience: test keyboard navigation, focus, zoom, panel height and a narrow viewport. Check player startup with Monitor.
- ALM: verify each organization origin, app ID, solution dependency and environment-specific configuration in test before production.
- Data freshness: document how the app refreshes after its own writes and after form saves. Keep unsaved state explicit.
Keep the contract small
A thin host works well when the canvas experience needs saved-record context and independent reuse. Start with the one-way contract, make lifecycle behavior predictable, and add complexity only when the workflow requires it. Session recovery, environment-variable configuration and a comparison with custom pages are useful next steps.
References and source files
The downloads are illustrative starting points. They require configuration and testing in your tenant; no Dataverse runtime validation has been performed for this article.
- Microsoft Learn: canvas app embedding and launch parameters
- Microsoft Learn: getContentWindow and tab initialization
- Microsoft Learn: PCF support boundaries
Related: Dataverse Custom APIs for canvas apps.