Skip to main content

PCF ControlManifest.Input.xml — Schema, Properties, and Dataset Configuration

This chapter provides an exhaustive, production-grade guide to the Power Apps Component Framework (PCF) manifest file (ControlManifest.Input.xml). It covers the XML schema, property node definitions for all supported types, dataset and column configurations for grid controls, usage types, schema validation, and advanced Application Lifecycle Management (ALM) practices.


1. Conceptual Foundation

The ControlManifest.Input.xml file is the foundational metadata contract of any Power Apps Component Framework (PCF) control. It acts as the declarative interface between the custom component's TypeScript implementation and the hosting Power Platform runtime environments (Unified Interface for model-driven apps, Canvas App player, and Power Pages).

+-----------------------------------------------------------------------+
| Power Apps Runtime |
| (Unified Interface / Canvas App Player / Power Pages / Test Harness) |
+-----------------------------------------------------------------------+
|
| Reads Contract & Binds Data
v
+-----------------------------------------------------------------------+
| ControlManifest.Input.xml |
| - Defines properties (bound, input, output) |
| - Declares datasets & property-sets |
| - Requests platform features (WebAPI, Utility, Device APIs) |
| - Lists resource dependencies (TypeScript, CSS, RESX) |
+-----------------------------------------------------------------------+
|
| Generates Typings (npm run build)
v
+-----------------------------------------------------------------------+
| TypeScript Component |
| - Implements ComponentFramework.StandardControl<IInputs, IOutputs> |
| - Consumes context.parameters |
| - Returns updated values via getOutputs() |
+-----------------------------------------------------------------------+

When the Power Platform runtime loads a form, view, or dashboard containing a PCF control, it parses this manifest to determine:

  1. Data Binding Requirements: Which columns (fields) or tables (datasets) the control expects to read from or write to.
  2. Configuration Parameters: Which static inputs or dynamic expressions the app maker must provide during design-time configuration.
  3. Resource Dependencies: The JavaScript/TypeScript bundles, stylesheets, and localized resource files (.resx) that must be loaded into the browser.
  4. Security and Feature Permissions: Which native device capabilities (e.g., camera, geolocation) or platform APIs (e.g., Dataverse WebAPI, Utility service) the control requires.

The Manifest Schema Structure

The manifest is validated against a strict XML Schema Definition (XSD) maintained by Microsoft. The root element is <manifest>, which contains a single <control> element. The <control> element defines the identity of the component and houses the configuration nodes:

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="DeveloperNamespace"
constructor="ControlClassName"
version="1.0.0"
display-name-key="Control_Display_Name"
description-key="Control_Description"
control-type="standard">

<!-- 1. Type Groups (Optional) -->
<type-group name="NumericTypes">
<type>Whole.None</type>
<type>Decimal</type>
<type>FP</type>
<type>Currency</type>
</type-group>

<!-- 2. Properties (Optional, multiple) -->
<property name="sampleProperty" display-name-key="Prop_Display" of-type="SingleLine.Text" usage="bound" required="true" />

<!-- 3. Datasets (Optional, multiple) -->
<data-set name="gridDataset" display-name-key="Dataset_Display">
<property-set name="associatedField" display-name-key="Field_Display" of-type="SingleLine.Text" usage="bound" required="true" />
</data-set>

<!-- 4. Resources (Required, exactly one) -->
<resources>
<code path="index.ts" order="1" />
<css path="css/styles.css" order="1" />
<resx path="strings/Strings.1033.resx" version="1.0.0" />
</resources>

<!-- 5. Feature Usages (Optional) -->
<feature-usage>
<uses-feature name="WebAPI" required="true" />
</feature-usage>

</control>
</manifest>

Property Nodes and Supported Types

Properties represent individual data points exchanged between the host application and the PCF control. Each <property> node must define its data type using either the of-type attribute (for a single explicit type) or the of-type-group attribute (referencing a pre-defined <type-group>).

The framework supports several core data types, each mapping to specific Dataverse column types and TypeScript interfaces:

1. SingleLine.Text

Maps to the Dataverse "Single Line of Text" column. In TypeScript, this is represented as ComponentFramework.PropertyTypes.StringProperty. It supports various format options such as SingleLine.Email, SingleLine.Phone, SingleLine.URL, SingleLine.TextArea, and SingleLine.Ticker.

2. Whole.Number

Maps to the Dataverse "Whole Number" column. In TypeScript, this is represented as ComponentFramework.PropertyTypes.NumberProperty. Format options include:

  • Whole.None: A standard integer.
  • Whole.Duration: Stored as minutes but formatted as hours/days in the UI.
  • Whole.Language: Stored as an LCID integer (e.g., 1033 for English).
  • Whole.TimeZone: Stored as a timezone code integer.

3. Lookup.Simple

Allows the control to bind to a Dataverse lookup column, establishing a reference to a record in a target table. In TypeScript, this maps to ComponentFramework.PropertyTypes.LookupProperty.

Warning: If the manifest contains at least one <data-set> element, any properties of type Lookup.Simple must be wrapped inside the <data-set> element to prevent schema validation failures. Currently, Lookup.Simple is supported only in model-driven apps.

