--- name: power-automate description: "Use when operating Microsoft Power Automate cloud flows from code — create, enable, update, list or delete via the Dataverse Web API (`workflow` table, category 5) with Entra ID OAuth2, plus run-history debugging. NOT designing the flow definition (that is `automation-flows`), NOT picking a platform by billing model (that is `automation-strategy`)." tags: [power-automate, dataverse, microsoft, flows, entra, oauth, mcp, m365, automation] recommends: [automation-flows, automation-strategy, api-connector-builder, webhooks] profiles: [full] origin: risco --- # 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: 1. **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. 2. **`api.flow.microsoft.com` is 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 the `workflow` table. For admin-style operations the **Power Automate Management** connector is the other supported path. 3. **The unified Power Platform API (`api.powerplatform.com`) is maturing fast.** It already lists cloud flows (api-version `2024-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= PA_CLIENT_ID= PA_CLIENT_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: ```bash 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: ```json { "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) then `actions`. 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 into `definition`. Fastest reliable way to get a real one: build the flow once in the maker portal, export the solution, and copy its `clientdata`. - **`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 a `PATCH 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): ```bash 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): ```bash 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": "" }' # → 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): ```bash curl -s -X PATCH "$PA_DATAVERSE_URL/api/data/v9.2/workflows()" \ -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()"`). 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: ```bash curl -s -X DELETE "$PA_DATAVERSE_URL/api/data/v9.2/workflows()" \ -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.md` for 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 - [ ] `.env` set: `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 delegated `user_impersonation`). - [ ] Create payload has `category:5`, `type:1`, `primaryentity:"none"`, and `clientdata` as an **escaped string**. - [ ] Every `connectionReferences` entry maps to an authorized connection before enabling. - [ ] Flow enabled via `PATCH statecode=1` and verified on read-back. - [ ] Solution **exported** before any `DELETE`. - [ ] No secrets or org URLs hardcoded; FlowStudio MCP (if used) flagged as third-party.