Friday, April 24, 2026

Power Pages Complete Guide

 

Power Pages Complete Guide

From Basics to Advanced — Web API, Auth, Liquid, Scenarios & Cheat Sheet


Table of Contents

  1. Basic Questions
  2. Security & Access
  3. Data & Forms
  4. Customization & Code
  5. Advanced — Web API
  6. Advanced — Custom Authentication
  7. Advanced — Performance Tuning
  8. Advanced — ALM & CI/CD
  9. Scenario-Based — Troubleshooting
  10. Scenario-Based — Web API Debugging
  11. Scenario-Based — Portal Design
  12. Scenario-Based — Security Scenarios
  13. Liquid Templating — Advanced Patterns
  14. Liquid Templating — Web Templates
  15. Liquid Templating — Performance
  16. Liquid Templating — Gotchas & Traps
  17. Liquid Cheat Sheet

1. Basic Questions

What is Microsoft Power Pages?

Power Pages is a low-code SaaS platform for building secure, data-driven external-facing websites. It is part of the Microsoft Power Platform and connects to Dataverse as its backend data store.


What are the main components of a Power Pages site?

Pages, Web templates, Table lists, Table forms, Web roles, Site settings, Content snippets, and Entity permissions. Together they control layout, data access, and security.


What is Dataverse and why is it important in Power Pages?

Dataverse is the cloud data platform that backs Power Pages. All data rendered on the portal — tables, forms, lists — is stored and retrieved from Dataverse. Role-based security on Dataverse tables controls what portal users can see or edit.


What is the difference between Power Pages and Power Apps portals?

Power Pages is the rebranded and evolved version of Power Apps portals (announced 2022). It adds a dedicated design studio, Pages workspace, Copilot features, and a new standalone licensing model, while retaining all portal functionality.


What tools are used to build Power Pages sites?

Three main tools:

  1. Design Studio — WYSIWYG low-code editor in browser.
  2. Power Pages Management App — advanced configuration in model-driven app.
  3. Power Platform CLI / VS Code extension — code-first development and ALM.

What are Content Snippets?

Content Snippets are reusable text/HTML fragments managed in the portal back-end. They allow non-developers to edit labels, messages, and static text across the site without touching templates. Referenced in Liquid as {% snippets['snippet_name'] %}.


What are Site Settings and give two examples?

Site Settings are key-value configuration pairs that control portal behavior. Examples:

  • Authentication/Registration/Enabled — enables/disables user self-registration.
  • Webapi/<tablename>/enabled — enables the Web API for a specific table.

2. Security & Access

What are Web Roles in Power Pages?

Web Roles group portal users and control what they can access. Every authenticated user is assigned one or more web roles. Two built-in roles exist: Authenticated Users (all logged-in users) and Anonymous Users (not logged in).


What are Table Permissions and why are they critical?

Table Permissions define CRUD access on Dataverse tables for a specific web role. Without proper table permissions, users see no data even if the page is accessible. They support scopes: Global, Contact, Account, Parent, and Self.


What is the difference between Page-level security and Table Permissions?

  • Page-level security controls who can navigate to a page (via web role on the page).
  • Table permissions control what data a user can read/write from Dataverse.

Both layers must be configured for secure, functional data access.


What authentication providers does Power Pages support?

Azure AD (Entra ID), Azure AD B2C, SAML 2.0, OpenID Connect, OAuth 2.0 providers (Google, Facebook, LinkedIn, Twitter), and local identity (email + password). Multiple providers can be enabled simultaneously.


3. Data & Forms

What is a Table List (Entity List)?

A Table List renders rows from a Dataverse table on a page as a grid or gallery. It supports filtering, sorting, searching, and actions like create/edit/delete (subject to table permissions).


What is a Table Form (Basic Form) vs a Multi-step Form (Advanced Form)?

  • Table Form (Basic Form): Single-step form bound to one Dataverse form. Used for simple create/edit/read operations.
  • Multi-step Form (Advanced Form): Wizard-style form spanning multiple steps/tables. Supports conditional branching and sequential data capture.

How does Power Pages handle file attachments in forms?

Files are stored as Notes (annotations) attached to the Dataverse record, or via SharePoint integration. You must enable the attachment option on the form and grant the Notes table permissions to the relevant web role.


4. Customization & Code

What is Liquid in Power Pages?

Liquid is an open-source templating language used in Power Pages web templates and content snippets to dynamically render data, conditional content, and loops. It allows server-side access to Dataverse data without custom code.

Example:

{% entitylist id: page.adx_entitylist.id %}

How can you integrate Power Automate with Power Pages?

Power Automate flows can be triggered from portal forms using the Power Pages connector (cloud flows) or by using the Web API. Common use case: send email notifications when a form is submitted.


What is the Power Pages Web API?

A RESTful API that enables client-side JavaScript to perform CRUD operations on Dataverse tables directly from the portal without a page postback. It respects table permissions. Endpoint pattern: /_api/<tablename>.


How do you deploy or migrate a Power Pages site between environments?

Using Power Platform solutions — package the portal components into a solution and deploy via Solution import, or use the Power Platform CLI commands:

pac paportal download
pac paportal upload

with source control for CI/CD.


How does caching work in Power Pages?

Power Pages caches portal metadata (templates, settings, entity lists) server-side. Changes made in the management app may not reflect immediately. Clear cache via:

<siteurl>/_services/about

or by restarting the portal from the Power Platform admin center.


5. Advanced — Web API

How do you enable the Web API for a specific table?

Create two Site Settings:

  • Webapi/<tablename>/enabledtrue
  • Webapi/<tablename>/fields → comma-separated column names (or * for all)

Then grant the appropriate Table Permission (Read/Write/Create/Delete) to the web role. Without both, the API returns 403.


What HTTP methods does the Power Pages Web API support?

  • GET — read records (supports $filter, $select, $orderby, $top, $expand)
  • POST — create a record
  • PATCH — update an existing record
  • DELETE — delete a record

Base URL pattern: /_api/<tablename>(guid)

Note: PUT is not supported.


How is CSRF protection handled in Web API calls?

All write operations (POST, PATCH, DELETE) require an anti-forgery token sent in the request header as __RequestVerificationToken. Retrieve it via:

$('[name=__RequestVerificationToken]').val()
// or from:
/_api/antiforgery/token

Tip: Forgetting the anti-forgery token is the #1 reason Web API writes fail silently in demos.


How do you filter and expand related records using the Web API?

/_api/contacts?$filter=firstname eq 'John'
&$select=fullname,emailaddress1
&$expand=parentcustomerid_account($select=name)
&$top=10

Expanding requires Table Permissions on the related table too.


Can anonymous users call the Power Pages Web API?

Yes, but only if Table Permissions with the required privilege are assigned to the Anonymous Users web role. By default no permissions exist for anonymous users, so all API calls return 403.


