top of page

Power Pages to Power Automate: secure dropdowns with a mapped flow

  • Writer: Sofia Ng
    Sofia Ng
  • Sep 30
  • 8 min read

Sometimes a form needs values that live outside Dataverse. This post shows a simple setup that calls a mapped Power Automate flow from Power Pages, gets a JSON list from your API, and fills one or more dropdowns. The call stays on your domain, the portal adds its anti-forgery header, and only signed-in users can reach it.



Cute platypus at a computer loading “Dropdown A” and “Dropdown B” from a secure Cloud Flow; simple Page → Flow → API → JSON diagram.

The idea at a glance

  1. The page calls the mapped flow with a small payload like { mode: "locations" }.

  2. The flow parses the payload, calls the API, and normalises the list to { id, text }.

  3. The page renders the items in a <select> and keeps a hidden field in sync for form submit.


What we’re building

  • A Cloud Flow mapped to the site with four trigger inputs: mode (Text), search (Text), locationId (Text), top (Number).

  • The flow calls your API and returns an array of { id, text }.

  • The page calls the flow to fill Dropdown A. If needed, it calls again to fill Dropdown B using the selected id from A.

  • Hidden inputs capture the selected ids for the form submit.


Create and map the flow

  1. In Power Automate, create a Cloud flow with the Power Pages trigger.

  2. Add these four inputs to the trigger (exactly as shown in your screenshot):

    • mode (Text)

    • search (Text)

    • locationId (Text)

    • top (Number)

      Power Automate trigger setup with 4 inputs, mode, search, locationid, top
  3. Call external API as needed and form a return array

  4. Return the values to the caller with a return values to Power Pages action

    Power Automate return action - return values ot Power Pages, resultjson
  5. In Power Pages → Set up→ Cloud Flows, map this flow to your site.

    ree
  6. Copy the generated endpoint id - Only signed-in users on your site can reach it. The portal adds the anti-forgery header automatically.

    ree

Add the dropdown script to Power Pages

Prep the form

  • Decide where the dropdowns live. I’ll call them Dropdown A and Dropdown B (optional, depends on A).

  • Add two text columns to your Dataverse table to store the selected ids. Example names:

    • dropdownaid (Single line of text)

    • dropdownbid (Single line of text)

  • Put both columns on the form. Set them to Hidden in the form designer.These are only used to capture the selected ids when the user submits.


Create a web file for the script

  • In Power Pages, create a Web File.

  • Name: secure-dropdowns.cloudflow.js

  • Partial URL: something short, for example /secure-dropdowns.portal.js

  • Save.

  • We will upload the file later

    ree

Reference the script on the page

  • Open the page that hosts your form.

  • In the page content (or the Web Template), reference the web file after the form renders. Order matters: the script should load after {% entityform %} so it can find the inputs.

  • Publish the page.


Point the script at your flows

Before you paste the code (next step), have these ready:

  • The mapped flow URL for Dropdown A Example: /_api/cloudflow/v1.0/trigger/<guid-for-A>

  • The mapped flow URL for Dropdown B Example: /_api/cloudflow/v1.0/trigger/<guid-for-B>

  • The four trigger inputs you created in the flow:mode (Text), search (Text), locationId (Text), top (Number). The script will send those as named fields.


Match ids so values are stored

You’ll set four ids in the script so it knows which controls to wire up:

  • Visible selects it creates: ppDropdownA, ppDropdownB.

  • Hidden form inputs (the two columns you added): st_dropdownaid, st_dropdownbid.

If your form uses different ids, change them in the script so they match. The script writes the selected values into those hidden inputs right before submit.


What happens on the page

When the page loads:

  1. The script calls your Dropdown A flow with { mode: "dropdownA", search: "", locationId: "", top: 1000 }.

  2. The flow calls your API, normalises the list to { id, text }, and returns JSON.

  3. The script renders the options in Dropdown A.

  4. When the user picks from A, the script calls the Dropdown B flow with { mode: "dropdownB", search: "", locationId: <A’s id>, top: 1000 } and fills B.

  5. Both selections are mirrored into the hidden inputs so the record stores the ids on submit.


All calls are same-origin through /_api/cloudflow/.... The portal adds the anti-forgery header. Only signed-in users on this site can reach it.


