Power Automate — operate cloud flows through Dataverse, honestly
Drive Microsoft Power Automate cloud flows from code: create, enable, update, list, and delete them, plus pull run history for debugging. This skill operates the live surface. Deciding what the flow should do and shaping its trigger→actions→error definition is design — that lives in ../automation-flows/SKILL.md; this skill wraps that definition, ships it, and manages it.
Read these three facts before you write a line — they set the boundary of what is even possible:
- Only flows inside a Dataverse solution are code-manageable. Classic personal "My Flows" cannot be created or edited by code — the Microsoft docs say so explicitly. If the target is a My Flow, the honest answer is: move it into a solution first, or drive it by hand. There is no API workaround.
api.flow.microsoft.comis unsupported. Microsoft's own words: use it "at your own risk," it is subject to breaking changes. The supported programmatic surface is the Dataverse Web API (or the .NET SDK) against theworkflowtable. For admin-style operations the Power Automate Management connector is the other supported path.- The unified Power Platform API (
api.powerplatform.com) is maturing fast. It already lists cloud flows (api-version2024-10-01) and its Inventory API went GA in early 2026. It may eventually supersede the Dataverse path for flow management. Treat the Dataverse-vs-Power-Platform-API split as fast-moving — verify the current recommendation at author time.
Connect first — API vs MCP
Everything runs against your org's Dataverse Web API. Base URL: https://{org}.{region}.dynamics.com/api/data/v9.2 (find yours under Power Platform admin → your environment → developer resources). Auth is OAuth2 / Entra ID — a user token or, for CI, a service principal (app registration). For a service principal the org needs a Dataverse application user mapped to that app plus a security role; the token audience is the Dataverse URL. Full setup, including the delegated user_impersonation vs app .default scope split and the 401/403 causes, is in references/entra-auth-setup.md.
Put these in .env (never inline a secret in a flow or a script):
PA_DATAVERSE_URL=https://contoso.crm.dynamics.com # no trailing slash, no /api/...
PA_TENANT_ID=<entra-tenant-guid>
PA_CLIENT_ID=<app-registration-client-id>
PA_CLIENT_SECRET=<app-registration-secret> # service-principal flow only
When to use the REST API vs the MCP:
| You need to… | Use | Why |
|---|---|---|
| Create / enable / update / delete / list flows | Dataverse Web API | The only supported CRUD surface; scriptable, CI-friendly, service-principal auth. |
| Read a flow's action-level run inputs/outputs to debug a failure | FlowStudio MCP (third-party) | Dataverse exposes run records (flowrun table) but not per-action I/O; the MCP does. See references/flowstudio-mcp-and-limits.md. |
| Admin-scope operations (turn on/off across an environment) | Power Automate Management connector | Supported management surface when raw Dataverse is awkward. |
FlowStudio MCP is NOT Microsoft-affiliated — it is a de-facto third-party server (mcp.flowstudio.app/mcp). Microsoft's own MCP story is Copilot Studio consuming MCP servers and a Dataverse MCP, neither of which authors Power Automate flows. Flag the third-party dependency to anyone before wiring it into a pipeline.
The workflow table — the data model
Cloud flows are rows in the Dataverse Process (workflow) table. The columns that matter:
| Column | Meaning | Values you use |
|---|---|---|
category |
Kind of process | 5 = modern cloud flow (automated / instant / scheduled). (0 classic workflow, 4 business process flow, 6 desktop flow.) |
type |
Definition vs template | 1 = Definition (a runnable flow). |
statecode |
On/off state | 0 = Draft (Off), 1 = Activated (On), 2 = Suspended. |
name |
Display name | your string |
primaryentity |
Bound table | "none" for automated/instant/scheduled flows |
clientdata |
The flow itself | string-encoded JSON (see below) |
workflowid |
GUID key | returned on create; used in workflows({id}) |
List the cloud flows that are on:
curl -s "$PA_DATAVERSE_URL/api/data/v9.2/workflows?\$filter=category eq 5 and statecode eq 1&\$select=name,statecode,type,workflowid" \
-H "Authorization: Bearer $TOKEN" -H "OData-Version: 4.0" -H "Accept: application/json"
clientdata — the payload, and the trap that bites everyone
clientdata is a JSON string, not a nested JSON object. It is the serialized form of:
{
"properties": {
"connectionReferences": { "shared_commondataserviceforapps": { "runtimeSource": "embedded", "connection": {}, "api": { "name": "shared_commondataserviceforapps" } } },
"definition": { "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", "contentVersion": "1.0.0.0", "triggers": { }, "actions": { } }
},
"schemaVersion": "1.0.0.0"
}
Two load-bearing parts:
definition— a Logic Apps workflow definition:triggers(exactly one) thenactions. This is the design artifact. Do not invent it from scratch here — get the trigger→actions→branch→error shape from../automation-flows/SKILL.md, then drop it intodefinition. Fastest reliable way to get a real one: build the flow once in the maker portal, export the solution, and copy itsclientdata.connectionReferences— the map from the definition's connectors to actual connections. A flow whose connection references are not authorized will not turn on. In a solution these are connection-reference records the target environment must resolve; unresolved references are the #1 reason aPATCH statecode=1"succeeds" but the flow never runs.
The trap: clientdata must be escaped into a string before it goes in the request body — a nested object is rejected. In a script, JSON.stringify(clientDataObject) and assign the result; do not paste the object raw. Full annotated example — plus the endpoint cheat-sheet (token, list/filter, create, enable, update, delete, ExportSolution, share), OData headers and error handling — in references/dataverse-web-api.md.
Dynamic lifecycle — token → create → validate → enable → manage → delete
1. Get a token (service-principal / client-credentials shown):
TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/$PA_TENANT_ID/oauth2/v2.0/token" \
-d "grant_type=client_credentials" -d "client_id=$PA_CLIENT_ID" \
-d "client_secret=$PA_CLIENT_SECRET" -d "scope=$PA_DATAVERSE_URL/.default" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
2. Create the flow (comes up statecode=0, Off — expected):
curl -s -i -X POST "$PA_DATAVERSE_URL/api/data/v9.2/workflows" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "OData-Version: 4.0" \
-d '{ "category": 5, "type": 1, "name": "Nightly sync", "primaryentity": "none", "clientdata": "<string-encoded JSON>" }'
# → 204 No Content. The workflowid is in the OData-EntityId response header:
# OData-EntityId: .../workflows(00aa00aa-bb11-cc22-dd33-44ee44ee44ee)
3. Validate before enabling. Read it back, confirm category/type are right, and confirm every connectionReferences entry resolves to an authorized connection in this environment. Enabling a flow with dangling connections is the classic silent failure.
4. Enable — flip statecode to 1 (use If-Match: * for the update):
curl -s -X PATCH "$PA_DATAVERSE_URL/api/data/v9.2/workflows(<workflowid>)" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "OData-Version: 4.0" -H "If-Match: *" \
-d '{ "statecode": 1 }'
# → 204 No Content
5. Manage — update the definition or owner with the same PATCH (send only the fields you change; to reassign use "ownerid@odata.bind": "systemusers(<id>)"). To turn a flow off, PATCH statecode=0.
6. Delete — irreversible; export first. There is no undo on DELETE. Before deleting, export the containing solution (POST /api/data/v9.2/ExportSolution → base64 zip you save to source control), so the flow can be reimported:
curl -s -X DELETE "$PA_DATAVERSE_URL/api/data/v9.2/workflows(<workflowid>)" \
-H "Authorization: Bearer $TOKEN"
# → 204 No Content
Debug a run — Dataverse's flowrun table lists run records but not per-action I/O. To see which action failed and with what payload, use FlowStudio MCP: list_live_flows → get_live_flow_runs → get_live_flow_run_action_outputs. See references/flowstudio-mcp-and-limits.md.
Boundaries — when it is not a flow you operate here
Power Automate is Microsoft's iPaaS across M365 and Dynamics; flows live in Dataverse and glue Outlook, Teams, SharePoint, Dynamics, and hundreds of connectors. This skill is for when the automation genuinely is a Power Automate flow you must operate by code. When it isn't:
- "Send this email / read this calendar via Microsoft Graph from my own backend code" — not a flow at all; build a typed Graph client with
../api-connector-builder/SKILL.md(there is no first-party MS-Graph skill), or../google-workspace/SKILL.mdfor the Google equivalent. Same route for a general typed client with auth/pagination/backoff — this skill uses the Dataverse endpoints surgically, it does not build a client. - "Power Automate or n8n/Make/Zapier?" — billing model and constraints decide that:
../automation-strategy/SKILL.md, before you commit to operating here. - "Receive my flow's HTTP-trigger POST in my app" — that inbound endpoint is
../webhooks/SKILL.md; this skill triggers and operates flows, it does not build the receiver.
Anti-patterns
| Anti-pattern | Why it bites | Do instead |
|---|---|---|
| Trying to CRUD a My Flow by code | Unsupported — silently impossible, not a bug you can fix | Move it into a Dataverse solution, or drive it by hand |
clientdata sent as a nested object |
Request rejected; the column expects an escaped string | Serialize (JSON.stringify) the definition+connectionReferences before sending |
PATCH statecode=1 "worked" but the flow never runs |
Connection references unresolved/unauthorized in the target environment | Authorize every connection reference before enabling; validate on read-back |
Building against api.flow.microsoft.com |
Unsupported; breaks without warning | Dataverse Web API, or the Power Automate Management connector |
DELETE with no export |
No undo; the flow and its history are gone | ExportSolution first, save the zip, then delete |
| Hardcoding the Dataverse URL / secret | Env-specific, leaks in source | .env: PA_DATAVERSE_URL, PA_TENANT_ID, PA_CLIENT_ID, PA_CLIENT_SECRET |
| Assuming the Dataverse path is permanent | Power Platform API is superseding surfaces piecemeal | Re-check api.powerplatform.com coverage at author time; the split is fast-moving |
Checklist
-
.envset:PA_DATAVERSE_URL(no trailing slash),PA_TENANT_ID,PA_CLIENT_ID,PA_CLIENT_SECRET. - Confirmed the target is a solution-aware flow, not a My Flow.
- Token acquired against scope
{PA_DATAVERSE_URL}/.default(or delegateduser_impersonation). - Create payload has
category:5,type:1,primaryentity:"none", andclientdataas an escaped string. - Every
connectionReferencesentry maps to an authorized connection before enabling. - Flow enabled via
PATCH statecode=1and verified on read-back. - Solution exported before any
DELETE. - No secrets or org URLs hardcoded; FlowStudio MCP (if used) flagged as third-party.