What are the limitations of the Power Pages Web API compared to Dataverse Web API?

  • No batch requests ($batch not supported)
  • No executing actions or functions directly
  • No metadata endpoint (/$metadata)
  • Field access restricted to columns listed in the fields site setting
  • Rate limits apply (portal-level throttling)
  • No server-side pagination tokens — use $top and $skip

6. Advanced — Custom Authentication

How do you configure Azure AD B2C as an identity provider?

Register an app in Azure AD B2C, create user flows (sign-up/sign-in). Then set Site Settings:

  • Authentication/OpenIdConnect/AzureADB2C/Authority → B2C tenant URL
  • Authentication/OpenIdConnect/AzureADB2C/ClientId → app client ID
  • Authentication/OpenIdConnect/AzureADB2C/RedirectUri → portal callback URL

Add the redirect URI to the B2C app registration's allowed redirect URIs.


What is contact mapping and why is it important?

When a user authenticates, Power Pages maps the external identity to a Dataverse Contact record. This mapping determines what data the user can access. Without a matching Contact, the user may authenticate but have no portal profile or table-permission scope.


How do you implement invitation-based registration?

Create an Invitation record in Dataverse linked to the Contact, set expiry and type (single/group). Send the invitation link to the user. When they redeem it, a portal identity is created and linked to the Contact.


What is the difference between local identity and external identity providers?

  • Local identity: credentials stored within Power Pages using ASP.NET Identity. Supports email confirmation, password reset, and lockout.
  • External providers (Azure AD, B2C, Google, etc.): authentication is delegated via OAuth/OIDC. Power Pages only receives a token and maps it to a Contact — it never stores passwords.

How do you implement MFA on Power Pages?

Power Pages itself does not provide MFA — delegate auth to an external provider that supports MFA (Azure AD Conditional Access or Azure AD B2C MFA user flows). The portal trusts the token issued after MFA is satisfied.

Tip: The correct answer is always — push MFA to the identity provider, never try to build it inside the portal.


7. Advanced — Performance Tuning

What causes slow portal page loads and how do you diagnose them?

Common causes: excessive Liquid Dataverse queries, large unbounded table lists, unoptimised fetchXML, heavy JavaScript bundles, cache misses.

Diagnose using:

  • Browser DevTools Network tab
  • /_services/about — check cache state
  • Azure Application Insights (if configured)
  • Power Pages telemetry in the admin centre

How does server-side caching work and how do you control it?

Power Pages caches portal metadata in memory. To clear:

  • Navigate to <site>/_services/about → click Clear cache
  • Restart the portal from Power Platform admin centre

For live data, use the Web API client-side instead of server-side Liquid queries.


How do you optimise a slow Table List?

  • Enable pagination — set a sensible page size (25–50)
  • Use $select to fetch only required columns
  • Add filters so the Dataverse query is targeted
  • Add Dataverse indexes on columns used in filter/sort
  • Avoid expanding many lookups in a single list view
  • Consider replacing with a client-side Web API call + JS rendering

How do you implement CDN for Power Pages assets?

Power Pages has a built-in Azure CDN integration. Enable it in the Power Platform admin centre under portal settings. Once enabled, static assets (CSS, JS, images in web files) are served from the CDN edge. Custom domain + SSL is required before CDN can be enabled.


8. Advanced — ALM & CI/CD

How do you use Power Platform CLI to manage Power Pages in source control?

# Download site content
pac paportal download --path ./portal --webSiteId <guid>

# Upload to target environment
pac paportal upload --path ./portal

# Auth with service principal
pac auth create

How do you handle environment-specific configuration during deployment?

Use deployment settings files (deploymentSettings.json) with the Power Platform Build Tools or CLI. These files override Site Setting values per environment (dev/test/prod) at import time — keeping secrets and environment URLs out of source control.


How do you automate Power Pages deployment in Azure DevOps?

  1. Power Platform Tool Installer
  2. Power Platform Export Solution (from source env)
  3. Commit to repo
  4. Power Platform Import Solution (to target env)
  5. pac paportal upload step via CLI task for portal content files

Use a service principal with System Administrator role on the target environment.


9. Scenario-Based — Troubleshooting

Scenario: User logs in but sees no data in the Table List. No error shown.

Diagnose in order:

  1. Confirm the user's web role via the Contact record in Portal Management App.
  2. Check Table Permissions for that web role — verify Read privilege is granted.
  3. Check the scope. If scope is Contact or Account, the parent relationship record must exist and be linked correctly.
  4. Clear the portal cache at /_services/about.
  5. Check if the Table List has a filter that excludes records for this user.

Root cause 90% of the time: Missing Table Permission or wrong scope.


Scenario: Form submission silently fails — page reloads, no record created.

  1. Open DevTools → Network tab → re-submit. Look for a non-200 POST response.
  2. Check Table Permissions — web role must have Create privilege.
  3. Check if required Dataverse fields are missing from the portal form.
  4. Verify form mode is set to Insert, not Edit or ReadOnly.
  5. Check console for JS errors blocking submission.
  6. Check for Dataverse business rules blocking create.

Tip: Add ?debug to the URL to surface additional error detail.


Scenario: After deploying a solution, portal still shows old content.

This is a portal metadata cache issue. Fix:

  1. Navigate to <portalurl>/_services/about
  2. Click Clear cache
  3. If unresponsive, restart from Power Platform Admin Centre → Environments → Resources → Portals → Restart
  4. For CI/CD, add a cache-clear step post-deployment.

Scenario: User can edit records they should not be able to.

  1. Check if the user has a second web role with Write/Edit permission.
  2. Check Table Permissions on Authenticated Users built-in role — if Write is there, every logged-in user inherits it.
  3. Confirm scope — Global scope incorrectly set allows editing all records.

Warning: Never rely solely on hiding the Edit button. Always enforce at the Table Permission level — UI controls are bypassable via direct URL.


10. Scenario-Based — Web API Debugging

Scenario: Web API POST returns 403 Forbidden.

  1. Confirm Site Setting Webapi/<tablename>/enabled is true (exact logical name, case-sensitive).
  2. Confirm Webapi/<tablename>/fields lists the columns being written, or is *.
  3. Confirm the user's web role has a Table Permission with Create privilege.
  4. Check request headers — POST/PATCH/DELETE require __RequestVerificationToken. Missing token = 403.
  5. Clear portal cache.
  6. Test with Global-scope permission temporarily to isolate scope vs. permission issues.

Key distinction: 403 on GET = missing Read permission. 403 on POST = missing Create permission OR missing anti-forgery token.


Scenario: Web API GET returns empty array but records exist in Dataverse.

Empty array (not 403) means the API is enabled and Read permission exists — but scope is filtering records out.

  1. Check Table Permission scope. Contact scope only returns records linked to the logged-in Contact.
  2. Verify the relationship column is populated on the records.
  3. Try Global scope temporarily in dev to confirm.

Scenario: PATCH request succeeds but one field is always ignored.

  1. Check Webapi/<tablename>/fields — if it's a list (not *), confirm the field is included.
  2. Check Dataverse Field Security Profiles — update silently dropped if field-level security restricts it.
  3. Check for Dataverse business rules or plugins resetting the field.
  4. Verify you're sending the correct logical field name (lowercase, publisher prefix).