Test checklist

  • Page is Authenticated and your web role has access.

  • Open DevTools → Network. You should see a POST to /_api/cloudflow/... as the page loads.

  • Dropdown A shows options from your API.

  • Choosing A triggers a second POST for Dropdown B, which then shows options.

  • Submitting the form writes the ids into your two hidden columns.


Troubleshooting

  • Empty dropdown: check the Flow run history; confirm the HTTP step returned an array.

  • “Failed to load” message: the flow likely returned a non-200 or invalid JSON; verify the Return value(s) to Power Pages is set to your response string.

  • Wrong ids saved: the hidden input ids in the script must match your form field ids exactly.

  • Large lists: cap top in the flow and consider server-side search in Part 2.


Security notes

  • Keep keys in the Flow connection or environment variables, not in the page.

  • Validate mode, locationId, and top in the flow. Return an empty list for anything unexpected.

  • Scope access to the page with web roles only where needed.


Configure Web page

Add the script to the page (under Copy (HTML)) containing the form where the drop downs should be added.


<script>

// MS-recommended wrapper that adds the __RequestVerificationToken header

(function (webapi, $) {

  function safeAjax(ajaxOptions) {

    var d = $.Deferred();

    shell.getTokenDeferred().done(function (token) {

      ajaxOptions.headers = ajaxOptions.headers || {};

      ajaxOptions.headers["__RequestVerificationToken"] = token;

      $.ajax(ajaxOptions).done(d.resolve).fail(d.reject);

    }).fail(d.reject);

    return d.promise();

  }

  window.webapi = window.webapi || {};

  window.webapi.safeAjax = safeAjax;

})(window.webapi || {}, jQuery);

</script>

{% if user %}

  <script>

    // Contact GUID of the signed-in portal user

    window.PP_CONTACT_ID = "{{ user.id }}";

  </script>

{% else %}

  <script>window.PP_CONTACT_ID = "";</script>

{% endif %}

<metadata:childfiles

<div class="row sectionBlockLayout text-start" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">

  <div class="container" style="display: flex; flex-wrap: wrap;"><div class="col-lg-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 250px; padding: 16px; margin: 60px 0px;">{% entityform name: 'MyDynamicForm' %}</div></div>

</div>

 <script type="text/javascript" src="~/secure-dropdowns.portal.js"></script

></metadata:childfiles>


If you are looking at making your Power Pages more Power reach out to us, we LOVE this stuff!


Code


<!-- secure-dropdowns.cloudflow.js -->

/* secure-dropdowns.cloudflow.js

- Calls a mapped Cloud Flow from Power Pages

- Two searchable dropdowns (A, then B filtered by A) using Select2

*/

