PL-400 Power Platform Developer — Complete Study Guide
Everything you need to pass the PL-400 exam: technical design decisions, ALM, Power Apps, PCF, plug-ins, custom connectors, platform APIs, Azure Functions, and data integrations — with code, decision tables, and flashcards.
This guide covers the six PL-400 skill areas in the order they appear on the exam outline (March 2026 refresh). The heaviest domain — Extend the platform — gets the most depth because it is 30–35 % of the exam on its own. Use the flashcard sections at the end of each domain to test recall before moving on.
Passing score: 700 / 1000. Recommended prep time: 8–12 weeks.
| Domain | Weight | Priority |
|---|---|---|
| Create a technical design | 10–15% | Study second |
| Build Power Platform solutions | 10–15% | Study third |
| Implement Power Apps improvements | 10–15% | Study fourth |
| Extend the user experience | 10–15% | Study fifth |
| Extend the platform | 30–35% | Study first |
| Develop integrations | 10–15% | Study sixth |
1. Create a technical design
The exam opens with judgment calls: given a scenario, pick the right place to put logic and the right kind of storage.
Where to put business logic
Push logic as close to the data as the requirement allows. If it must fire regardless of how data is written, it belongs server-side.
| Mechanism | Runs where | Use when |
|---|---|---|
| Business rule | Client + server (scope-dependent) | Simple field-level logic with no code |
| Client script (JS) | Browser, model-driven forms only | UI behavior tied to form events |
| Power Fx | Client | Canvas app and command logic |
| Cloud flow | Cloud, async | Cross-system orchestration, approvals, scheduled jobs |
| Plug-in | Server, in transaction | Logic that must run for every write path — UI, Web API, import |
Decision rule: if the requirement says "must always run" or "must be server-side", the answer is a plug-in, not a client script or flow. Client scripts only fire inside the form UI.
Which table type
| Table type | Data lives | Use when |
|---|---|---|
| Standard | Dataverse | Default — full pipeline, auditing, relationships |
| Virtual | External source | System of record stays external; real-time reads needed |
| Elastic | Azure Cosmos DB–backed | Very high volume, sparse, write-heavy (IoT telemetry, logs) |
| Connector | Another service | You only need occasional reads/writes via flow or app |
Authentication and authorization strategy
- Interactive users → Microsoft Entra ID user auth (OAuth delegated).
- Unattended / server-to-server → service principal (app registration) or managed identity for Azure-hosted code.
- Plug-ins run under the calling user by default; you can override to a specific user in the step registration.
Security features that constrain solutions
- DLP policies — classify connectors as Business / Non-business / Blocked. Mixing Business and Non-business connectors in one flow is blocked. Plan connector classification before building.
- Security roles — privilege matrix: Create / Read / Write / Delete / Append / AppendTo / Assign / Share at access levels None / User / BU / Parent:Child BU / Org.
- Teams (owner vs access teams) and business units define row-ownership boundaries.
- Row sharing grants record-level access outside the role hierarchy — powerful but hard to audit at scale.
Reusable component types
| Component | Scope | Language |
|---|---|---|
| Canvas component / component library | Canvas apps | Power Fx |
| PCF code component | Canvas + model-driven | TypeScript |
| Custom connector | Flows + apps | OpenAPI definition |
| Custom API | Dataverse server | C# plug-in |
| Power Fx function | Canvas + custom pages | Power Fx |
Custom API replaces the older "custom action" pattern for code-first scenarios. Know the difference: custom APIs support private visibility, plug-in-only execution, and are fully solution-aware.
Flashcards — Technical design
Q: Logic must run whether data is saved from the UI, an import job, or the Web API. What extension point do you use? A: A plug-in. Client scripts only fire inside the form UI.
Q: You need to surface data that lives in an external system inside a Dataverse table in real time — no copy. A: Virtual table.
Q: A flow uses one Business connector and one Non-business connector. What happens? A: The DLP policy blocks the flow.
Q: You want a reusable, named, server-side Dataverse operation that other teams can call. A: Custom API.
2. Build Power Platform solutions
Everything you build travels inside a solution. The exam tests ALM mechanics and the CI/CD toolchain.
Solution types and the golden rule
| Solution type | Editable? | Use in |
|---|---|---|
| Unmanaged | Yes | Development environments |
| Managed | No (sealed) | Test and production environments |
Golden rule: develop in unmanaged, deploy as managed. Never import an unmanaged solution into production.
Environment variables
Externalize any value that changes per environment — API URLs, IDs, keys — so you can import the same solution package everywhere and set the current value in each environment after import.
Solution definition: env var "ServiceBaseUrl"
Dev current value: https://dev-api.contoso.com
Prod current value: https://api.contoso.com
Solution layers and dependencies
- Layers — multiple managed solutions can touch the same component. The active (unmanaged) layer wins for editing. Use See solution layers to diagnose unexpected values.
- Dependencies — a component that references another component creates a dependency. You cannot delete a component another solution requires. Use Show dependencies before removing anything.
ALM toolchain
| Tool | What it does |
|---|---|
Power Platform CLI (pac) | Export/import solutions, scaffold PCF, manage environments, run pipelines |
| Power Platform Pipelines | In-product deployment dev → test → prod, extendable with pre/post steps |
| Power Platform Build Tools | Azure DevOps / GitHub tasks for full CI/CD |
A typical Azure DevOps pipeline:
steps:
- task: PowerPlatformToolInstaller@2
- task: PowerPlatformExportSolution@2
inputs:
Environment: $(DevEnvUrl)
SolutionName: ContosoCore
SolutionOutputFile: $(Build.ArtifactStagingDirectory)/ContosoCore.zip
- task: PowerPlatformUnpackSolution@2 # unpack to source control
# — release pipeline —
- task: PowerPlatformImportSolution@2
inputs:
Environment: $(ProdEnvUrl)
SolutionInputFile: ContosoCore_managed.zip
Connection references
A connection reference abstracts a connector's connection so flows can be imported into a new environment and the connection can be set there without editing every flow.
Flashcards — Build solutions
Q: Which solution type do you import into production? A: Managed. Never unmanaged.
Q: Your API URL differs between dev and prod. How do you avoid hardcoding it? A: Environment variable — set the current value per environment after import.
Q: You can't delete a Dataverse table. Why? A: Another component depends on it. Check Show dependencies.
Q: What abstraction lets you import a flow into a new environment and set its connection there? A: Connection reference.
3. Implement Power Apps improvements
Canvas app performance and Power Fx fluency are tested here.
Delegation — the most tested canvas concept
When a query cannot be delegated, Power Apps runs it locally against only the first N rows of the data source (default 500, max 2000). The filter silently truncates results. The blue underline in the editor is a delegation warning.
Delegable operations on Dataverse: Filter, Search, LookUp using equality (=), comparisons (<, >, <=, >=), And, Or, field-level StartsWith. CountRows on a table is also delegable.
Non-delegable: CountIf, SortByColumns on non-indexed columns, First/Last after a sort.
// Delegable — Dataverse sends the query, only matching rows return
ClearCollect(
colTopAccounts,
SortByColumns(
Filter(Accounts, Revenue > 1000000 && StatusCode = "Active"),
"Revenue",
SortOrder.Descending
)
);
// Patch a new record back to Dataverse
Patch(
Accounts,
Defaults(Accounts),
{ Name: txtName.Text, Revenue: Value(txtRevenue.Text) }
);
Performance optimization
- Monitor — first stop for "app is slow" or "this shows wrong data." Attach to a live session; see every network call, formula evaluation, and timing.
- Concurrent() — load multiple data sources in parallel inside
OnStartto avoid serial round trips. - Component libraries — build a control once, share it across many apps; reduces per-app load.
- Model-driven performance — minimize columns on views, limit form scripts, hide related sub-grids that fire on load.
Calling cloud flows from canvas
// Call a flow and use its response
Set(
varResult,
ApproveExpense.Run(ThisItem.ID, txtComment.Text)
);
Notify(varResult.message, NotificationType.Success);
Flashcards — Power Apps
Q: A blue underline appears under your
Filtercall. What does it mean? A: Delegation warning — the filter can't run server-side. Results may be silently truncated.
Q: What are the default and maximum row limits for non-delegable queries? A: Default 500, max 2000 (set in app settings).
Q: A user says the app is slow on first open. What tool do you open first? A: Monitor.
Q: You want to load three SharePoint lists at the same time in
OnStart. What Power Fx function do you use? A:Concurrent().
4. Extend the user experience
Client scripting — model-driven forms
Client API runs against formContext, globalContext, attributes, and controls.
function onAccountLoad(executionContext) {
// Always get formContext from executionContext — never use the deprecated Xrm.Page
const formContext = executionContext.getFormContext();
const revenue = formContext.getAttribute("revenue").getValue();
if (revenue > 1000000) {
formContext.getControl("creditlimit").setVisible(true);
}
// Register a field-level change handler
formContext.getAttribute("industrycode")
.addOnChange(onIndustryChange);
}
Registration points: OnLoad, OnSave, field OnChange, TabStateChange. Always pass executionContext as the first parameter.
Calling the Dataverse Web API from client script
Xrm.WebApi.retrieveMultipleRecords(
"account",
"?$select=name,revenue&$filter=revenue gt 1000000&$top=10"
).then(
result => result.entities.forEach(e => console.log(e.name)),
error => console.error(error.message)
);
Power Apps Component Framework (PCF)
PCF lets you build custom TypeScript controls for both canvas and model-driven apps.
Lifecycle order — memorize this:
init → updateView → getOutputs → destroy
| Method | When it runs |
|---|---|
init | Once, at start — set up DOM and cache context |
updateView | Every time bound data or context changes |
getOutputs | When you call notifyOutputChanged() |
destroy | Clean-up — remove event listeners |
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class LinearInput
implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _value: number;
private _notifyOutputChanged: () => void;
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this._notifyOutputChanged = notifyOutputChanged;
// build UI, attach listeners
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
this._value = context.parameters.sampleProperty.raw || 0;
}
public getOutputs(): IOutputs {
return { sampleProperty: this._value };
}
public destroy(): void { /* remove listeners */ }
}
The manifest (ControlManifest.Input.xml) declares properties, feature usage (feature-usage section lists Device, WebAPI, Utility), and the component type (field vs dataset).
# Scaffold, build, and push a PCF component
pac pcf init --namespace Contoso --name LinearInput --template field
npm install && npm run build
pac pcf push --publisher-prefix contoso
Modern command bar
Use Power Fx commands (low-code, model-driven) or JavaScript commands. Use Xrm.Navigation.navigateTo to open a custom page from a command button.
Flashcards — User experience
Q: What is the recommended way to get
formContextin an OnLoad handler? A: From the execution context passed as the first parameter — never the deprecated globalXrm.Page.
Q: Put the PCF lifecycle methods in order. A:
init→updateView→getOutputs→destroy.
Q: Where does a PCF manifest declare that the control needs camera or Web API access? A: In the
feature-usagesection ofControlManifest.Input.xml.
Q: A field's value changed and you want the PCF to push the new value back to the form. What do you call? A:
notifyOutputChanged()— the framework then callsgetOutputs().
5. Extend the platform — the biggest domain (30–35%)
Study this section the most. It covers plug-ins, custom connectors, platform APIs, Azure Functions, and cloud flow developer concerns.
Dataverse plug-ins
A plug-in is a .NET class implementing IPlugin, registered against a message (Create, Update, Delete, or a custom API) and a pipeline stage.
Event pipeline stages
| Stage | Number | Runs | Typical use |
|---|---|---|---|
| Pre-validation | 10 | Before the main transaction | Validate and cancel early; runs before security checks |
| Pre-operation | 20 | Inside transaction, before the DB write | Modify the Target before it is saved |
| Post-operation | 40 | Inside transaction, after the write | React to the saved record; create related rows |
Synchronous plug-ins participate in the transaction — throwing an InvalidPluginExecutionException rolls everything back. Asynchronous plug-ins run after, via the system job queue.
A complete plug-in example
using Microsoft.Xrm.Sdk;
public class AccountPostCreate : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)
serviceProvider.GetService(typeof(IPluginExecutionContext));
if (context.MessageName != "Create" || context.Stage != 40)
return;
if (context.InputParameters["Target"] is Entity target)
{
var factory = (IOrganizationServiceFactory)
serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = factory.CreateOrganizationService(context.UserId);
// Create a related task
var task = new Entity("task")
{
["subject"] = "Review new account",
["regardingobjectid"] = new EntityReference("account", target.Id)
};
service.Create(task);
}
}
}
Pre Images and Post Images
Images are snapshots registered in the Plug-in Registration Tool alongside the step.
- Pre Image — record state before the operation. Available at Pre and Post stages. Required on Update when you need old field values, because
Targetonly carries the columns that changed. - Post Image — record state after the operation. Available at Post-operation only.
Entity preImage = context.PreEntityImages.Contains("PreImage")
? context.PreEntityImages["PreImage"]
: null;
var oldStatus = preImage?.GetAttributeValue<OptionSetValue>("statuscode");
On an Update plug-in, the Target entity only contains the columns the user changed. To read an unchanged column's old value, register a Pre Image.
Performance rules for plug-ins
- Keep synchronous plug-ins fast — they hold the database transaction. The platform enforces a 2-minute execution limit.
- Avoid querying inside loops; use
QueryExpressionwith the exact columns needed. - Move long-running work to an asynchronous registration or an Azure Function.
Registration
Use the Plug-in Registration Tool (pac tool prt) to register assemblies, steps, images, service endpoints, and webhooks.
Custom connectors
Wrap a REST API so it works natively in flows and apps.
- Define from an OpenAPI (Swagger) file, a Postman collection, or build from scratch.
- Auth options: No auth, API key, Basic, OAuth 2.0, Microsoft Entra ID.
- Policy templates — tweak headers, route requests, or convert data at runtime without changing the backend.
- Back a connector with an Azure Function when the API needs custom transformation logic.
// Azure Function backing a custom connector
[FunctionName("TransformOrder")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
string body = await new StreamReader(req.Body).ReadToEndAsync();
var order = JsonSerializer.Deserialize<Order>(body);
order.Total = order.Items.Sum(i => i.Price * i.Qty);
return new OkObjectResult(order);
}
Platform APIs
| API | Use from | Style |
|---|---|---|
| Dataverse Web API | JavaScript, external apps, any language | REST / OData v4 / JSON |
| Organization service | Plug-ins, .NET server code | .NET SDK (IOrganizationService) |
Rate limiting (HTTP 429): honor the Retry-After header and retry with exponential backoff.
catch (FaultException<OrganizationServiceFault> ex)
when (ex.Detail.ErrorCode == -2147015902) // rate-limit error code
{
var wait = ex.Detail.RetryAfter ?? TimeSpan.FromSeconds(5);
await Task.Delay(wait);
// retry here
}
Bulk operations — always batch instead of N round trips:
var multi = new ExecuteMultipleRequest
{
Settings = new ExecuteMultipleSettings
{ ContinueOnError = true, ReturnResponses = false },
Requests = new OrganizationRequestCollection()
};
foreach (var e in entities)
multi.Requests.Add(new CreateRequest { Target = e });
service.Execute(multi);
Also available: CreateMultiple / UpdateMultiple for high-throughput Dataverse write scenarios.
Optimistic concurrency: use If-Match with a row version to prevent overwriting concurrent changes.
Transactions: use ExecuteTransactionRequest when multiple writes must all succeed or all fail.
Azure Functions for Power Platform
Use Azure Functions when a plug-in cannot handle the work:
| Scenario | Reason |
|---|---|
| Long-running processing | Plug-in limit is 2 minutes |
| Scheduled jobs | Timer trigger |
| Event-driven from Service Bus / Event Hub | Queue / Event Hub trigger |
| Complex transformations for custom connectors | HTTP trigger |
Authenticate to Dataverse using a managed identity — no stored secrets:
var credential = new ManagedIdentityCredential();
var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { $"{environmentUrl}/.default" }));
// use token as Bearer in Dataverse Web API calls
Cloud flows — developer concerns
| Concern | Practice |
|---|---|
| Avoid unnecessary runs | Set trigger conditions on Dataverse triggers |
| Secrets | Store in Azure Key Vault; never inline |
| Sensitive data | Mark inputs/outputs as Secure — not logged in run history |
| Error handling | Use Configure run after (on failure/skipped) + Scope blocks (Try/Catch) |
| Reusable logic | Child flows — package once, call from any parent (must be in a solution) |
| Unattended identity | Service principal / Entra ID app registration |
| Complex expressions | Workflow Definition Language (WDL): coalesce(), if(), formatDateTime(), outputs() |
// WDL expression in a Power Automate step
coalesce(triggerOutputs()?['body/name'], 'Unnamed')
Flashcards — Extend the platform
Q: On an Update plug-in, you need the field value before it changed. What do you register? A: A Pre Image. The
Targetonly contains columns that were modified.
Q: You want to react to a record after it is saved and create a related record. Which stage? A: Post-operation (stage 40).
Q: You need to validate and cancel an operation before the transaction even starts. Which stage? A: Pre-validation (stage 10).
Q: A job will take 10 minutes. Plug-in or Azure Function? A: Azure Function — plug-ins have a 2-minute execution limit.
Q: How should an Azure Function authenticate to Dataverse without storing credentials? A: Managed identity.
Q: Where do you store secrets used by a cloud flow? A: Azure Key Vault — retrieve at runtime, never inline.
Q: You hit a 429 response from the Dataverse API. What do you do? A: Honor the
Retry-Afterheader and retry with exponential backoff.
Q: You want to call 500 Dataverse Create operations without 500 round trips. What do you use? A:
ExecuteMultipleRequestorCreateMultiple.
6. Develop integrations
Sending Dataverse events out
| Listener | Style | Use when |
|---|---|---|
| Webhook | HTTP POST, can be synchronous | Real-time callout to your own API |
| Azure Service Bus | Async, queued, reliable | Decoupled, durable messaging |
| Azure Event Hub | Async, high-throughput | High-volume event streaming / telemetry |
Register service endpoints in the Plug-in Registration Tool. From inside a plug-in, post the execution context directly:
var cloudService = (IServiceEndpointNotificationService)
serviceProvider.GetService(typeof(IServiceEndpointNotificationService));
cloudService.Execute(
new EntityReference("serviceendpoint", serviceEndpointId), context);
Webhooks support synchronous registration — they can affect the outcome of the Dataverse operation. Service Bus and Event Hub are always asynchronous.
Keeping external systems in sync
Change tracking — enable on a table and poll only records that changed since a stored token, instead of reading all rows every time:
var query = new RetrieveEntityChangesRequest
{
EntityName = "account",
Columns = new ColumnSet("name", "revenue"),
DataVersion = lastToken, // null on first run
PageInfo = new PagingInfo { Count = 5000, PageNumber = 1 }
};
var response = (RetrieveEntityChangesResponse)service.Execute(query);
// store response.EntityChanges.DataToken for next run
Alternate keys — define a unique key on a business column (e.g., an external ID) so you can reference and upsert records without knowing the Dataverse GUID:
var account = new Entity("account", "external_id", "ACC-00421");
UpsertRequest — insert if the record doesn't exist, update if it does — a single idempotent call:
var upsert = new UpsertRequest
{
Target = new Entity("account", "external_id", "ACC-00421")
{
["name"] = "Contoso Ltd",
["revenue"] = new Money(2500000)
}
};
var resp = (UpsertResponse)service.Execute(upsert);
bool created = resp.RecordCreated;
Flashcards — Integrations
Q: You need real-time, high-volume event streaming out of Dataverse to an analytics pipeline. A: Azure Event Hub.
Q: An external system syncs records using its own IDs, not Dataverse GUIDs. How do you support this? A: Alternate key (keyed on the external ID), plus UpsertRequest.
Q: Insert if new, update if it exists — one message, idempotent. A: UpsertRequest.
Q: You want to pull only records that changed since your last sync, not the full table. A: Change tracking with a stored data token.
Q: A webhook can run synchronously. What does that allow it to do that Service Bus cannot? A: Affect the outcome of the Dataverse operation (block or modify it).
Quick-reference cheat sheet
Print this section and review it the night before your exam.
| Scenario | Answer |
|---|---|
| Logic must run for every write path | Plug-in |
| Need old field values on Update | Pre Image |
| React after save / create related rows | Post-operation (stage 40) |
| Cancel before transaction starts | Pre-validation (stage 10) |
| Huge, flexible, non-relational data | Elastic table |
| External system of record, real-time read | Virtual table |
| Insert-or-update from outside Dataverse | UpsertRequest + alternate key |
| Keep external store current efficiently | Change tracking |
| Long-running or scheduled job | Azure Function (not a sync plug-in) |
| Flow secrets | Azure Key Vault |
| Unattended identity | Service principal / managed identity |
| Business + Non-business connector together | Blocked by DLP |
| Reusable named server-side operation | Custom API |
| Shipping to production | Managed solution |
| Config that differs per environment | Environment variable |
| App is slow | Open Monitor |
| Blue underline on a filter | Delegation warning (500 / 2000 row limit) |
| PCF lifecycle order | init → updateView → getOutputs → destroy |
| Too many API round trips | ExecuteMultipleRequest / CreateMultiple |
| API returns 429 | Retry-After + exponential backoff |
| Batch write must be atomic | ExecuteTransactionRequest |
| Flow runs on wrong records | Trigger conditions |
| Sensitive flow values in logs | Mark inputs/outputs as Secure |
Free resources
How I went from a CPA obsessing over spreadsheets to a full-time Power Platform developer by building apps on nights and weekends.
Ring in the New Year with a playful Power Apps greeting that showcases your customization skills and spreads cheer.
Combine your love for cats and low-code with this easy Power Apps project. Learn how to use collections, random selection, and galleries to create a festive seasonal app.