4. OptionSet

Maps to Dataverse "Choice" (formerly OptionSet) columns. In TypeScript, this is represented as ComponentFramework.PropertyTypes.OptionSetProperty. It provides access to the selected integer value, the formatted text label, and the complete list of available options via the attributes metadata.

5. DateAndTime

Maps to Dataverse "Date and Time" columns. In TypeScript, this is represented as ComponentFramework.PropertyTypes.DateTimeProperty. It supports two formats:

  • DateAndTime.DateAndTime: Displays and stores both date and time.
  • DateAndTime.DateOnly: Displays and stores only the date portion.

Property Usage Types

The usage attribute on a <property> or <property-set> node defines how data flows between the host application and the PCF control:

  • bound: The property is bidirectionally linked to a specific table column. The control can read the current value from the column and write updates back to it by calling context.parameters.propertyName.raw and notifying the host via notifyOutputChanged().
  • input: The property is read-only. The app maker can bind it to a column or provide a static value (e.g., a configuration string or numeric limit) in the maker portal. The control cannot write updates back to this property.
  • output: The property is write-only from the perspective of the host app. The control calculates a value internally and exposes it to the host application (primarily used in Canvas apps to output values to other controls or formulas).

Dataset and Column Configuration

For grid and list controls, the <data-set> element defines a configurable representation of a set of table records.

  • Primary vs. Secondary Datasets: You can define multiple <data-set> elements. The first dataset declared in the manifest is the primary dataset and is automatically named Items in Canvas apps. Secondary datasets will have a _Items suffix appended to their configured name.
  • Property-Sets: A <property-set> is an inner configuration node within a <data-set>. It forces the app maker to map a specific column from the bound dataset to a property expected by the control. For example, a map control might require a property-set named Latitude of type Decimal to extract coordinates from the dataset rows.
  • CDS Data Set Options: The cds-data-set-options attribute (applicable to model-driven apps) controls the integration of platform grid features. It accepts a semicolon-delimited string: displayCommandBar:true;displayViewSelector:true;displayQuickFind:true.

2. Architecture & Decision Matrix

When designing custom user interface extensions in the Power Platform, architects must choose the correct extensibility mechanism. The table below compares PCF controls with alternative approaches:

