What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Integrations
Render Agility content on a Microsoft Power Pages site: server logic calls the Fetch API on each page load, Liquid renders it, and editors preview and edit in context with Web Studio.
We last checked this guide on November 2, 2026, by building a Power Pages site that reads its content from Agility. Microsoft updates Power Pages often, so if something here doesn't match what you see, Microsoft's documentation has the latest.
Many organizations run their intranet on Power Pages. IT likes it: it's part of the Microsoft stack, and people sign in with their work accounts. But Power Pages wasn't built for communicators, so every news post or policy update turns into a request to IT. Agility fills that gap. Editors write, review and publish in Agility, and the Power Pages site shows the change about 30 seconds later.
| Piece | Lives in | Does |
|---|---|---|
| Content models, lists and items | Agility | What editors create, approve and publish |
| Server logic (JavaScript) | Power Pages | Calls the Agility Fetch API with keys held in site settings, and returns JSON |
| Web template (Liquid) | Power Pages | Calls the server logic with the serverlogic tag and renders the HTML |
| Site settings | Power Pages | Hold the instance GUID, API keys and the expected preview key |
| Sign-in, web roles, page layout | Power Pages | Who can see the site, and the page structure |
The API keys stay on the server. The browser only receives the rendered HTML.
ServerLogic/Enabled controls it, and ServerLogic/AllowedDomains can restrict which hosts it may call. If you restrict domains, allow the Agility Fetch API host for your data region (see Content Security Policy and Network Allowlist).Start with the content, not the page. Each kind of thing the site shows becomes a shared content list in Agility. For an intranet that might be:
| List (reference name) | Fields |
|---|---|
announcements | Title, Summary, Body (HTML), Image, Category, Post Date, Pinned (True/False) |
quicklinks | Title, Link (URL), Description |
documentlinks | Title, Description, Document (URL to the file in SharePoint), Category, Department |
handbooksections | Title, Summary, Body (HTML), Sort Order |
The Fetch API reads a list by its reference name, in lowercase: /fetch/en-us/list/announcements. Turn on approvals for the lists that need review. See Content Models.
Site settings are where Power Pages keeps configuration. Putting the keys there keeps them out of your code and away from the browser. In the Power Pages Management app, add:
| Site setting | Value |
|---|---|
Agility/Guid | Your instance GUID |
Agility/FetchKey | The Fetch API key (published content) |
Agility/PreviewKey | The Preview API key (staging content) |
Agility/PreviewToken | The expected agilitypreviewkey value (see Step 5) |
The setting names are your choice. Keep them consistent with the server logic.
Server logic is JavaScript that runs on Power Pages' servers. It's the only piece that talks to Agility.
In design studio, go to Set up > Server logic, create a server logic record named agility, and assign it to the web roles that can see the pages it serves (for example Authenticated Users, plus Anonymous Users if the site is public). Then edit its code.
This version exposes one operation, list, that reads a content list. It only reads the lists you name, and it reads staging content only when the preview key checks out.
const ALLOWED_LISTS = ["announcements", "quicklinks", "documentlinks", "handbooksections"];
const API_HOST = "https://api.aglty.io"; // use the Fetch API host for your data region
async function fetchList(ref, preview, take) {
if (ALLOWED_LISTS.indexOf(ref) < 0) {
return { ok: false, error: "Unknown list: " + ref, items: [] };
}
const guid = Server.SiteSetting.Get("Agility/Guid");
const key = Server.SiteSetting.Get(preview ? "Agility/PreviewKey" : "Agility/FetchKey");
const url = API_HOST + "/" + guid + "/" + (preview ? "preview" : "fetch") +
"/en-us/list/" + ref + "?take=" + take + "&contentLinkDepth=0";
const response = JSON.parse(await Server.Connector.HttpClient.GetAsync(url, { APIKey: key }));
if (!response.IsSuccessStatusCode) {
return { ok: false, error: "Agility returned " + response.StatusCode, items: [] };
}
const body = JSON.parse(response.Body);
const items = (body.items || []).map((item) => {
return { contentID: item.contentID, fields: item.fields || {} };
});
return { ok: true, preview: preview, items: items };
}
// The expected agilitypreviewkey value is stored in a site setting (see Step 5).
function isPreviewKey(key) {
const expected = Server.SiteSetting.Get("Agility/PreviewToken");
// A '+' in the key can arrive as a space after URL decoding.
return !!key && !!expected && String(key).replace(/ /g, "+") === expected;
}
// Called from Liquid: {% serverlogic name: 'agility', operation: 'list', input: ..., output: ... %}
async function list() {
try {
const input = JSON.parse(Server.Context.Input || "{}");
const take = Math.min(parseInt(input.take || "50", 10) || 50, 100);
const preview = isPreviewKey(input.previewKey);
return JSON.stringify(await fetchList(String(input.ref || ""), preview, take));
} catch (e) {
return JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e), items: [] });
}
}
Use arrow functions for callbacks. In our testing, an anonymous function (item) { … } expression passed to .map made the whole script fail with only "Exception occurred while processing this request", even inside try/catch. Named top-level function declarations work.
A content list returns 10 items unless you pass take. The maximum is 250. See Content Fetch API.
This is where content becomes HTML. Create a web template (and a page template that uses it), then call the server logic once per list. The serverlogic tag returns an object with success and data, where data is the parsed JSON your function returned.
{% assign pkey = request.params['agilitypreviewkey'] | default: '' | replace: '"', '' %}
{% capture in_news %}{"ref":"announcements","previewKey":"{{ pkey }}","take":20}{% endcapture %}
{% serverlogic name: 'agility', operation: 'list', input: in_news, output: news %}
{% if news.data.preview %}<div class="preview-banner">Preview: showing unpublished content</div>{% endif %}
<section id="news">
<h2>News</h2>
{% if news.success and news.data.ok %}
{% for a in news.data.items %}
<article>
<h3>{{ a.fields.title | escape }}</h3>
<p>{{ a.fields.summary | escape }}</p>
<div>{{ a.fields.body }}</div>
</article>
{% endfor %}
{% else %}
<p>News couldn't be loaded. {{ news.data.error | default: news.error | escape }}</p>
{% endif %}
</section>
| escape. Output HTML fields (such as Body) unescaped only when they come from an Agility HTML field your editors control.serverlogic call adds a request to page rendering. Call only the lists a page shows, and keep take small.sort and direction parameters.Editors want to see a change before anyone else does. When they click Preview, Agility opens your site with a key in the URL. Your job is to check that key, so drafts show for editors and nobody else.
The key arrives as ?agilitypreviewkey=. The @agility/nextjs and .NET SDKs check it for you; on Power Pages you do it yourself.
The expected value is the Base64-encoded SHA-512 hash of the UTF-16LE string -1_{securityKey}_Preview, where {securityKey} is your instance's Security Key (Settings > API Keys). Server logic has no hashing functions, so compute the value once and store it in the Agility/PreviewToken site setting. With Node.js:
node -e 'const k=process.argv[1];console.log(require("crypto").createHash("sha512").update(Buffer.from(`-1_${k}_Preview`,"utf16le")).digest("base64"))' "YOUR_SECURITY_KEY"
Recompute it if the Security Key is reset.
Don't switch to staging content just because the parameter is present, or because of a parameter such as ?preview=true. Anyone could add it to a URL and read unpublished content.
Then register the site so editors can open it from Agility: Settings > Sitemaps > Setup Deployment > Custom Deployment, with your Power Pages URL. See Setting Up Preview.
Web Studio shows the real site inside Agility. Editors hover over any piece of content, click it, and watch their change appear as they type. The general setup is in Set Up Web Studio with Any Framework. On Power Pages:
HTTP/Content-Security-Policy = frame-ancestors 'self' https://app.agilitycms.com; and HTTP/SameSite/Default = None.home for /home), and put its page ID on the element that wraps your content: data-agility-page="2".<body>, so set the GUID from a script: document.body.setAttribute("data-agility-guid", "YOUR_INSTANCE_GUID").<script src="https://unpkg.com/@agility/web-studio-sdk@latest/dist/index.js"></script>.data-agility-nested-listitem, not data-agility-component. Items read from a list are not components on the Agility page. Tagged as components, Web Studio opened a blank new item; tagged as nested list items, it opens the real item and updates it live as the editor types.<article data-agility-nested-listitem="{{ a.contentID }}">
<h3 data-agility-field="title">{{ a.fields.title | escape }}</h3>
<div data-agility-field="body" data-agility-html="true">{{ a.fields.body }}</div>
</article>
A Power Pages site that requires Microsoft sign-in does not load inside Web Studio. Inside the cross-site frame, the browser doesn't send the site's sign-in cookie, so Power Pages redirects to Microsoft sign-in, and Microsoft sign-in refuses to load in a frame. Signing in on another tab doesn't help.
You have two options:
| Option | What editors get | Trade-off |
|---|---|---|
| Keep the site private | Preview links that open in a new browser tab, where the editor is signed in | No in-context editing in Web Studio |
| Make a site public for Web Studio (for example a separate non-production site) | Full Web Studio: outlines, click-to-edit, live updates | Anyone with the URL can read the published content. Staging content still needs a valid preview key |
On a trial or other non-production environment, making a site public also needs a tenant setting: Power Platform admin center > Manage > Power Pages > Settings > Site visibility for non-production sites. Until it allows the site, pac power-pages update-site-visibility fails with a bare HTTP 400.
| Change | How |
|---|---|
| Web templates, page templates, site settings that already exist | pac pages upload --path <site folder> --modelVersion 2 |
| Server logic code | Edit it in design studio, or upload the record's .sl file content through the Dataverse Web API. pac pages doesn't handle server logic |
| New records on enhanced data model sites | Create them in design studio or through the Dataverse Web API. In our testing (pac 2.14.1), pac pages upload left empty records it couldn't update or delete |
Server logic code uploaded through the Dataverse Web API wasn't picked up until the site restarted: pac power-pages restart-website --id <portal id>, then about a minute.
| Symptom | Likely cause |
|---|---|
| "Exception occurred while processing this request" from server logic | An anonymous function (…) expression, or reading a missing query parameter. Server.Context.QueryParameters["x"] throws when x isn't there, so wrap reads in try/catch |
| A code change has no effect | The site hasn't restarted since the upload |
news.success is false | The server logic record isn't assigned to the current user's web role, outbound calls are blocked, or the Agility host isn't in ServerLogic/AllowedDomains |
Agility returned 401 | Wrong key in site settings, or the Preview key used against /fetch (or the Fetch key against /preview) |
| A published change takes a while to show | Power Pages doesn't add its own caching. Allow about 30 seconds for Agility's CDN and refresh |
| Preview shows published content | Agility/PreviewToken doesn't match the current Security Key, or the key was mangled (+ arriving as a space) |
| Web Studio shows Microsoft sign-in, or a refused-to-connect message | The site is private, or the CSP site setting is missing. See Private sites and Web Studio and Troubleshooting Web Studio |