Scenario: Works in dev, returns 403 in production.

  1. Compare Site Settings between environments — enabled and fields settings may not have been deployed to prod.
  2. Compare Table Permissions — if not in the solution, they need to be recreated in prod.
  3. Check web roles — the Contact record in prod may not have the same web role assigned.
  4. Clear cache in prod.

Root cause: Environment parity issues. Always include Table Permissions and Site Settings in your solution or deployment checklist.


11. Scenario-Based — Portal Design

Design: Multi-role portal — customers see own cases, partners see account cases, admins see all.

Create three web roles with Table Permissions on the Case table:

Role Scope Privilege
Customer Contact (customerid) Read
Partner Account (customerid) Read
Admin Global Full CRUD

Use a single Table List on one page — the data returned automatically filters by scope. No code needed for the filtering logic.

Key insight: One page + one table list + three scopes = three different views automatically.


Design: Multi-step onboarding form spanning three tables.

Use a Multi-step Form (Advanced Form):

  1. Step 1: Account table, mode = Insert
  2. Step 2: Contact table, mode = Insert, auto-populate Account lookup from step 1
  3. Step 3: Custom subscription table, mode = Insert, pre-populate Account lookup via Referenced Entity
  4. Grant Table Permission Create on all three tables for the relevant web role.

Design: Real-time data dashboard (records reflect within seconds without page reload).

Server-side Liquid/Table Lists cannot do real-time — they render at page load only.

Use the Power Pages Web API client-side:

setInterval(async () => {
  const res = await fetch('/_api/cr123_orders?$filter=...');
  const data = await res.json();
  // update DOM with data.value
}, 5000);

Tip: Polling every 5–10 seconds handles most "real-time" business requirements without additional infrastructure.


Design: Anonymous lead capture form — no login, no data visible.

  1. Basic Table Form on Lead table, mode = Insert, no authentication required.
  2. Grant Anonymous Users web role a Table Permission with only Create privilege — no Read, Write, or Delete.
  3. No Table List on any anonymous page.
  4. Add CAPTCHA via custom Liquid/JS integration (Google reCAPTCHA).
  5. Set Authentication/Registration/Enabled to false if no self-registration needed.

12. Scenario-Based — Security Scenarios

Scenario: Pen test finds users can access other users' records by manipulating the GUID in the URL.

Fix: Change Table Permission scope from Global to Contact.

With Contact scope, fetching a record not linked to the user's Contact returns 403 — even with the correct GUID.

Warning: Global Read scope is the single most common security misconfiguration in Power Pages portals. Default to Contact or Account scope unless Global is explicitly required.


Scenario: Prevent portal from being used as open relay to spam Dataverse.

  1. Ensure only authenticated users have Create permission.
  2. Add CAPTCHA on public forms.
  3. Use Power Automate to detect unusual create volume.
  4. Dataverse API limits apply — 6000 requests per 5 minutes per user.
  5. Route public form submissions through a Power Automate HTTP trigger for extra validation.

Scenario: Company A must never see Company B data (multi-tenant portal).

  1. Ensure every Contact is linked to the correct parent Account.
  2. Ensure all data records have a lookup to the Account.
  3. Set all Table Permissions to Account scope.
  4. Use Parent scope for child records (e.g., Order Lines linked to Orders).

Tip: Account scope is the correct architecture for B2B multi-tenant portals. Contact scope works for B2C (individual consumer) portals.


Scenario: Sensitive columns (salary, SSN) must never be exposed via the Web API.

  1. In Site Setting Webapi/<tablename>/fields, explicitly list only safe columns — never use * on sensitive tables.
  2. Apply Dataverse Field Security Profiles to sensitive columns as a second layer.
  3. Only add permitted fields to the Dataverse form used by the portal.

Warning: Using Webapi/table/fields = * on a table with sensitive data is a critical misconfiguration. Always use an explicit allowlist.


13. Liquid Templating — Advanced Patterns

What is the difference between {% assign %} and {% capture %}?

  • assign stores a simple value or object reference.
  • capture renders a block of Liquid/HTML into a string variable.
{% assign username = user.fullname %}

{% capture welcome_msg %}
  <p>Welcome back, {{ user.fullname }}!</p>
{% endcapture %}
{{ welcome_msg }}

How do you query Dataverse records using fetchxml in Liquid?

{% fetchxml my_cases %}
<fetch top="10">
  <entity name="incident">
    <attribute name="title"/>
    <attribute name="statecode"/>
    <filter>
      <condition attribute="customerid"
        operator="eq" value="{{ user.id }}"/>
    </filter>
  </entity>
</fetch>
{% endfetchxml %}

{% for case in my_cases.results.entities %}
  <p>{{ case.title }} — {{ case.statecode.label }}</p>
{% endfor %}

How do you access the currently logged-in user's details?

{% if user %}
  Hello, {{ user.fullname }}
  Contact ID: {{ user.id }}
  Email: {{ user.emailaddress1 }}
  Account: {{ user.parentcustomerid.name }}
{% else %}
  <a href="/signin">Please sign in</a>
{% endif %}

Tip: user.roles returns the collection of web roles. Use user.roles | where: 'name', 'Admin' | first to check for a specific role.


How do you conditionally show content based on web role?

{% assign is_partner = user.roles
    | where: 'name', 'Partner'
    | first %}

{% if is_partner %}
  <a href="/partner-dashboard">Partner portal</a>
{% endif %}

Warning: This controls UI rendering only — it does not replace Table Permissions.


How do you read URL query string parameters in Liquid?

{% comment %} URL: /orders?status=active&page=2 {% endcomment %}

{{ request.params['status'] }}   → active
{{ request.params['page'] }}     → 2

Security Note: Never use raw query string values directly in fetchxml conditions without validation.


14. Liquid Templating — Web Templates

What is a Web Template and how does it differ from a Page Template?

  • Web Template: contains the actual Liquid + HTML markup (the code layer).
  • Page Template: configuration record referencing a Web Template, defining how it is used.

Chain: Web Page → Page Template → Web Template


How do you create a reusable component across multiple pages?

{%- comment -%} Web Template: "case-card" {%- endcomment -%}
<div class="card">
  <h3>{{ case.title }}</h3>
  <p>{{ case.statecode.label }}</p>
</div>

{%- comment -%} Parent Template {%- endcomment -%}
{% for case in my_cases.results.entities %}
  {% include 'case-card' %}
{% endfor %}

How do you pass parameters into an included Web Template?

{% include 'alert-banner'
    message: 'Your submission was received.'
    type: 'success' %}

{%- comment -%} Web Template: "alert-banner" {%- endcomment -%}
<div class="alert alert-{{ type }}">
  {{ message }}
</div>

What is the block and extends pattern in Web Templates?