Feature / DimensionPCF Controls (Field/Dataset)Model-Driven Web Resources (HTML/JS)Canvas App Custom ControlsDataverse Plugins (C#)Power Automate Cloud Flows
ComplexityHigh (TypeScript, React, Webpack, XML Manifest)Medium (HTML, CSS, JavaScript)Low (No-code / Low-code formulas)High (C#, .NET Framework, SDK)Low to Medium (Declarative designers)
ScalabilityHigh (Compiled, bundled, client-side execution)Medium (Unstructured files, manual optimization)High (Managed by Canvas runtime)High (Server-side execution, transactional)Medium (Subject to API limits and run-time throttling)
Execution ContextClient-side (Runs inline within form or grid DOM)Client-side (Runs inside an isolated iframe)Client-side (Runs within Canvas player)Server-side (Runs within Dataverse pipeline)Server-side (Asynchronous or synchronous API call)
Offline SupportYes (Fully integrated with mobile offline profiles)No (Requires custom caching and offline logic)Yes (Using SaveData and LoadData functions)No (Requires active server connection)No (Requires active cloud connectivity)
LicensingStandard (unless using premium APIs/external services)StandardStandardStandard (requires Dataverse capacity)Premium (depending on connectors used)
PL-400 RelevanceHigh (Core focus of pro-developer questions)Medium (Legacy customization, still tested)Low (Focus is on pro-dev tools)High (Core focus of server-side questions)Medium (Integration and orchestration focus)

Architectural Decision Flowchart

Use the following decision rules to determine if a PCF control is the correct architectural choice:

  1. Is the requirement purely server-side validation, data manipulation, or transactional integrity?
    • Yes: Use a Dataverse Plugin.
    • No: Proceed to step 2.
  2. Is the requirement to build a highly reusable, performant UI component that must look and feel like a native platform control in both Model-Driven and Canvas apps?
    • Yes: Use a PCF Control.
    • No: Proceed to step 3.
  3. Is the requirement to embed a single-use, complex custom page or dashboard with external API integrations inside a Model-Driven form?
    • Yes: Consider a Canvas App (Embedded) or a Custom Page.
    • No: Proceed to step 4.
  4. Is the requirement a simple UI tweak (e.g., hiding a field, showing a basic alert, or changing a tab color)?
    • Yes: Use Form XML / Client API (JavaScript Web Resource).

3. Step-by-Step Implementation Guide

This guide walks through initializing, configuring, implementing, and deploying a PCF control that utilizes all supported property types and a dataset configuration.

Step 1: Initialize the PCF Project

Open a developer command prompt (such as the VS Code integrated terminal or the Developer Command Prompt for Visual Studio) and create a new directory for your project.

# Create project directory
mkdir EnterpriseGridControl
cd EnterpriseGridControl

# Initialize PCF project using the dataset template
pac pcf init --namespace Contoso --name EnterpriseGrid --template dataset --framework react --run-npm-install
  • --namespace: Defines the logical namespace (Contoso).
  • --name: Defines the class name of the control (EnterpriseGrid).
  • --template: Specifies dataset (use field for individual column controls).
  • --framework: Specifies react to pre-configure React and Fluent UI dependencies.
  • --run-npm-install: Automatically runs npm install to fetch node modules.

Step 2: Configure the Control Manifest

Open the generated EnterpriseGrid/ControlManifest.Input.xml file. Replace its contents with the following configuration to define properties for all supported types, a dataset, and platform feature usages.

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="Contoso"
constructor="EnterpriseGrid"
version="1.0.0"
display-name-key="Enterprise_Grid_Display_Name"
description-key="Enterprise_Grid_Description"
control-type="standard">

<!-- Define a type group for numeric properties -->
<type-group name="NumericGroup">
<type>Whole.None</type>
<type>Decimal</type>
<type>FP</type>
<type>Currency</type>
</type-group>

<!-- Primary Dataset Configuration -->
<data-set name="gridDataset"
display-name-key="Grid_Dataset_Display"
cds-data-set-options="displayCommandBar:true;displayViewSelector:true;displayQuickFind:true">

<!-- Property-set mapped to a text column in the dataset -->
<property-set name="rowTitle"
display-name-key="Row_Title_Display"
description-key="The primary text column for the row."
of-type="SingleLine.Text"
usage="bound"
required="true" />

<!-- Property-set mapped to a numeric column in the dataset -->
<property-set name="rowValue"
display-name-key="Row_Value_Display"
description-key="The numeric value column for calculations."
of-type-group="NumericGroup"
usage="bound"
required="true" />
</data-set>

<!-- Configurable Input Properties (Field Level) -->
<property name="textInput"
display-name-key="Text_Input_Display"
description-key="A configurable text input parameter."
of-type="SingleLine.Text"
usage="input"
required="false" />

<property name="numberInput"
display-name-key="Number_Input_Display"
description-key="A configurable numeric input parameter."
of-type-group="NumericGroup"
usage="input"
required="false" />

<property name="choiceInput"
display-name-key="Choice_Input_Display"
description-key="A configurable choice/optionset parameter."
of-type="OptionSet"
usage="input"
required="false" />

<property name="dateTimeInput"
display-name-key="DateTime_Input_Display"
description-key="A configurable date and time parameter."
of-type="DateAndTime.DateAndTime"
usage="input"
required="false" />

<!-- Resource Declarations -->
<resources>
<code path="index.ts" order="1" />
<css path="css/EnterpriseGrid.css" order="1" />
<resx path="strings/EnterpriseGrid.1033.resx" version="1.0.0" />
</resources>

<!-- Request Platform Features -->
<feature-usage>
<uses-feature name="WebAPI" required="true" />
<uses-feature name="Utility" required="true" />
</feature-usage>

</control>
</manifest>

Step 3: Generate Manifest Typings

Run the build utility to parse the manifest and generate the strongly-typed TypeScript interface file (generated/ManifestTypes.d.ts).

npm run refreshTypes

This command generates the IInputs and IOutputs interfaces, ensuring compile-time safety when accessing properties and datasets in your code.

Step 4: Create Resource Directories and Files

Create the CSS and localization resource files declared in the manifest.

# Create directories
mkdir EnterpriseGrid/css
mkdir EnterpriseGrid/strings

# Create empty CSS file
New-Item -Path "EnterpriseGrid/css/EnterpriseGrid.css" -ItemType File

# Create RESX file for English (LCID 1033)
New-Item -Path "EnterpriseGrid/strings/EnterpriseGrid.1033.resx" -ItemType File

Open EnterpriseGrid/strings/EnterpriseGrid.1033.resx and populate it with standard XML localization schema:

<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="0" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Enterprise_Grid_Display_Name" xml:space="preserve">
<value>Enterprise React Grid Control</value>
</data>
<data name="Enterprise_Grid_Description" xml:space="preserve">
<value>A high-performance React grid control for enterprise data visualization.</value>
</data>
<data name="Grid_Dataset_Display" xml:space="preserve">
<value>Records Dataset</value>
</data>
<data name="Row_Title_Display" xml:space="preserve">
<value>Row Title Column</value>
</data>
<data name="Row_Value_Display" xml:space="preserve">
<value>Row Value Column</value>
</data>
<data name="Text_Input_Display" xml:space="preserve">
<value>Configuration Text</value>
</data>
<data name="Number_Input_Display" xml:space="preserve">
<value>Threshold Limit</value>
</data>
<data name="Choice_Input_Display" xml:space="preserve">
<value>Default Status</value>
</data>
<data name="DateTime_Input_Display" xml:space="preserve">
<value>Effective Date</value>
</data>
</root>

Step 5: Implement the TypeScript Control Logic

Open EnterpriseGrid/index.ts. Replace its contents with the complete, production-grade implementation provided in Section 4.

Step 6: Build and Validate the Component

Compile the TypeScript code and validate the manifest against the framework's schema.

# Build the component
npm run build

This command compiles the TypeScript code into a single bundle.js file inside the out folder and reformats the manifest into out/ControlManifest.xml.

Step 7: Test Locally in the Test Harness

Launch the local PCF test harness to verify control rendering, property binding, and dataset interactions.

npm start watch

This command opens a browser window pointing to http://localhost:8181. You can use the control panel on the right to modify input properties, load mock CSV data into the dataset, and inspect output values.


4. Complete Code Reference

TypeScript Component Implementation (index.ts)

This production-grade TypeScript file implements the PCF control lifecycle. It handles dataset paging, sorting, filtering, container resizing, and localized string retrieval.

/**
* Contoso Enterprise Grid PCF Control
* Implements standard dataset and field-level property bindings.
*/

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as React from "react";
import * as ReactDOM from "react-dom";
import {
DetailsList,
IColumn,
DetailsListLayoutMode,
SelectionMode,
PrimaryButton,
Label,
Stack,
Separator
} from "@fluentui/react";

export class EnterpriseGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private notifyOutputChanged: () => void;
private context: ComponentFramework.Context<IInputs>;
private selectedRecordIds: string[] = [];

/**
* Empty constructor.
*/
constructor() {}

/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via the Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the utility node.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.context = context;
this.notifyOutputChanged = notifyOutputChanged;
this.container = container;

// Enable container resize tracking to dynamically adjust layout
this.context.mode.trackContainerResize(true);
}

/**
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container size, layout configs, etc.
* @param context The entire property bag available to control via the Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
*/
public updateView(context: ComponentFramework.Context<IInputs>): void {
this.context = context;

const dataset = context.parameters.gridDataset;
if (dataset.error) {
this.renderError(dataset.errorMessage || "An error occurred while loading the dataset.");
return;
}

// Map dataset columns to Fluent UI DetailsList columns
const columns: IColumn[] = dataset.columns.map((col) => {
return {
key: col.name,
name: col.displayName,
fieldName: col.name,
minWidth: 100,
maxWidth: col.visualSizeFactor ? col.visualSizeFactor * 150 : 200,
isResizable: true,
isSorted: col.isSorted,
isSortedDescending: col.isSortedDescending,
onColumnClick: (ev, column) => this.onColumnHeaderClick(column)
};
});

// Map dataset records to Fluent UI items
const items = dataset.sortedRecordIds.map((id) => {
const record = dataset.records[id];
const item: { [key: string]: string | number | boolean | Date } = {
key: id
};
dataset.columns.forEach((col) => {
item[col.name] = record.getFormattedValue(col.name);
});
return item;
});

// Retrieve localized strings and input properties
const configText = context.parameters.textInput.raw || "N/A";
const threshold = context.parameters.numberInput.raw !== null ? context.parameters.numberInput.raw : "N/A";
const choiceVal = context.parameters.choiceInput.raw !== null ? context.parameters.choiceInput.raw : "N/A";
const dateVal = context.parameters.dateTimeInput.raw ? context.parameters.dateTimeInput.raw.toLocaleDateString() : "N/A";

// Render the React Component Tree
ReactDOM.render(
React.createElement(
Stack,
{ tokens: { childrenGap: 15 }, styles: { root: { width: "100%", height: "100%" } } },

// Header Metadata Panel
React.createElement(
Stack,
{ horizontal: true, tokens: { childrenGap: 20 }, styles: { root: { padding: 10, background: "#f3f2f1" } } },
React.createElement(Label, null, `Config Text: `${configText}``),
React.createElement(Label, null, `Threshold: `${threshold}``),
React.createElement(Label, null, `Choice Value: `${choiceVal}``),
React.createElement(Label, null, `Effective Date: `${dateVal}``)
),

React.createElement(Separator, null),

// Fluent UI DetailsList Grid
React.createElement(
DetailsList,
{
items: items,
columns: columns,
setKey: "set",
layoutMode: DetailsListLayoutMode.justified,
selectionMode: SelectionMode.multiple,
onActiveItemChanged: (item) => this.onRowSelectionChange(item),
compact: false
}
),

// Paging Controls Footer
React.createElement(
Stack,
{ horizontal: true, horizontalAlign: "space-between", verticalAlign: "center", styles: { root: { padding: 10 } } },
React.createElement(Label, null, `Total Records: `${dataset.paging.totalResultCount}``),
React.createElement(
Stack,
{ horizontal: true, tokens: { childrenGap: 10 } },
React.createElement(
PrimaryButton,
{
text: "Previous Page",
disabled: !dataset.paging.hasPreviousPage,
onClick: () => dataset.paging.loadPreviousPage()
}
),
React.createElement(
PrimaryButton,
{
text: "Next Page",
disabled: !dataset.paging.hasNextPage,
onClick: () => dataset.paging.loadNextPage()
}
)
)
)
),
this.container
);
}

/**
* Handles column header clicks to apply sorting to the dataset.
*/
private onColumnHeaderClick(column: IColumn): void {
const dataset = this.context.parameters.gridDataset;
const currentSort = dataset.sorting.find((s) => s.name === column.key);

const newSort = {
name: column.key,
sortDirection: currentSort && currentSort.sortDirection === 0 ? 1 : 0
};

dataset.sorting = [newSort];
dataset.refresh();
}

/**
* Handles row selection changes and updates the selected record IDs.
*/
private onRowSelectionChange(item: any): void {
if (item && item.key) {
const index = this.selectedRecordIds.indexOf(item.key);
if (index > -1) {
this.selectedRecordIds.splice(index, 1);
} else {
this.selectedRecordIds.push(item.key);
}
this.context.parameters.gridDataset.setSelectedRecordIds(this.selectedRecordIds);
}
}

/**
* Renders a standard error message in the container.
*/
private renderError(message: string): void {
ReactDOM.render(
React.createElement(
Stack,
{ horizontalAlign: "center", verticalAlign: "center", styles: { root: { height: "100%", color: "#a80000" } } },
React.createElement(Label, { styles: { root: { fontWeight: "bold" } } }, "Error Loading Component"),
React.createElement(Label, null, message)
),
this.container
);
}

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object of the type IOutputs
*/
public getOutputs(): IOutputs {
return {};
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing event listeners, disposing React components, etc.
*/
public destroy(): void {
ReactDOM.unmountComponentAtNode(this.container);
}
}

5. Configuration & Environment Setup

To build and deploy PCF controls, your local development environment must be configured with the correct dependencies and environment variables.

Prerequisites and Tooling

  1. Node.js LTS: Install Node.js (v18.x or v20.x recommended).
  2. Power Platform CLI (pac): Install via the Power Platform Tools extension for VS Code or as a standalone MSI installer.
  3. MSBuild / .NET Build Tools: Install the .NET SDK (v6.0 or later) and Visual Studio Build Tools (including the "MSBuild" component).

Solution Publisher Prefix Rules

When packaging PCF controls into Dataverse solutions, you must use a valid solution publisher prefix.

  • The prefix must be 2 to 8 characters long.
  • It can only contain alphanumeric characters ([A-Z, a-z, 0-9]).
  • It must start with a letter.
  • It cannot start with reserved prefixes such as mscrm, microsoft, or sys.

Environment Variables

Ensure the following paths are added to your system's PATH environment variable:

  • C:\Program Files\nodejs\
  • C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin (or your corresponding MSBuild path)

Packaging Configuration (Bicep / ARM Deployment)

To automate the deployment of the Dataverse solution containing your PCF control, you can use an ARM or Bicep template to provision the environment settings. The following Bicep snippet enables the Power Apps Component Framework for Canvas Apps in the target environment:

param environmentName string
param location string = resourceGroup().location

resource environmentSettings 'Microsoft.PowerPlatform/environments/settings@2020-10-01' = {
name: '`${environmentName}`/web'
location: location
properties: {
// Enable PCF controls for Canvas Apps in the environment
isFlexOrgPCFEnabled: true
}
}

output pcfEnabled bool = environmentSettings.properties.isFlexOrgPCFEnabled

6. Security & Permission Matrix

PCF controls execute within the security context of the logged-in user. They cannot bypass Dataverse security roles, field-level security profiles, or column-level permissions.

PrincipalPermission / PrivilegeScopeReason
System Customizer / AdministratorCreate, Write on WebResource tableOrganizationRequired to import, update, and publish the compiled PCF control bundle.
System Customizer / AdministratorCreate, Write on CanvasApp tableOrganizationRequired to import and configure PCF controls within Canvas Apps.
End UserRead on WebResource tableOrganizationRequired to download and execute the compiled bundle.js in the browser.
End UserRead, Write, AppendTo on target tablesUser / Business Unit / OrgRequired to read and update records bound to the control's properties or datasets.
End UserField-Level Security Profile (Read/Write)ColumnIf a column bound to a PCF property is secured, the user must have explicit read/write access in their profile, or the control will receive null or fail to update.
App Registration (CI/CD)prvWriteWebResource, prvPublishCustomizationsOrganizationRequired for automated deployment pipelines to push solution packages containing PCF controls.

7. Pipeline Execution Internals

Understanding the client-side execution pipeline and lifecycle hooks of a PCF control is critical for managing state, performance, and transactional boundaries.

[Host App Initialization]
|
v
[init()] <--- 1. Executed once. Receives container, context, and notifyOutputChanged.
|
v
[updateView()] <--- 2. Executed on load and whenever bound data, container size, or parameters change.
|
+-----------------------+
| |
(User Action) (External Data Change)
| |
v v
[notifyOutputChanged()] [updateView()]
|
v
[getOutputs()] <--- 3. Framework queries control for updated values.
|
v
[updateView()] <--- 4. Framework pushes updated values back to control.
|
v
[destroy()] <--- 5. Executed when control is removed from DOM. Clean up resources.

1. The Lifecycle Hooks

  • init(context, notifyOutputChanged, state, container): Executed exactly once when the control is loaded. It is used to initialize local variables, establish event listeners, and kick off asynchronous calls. Dataset values are not guaranteed to be populated during this phase.
  • updateView(context): Executed immediately after init and subsequently whenever any bound property, dataset, container size, or framework state changes. This method must be idempotent; it should completely redraw the UI based on the current context state.
  • getOutputs(): Executed by the framework immediately after the control calls notifyOutputChanged(). It returns an object matching the IOutputs interface containing the updated values of bound properties.
  • destroy(): Executed when the control is removed from the DOM (e.g., navigating away from a form or closing a tab). Developers must unmount React trees, remove event listeners, and clear intervals to prevent memory leaks.

2. Transaction Boundaries and Async Behavior

  • No Database Transactions: PCF controls operate entirely on the client side. Updates sent via notifyOutputChanged() or the WebAPI service do not participate in Dataverse database transactions. If a form save fails server-side validation, the PCF control's changes may be rolled back in the UI, but any direct WebAPI calls made by the control are already committed.
  • Async Data Loading: Datasets load asynchronously. When a dataset is paging or sorting, updateView is called with the dataset.loading property set to true. The control should display a loading spinner during this state.

3. Sandbox Mode and Callout Restrictions

  • Canvas Apps Sandbox: In Canvas Apps, PCF controls are executed inside an isolated iframe hosted on a different domain (ur.apps.appsplatform.us). This sandbox enforces strict Cross-Origin Resource Sharing (CORS) policies. Direct DOM access to the parent window is blocked.
  • Model-Driven Inline Execution: In Model-Driven apps, PCF controls run inline within the main application DOM. While this allows direct DOM manipulation, it is highly discouraged as it can break during platform updates.
  • External Network Calls: To make network calls to external APIs, the control must declare the domains in the <external-service-usage> element in the manifest. If not declared, the calls will be blocked by the browser's Content Security Policy (CSP) in Canvas Apps.

8. Error Handling & Retry Patterns

Robust error handling is essential to prevent a single failing PCF control from crashing the hosting form or grid.

Common Failure Modes and Error Codes

Error ScenarioRoot CauseDiagnostic StepsRemediation
Dataset Load FailureInvalid view configuration or missing user read privileges on the target table.Check context.parameters.gridDataset.error and inspect errorMessage.Wrap the control rendering in an error boundary and display a localized error message.
WebAPI Rate LimitingExceeding Dataverse API limits (Service Protection Limits).Inspect browser network tab for HTTP 429 Too Many Requests.Implement exponential back-off retry logic for all WebAPI calls.
Invalid Property BindingMaker bound a numeric property to a text column.Check browser console for schema mismatch warnings during initialization.Ensure the manifest <property> node uses the correct of-type or of-type-group.

TypeScript Implementation: Exponential Back-off Retry Pattern

When executing Dataverse WebAPI calls from a PCF control, use the following pattern to handle transient network failures and API rate limits (HTTP 429):

export class WebApiRetryHandler {
private webAPI: ComponentFramework.WebApi;

constructor(webAPI: ComponentFramework.WebApi) {
this.webAPI = webAPI;
}

/**
* Executes a Dataverse WebAPI retrieve operation with exponential back-off retry logic.
* @param entityLogicalName The logical name of the table.
* @param id The record GUID.
* @param options OData system query options.
* @param maxRetries Maximum number of retry attempts.
* @param delay Initial delay in milliseconds.
*/
public async retrieveRecordWithRetry(
entityLogicalName: string,
id: string,
options: string,
maxRetries: number = 3,
delay: number = 1000
): Promise<ComponentFramework.WebApi.Entity> {
let attempt = 0;

while (attempt < maxRetries) {
try {
return await this.webAPI.retrieveRecord(entityLogicalName, id, options);
} catch (error: any) {
attempt++;
// Check for HTTP 429 (Too Many Requests) or transient network errors
if (error.status === 429 || error.status >= 500) {
if (attempt >= maxRetries) {
throw new Error(`Failed after `${maxRetries}` attempts. Error: `${error.message}``);
}
// Calculate exponential back-off delay with jitter
const backoffDelay = delay * Math.pow(2, attempt) + Math.random() * 100;
console.warn(`WebAPI call rate-limited. Retrying in `${backoffDelay.toFixed(0)}`ms...`);
await this.sleep(backoffDelay);
} else {
// Do not retry client errors (e.g., 400 Bad Request, 403 Forbidden)
throw error;
}
}
}
throw new Error("Maximum retry attempts reached.");
}

private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

9. Performance Optimisation & Limits

To ensure smooth rendering and stay within platform resource limits, PCF controls must be optimized for data retrieval and DOM rendering.

Delegation and Paging Limits

  • Dataset Paging Size: By default, Dataverse datasets return 50 records per page. You can adjust this value using context.parameters.gridDataset.paging.setPageSize(pageSize). Setting this value too high (e.g., > 5000) will degrade client-side rendering performance and increase memory usage.
  • Delegation in Canvas Apps: When bound to a Canvas App dataset, ensure that any filtering or sorting applied via context.parameters.gridDataset.filtering or sorting uses delegable operators. Non-delegable queries will trigger delegation warnings and limit the returned dataset to the app's delegation limit (default 500 records).

Container Resize Optimization

The updateView method is called repeatedly during browser window resizing if trackContainerResize(true) is enabled. To prevent rendering lag, implement a debounce pattern to delay heavy UI recalculations:

import { debounce } from "@fluentui/react";

export class EnterpriseGrid {
private debouncedRender: () => void;

public init(context: any, notifyOutputChanged: any, state: any, container: HTMLDivElement) {
context.mode.trackContainerResize(true);

// Debounce the render function to execute at most once every 150ms during resize
this.debouncedRender = debounce(() => {
this.renderUI();
}, 150);
}

public updateView(context: any) {
this.debouncedRender();
}

private renderUI() {
// Perform heavy DOM rendering or React updates here
}
}

10. ALM & Deployment Checklist

Moving a PCF control from development to production requires strict adherence to versioning rules and automated packaging pipelines.

Versioning Rules

The <control> element in the manifest defines the component version using a Major.Minor.Patch semantic versioning format (e.g., 1.0.0).

  • Patch Version Increment: When deploying an update to an existing PCF control, you must increment the Patch version (e.g., 1.0.0 to 1.0.1). If the version is not incremented, the Dataverse solution import will succeed, but the browser will continue to load the cached older version of the control.
  • CLI Versioning Command: You can automate version increments in your build pipeline using the Power Platform CLI:
    # Automatically increment the patch version in the manifest
    pac pcf version --strategy manifest

Automated Build and Deployment Pipeline (GitHub Actions)

The following YAML workflow automates building, packaging, and deploying the PCF control to a Dataverse environment using the Microsoft Power Platform Build Tools:

name: Build and Deploy PCF Control

on:
push:
branches: [ main ]

jobs:
build-pcf:
runs-on: windows-latest

steps:
- name: Checkout Code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-size: 18.x

- name: Install Dependencies
run: npm install

- name: Build PCF Control
run: npm run build

- name: Increment PCF Version
run: pac pcf version --strategy manifest

- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1

- name: Initialize Solution Project
run: |
mkdir Solution
cd Solution
pac solution init --publisher-name ContosoPublisher --publisher-prefix contoso
pac solution add-reference --path ..\

- name: Build Solution (Generate Managed Solution)
run: |
cd Solution
msbuild /p:configuration=Release /restore

- name: Import Solution to Dataverse
uses: microsoft/powerplatform-actions/import-solution@v0
with:
environment-url: 'https://org.crm.dynamics.com'
app-id: `${{ secrets.POWERPLATFORM_CLIENT_ID }`}
client-secret: `${{ secrets.POWERPLATFORM_CLIENT_SECRET }`}
tenant-id: `${{ secrets.POWERPLATFORM_TENANT_ID }`}
solution-file: 'Solution\bin\Release\Solution.zip'
force-overwrite: true
publish-changes: true

