Tutorials//PL-400 Power Platform Developer — Complete Study Guide
advanced

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.

PE
PowerStack Editorial
@powerstack · Published June 21, 2026

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.

DomainWeightPriority
Create a technical design10–15%Study second
Build Power Platform solutions10–15%Study third
Implement Power Apps improvements10–15%Study fourth
Extend the user experience10–15%Study fifth
Extend the platform30–35%Study first
Develop integrations10–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.

MechanismRuns whereUse when
Business ruleClient + server (scope-dependent)Simple field-level logic with no code
Client script (JS)Browser, model-driven forms onlyUI behavior tied to form events
Power FxClientCanvas app and command logic
Cloud flowCloud, asyncCross-system orchestration, approvals, scheduled jobs
Plug-inServer, in transactionLogic 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 typeData livesUse when
StandardDataverseDefault — full pipeline, auditing, relationships
VirtualExternal sourceSystem of record stays external; real-time reads needed
ElasticAzure Cosmos DB–backedVery high volume, sparse, write-heavy (IoT telemetry, logs)
ConnectorAnother serviceYou 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

ComponentScopeLanguage
Canvas component / component libraryCanvas appsPower Fx
PCF code componentCanvas + model-drivenTypeScript
Custom connectorFlows + appsOpenAPI definition
Custom APIDataverse serverC# plug-in
Power Fx functionCanvas + custom pagesPower Fx
Common exam trap

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 typeEditable?Use in
UnmanagedYesDevelopment environments
ManagedNo (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

ToolWhat it does
Power Platform CLI (pac)Export/import solutions, scaffold PCF, manage environments, run pipelines
Power Platform PipelinesIn-product deployment dev → test → prod, extendable with pre/post steps
Power Platform Build ToolsAzure 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 OnStart to 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 Filter call. 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:

initupdateViewgetOutputsdestroy

MethodWhen it runs
initOnce, at start — set up DOM and cache context
updateViewEvery time bound data or context changes
getOutputsWhen you call notifyOutputChanged()
destroyClean-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 formContext in an OnLoad handler? A: From the execution context passed as the first parameter — never the deprecated global Xrm.Page.

Q: Put the PCF lifecycle methods in order. A: initupdateViewgetOutputsdestroy.

Q: Where does a PCF manifest declare that the control needs camera or Web API access? A: In the feature-usage section of ControlManifest.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 calls getOutputs().


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

StageNumberRunsTypical use
Pre-validation10Before the main transactionValidate and cancel early; runs before security checks
Pre-operation20Inside transaction, before the DB writeModify the Target before it is saved
Post-operation40Inside transaction, after the writeReact 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 Target only 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");
Key exam point

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 QueryExpression with 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

APIUse fromStyle
Dataverse Web APIJavaScript, external apps, any languageREST / OData v4 / JSON
Organization servicePlug-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:

ScenarioReason
Long-running processingPlug-in limit is 2 minutes
Scheduled jobsTimer trigger
Event-driven from Service Bus / Event HubQueue / Event Hub trigger
Complex transformations for custom connectorsHTTP 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

ConcernPractice
Avoid unnecessary runsSet trigger conditions on Dataverse triggers
SecretsStore in Azure Key Vault; never inline
Sensitive dataMark inputs/outputs as Secure — not logged in run history
Error handlingUse Configure run after (on failure/skipped) + Scope blocks (Try/Catch)
Reusable logicChild flows — package once, call from any parent (must be in a solution)
Unattended identityService principal / Entra ID app registration
Complex expressionsWorkflow 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 Target only 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-After header and retry with exponential backoff.

Q: You want to call 500 Dataverse Create operations without 500 round trips. What do you use? A: ExecuteMultipleRequest or CreateMultiple.


6. Develop integrations

Sending Dataverse events out

ListenerStyleUse when
WebhookHTTP POST, can be synchronousReal-time callout to your own API
Azure Service BusAsync, queued, reliableDecoupled, durable messaging
Azure Event HubAsync, high-throughputHigh-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);
Synchronous vs async

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.

ScenarioAnswer
Logic must run for every write pathPlug-in
Need old field values on UpdatePre Image
React after save / create related rowsPost-operation (stage 40)
Cancel before transaction startsPre-validation (stage 10)
Huge, flexible, non-relational dataElastic table
External system of record, real-time readVirtual table
Insert-or-update from outside DataverseUpsertRequest + alternate key
Keep external store current efficientlyChange tracking
Long-running or scheduled jobAzure Function (not a sync plug-in)
Flow secretsAzure Key Vault
Unattended identityService principal / managed identity
Business + Non-business connector togetherBlocked by DLP
Reusable named server-side operationCustom API
Shipping to productionManaged solution
Config that differs per environmentEnvironment variable
App is slowOpen Monitor
Blue underline on a filterDelegation warning (500 / 2000 row limit)
PCF lifecycle orderinit → updateView → getOutputs → destroy
Too many API round tripsExecuteMultipleRequest / CreateMultiple
API returns 429Retry-After + exponential backoff
Batch write must be atomicExecuteTransactionRequest
Flow runs on wrong recordsTrigger conditions
Sensitive flow values in logsMark inputs/outputs as Secure

Free resources