{%- comment -%} Base layout Web Template {%- endcomment -%}
<html>
  <body>
    <header>{% block header %}Default header{% endblock %}</header>
    <main>{% block content %}{% endblock %}</main>
    <footer>{% block footer %}Default footer{% endblock %}</footer>
  </body>
</html>

{%- comment -%} Child Web Template {%- endcomment -%}
{% extends 'base-layout' %}
{% block content %}
  <h1>My Page Content</h1>
{% endblock %}

15. Liquid Templating — Performance

Why is putting multiple fetchxml blocks on one page a performance problem?

Each {% fetchxml %} is a synchronous round-trip to Dataverse during server-side rendering. Five fetchxml blocks = five blocking Dataverse calls before the user sees anything.

Fixes:

  • Merge related queries using link-entity fetchxml.
  • Move non-critical data fetches client-side using the Web API.
  • Cache expensive results in a Content Snippet updated by a flow.
  • Always use top in fetchxml — never fetch unbounded result sets.

Target: 1–2 server-side queries max per page.


What is the N+1 fetchxml anti-pattern?

{%- comment -%} NEVER do this {%- endcomment -%}
{% for order in orders.results.entities %}
  {% fetchxml lines %}
    <fetch><entity name="salesorderdetail">
      <filter><condition attribute="salesorderid"
        operator="eq" value="{{ order.id }}"/>
      </filter>
    </entity></fetch>
  {% endfetchxml %}
{% endfor %}

Fix: Use <link-entity> in a single fetchxml query.


How do you use link-entity in fetchxml to avoid multiple queries?

{% fetchxml orders_with_account %}
<fetch top="20">
  <entity name="salesorder">
    <attribute name="name"/>
    <attribute name="totalamount"/>
    <link-entity name="account"
      from="accountid" to="customerid"
      alias="acct">
      <attribute name="name"/>
    </link-entity>
  </entity>
</fetch>
{% endfetchxml %}

{% for o in orders_with_account.results.entities %}
  {{ o.name }} — {{ o['acct.name'] }}
{% endfor %}

16. Liquid Templating — Gotchas & Traps

Why does {{ value | default: 'N/A' }} sometimes still output blank?

The default filter only triggers when the value is nil, false, or "". If the Dataverse field returns an empty object (e.g., a lookup), default does not fire.

{% comment %} Safe pattern for lookups {% endcomment %}
{% if entity.parentcustomerid %}
  {{ entity.parentcustomerid.name }}
{% else %}
  N/A
{% endif %}

Why does modifying a variable inside a {% for %} loop not persist after the loop ends?

Liquid has block scoping — variables assigned inside a for loop do not leak out to the parent scope.

{% assign total = 0 %}
{% for item in items %}
  {% assign total = total | plus: item.amount %}
{% endfor %}
{{ total }}  ← always outputs 0!

Fix: Use fetchxml aggregate functions or calculate client-side with JavaScript.