11. Common Pitfalls & Troubleshooting Guide

1. Missing Lookup Wrapper in Dataset Manifests

  • Symptom: Schema validation error pcf-1012: Lookup.Simple properties must be wrapped in a dataset element during build.
  • Root Cause: The manifest contains a <data-set> element, and a separate <property> of type Lookup.Simple is declared at the root level of the <control>.
  • Diagnostic Steps: Check if both <data-set> and <property of-type="Lookup.Simple"> exist as direct siblings.
  • Fix: Move the lookup property definition inside the <data-set> element as a <property-set>.

2. Mismatched getOutputs Keys

  • Symptom: Changes made in the PCF control UI are not saved to the Dataverse form, and no network calls are triggered.
  • Root Cause: The keys returned in the object from the getOutputs() method do not match the exact casing and names of the bound properties defined in the manifest.
  • Diagnostic Steps: Compare the property names in ControlManifest.Input.xml with the keys returned in getOutputs().
  • Fix: Ensure exact case-sensitive matching:
    public getOutputs(): IOutputs {
    return {
    textInput: this.currentValue // Must match name="textInput" in manifest
    };
    }

3. Canvas App Sandbox CORS Block

  • Symptom: External API calls fail with Access-Control-Allow-Origin or CSP violations in Canvas Apps, but work in the local test harness.
  • Root Cause: Canvas Apps execute PCF controls in a sandboxed iframe on a different domain.
  • Diagnostic Steps: Open browser developer tools (F12) and check the Console tab for Content Security Policy (CSP) errors.
  • Fix: Declare the target domains in the <external-service-usage> element in the manifest:
    <external-service-usage enabled="true">
    <domain>api.contoso.com</domain>
    </external-service-usage>