(() => {

"use strict";


const log = (...a) => console.log("[pp-sec]", ...a);


// ----- Cloud Flow caller (payload stays wrapped in eventData) -----

function callPowerAutomateFlow(flowUrl, payload) {

return new Promise((resolve, reject) => {

if (!flowUrl) return reject(new Error("Flow URL is required"));

if (!payload) return reject(new Error("Payload is required"));

const requestPayload = { eventData: JSON.stringify(payload) };

shell

.ajaxSafePost({ type: "POST", url: flowUrl, data: requestPayload })

.done(resp => { try { resolve(JSON.parse(resp)); } catch (e) { reject(new Error("Failed to parse response: " + e.message)); } })

.fail((xhr, status, err) => reject(new Error("Flow call failed: " + (err || status || "Unknown error"))));

});

}


// ----- CONFIG: update these to your mapped flow URLs and field IDs -----

const FLOWS = {

DROPDOWN_A: "/_api/cloudflow/v1.0/trigger/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx",

DROPDOWN_B: "/_api/cloudflow/v1.0/trigger/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"

};


// Hidden form inputs that will store the selected ids

const FIELD_SELECTORS = {

dropdownA: 'input[id="st_dropdownaid"]',

dropdownB: 'input[id="st_dropdownbid"]'

};


// IDs for the visible selects the script injects

const UI_IDS = { dropdownA: "ppDropdownA", dropdownB: "ppDropdownB" };


const NET = { timeoutMs: 15000, pageSize: 1000 };


// ----- utils -----

const escapeHtml = s => { const d = document.createElement("div"); d.textContent = String(s ?? ""); return d.innerHTML; };

async function waitFor(sel,{timeout=10000,interval=100}={}){ const t0=Date.now(); while(Date.now()-t0<timeout){ const el=document.querySelector(sel); if(el) return el; await new Promise(r=>setTimeout(r,interval)); } throw new Error(`Timed out waiting for ${sel}`); }

const norm = s => (s||"").toString().normalize("NFD").replace(/\p{Diacritic}/gu,"").toLowerCase();


// ----- Select2 loader and init -----

async function ensureSelect2(){

const jq = window.jQuery;

if (jq && jq.fn && jq.fn.select2) return;


const cssHref = "https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css";

if (![...document.styleSheets].some(s=>s.href && s.href.includes("select2"))) {

const l = document.createElement("link"); l.rel = "stylesheet"; l.href = cssHref; document.head.appendChild(l);

}

const style = document.createElement("style");

style.textContent = ".select2-search--dropdown{display:block!important}";

document.head.appendChild(style);


await new Promise((res, rej) => {

const s = document.createElement("script");

s.src = "https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js";

s.onload = res; s.onerror = () => rej(new Error("Failed to load Select2"));

document.head.appendChild(s);

});

}

function initSelect2(el, placeholder){

const $ = window.jQuery;

if (!($ && $.fn && $.fn.select2)) return;

const parent = el.closest(".secure-dropdowns") || el.parentElement || document.body;

$(el).select2({

width: "100%",

placeholder,

allowClear: true,

minimumResultsForSearch: 0,

closeOnSelect: true,

dropdownParent: parent,

matcher: function(params, data){

if (!params.term || !data.text) return data;

const term = norm(params.term);

const text = norm(data.text);

return (text.startsWith(term) || text.split(/\s+/).some(w => w.startsWith(term))) ? data : null;

}

}).on("select2:open", () => {

document.querySelector(".select2-container--open .select2-search__field")?.focus();

});

}

function reinitSelect2(el, placeholder){

const $ = window.jQuery;

if ($ && $.fn && $.fn.select2) { $(el).select2("destroy"); }

initSelect2(el, placeholder);

}


// ----- flex response shape: returns [{id,text}, ...] -----

function unpackItems(resp) {

const toObj = v => { if (v==null) return null; if (typeof v === "string") { try { return JSON.parse(v.trim()); } catch { return null; } } return v; };

const top = toObj(resp) || {};

if (Array.isArray(top)) return top;

if (Array.isArray(top.value)) return top.value;

for (const k of ["resultjson","ResultJson","result","Result","payload","Payload","body"]) {

const inner = toObj(top[k]);

if (Array.isArray(inner)) return inner;

if (Array.isArray(inner?.value)) return inner.value;

}

return [];

}


function wireSyncToHidden(selectEl, sync) {

selectEl.addEventListener("change", sync, { passive: true });

const $ = window.jQuery;

if ($ && $.fn && $.fn.select2) {

$(selectEl).off(".ppsync");

$(selectEl)

.on("select2:select.ppsync select2:unselect.ppsync select2:clear.ppsync", sync)

.on("select2:close.ppsync", sync);

}

}


// ----- data fetchers (send the 4 inputs: mode, search, locationId, top) -----

async function loadDropdownAItems() {

const response = await callPowerAutomateFlow(FLOWS.DROPDOWN_A, { mode: "dropdownA", search: "", top: NET.pageSize });

return unpackItems(response);

}

async function loadDropdownBItems(aId) {

const payload = { mode: "dropdownB", locationId: aId, search: "", top: NET.pageSize }; // keep key name 'locationId' to match your flow

log("fetch B for", aId, payload);

const response = await callPowerAutomateFlow(FLOWS.DROPDOWN_B, payload);

return unpackItems(response);

}


// ----- UI helpers -----

function populateSelect(selectEl, options, placeholder){

selectEl.innerHTML = "";

const blank = document.createElement("option"); blank.value = ""; blank.textContent = placeholder || "Select an option"; selectEl.appendChild(blank);

(options||[]).forEach(o => {

if (!o?.id || !o?.text) return;

const opt = document.createElement("option");

opt.value = String(o.id);

opt.innerHTML = escapeHtml(o.text);

selectEl.appendChild(opt);

});

}


function buildUIContainer(){

const firstTarget = document.querySelector(FIELD_SELECTORS.dropdownA) || document.querySelector(FIELD_SELECTORS.dropdownB);

if(!firstTarget) throw new Error("Hidden target fields not found. Check FIELD_SELECTORS.");

const wrap = document.createElement("div");

wrap.innerHTML=`

<div class="secure-dropdowns">

<div class="mb-3">

<label class="form-label">Dropdown A</label>

<select id="${UI_IDS.dropdownA}" class="form-control"><option>Loading...</option></select>

<div class="dropdown-status a" style="font-size:.8em;color:#666;margin-top:4px;"></div>

</div>

<div class="mb-3">

<label class="form-label">Dropdown B</label>

<select id="${UI_IDS.dropdownB}" class="form-control" disabled><option>Select from A first...</option></select>

<div class="dropdown-status b" style="font-size:.8em;color:#666;margin-top:4px;"></div>

</div>

</div>`;

const control = firstTarget.closest(".control") || firstTarget.parentElement;

control.parentElement.insertBefore(wrap, control);

[FIELD_SELECTORS.dropdownA, FIELD_SELECTORS.dropdownB].forEach(sel=>{ const el=document.querySelector(sel); if(el) el.type="hidden"; });

return {

selA: document.getElementById(UI_IDS.dropdownA),

selB: document.getElementById(UI_IDS.dropdownB),

statusA: wrap.querySelector(".dropdown-status.a"),

statusB: wrap.querySelector(".dropdown-status.b")

};

}


function syncHidden(selA, selB){

const hfA = document.querySelector(FIELD_SELECTORS.dropdownA);

const hfB = document.querySelector(FIELD_SELECTORS.dropdownB);

if (hfA) { hfA.disabled = false; hfA.value = selA.value || ""; hfA.setAttribute("value", hfA.value); }

if (hfB) { hfB.disabled = false; hfB.value = selB.value || ""; hfB.setAttribute("value", hfB.value); }

if (typeof window.setIsDirty === "function") {

if (hfA) window.setIsDirty(hfA.id);

if (hfB) window.setIsDirty(hfB.id);

}

}


// ----- init -----

async function init(){

await waitFor(`${FIELD_SELECTORS.dropdownA},${FIELD_SELECTORS.dropdownB}`);

await ensureSelect2();


const { selA, selB, statusA, statusB } = buildUIContainer();


initSelect2(selA, "Select an option...");

initSelect2(selB, "Select an option...");

wireSyncToHidden(selA, () => syncHidden(selA, selB));

wireSyncToHidden(selB, () => syncHidden(selA, selB));


// Load A once

try{

statusA.textContent = "Loading options...";

const itemsA = await loadDropdownAItems();

populateSelect(selA, itemsA, "Select an option...");

statusA.textContent = `${itemsA.length} item(s)`;

reinitSelect2(selA, "Select an option...");

wireSyncToHidden(selA, () => syncHidden(selA, selB));

}catch(e){

log("A load failed:", e);

populateSelect(selA, [], "Failed to load");

statusA.textContent = "Failed to load";

reinitSelect2(selA, "Select an option...");

wireSyncToHidden(selA, () => syncHidden(selA, selB));

}


// When A changes, load B (cache per A)

const CACHE = {};

selA.addEventListener("change", async ()=>{

const aId = selA.value || "";

log("A changed:", aId);

selB.value = "";


if(!aId){

populateSelect(selB, [], "Select from A first...");

selB.disabled = true;

statusB.textContent = "Select from A first...";

reinitSelect2(selB, "Select an option...");

wireSyncToHidden(selB, () => syncHidden(selA, selB));

return syncHidden(selA, selB);

}


selB.disabled = true;

statusB.textContent = "Loading options...";


if(!CACHE[aId]){

try { CACHE[aId] = await loadDropdownBItems(aId); }

catch(e){ log("B load failed:", e); CACHE[aId] = []; statusB.textContent = "Failed to load"; }

}


const rows = CACHE[aId] || [];

populateSelect(selB, rows, "Select an option...");

selB.disabled = false;

statusB.textContent = `${rows.length} item(s)`;

reinitSelect2(selB, "Select an option...");

wireSyncToHidden(selB, () => syncHidden(selA, selB));


syncHidden(selA, selB);

});


selB.addEventListener("change", ()=>syncHidden(selA, selB));

const form = selA.closest("form") || document.querySelector("form");

if(form) form.addEventListener("submit", ()=>syncHidden(selA, selB));

}


if(document.readyState==="loading") document.addEventListener("DOMContentLoaded", ()=>init().catch(console.error));

else init().catch(console.error);

})();

Contact Us

QUESTIONS?

WE'RE HERE TO HELP

  • LinkedIn

© 2023 by Ava Technology Solutions. Proudly created with Wix.com

bottom of page