How do you output a literal {{ or {% without Liquid interpreting it?

{% raw %}
  Vue template: {{ message }}
  Angular binding: {{ user.name }}
  Handlebars: {{#each items}}
{% endraw %}

Essential when embedding JavaScript frameworks (Vue, Angular, Handlebars) inside a Web Template.


Why does {% include 'my-template' %} silently render nothing?

Three common causes:

  1. The Web Template name is incorrect — names are case-sensitive.
  2. The included template has a Liquid error — Liquid silently swallows errors in includes.
  3. The included template references variables not in scope at the point of inclusion.

Liquid fetchxml returns empty but XrmToolBox shows records. Why?

The portal executes fetchxml in the context of the portal application user — not as an admin.

  1. Table Permissions: the web role lacks Read permission on the table.
  2. Record ownership: with Contact scope, no records are linked to the current Contact.
  3. Business Unit security: the portal application user's Dataverse role may not have org-level read.

Warning: Always test fetchxml in the browser as a portal user, not as an admin in XrmToolBox.


17. Liquid Cheat Sheet

Global Objects

Object Description Key Properties
user Logged-in Contact record. null for anonymous. .fullname, .id, .emailaddress1, .parentcustomerid.name, .roles
request Current HTTP request .url, .path, .params['key']
page Current web page record .title, .url, .parent.title
website Portal website record .name, .id
sitemarkers Named page pointers sitemarkers['Name'].url
settings Site Settings values settings['Key/Name']
snippets Content Snippets snippets['Snippet/Name']
weblinks Web link sets (nav menus) weblinks['Set Name'].weblinks

Essential Tags

Tag Purpose
{% fetchxml result %}...{% endfetchxml %} Query Dataverse. Results in result.results.entities
{% include 'template-name' key: val %} Include another Web Template with optional params
{% extends 'layout' %} Inherit from a base layout template
{% block name %}...{% endblock %} Define/override a named block region
{% assign var = value %} Store a value in a variable
{% capture var %}...{% endcapture %} Render Liquid block into a string variable
{% raw %}...{% endraw %} Output {{ }} literally — for Vue/Angular/Handlebars
{% unless condition %} Render when condition is false
{% for item in collection %} Loop over a collection
{% if / elsif / else / endif %} Conditional rendering

Essential Filters

Filter Example Output
date {{ date | date: "%d %b %Y" }} 18 Apr 2025
where user.roles | where: 'name', 'Admin' Filtered array
first / last collection | first First/last item
map roles | map: 'name' Array of names
join arr | join: ', ' A, B, C
size entities | size Count
truncate text | truncate: 80 Shortened string
default value | default: 'N/A' Fallback (strings/nil only)
downcase / upcase email | downcase Normalised case
replace str | replace: 'a','b' Substituted string
plus / minus / times / divided_by 10 | plus: 5 Arithmetic result

Critical Patterns to Memorise

{% comment %} Role check {% endcomment %}
{% assign is_admin = user.roles | where: 'name', 'Admin' | first %}
{% if is_admin %}...{% endif %}

{% comment %} Null-safe lookup {% endcomment %}
{% if entity.parentcustomerid %}
  {{ entity.parentcustomerid.name }}
{% endif %}

{% comment %} Whitelist URL params {% endcomment %}
{% assign allowed = 'active,inactive' | split: ',' %}
{% assign status = request.params['status'] %}
{% if allowed contains status %}
  use {{ status }} safely
{% endif %}

{% comment %} link-entity JOIN (avoid N+1) {% endcomment %}
<link-entity name="account" from="accountid"
  to="customerid" alias="acct">
  <attribute name="name"/>
</link-entity>
{{ entity['acct.name'] }}

Top 3 Traps

  1. Loop scope{% assign %} inside {% for %} does NOT persist after the loop.
  2. default filter — fails silently on empty lookup objects. Use {% if %} guards instead.
  3. fetchxml inside a loop — causes N+1 Dataverse queries. Always use link-entity for related data.


Parsing Copilot Studio Conversation Transcripts in Power Apps

Parsing Copilot Studio Conversation Transcripts in Power Apps

Ever stared at a Copilot Studio transcript JSON and wondered how to turn that nested mess into a clean, readable table? 

The raw transcript comes as a single JSON string with dozens of activities: traces, events, debug plans, tool calls, and somewhere buried in there, the actual user prompts and bot responses. Most of it is plumbing. What you really care about are the messages where role = 1 (user) and role = 0 (bot), paired together by the replyToId field.

The trick in Power Fx is ParseJSON combined with a Clear + Collect pattern. ForAll iterates over each transcript, filters activities down to type = "message", then pairs each user prompt with its matching bot response using the replyToId. Timestamps come through as Unix epoch seconds, so a quick DateAdd against DateTime(1970,1,1,0,0,0) with TimeUnit.Seconds converts them to real datetimes.

Here's the full formula I dropped into App.OnStart:

Clear(colChatTurns);
ForAll(
    ConversationTranscripts As Transcript,
    With(
        { parsed: ParseJSON(Transcript.Content) },
        With(
            {
                allMessages: Filter(
                    Table(parsed.activities),
                    Text(ThisRecord.Value.type) = "message"
                )
            },
            ForAll(
                Filter(allMessages, Text(ThisRecord.Value.from.role) = "1") As UserMsg,
                Collect(
                    colChatTurns,
                    {
                        ReplyToId: Text(UserMsg.Value.id),
                        UserPrompt: Text(UserMsg.Value.text),
                        PromptTimestamp: DateAdd(
                            DateTime(1970,1,1,0,0,0),
                            Value(UserMsg.Value.timestamp),
                            TimeUnit.Seconds
                        ),
                        BotResponse: Text(
                            First(
                                Filter(
                                    allMessages,
                                    Text(ThisRecord.Value.from.role) = "0" 
                                    And Text(ThisRecord.Value.replyToId) = Text(UserMsg.Value.id)
                                )
                            ).Value.text
                        ),
                        ResponseTimestamp: DateAdd(
                            DateTime(1970,1,1,0,0,0),
                            Value(
                                First(
                                    Filter(
                                        allMessages,
                                        Text(ThisRecord.Value.from.role) = "0" 
                                        And Text(ThisRecord.Value.replyToId) = Text(UserMsg.Value.id)
                                    )
                                ).Value.timestamp
                            ),
                            TimeUnit.Seconds
                        )
                    }
                )
            )
        )
    )
)

A few gotchas worth knowing. ParseJSON returns untyped objects, so every field needs explicit coercion with Text() or Value(). The role field can behave as either a number or a string depending on the source, so comparing with "1" and "0" as strings is safer than numeric comparisons. Ungroup sounds like the right tool for flattening nested results, but it chokes on string identifiers — Clear + Collect inside a ForAll loop is cleaner and easier to debug. And always check View → Collections to see if your data is actually landing where you think it is.

Once the collection is populated, binding it to a Data Table gives you an instant conversation viewer. Load it on App.OnStart for speed, or on Screen.OnVisible if you need fresh data every visit. Skip the intermediate gallery and point straight at your data source to keep things lean.

From messy telemetry JSON to a working chat review dashboard.

Tags: PowerApps, PowerFx, CopilotStudio, LowCode, MicrosoftPowerPlatform, JSON, ParseJSON, Dataverse, SharePoint, ConversationAnalytics, ChatbotAnalytics, AIAgents, PowerAppsTutorial, EnterpriseIT, SolutionArchitecture

Wednesday, April 22, 2026

How to Set Global Parameters in an Existing Azure Data Factory Using PowerShell

How to Set Global Parameters in an Existing Azure Data Factory Using PowerShell

Global parameters in Azure Data Factory (ADF) let you define constants that can be reused across all pipelines in a factory — think environment names, retry counts, API base URLs, and more. Managing them via PowerShell gives you repeatability, version control, and automation that clicking through the Azure Portal simply cannot match.

In this post, I'll walk you through a clean, production-ready script that sets global parameters on an existing ADF instance without recreating it.


Why Global Parameters?

ADF global parameters solve a common problem: hardcoded values scattered across dozens of pipelines. Instead of updating each pipeline individually when you promote from UAT to Production, you update a single global parameter and every pipeline picks it up automatically.

Common use cases:

  • EnvironmentDev, UAT, Production
  • MaxRetryCount — control retry logic centrally
  • StorageAccountUrl — swap endpoints per environment
  • NotificationEmail — route alerts without pipeline changes

Prerequisites

  • PowerShell 5.1 or later
  • An Azure subscription with Contributor access on the ADF resource
  • The Az.DataFactory PowerShell module (the script installs it automatically)

The Script

if (-not (Get-Module -ListAvailable -Name Az.DataFactory)) {
    Install-Module -Name Az.DataFactory -Scope CurrentUser -Force
}

Import-Module Az.DataFactory

Connect-AzAccount -SubscriptionId "6f50c2eb-40ba-45e8-be26-98443a01899f"

# Define Resource Details
$resourceGroupName = "rg1"
$dataFactoryName   = "adf1"

# Add or remove parameters in this JSON array
$paramJson = @'
[
  { "Name": "Environment",   "Type": "String", "Value": "Production" },
  { "Name": "MaxRetryCount", "Type": "Int",    "Value": 3            }
]
'@ | ConvertFrom-Json

$globalParams = New-Object "System.Collections.Generic.Dictionary[string, Microsoft.Azure.Management.DataFactory.Models.GlobalParameterSpecification]"

foreach ($entry in $paramJson) {
    $spec       = New-Object Microsoft.Azure.Management.DataFactory.Models.GlobalParameterSpecification
    $spec.Type  = $entry.Type
    $spec.Value = $entry.Value
    $globalParams.Add($entry.Name, $spec)
}

# Fetch the factory's real location — avoids location mismatch errors
$existingFactory = Get-AzDataFactoryV2 -ResourceGroupName $resourceGroupName -Name $dataFactoryName
$location        = $existingFactory.Location

Set-AzDataFactoryV2 `
  -ResourceGroupName         $resourceGroupName `
  -Name                      $dataFactoryName `
  -GlobalParameterDefinition $globalParams `
  -Location                  $location `
  -Force

Key Design Decisions

1. Install the Module Only When Missing

if (-not (Get-Module -ListAvailable -Name Az.DataFactory)) {
    Install-Module -Name Az.DataFactory -Scope CurrentUser -Force
}

Running Install-Module unconditionally adds several seconds of overhead on every run. This guard makes the script safe to call repeatedly in a CI/CD pipeline.

2. Inline JSON Array for Parameters

Parameters are defined as an inline JSON array, making it trivial to add a new one — no extra files, no hashtable type-conversion issues:

[
  { "Name": "Environment",   "Type": "String", "Value": "Production" },
  { "Name": "MaxRetryCount", "Type": "Int",    "Value": 3            }
]

Supported Type values: String, Int, Float, Bool, Array, Object.

3. Dynamically Fetch the Factory Location

A common gotcha: Set-AzDataFactoryV2 requires the -Location parameter, and if you hard-code the wrong display name (e.g., "East US" instead of "East US 2"), Azure throws a Conflict / InvalidResourceLocation error. Fetching the location from the existing factory object eliminates this entirely:

$location = (Get-AzDataFactoryV2 -ResourceGroupName $resourceGroupName -Name $dataFactoryName).Location

4. -Force Skips the Confirmation Prompt

Without -Force, PowerShell asks "A data factory with this name exists. Are you sure?" — which breaks unattended runs. Since we are intentionally updating an existing factory, -Force is the correct flag to use.


Adding a New Global Parameter

Open the script and add a line to the JSON array:

[
  { "Name": "Environment",       "Type": "String", "Value": "Production"                          },
  { "Name": "MaxRetryCount",     "Type": "Int",    "Value": 3                                     },
  { "Name": "StorageAccountUrl", "Type": "String", "Value": "https://mystorage.blob.core.windows.net" }
]

Run the script. Done.

Note: Set-AzDataFactoryV2 replaces the entire global parameter set. Parameters not included in the array will be removed. Always include all parameters you want to keep.

Running the Script

.\Untitled-1.ps1

A browser window opens for Azure sign-in. After authentication, the script fetches the factory, applies the parameters, and exits.


Integrating with CI/CD

For automated pipelines (GitHub Actions, Azure DevOps), replace browser sign-in with a service principal:

Connect-AzAccount -ServicePrincipal `
  -TenantId   $env:ARM_TENANT_ID `
  -Credential (New-Object PSCredential($env:ARM_CLIENT_ID, (ConvertTo-SecureString $env:ARM_CLIENT_SECRET -AsPlainText -Force)))

Store credentials as pipeline secrets, never in source code.


Conclusion

Managing ADF global parameters through PowerShell is straightforward once you know the two key pitfalls — location mismatch and the replace-all behavior. With an inline JSON array as your parameter source, the script stays readable, easily extendable, and pipeline-friendly.


#AzureDataFactory, #PowerShell, #ADF, #Azure, #DataEngineering, #CloudAutomation, #ETL, #DevOps, #AzureAutomation, #DataPipeline

Wednesday, April 15, 2026

Power Platform + Postman: The Complete Developer Guide

Power Platform Developer Series

Power Platform + Postman:
The Complete Developer Guide

✍ Sreekanth Udayagiri  |  📅 April 2026  |  ⏱ 15 min read

Postman is one of the most popular API development and testing tools used by developers worldwide. When working with Microsoft Power Platform, Postman plays multiple roles: as an API client, a testing harness, a debugging tool, a connector generator, and even as an integration target.

In this post, I'll walk you through all 6 ways Postman is used with Power Platform — with sample code and step-by-step instructions for each, based entirely on official Microsoft documentation.

💡
Based on Official Microsoft DocsMicrosoft officially supports Postman for Power Platform development. Their documentation lists Postman alongside Insomnia, Bruno, and curl as recommended API client tools for Dataverse Web API. All steps in this guide are verified against Microsoft Learn.
01

Dataverse Web API

Connect, authenticate, and run CRUD operations on Dataverse tables

02

Power Automate HTTP Trigger

Test and trigger Power Automate flows via HTTP endpoints

03

Custom Connectors

Import Postman Collections to auto-generate custom connectors

04

Custom APIs in Dataverse

Test plugin-backed custom API messages via Postman

05

Power Pages Web API

Test portal table permissions and data operations

06

Native Power Automate Integration

Backup collections, send monitor results to Power Automate


1

Postman with Dataverse Web API — Setup + CRUD

Connect Postman to your Power Apps / Dataverse environment and run Create, Read, Update, Delete operations

Microsoft Dataverse exposes a RESTful Web API based on OData v4. Postman lets you authenticate to this API and run queries — without writing any code. This is useful for debugging, data exploration, and verifying Power Platform logic.

Step 1 — Get Your Dataverse Web API URL

1
Go to make.powerapps.com— Select your environment from the top-right environment picker.
2
Click the Settings (⚙) icon→ select Developer resources.
3
Copy the Web API endpoint.It looks like:
https://yourorg.api.crm.dynamics.com/api/data/v9.2
Remove /api/data/v9.2 — keep only the part ending in .dynamics.com

Step 2 — Create Postman Environment (6 Variables)

In Postman, go to Environments → + New. Add these 6 variables:

VariableInitial ValueNotes
urlhttps://yourorg.api.crm.dynamics.comYour Dataverse org URL — no trailing slash
clientid51f81489-12ee-4a9e-aaae-a2591f45987dMicrosoft's pre-registered app ID — works for all Dataverse environments. No Azure App Registration needed.
version9.2Latest stable Web API version
webapiurl{{url}}/api/data/v{{version}}/Composite variable — base URL for all requests
redirecturlhttps://localhostOAuth redirect URI
authurlhttps://login.microsoftonline.com/common/oauth2/authorize?resource={{url}}Azure AD authorization endpoint
⚠️
ImportantThe clientid value 51f81489-12ee-4a9e-aaae-a2591f45987d is a Microsoft-provided application ID pre-approved for all Dataverse environments. This is documented on Microsoft Learn. You do NOT need to register your own Azure AD app to get started.

Step 3 — Authenticate with OAuth 2.0

Create a new HTTP request. In the Authorization tab, set:

Postman Auth Tab ConfigurationOAuth 2.0
// Authorization Tab → Type: OAuth 2.0
// Add auth to: Request Headers
// Configure New Token section:

Token Name    : DataverseToken
Grant Type    : Implicit
Auth URL      : {{authurl}}
Client ID     : {{clientid}}
Redirect URL  : {{redirecturl}}
Client Auth   : Send client credentials in body

// Click "Get New Access Token" → Sign in with your M365 account
// Click "Use Token" → Token is added to Authorization header automatically

Step 4 — Verify Connection with WhoAmI

WhoAmI — Test ConnectionGET
URL: {{webapiurl}}WhoAmI

// Required Headers (add via Header Manager)
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0

// Expected Response — 200 OK
{
  "@odata.context": "https://yourorg.api.crm.dynamics.com/api/data/v9.2/$metadata#...",
  "BusinessUnitId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "UserId":         "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "OrganizationId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

CRUD Operations — Sample Requests

GET Retrieve Accounts (top 3, with filter)

Retrieve RecordsGET
// Get top 3 accounts — select specific fields
URL: {{webapiurl}}accounts?$select=name,telephone1,emailaddress1&$top=3

// Get single record by GUID
URL: {{webapiurl}}accounts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)?$select=name,revenue

// Filter — accounts in a specific city
URL: {{webapiurl}}accounts?$filter=address1_city eq 'Bangalore'&$select=name,address1_city

// Expand — contacts linked to an account
URL: {{webapiurl}}accounts?$select=name&$expand=contact_customer_accounts($select=fullname,emailaddress1)

POST Create a new Contact

Create RecordPOST
URL: {{webapiurl}}contacts

// Headers
Content-Type    : application/json
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0

// Body — raw JSON
{
  "firstname":      "Sreekanth",
  "lastname":       "Test",
  "emailaddress1":  "test@example.com",
  "telephone1":     "+91-9999999999",
  "jobtitle":       "M365 Administrator"
}

// Response: 204 No Content
// New record GUID is in the OData-EntityId response header

PATCH Update an existing record

Update RecordPATCH
URL: {{webapiurl}}contacts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)

// Headers — include If-Match: * to avoid 412 Precondition Failed
Content-Type    : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0
If-Match        : *

// Body — only include fields you want to update
{
  "jobtitle":   "Senior M365 Administrator",
  "telephone1": "+91-8888888888"
}

// Response: 204 No Content = success

DELETE Delete a record

Delete RecordDELETE
URL: {{webapiurl}}contacts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)

// No request body needed
// Response: 204 No Content = successfully deleted

2

Testing Power Automate HTTP Request Triggers

Use Postman to manually fire Power Automate flows via HTTP endpoints

Power Automate's "When an HTTP request is received" trigger generates a unique URL endpoint. Postman is the ideal tool to test these flows — send JSON payloads, inspect responses, and validate flow logic without building a frontend first.

Scenario A — Anonymous Trigger (Anyone with URL)

The simplest case — no authentication required. Suitable for dev/test only; not recommended for production.

Power Automate Anonymous HTTP TriggerPOST
// Copy the HTTP POST URL from the trigger card in Power Automate
URL: https://prod-xx.westus.logic.azure.com:443/workflows/<workflow-id>/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=<signature>

// Headers
Content-Type: application/json
Accept      : application/json

// Body — must match the JSON Schema defined in the trigger
{
  "employeeId":   "EMP001",
  "employeeName": "John Doe",
  "department":   "IT",
  "action":       "offboard"
}

// Response: 202 Accepted — flow triggered successfully

JSON Schema to Define in Power Automate Trigger

JSON Schema — Paste into Power Automate triggerJSON
{
  "type": "object",
  "properties": {
    "employeeId":   { "type": "string" },
    "employeeName": { "type": "string" },
    "department":   { "type": "string" },
    "action":       { "type": "string" }
  }
}

Scenario B — Tenant-Restricted Trigger (Entra ID OAuth Required)

When a flow is restricted to "Any user in my tenant", Postman must pass a bearer token. Two steps: get the token first, then call the flow.

Step 1 — Get Bearer Token from Entra IDPOST
URL: https://login.microsoftonline.com/<YOUR_TENANT_ID>/oauth2/v2.0/token

// Body: x-www-form-urlencoded (NOT JSON)
grant_type   : client_credentials
client_id    : <YOUR_ENTRA_APP_CLIENT_ID>
client_secret: <YOUR_ENTRA_APP_CLIENT_SECRET>
scope        : https://service.flow.microsoft.com//.default

// ⚠️ CRITICAL: Use double slash before .default in scope
// Single slash → "MisMatchingOAuthClaims" error

// Response
{
  "access_token": "eyJ0eXAiOiJKV1Qi...",
  "token_type":   "Bearer",
  "expires_in":   3599
}
Step 2 — Call the Tenant-Restricted FlowPOST
URL: https://<environmentID>.environment.api.powerplatform.com/powerautomate/automations/direct/workflows/<flow-id>/triggers/manual/paths/invoke?api-version=1&sp=/triggers/manual/run&sv=1.0&sig=<sig>

// Headers
Authorization: Bearer <access_token_from_step_1>
Content-Type : application/json
Accept       : application/json

// Body
{
  "message":     "Hello from Postman",
  "requestedBy": "Sreekanth"
}
💡
Pro Tip — Auto Token Refresh with Pre-request ScriptAdd a Pre-request Script in Postman to auto-fetch a fresh token before every request. This saves time when testing repeatedly.
Pre-request Script — Auto Token RefreshJavaScript
// Paste this in: Pre-request Script tab of your collection/request
const tokenUrl = `https://login.microsoftonline.com/${pm.environment.get('tenantId')}/oauth2/v2.0/token`;

pm.sendRequest({
  url   : tokenUrl,
  method: 'POST',
  header: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body  : {
    mode: 'urlencoded',
    urlencoded: [
      { key: 'grant_type',    value: 'client_credentials' },
      { key: 'client_id',     value: pm.environment.get('clientId') },
      { key: 'client_secret', value: pm.environment.get('clientSecret') },
      { key: 'scope',         value: 'https://service.flow.microsoft.com//.default' }
    ]
  }
}, (err, res) => {
  if (!err) {
    pm.environment.set('bearerToken', res.json().access_token);
  }
});

3

Creating Custom Connectors from a Postman Collection

Import a Postman Collection into Power Automate to auto-generate a Custom Connector — no Swagger file needed

If you have a third-party REST API, you can build a Postman Collection for it, export it, and Power Automate will automatically generate a Swagger definition and create a Custom Connector. This saves hours of manual API definition work.

Step 1 — Build and Export Your Postman Collection

Sample Collection JSON (V1 format)JSON
{
  "info": {
    "name":   "ServiceNow KB API",
    "schema": "https://schema.getpostman.com/json/collection/v1/collection.json"
  },
  "item": [
    {
      "name": "Get KB Articles",
      "request": {
        "method": "GET",
        "header": [
          { "key": "Accept",        "value": "application/json" },
          { "key": "Authorization", "value": "Bearer {{token}}" }
        ],
        "url": "{{baseUrl}}/api/now/table/kb_knowledge?sysparm_limit=10"
      }
    },
    {
      "name": "Create KB Article",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json" }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\"short_description\":\"Test Article\",\"text\":\"Content here\"}"
        },
        "url": "{{baseUrl}}/api/now/table/kb_knowledge"
      }
    }
  ]
}
1
In Postman— right-click your collection →Export
2
Select"Collection v1"format → click Export → save the JSON file locally.

Step 2 — Import into Power Automate

1
Go tomake.powerautomate.com → Data → Custom Connectors → + New custom connector → Import a Postman collection
2
Upload your exported V1 JSON file. Power Automate parses it and generates Swagger automatically.
3
Review auto-generated Actions. Configure theSecurity tab(OAuth 2.0, API Key, or Basic Auth).
4
ClickCreate Connector. Now available in Power Automate flows and Power Apps.
🎯
Best Use CaseWhen you have a third-party API (ServiceNow, Salesforce, any internal REST API) and want to call it from Power Automate or Power Apps without writing code — and you don't have a Swagger/OpenAPI spec ready.

4

Testing Custom APIs in Dataverse

Invoke and test your custom Dataverse API messages — plugin-backed actions, functions, and business events

Dataverse Custom APIs let developers define their own messages (actions or functions) — typically backed by C# plugins. Postman is the recommended tool for testing these during development. All three types of custom API calls are shown below.

Testing a Custom Unbound Action

Custom Unbound ActionPOST
// Unbound = not tied to a specific table record
URL: {{webapiurl}}myapi_CustomUnboundAPI
// Format: [publisher_prefix]_[APIName]

// Headers
Content-Type    : application/json
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0

// Body — your custom API's input parameters
{
  "InputParam1": "SampleValue",
  "InputParam2": 42,
  "FlagParam":   true
}

Testing a Custom Bound Action (tied to a record)

Custom Bound ActionPOST
// Bound to a specific table and record GUID
URL: {{webapiurl}}accounts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)/Microsoft.Dynamics.CRM.myapi_BoundAction

// Body
{
  "ActionType": "Approve",
  "Comment":    "Approved via Postman test"
}

Testing a Custom Function (read-only, uses GET)

Custom FunctionGET
// Functions use GET — parameters go in the URL, not the body
URL: {{webapiurl}}myapi_GetEmployeeDetails(EmployeeId='EMP001',Department='IT')

// Expected response — your plugin's output parameters
{
  "@odata.context": "...",
  "EmployeeName":   "Sreekanth",
  "Status":         "Active",
  "RolesAssigned":  ["M365 Admin", "Power Platform Maker"]
}
📌
Create Custom APIs via Postman (Deep Insert)Microsoft supports creating Custom API records directly via Postman using deep insert — a single POST to {{webapiurl}}customapis. Useful for rapid iteration: create, test, delete, recreate — all without going to the maker portal.

5

Power Pages Web API Testing

Test your Power Pages portal's table permissions, anonymous vs. authenticated access, and data operations

Power Pages exposes a subset of Dataverse operations through the Portals Web API. This runs in portal context and respects table permissions (web roles). Postman is useful for verifying whether your portal's permissions are configured correctly.

⚠️
Power Pages Web API ≠ Dataverse Web APIThe base URL is your portal URL (not the Dataverse org URL). Authentication uses portal session cookies — not the Dataverse OAuth token. Keep these as separate Postman environments.

GET — Retrieve Data via Power Pages Web API

Power Pages Web API — RetrieveGET
// Base URL: your portal URL + /_api/
URL: https://yourportal.powerappsportals.com/_api/contacts?$select=fullname,emailaddress1&$top=5

// Required Headers
Accept                    : application/json
Content-Type              : application/json
__RequestVerificationToken: <antiforgery_token>

// For authenticated portal sessions, pass the cookie:
Cookie: .AspNet.ApplicationCookie=<cookie_value>

// Get antiforgery token first:
// GET https://yourportal.powerappsportals.com/_api/antiforgery/token

POST — Create a Record via Power Pages Web API

Power Pages Web API — CreatePOST
URL: https://yourportal.powerappsportals.com/_api/cr123_customentities

// Headers
Content-Type              : application/json
__RequestVerificationToken: <antiforgery_token>

// Body
{
  "cr123_name":   "Test Record from Postman",
  "cr123_status": "Active"
}

// 201 Created  = success
// 403 Forbidden = table permission not granted for the web role
// 401 Unauthorized = portal login required
💡
Debug Table Permissions FasterWhen Power Pages returns 403, test the same request with different portal session cookies (different web roles) to identify which web role is missing the required table permission. Much faster than navigating the portal admin UI repeatedly.

6

Native Postman ↔ Power Automate Integration

Postman natively integrates with Power Automate — backup collections, send monitor results, and post activity feeds

In this scenario, Postman is the event source and Power Automate is the target. Postman sends events — collection changes, monitor results, team activity — into Power Automate flows via webhooks. Useful for team notification workflows and automated backups.

Use Case 1 — Auto-backup a Postman Collection

1
In Power Automate, create a flow with When an HTTP request is received trigger. Copy the generated webhook URL.
2
In Postman:Home → Integrations → Browse All Integrations → Search "Microsoft Power Automate"
3
Select"Back up a collection". Enter the Nickname, choose your Workspace, choose your Collection, paste the Notification URL (webhook). ClickAdd Integration.
4
Postman detects collection changes and sends them to Power Automate. Your flow can then save the backup to SharePoint, OneDrive, or any storage connector.

Use Case 2 — Monitor Results → Teams/Email Notification

JSON Schema for Monitor Results WebhookJSON
// Use this schema in your Power Automate HTTP trigger to parse Postman monitor payloads
{
  "type": "object",
  "properties": {
    "collection_uid": { "type": "string" },
    "monitor_name":   { "type": "string" },
    "run_id":         { "type": "string" },
    "status":         { "type": "string" },
    "failed_count":   { "type": "integer" },
    "passed_count":   { "type": "integer" },
    "run_at":         { "type": "string" }
  }
}

// In Power Automate after trigger:
// 1. Parse JSON using the schema above
// 2. Condition: if failed_count > 0
// 3. Send Teams notification OR email via Outlook connector

Use Case 3 — Team Activity → Email Notification

Integration Setup — Team ActivityConfig
// In Postman Integration setup for "Post collection activity":
Nickname         : MyTeam-PA-Integration
Choose Workspace : Your shared workspace name
Choose Collection: Your collection to monitor
Notification URL : <Power Automate webhook URL>

// Power Automate flow actions after trigger:
// Parse JSON → Send Outlook email with dynamic content:
//   "Collection @{body('Parse_JSON')?['collection_name']} was updated
//    by @{body('Parse_JSON')?['triggered_by']} at @{utcNow()}"

⚡ Common Errors & Quick Fixes

401 Unauthorized

Token expired or missing. Click Get New Access Token in Postman Auth tab. For tenant-restricted flows, verify your Entra app has Power Automate API permissions with admin consent.

403 Forbidden

Authenticated but not authorized. For Dataverse: user lacks the required security role. For Power Pages: table permission not assigned to the web role.

406 Not Acceptable

Wrong or missing Accept header. Add Accept: application/json + OData-MaxVersion: 4.0 + OData-Version: 4.0 to all Dataverse requests.

415 Unsupported Media Type

Missing Content-Type: application/json on POST/PATCH requests. Always include this when sending a JSON body.

404 Not Found

Entity set name wrong or record GUID doesn't exist. Verify the entity set name using {{webapiurl}}$metadata — it's plural and lowercase (e.g., contacts, not Contact).

MisMatchingOAuthClaims

Power Automate API scope URL has a single slash. Must use double slash: https://service.flow.microsoft.com//.default

Required Headers — Quick Reference

Headers Cheat SheetReference
// ── Dataverse Web API (all requests) ────────────────────────
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0
Authorization   : Bearer {{accessToken}}

// ── Add for POST / PATCH (requests with a body) ──────────────
Content-Type    : application/json

// ── Add for PATCH to avoid 412 errors ────────────────────────
If-Match        : *

// ── Power Pages Web API ───────────────────────────────────────
Accept                    : application/json
Content-Type              : application/json
__RequestVerificationToken: <antiforgery_token>

// ── Power Automate HTTP Trigger ───────────────────────────────
Content-Type    : application/json
Accept          : application/json
Authorization   : Bearer <entra_token>   // only for tenant-restricted flows

Featured Post

Power Pages Complete Guide

  Power Pages Complete Guide From Basics to Advanced — Web API, Auth, Liquid, Scenarios & Cheat Sheet Table of Contents Basic Quest...

Popular posts