4. Stale Control Cache After Deployment

  • Symptom: The solution imports successfully, but the browser continues to render the old version of the PCF control.
  • Root Cause: The control version in the manifest was not incremented before packaging, causing the browser to load the cached bundle.js.
  • Diagnostic Steps: Inspect the network tab in browser developer tools, locate bundle.js, and check its headers or version query string.
  • Fix: Increment the patch version in ControlManifest.Input.xml (e.g., from 1.0.0 to 1.0.1) and redeploy.

5. trackContainerResize Not Called

  • Symptom: context.mode.allocatedWidth and allocatedHeight return -1 or stale values during window resizing.
  • Root Cause: context.mode.trackContainerResize(true) was not called during the init lifecycle hook.
  • Diagnostic Steps: Check the init method in index.ts for the registration call.
  • Fix: Add the call to init:
    public init(context: any, notifyOutputChanged: any, state: any, container: HTMLDivElement) {
    context.mode.trackContainerResize(true);
    }

6. Invalid Namespace or Constructor Casing

  • Symptom: Build fails with pcf-1001: Control namespace or constructor contains invalid characters.
  • Root Cause: The namespace or constructor attributes in the manifest contain spaces, special characters, or start with a number.
  • Diagnostic Steps: Inspect the <control> element attributes in the manifest.
  • Fix: Ensure they only contain alphanumeric characters and start with a letter.

7. Missing Resource Files in Manifest

  • Symptom: Runtime error Resource not found or styles are missing when the control is loaded in Dataverse.
  • Root Cause: A CSS, RESX, or image file exists in the project directory but was not declared in the <resources> node of the manifest.
  • Diagnostic Steps: Check the browser console for 404 errors loading resources and verify against the manifest <resources> node.
  • Fix: Add the missing resource element:
    <resources>
    <css path="css/EnterpriseGrid.css" order="1" />
    </resources>

8. Using Unsupported Types in Canvas Apps

  • Symptom: The PCF control does not appear in the list of available components when editing a Canvas App.
  • Root Cause: The manifest declares properties of type Lookup.Simple, which is currently only supported in Model-Driven apps.
  • Diagnostic Steps: Check the property types in the manifest.
  • Fix: Remove Lookup.Simple properties or replace them with SingleLine.Text (storing the GUID) if the control must support Canvas Apps.

9. Unhandled allocatedHeight = -1 in Model-Driven Subgrids

  • Symptom: The control renders with a height of 0 pixels and is invisible on Model-Driven forms.
  • Root Cause: In Model-Driven subgrids, allocatedHeight returns -1 to indicate there is no height restriction. If the control relies on this value to set its height, it will collapse.
  • Diagnostic Steps: Log context.mode.allocatedHeight to the console during rendering.
  • Fix: If allocatedHeight is -1, set the container's CSS height to 100% or a default minimum height (e.g., 400px).

10. Multiple Code Elements in Manifest

  • Symptom: Build fails with pcf-1022: Only one code element is allowed inside resources.
  • Root Cause: The <resources> node contains more than one <code path="..." /> element.
  • Diagnostic Steps: Inspect the <resources> node in the manifest.
  • Fix: Ensure there is exactly one <code> element pointing to the entry TypeScript file (usually index.ts). Webpack will bundle all other imported TypeScript/JavaScript files into the single output bundle.

12. Exam Focus: Key Facts & Edge Cases

The following list highlights critical technical details, limits, and behaviors that are frequently tested in the PL-400: Microsoft Power Platform Developer certification exam:

  • Manifest File Name: The input manifest file must be named exactly ControlManifest.Input.xml. During the build process, it is compiled and output as ControlManifest.xml.
  • Primary Dataset Name: In Canvas Apps, the primary dataset defined in the manifest is always exposed to the maker with the name Items. Any secondary datasets will have the suffix _Items appended to their configured name.
  • Lookup.Simple Limitation: Properties of type Lookup.Simple are only supported in Model-Driven apps. They are not supported in Canvas Apps.
  • Lookup Wrapper Rule: If a manifest contains a <data-set> element, any properties of type Lookup.Simple must be defined inside the <data-set> element as a <property-set>.
  • Usage Attribute Values: The usage attribute on a <property> node supports exactly three values: bound, input, and output.
  • Type-Group Resolution in Canvas Apps: When a <type-group> is used in Canvas Apps, the framework resolves it to the "most compatible" base type:
    • SingleLine.Text, Multiple, TextArea resolve to String.
    • Decimal, FP, Whole.None, Currency resolve to Decimal.
    • DateAndTime.DateAndTime, DateAndTime.DateOnly resolve to Date.
  • trackContainerResize Requirement: To receive updated width and height values in context.mode.allocatedWidth and allocatedHeight, you must call context.mode.trackContainerResize(true) inside the init method.
  • Version Increment Rule: To force the browser to clear its cache and load a newly deployed version of a PCF control, you must increment the Patch version (the third digit, e.g., 1.0.1) in the manifest.
  • Feature Usages: To access the Dataverse WebAPI (context.webAPI) or utility functions (context.utils), you must explicitly declare them in the <feature-usage> section of the manifest:
    <feature-usage>
    <uses-feature name="WebAPI" required="true" />
    <uses-feature name="Utility" required="true" />
    </feature-usage>
  • Standard vs. Virtual Controls: Standard controls receive a physical HTML container (HTMLDivElement) to render their DOM. Virtual (React) controls do not receive a container; instead, they return a React element directly to the framework, which handles rendering and reconciliation, improving performance.