mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-14 05:05:15 +00:00
feat: add flowstudio-power-automate-debug and flowstudio-power-automate-build skills (#899)
* feat: add flowstudio-power-automate-debug and flowstudio-power-automate-build skills Two companion skills for the FlowStudio Power Automate MCP server: - flowstudio-power-automate-debug: Debug workflow for failed Power Automate cloud flow runs - flowstudio-power-automate-build: Build & deploy flows from natural language descriptions Both require a FlowStudio MCP subscription: https://flowstudio.app These complement the existing flowstudio-power-automate-mcp skill (merged in PR #896). * fix: address all review comments — README, cross-refs, response shapes, step numbering - Add skills to docs/README.skills.md (fixes validate-readme CI check) - Update cross-skill references to use flowstudio- prefix (#1, #4, #7, #9) - Fix get_live_flow_run_action_outputs: returns array, index [0] (#2, #3) - Renumber Step 6→5, Step 7→6 — remove gap in build workflow (#8) - Fix connectionName note: it's the key, not the GUID (#10) - Remove invalid arrow function from Filter array expression (#11) * feat: add flowstudio-power-automate plugin bundling all 3 skills Plugin bundles: - flowstudio-power-automate-mcp (core connection & CRUD) - flowstudio-power-automate-debug (debug failed runs) - flowstudio-power-automate-build (build & deploy flows) Install: copilot plugin install flowstudio-power-automate@awesome-copilot Per @aaronpowell's suggestion in review.
This commit is contained in:
322
skills/flowstudio-power-automate-debug/SKILL.md
Normal file
322
skills/flowstudio-power-automate-debug/SKILL.md
Normal file
@@ -0,0 +1,322 @@
|
||||
---
|
||||
name: flowstudio-power-automate-debug
|
||||
description: >-
|
||||
Debug failing Power Automate cloud flows using the FlowStudio MCP server.
|
||||
Load this skill when asked to: debug a flow, investigate a failed run, why is
|
||||
this flow failing, inspect action outputs, find the root cause of a flow error,
|
||||
fix a broken Power Automate flow, diagnose a timeout, trace a DynamicOperationRequestFailure,
|
||||
check connector auth errors, read error details from a run, or troubleshoot
|
||||
expression failures. Requires a FlowStudio MCP subscription — see https://mcp.flowstudio.app
|
||||
---
|
||||
|
||||
# Power Automate Debugging with FlowStudio MCP
|
||||
|
||||
A step-by-step diagnostic process for investigating failing Power Automate
|
||||
cloud flows through the FlowStudio MCP server.
|
||||
|
||||
**Prerequisite**: A FlowStudio MCP server must be reachable with a valid JWT.
|
||||
See the `flowstudio-power-automate-mcp` skill for connection setup.
|
||||
Subscribe at https://mcp.flowstudio.app
|
||||
|
||||
---
|
||||
|
||||
## Source of Truth
|
||||
|
||||
> **Always call `tools/list` first** to confirm available tool names and their
|
||||
> parameter schemas. Tool names and parameters may change between server versions.
|
||||
> This skill covers response shapes, behavioral notes, and diagnostic patterns —
|
||||
> things `tools/list` cannot tell you. If this document disagrees with `tools/list`
|
||||
> or a real API response, the API wins.
|
||||
|
||||
---
|
||||
|
||||
## Python Helper
|
||||
|
||||
```python
|
||||
import json, urllib.request
|
||||
|
||||
MCP_URL = "https://mcp.flowstudio.app/mcp"
|
||||
MCP_TOKEN = "<YOUR_JWT_TOKEN>"
|
||||
|
||||
def mcp(tool, **kwargs):
|
||||
payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||
"params": {"name": tool, "arguments": kwargs}}).encode()
|
||||
req = urllib.request.Request(MCP_URL, data=payload,
|
||||
headers={"x-api-key": MCP_TOKEN, "Content-Type": "application/json",
|
||||
"User-Agent": "FlowStudio-MCP/1.0"})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=120)
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"MCP HTTP {e.code}: {body[:200]}") from e
|
||||
raw = json.loads(resp.read())
|
||||
if "error" in raw:
|
||||
raise RuntimeError(f"MCP error: {json.dumps(raw['error'])}")
|
||||
return json.loads(raw["result"]["content"][0]["text"])
|
||||
|
||||
ENV = "<environment-id>" # e.g. Default-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FlowStudio for Teams: Fast-Path Diagnosis (Skip Steps 2–4)
|
||||
|
||||
If you have a FlowStudio for Teams subscription, `get_store_flow_errors`
|
||||
returns per-run failure data including action names and remediation hints
|
||||
in a single call — no need to walk through live API steps.
|
||||
|
||||
```python
|
||||
# Quick failure summary
|
||||
summary = mcp("get_store_flow_summary", environmentName=ENV, flowName=FLOW_ID)
|
||||
# {"totalRuns": 100, "failRuns": 10, "failRate": 0.1,
|
||||
# "averageDurationSeconds": 29.4, "maxDurationSeconds": 158.9,
|
||||
# "firstFailRunRemediation": "<hint or null>"}
|
||||
print(f"Fail rate: {summary['failRate']:.0%} over {summary['totalRuns']} runs")
|
||||
|
||||
# Per-run error details (requires active monitoring to be configured)
|
||||
errors = mcp("get_store_flow_errors", environmentName=ENV, flowName=FLOW_ID)
|
||||
if errors:
|
||||
for r in errors[:3]:
|
||||
print(r["startTime"], "|", r.get("failedActions"), "|", r.get("remediationHint"))
|
||||
# If errors confirms the failing action → jump to Step 6 (apply fix)
|
||||
else:
|
||||
# Store doesn't have run-level detail for this flow — use live tools (Steps 2–5)
|
||||
pass
|
||||
```
|
||||
|
||||
For the full governance record (description, complexity, tier, connector list):
|
||||
```python
|
||||
record = mcp("get_store_flow", environmentName=ENV, flowName=FLOW_ID)
|
||||
# {"displayName": "My Flow", "state": "Started",
|
||||
# "runPeriodTotal": 100, "runPeriodFailRate": 0.1, "runPeriodFails": 10,
|
||||
# "runPeriodDurationAverage": 29410.8, ← milliseconds
|
||||
# "runError": "{\"code\": \"EACCES\", ...}", ← JSON string, parse it
|
||||
# "description": "...", "tier": "Premium", "complexity": "{...}"}
|
||||
if record.get("runError"):
|
||||
last_err = json.loads(record["runError"])
|
||||
print("Last run error:", last_err)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Locate the Flow
|
||||
|
||||
```python
|
||||
result = mcp("list_live_flows", environmentName=ENV)
|
||||
# Returns a wrapper object: {mode, flows, totalCount, error}
|
||||
target = next(f for f in result["flows"] if "My Flow Name" in f["displayName"])
|
||||
FLOW_ID = target["id"] # plain UUID — use directly as flowName
|
||||
print(FLOW_ID)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Find the Failing Run
|
||||
|
||||
```python
|
||||
runs = mcp("get_live_flow_runs", environmentName=ENV, flowName=FLOW_ID, top=5)
|
||||
# Returns direct array (newest first):
|
||||
# [{"name": "08584296068667933411438594643CU15",
|
||||
# "status": "Failed",
|
||||
# "startTime": "2026-02-25T06:13:38.6910688Z",
|
||||
# "endTime": "2026-02-25T06:15:24.1995008Z",
|
||||
# "triggerName": "manual",
|
||||
# "error": {"code": "ActionFailed", "message": "An action failed..."}},
|
||||
# {"name": "...", "status": "Succeeded", "error": null, ...}]
|
||||
|
||||
for r in runs:
|
||||
print(r["name"], r["status"], r["startTime"])
|
||||
|
||||
RUN_ID = next(r["name"] for r in runs if r["status"] == "Failed")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Get the Top-Level Error
|
||||
|
||||
```python
|
||||
err = mcp("get_live_flow_run_error",
|
||||
environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID)
|
||||
# Returns:
|
||||
# {
|
||||
# "runName": "08584296068667933411438594643CU15",
|
||||
# "failedActions": [
|
||||
# {"actionName": "Apply_to_each_prepare_workers", "status": "Failed",
|
||||
# "error": {"code": "ActionFailed", "message": "An action failed..."},
|
||||
# "startTime": "...", "endTime": "..."},
|
||||
# {"actionName": "HTTP_find_AD_User_by_Name", "status": "Failed",
|
||||
# "code": "NotSpecified", "startTime": "...", "endTime": "..."}
|
||||
# ],
|
||||
# "allActions": [
|
||||
# {"actionName": "Apply_to_each", "status": "Skipped"},
|
||||
# {"actionName": "Compose_WeekEnd", "status": "Succeeded"},
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
|
||||
# failedActions is ordered outer-to-inner. The ROOT cause is the LAST entry:
|
||||
root = err["failedActions"][-1]
|
||||
print(f"Root action: {root['actionName']} → code: {root.get('code')}")
|
||||
|
||||
# allActions shows every action's status — useful for spotting what was Skipped
|
||||
# See common-errors.md to decode the error code.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Read the Flow Definition
|
||||
|
||||
```python
|
||||
defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
|
||||
actions = defn["properties"]["definition"]["actions"]
|
||||
print(list(actions.keys()))
|
||||
```
|
||||
|
||||
Find the failing action in the definition. Inspect its `inputs` expression
|
||||
to understand what data it expects.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Inspect Action Outputs (Walk Back from Failure)
|
||||
|
||||
For each action **leading up to** the failure, inspect its runtime output:
|
||||
|
||||
```python
|
||||
for action_name in ["Compose_WeekEnd", "HTTP_Get_Data", "Parse_JSON"]:
|
||||
result = mcp("get_live_flow_run_action_outputs",
|
||||
environmentName=ENV,
|
||||
flowName=FLOW_ID,
|
||||
runName=RUN_ID,
|
||||
actionName=action_name)
|
||||
# Returns an array — single-element when actionName is provided
|
||||
out = result[0] if result else {}
|
||||
print(action_name, out.get("status"))
|
||||
print(json.dumps(out.get("outputs", {}), indent=2)[:500])
|
||||
```
|
||||
|
||||
> ⚠️ Output payloads from array-processing actions can be very large.
|
||||
> Always slice (e.g. `[:500]`) before printing.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Pinpoint the Root Cause
|
||||
|
||||
### Expression Errors (e.g. `split` on null)
|
||||
If the error mentions `InvalidTemplate` or a function name:
|
||||
1. Find the action in the definition
|
||||
2. Check what upstream action/expression it reads
|
||||
3. Inspect that upstream action's output for null / missing fields
|
||||
|
||||
```python
|
||||
# Example: action uses split(item()?['Name'], ' ')
|
||||
# → null Name in the source data
|
||||
result = mcp("get_live_flow_run_action_outputs", ..., actionName="Compose_Names")
|
||||
# Returns a single-element array; index [0] to get the action object
|
||||
if not result:
|
||||
print("No outputs returned for Compose_Names")
|
||||
names = []
|
||||
else:
|
||||
names = result[0].get("outputs", {}).get("body") or []
|
||||
nulls = [x for x in names if x.get("Name") is None]
|
||||
print(f"{len(nulls)} records with null Name")
|
||||
```
|
||||
|
||||
### Wrong Field Path
|
||||
Expression `triggerBody()?['fieldName']` returns null → `fieldName` is wrong.
|
||||
Check the trigger output shape with:
|
||||
```python
|
||||
mcp("get_live_flow_run_action_outputs", ..., actionName="<trigger-action-name>")
|
||||
```
|
||||
|
||||
### Connection / Auth Failures
|
||||
Look for `ConnectionAuthorizationFailed` — the connection owner must match the
|
||||
service account running the flow. Cannot fix via API; fix in PA designer.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Apply the Fix
|
||||
|
||||
**For expression/data issues**:
|
||||
```python
|
||||
defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
|
||||
acts = defn["properties"]["definition"]["actions"]
|
||||
|
||||
# Example: fix split on potentially-null Name
|
||||
acts["Compose_Names"]["inputs"] = \
|
||||
"@coalesce(item()?['Name'], 'Unknown')"
|
||||
|
||||
conn_refs = defn["properties"]["connectionReferences"]
|
||||
result = mcp("update_live_flow",
|
||||
environmentName=ENV,
|
||||
flowName=FLOW_ID,
|
||||
definition=defn["properties"]["definition"],
|
||||
connectionReferences=conn_refs)
|
||||
|
||||
print(result.get("error")) # None = success
|
||||
```
|
||||
|
||||
> ⚠️ `update_live_flow` always returns an `error` key.
|
||||
> A value of `null` (Python `None`) means success.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Verify the Fix
|
||||
|
||||
```python
|
||||
# Resubmit the failed run
|
||||
resubmit = mcp("resubmit_live_flow_run",
|
||||
environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID)
|
||||
print(resubmit)
|
||||
|
||||
# Wait ~30 s then check
|
||||
import time; time.sleep(30)
|
||||
new_runs = mcp("get_live_flow_runs", environmentName=ENV, flowName=FLOW_ID, top=3)
|
||||
print(new_runs[0]["status"]) # Succeeded = done
|
||||
```
|
||||
|
||||
### Testing HTTP-Triggered Flows
|
||||
|
||||
For flows with a `Request` (HTTP) trigger, use `trigger_live_flow` instead
|
||||
of `resubmit_live_flow_run` to test with custom payloads:
|
||||
|
||||
```python
|
||||
# First inspect what the trigger expects
|
||||
schema = mcp("get_live_flow_http_schema",
|
||||
environmentName=ENV, flowName=FLOW_ID)
|
||||
print("Expected body schema:", schema.get("triggerSchema"))
|
||||
print("Response schemas:", schema.get("responseSchemas"))
|
||||
|
||||
# Trigger with a test payload
|
||||
result = mcp("trigger_live_flow",
|
||||
environmentName=ENV,
|
||||
flowName=FLOW_ID,
|
||||
body={"name": "Test User", "value": 42})
|
||||
print(f"Status: {result['status']}, Body: {result.get('body')}")
|
||||
```
|
||||
|
||||
> `trigger_live_flow` handles AAD-authenticated triggers automatically.
|
||||
> Only works for flows with a `Request` (HTTP) trigger type.
|
||||
|
||||
---
|
||||
|
||||
## Quick-Reference Diagnostic Decision Tree
|
||||
|
||||
| Symptom | First Tool to Call | What to Look For |
|
||||
|---|---|---|
|
||||
| Flow shows as Failed | `get_live_flow_run_error` | `failedActions[-1]["actionName"]` = root cause |
|
||||
| Expression crash | `get_live_flow_run_action_outputs` on prior action | null / wrong-type fields in output body |
|
||||
| Flow never starts | `get_live_flow` | check `properties.state` = "Started" |
|
||||
| Action returns wrong data | `get_live_flow_run_action_outputs` | actual output body vs expected |
|
||||
| Fix applied but still fails | `get_live_flow_runs` after resubmit | new run `status` field |
|
||||
|
||||
---
|
||||
|
||||
## Reference Files
|
||||
|
||||
- [common-errors.md](references/common-errors.md) — Error codes, likely causes, and fixes
|
||||
- [debug-workflow.md](references/debug-workflow.md) — Full decision tree for complex failures
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `flowstudio-power-automate-mcp` — Core connection setup and operation reference
|
||||
- `flowstudio-power-automate-build` — Build and deploy new flows
|
||||
@@ -0,0 +1,188 @@
|
||||
# FlowStudio MCP — Common Power Automate Errors
|
||||
|
||||
Reference for error codes, likely causes, and recommended fixes when debugging
|
||||
Power Automate flows via the FlowStudio MCP server.
|
||||
|
||||
---
|
||||
|
||||
## Expression / Template Errors
|
||||
|
||||
### `InvalidTemplate` — Function Applied to Null
|
||||
|
||||
**Full message pattern**: `"Unable to process template language expressions... function 'split' expects its first argument 'text' to be of type string"`
|
||||
|
||||
**Root cause**: An expression like `@split(item()?['Name'], ' ')` received a null value.
|
||||
|
||||
**Diagnosis**:
|
||||
1. Note the action name in the error message
|
||||
2. Call `get_live_flow_run_action_outputs` on the action that produces the array
|
||||
3. Find items where `Name` (or the referenced field) is `null`
|
||||
|
||||
**Fixes**:
|
||||
```
|
||||
Before: @split(item()?['Name'], ' ')
|
||||
After: @split(coalesce(item()?['Name'], ''), ' ')
|
||||
|
||||
Or guard the whole foreach body with a condition:
|
||||
expression: "@not(empty(item()?['Name']))"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `InvalidTemplate` — Wrong Expression Path
|
||||
|
||||
**Full message pattern**: `"Unable to process template language expressions... 'triggerBody()?['FieldName']' is of type 'Null'"`
|
||||
|
||||
**Root cause**: The field name in the expression doesn't match the actual payload schema.
|
||||
|
||||
**Diagnosis**:
|
||||
```python
|
||||
# Check trigger output shape
|
||||
mcp("get_live_flow_run_action_outputs",
|
||||
environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID,
|
||||
actionName="<trigger-name>")
|
||||
# Compare actual keys vs expression
|
||||
```
|
||||
|
||||
**Fix**: Update expression to use the correct key name. Common mismatches:
|
||||
- `triggerBody()?['body']` vs `triggerBody()?['Body']` (case-sensitive)
|
||||
- `triggerBody()?['Subject']` vs `triggerOutputs()?['body/Subject']`
|
||||
|
||||
---
|
||||
|
||||
### `InvalidTemplate` — Type Mismatch
|
||||
|
||||
**Full message pattern**: `"... expected type 'Array' but got type 'Object'"`
|
||||
|
||||
**Root cause**: Passing an object where the expression expects an array (e.g. a single item HTTP response vs a list response).
|
||||
|
||||
**Fix**:
|
||||
```
|
||||
Before: @outputs('HTTP')?['body']
|
||||
After: @outputs('HTTP')?['body/value'] ← for OData list responses
|
||||
@createArray(outputs('HTTP')?['body']) ← wrap single object in array
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection / Auth Errors
|
||||
|
||||
### `ConnectionAuthorizationFailed`
|
||||
|
||||
**Full message**: `"The API connection ... is not authorized."`
|
||||
|
||||
**Root cause**: The connection referenced in the flow is owned by a different
|
||||
user/service account than the one whose JWT is being used.
|
||||
|
||||
**Diagnosis**: Check `properties.connectionReferences` — the `connectionName` GUID
|
||||
identifies the owner. Cannot be fixed via API.
|
||||
|
||||
**Fix options**:
|
||||
1. Open flow in Power Automate designer → re-authenticate the connection
|
||||
2. Use a connection owned by the service account whose token you hold
|
||||
3. Share the connection with the service account in PA admin
|
||||
|
||||
---
|
||||
|
||||
### `InvalidConnectionCredentials`
|
||||
|
||||
**Root cause**: The underlying OAuth token for the connection has expired or
|
||||
the user's credentials changed.
|
||||
|
||||
**Fix**: Owner must sign in to Power Automate and refresh the connection.
|
||||
|
||||
---
|
||||
|
||||
## HTTP Action Errors
|
||||
|
||||
### `ActionFailed` — HTTP 4xx/5xx
|
||||
|
||||
**Full message pattern**: `"An HTTP request to... failed with status code '400'"`
|
||||
|
||||
**Diagnosis**:
|
||||
```python
|
||||
actions_out = mcp("get_live_flow_run_action_outputs", ..., actionName="HTTP_My_Call")
|
||||
item = actions_out[0] # first entry in the returned array
|
||||
print(item["outputs"]["statusCode"]) # 400, 401, 403, 500...
|
||||
print(item["outputs"]["body"]) # error details from target API
|
||||
```
|
||||
|
||||
**Common causes**:
|
||||
- 401 — missing or expired auth header
|
||||
- 403 — permission denied on target resource
|
||||
- 404 — wrong URL / resource deleted
|
||||
- 400 — malformed JSON body (check expression that builds the body)
|
||||
|
||||
---
|
||||
|
||||
### `ActionFailed` — HTTP Timeout
|
||||
|
||||
**Root cause**: Target endpoint did not respond within the connector's timeout
|
||||
(default 90 s for HTTP action).
|
||||
|
||||
**Fix**: Add retry policy to the HTTP action, or split the payload into smaller
|
||||
batches to reduce per-request processing time.
|
||||
|
||||
---
|
||||
|
||||
## Control Flow Errors
|
||||
|
||||
### `ActionSkipped` Instead of Running
|
||||
|
||||
**Root cause**: The `runAfter` condition wasn't met. E.g. an action set to
|
||||
`runAfter: { "Prev": ["Succeeded"] }` won't run if `Prev` failed or was skipped.
|
||||
|
||||
**Diagnosis**: Check the preceding action's status. Deliberately skipped
|
||||
(e.g. inside a false branch) is intentional — unexpected skip is a logic gap.
|
||||
|
||||
**Fix**: Add `"Failed"` or `"Skipped"` to the `runAfter` status array if the
|
||||
action should run on those outcomes too.
|
||||
|
||||
---
|
||||
|
||||
### Foreach Runs in Wrong Order / Race Condition
|
||||
|
||||
**Root cause**: `Foreach` without `operationOptions: "Sequential"` runs
|
||||
iterations in parallel, causing write conflicts or undefined ordering.
|
||||
|
||||
**Fix**: Add `"operationOptions": "Sequential"` to the Foreach action.
|
||||
|
||||
---
|
||||
|
||||
## Update / Deploy Errors
|
||||
|
||||
### `update_live_flow` Returns No-Op
|
||||
|
||||
**Symptom**: `result["updated"]` is empty list or `result["created"]` is empty.
|
||||
|
||||
**Likely cause**: Passing wrong parameter name. The required key is `definition`
|
||||
(object), not `flowDefinition` or `body`.
|
||||
|
||||
---
|
||||
|
||||
### `update_live_flow` — `"Supply connectionReferences"`
|
||||
|
||||
**Root cause**: The definition contains `OpenApiConnection` or
|
||||
`OpenApiConnectionWebhook` actions but `connectionReferences` was not passed.
|
||||
|
||||
**Fix**: Fetch the existing connection references with `get_live_flow` and pass
|
||||
them as the `connectionReferences` argument.
|
||||
|
||||
---
|
||||
|
||||
## Data Logic Errors
|
||||
|
||||
### `union()` Overriding Correct Records with Nulls
|
||||
|
||||
**Symptom**: After merging two arrays, some records have null fields that existed
|
||||
in one of the source arrays.
|
||||
|
||||
**Root cause**: `union(old_data, new_data)` — `union()` first-wins, so old_data
|
||||
values override new_data for matching records.
|
||||
|
||||
**Fix**: Swap argument order: `union(new_data, old_data)`
|
||||
|
||||
```
|
||||
Before: @sort(union(outputs('Old_Array'), body('New_Array')), 'Date')
|
||||
After: @sort(union(body('New_Array'), outputs('Old_Array')), 'Date')
|
||||
```
|
||||
@@ -0,0 +1,157 @@
|
||||
# FlowStudio MCP — Debug Workflow
|
||||
|
||||
End-to-end decision tree for diagnosing Power Automate flow failures.
|
||||
|
||||
---
|
||||
|
||||
## Top-Level Decision Tree
|
||||
|
||||
```
|
||||
Flow is failing
|
||||
│
|
||||
├── Flow never starts / no runs appear
|
||||
│ └── ► Check flow State: get_live_flow → properties.state
|
||||
│ ├── "Stopped" → flow is disabled; enable in PA designer
|
||||
│ └── "Started" + no runs → trigger condition not met (check trigger config)
|
||||
│
|
||||
├── Flow run shows "Failed"
|
||||
│ ├── Step A: get_live_flow_run_error → read error.code + error.message
|
||||
│ │
|
||||
│ ├── error.code = "InvalidTemplate"
|
||||
│ │ └── ► Expression error (null value, wrong type, bad path)
|
||||
│ │ └── See: Expression Error Workflow below
|
||||
│ │
|
||||
│ ├── error.code = "ConnectionAuthorizationFailed"
|
||||
│ │ └── ► Connection owned by different user; fix in PA designer
|
||||
│ │
|
||||
│ ├── error.code = "ActionFailed" + message mentions HTTP
|
||||
│ │ └── ► See: HTTP Action Workflow below
|
||||
│ │
|
||||
│ └── Unknown / generic error
|
||||
│ └── ► Walk actions backwards (Step B below)
|
||||
│
|
||||
└── Flow Succeeds but output is wrong
|
||||
└── ► Inspect intermediate actions with get_live_flow_run_action_outputs
|
||||
└── See: Data Quality Workflow below
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expression Error Workflow
|
||||
|
||||
```
|
||||
InvalidTemplate error
|
||||
│
|
||||
├── 1. Read error.message — identifies the action name and function
|
||||
│
|
||||
├── 2. Get flow definition: get_live_flow
|
||||
│ └── Find that action in definition["actions"][action_name]["inputs"]
|
||||
│ └── Identify what upstream value the expression reads
|
||||
│
|
||||
├── 3. get_live_flow_run_action_outputs for the action BEFORE the failing one
|
||||
│ └── Look for null / wrong type in that action's output
|
||||
│ ├── Null string field → wrap with coalesce(): @coalesce(field, '')
|
||||
│ ├── Null object → add empty check condition before the action
|
||||
│ └── Wrong field name → correct the key (case-sensitive)
|
||||
│
|
||||
└── 4. Apply fix with update_live_flow, then resubmit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP Action Workflow
|
||||
|
||||
```
|
||||
ActionFailed on HTTP action
|
||||
│
|
||||
├── 1. get_live_flow_run_action_outputs on the HTTP action
|
||||
│ └── Read: outputs.statusCode, outputs.body
|
||||
│
|
||||
├── statusCode = 401
|
||||
│ └── ► Auth header missing or expired OAuth token
|
||||
│ Check: action inputs.authentication block
|
||||
│
|
||||
├── statusCode = 403
|
||||
│ └── ► Insufficient permission on target resource
|
||||
│ Check: service principal / user has access
|
||||
│
|
||||
├── statusCode = 400
|
||||
│ └── ► Malformed request body
|
||||
│ Check: action inputs.body expression; parse errors often in nested JSON
|
||||
│
|
||||
├── statusCode = 404
|
||||
│ └── ► Wrong URL or resource deleted/renamed
|
||||
│ Check: action inputs.uri expression
|
||||
│
|
||||
└── statusCode = 500 / timeout
|
||||
└── ► Target system error; retry policy may help
|
||||
Add: "retryPolicy": {"type": "Fixed", "count": 3, "interval": "PT10S"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Quality Workflow
|
||||
|
||||
```
|
||||
Flow succeeds but output data is wrong
|
||||
│
|
||||
├── 1. Identify the first "wrong" output — which action produces it?
|
||||
│
|
||||
├── 2. get_live_flow_run_action_outputs on that action
|
||||
│ └── Compare actual output body vs expected
|
||||
│
|
||||
├── Source array has nulls / unexpected values
|
||||
│ ├── Check the trigger data — get_live_flow_run_action_outputs on trigger
|
||||
│ └── Trace forward action by action until the value corrupts
|
||||
│
|
||||
├── Merge/union has wrong values
|
||||
│ └── Check union argument order:
|
||||
│ union(NEW, old) = new wins ✓
|
||||
│ union(OLD, new) = old wins ← common bug
|
||||
│
|
||||
├── Foreach output missing items
|
||||
│ ├── Check foreach condition — filter may be too strict
|
||||
│ └── Check if parallel foreach caused race condition (add Sequential)
|
||||
│
|
||||
└── Date/time values wrong timezone
|
||||
└── Use convertTimeZone() — utcNow() is always UTC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Walk-Back Analysis (Unknown Failure)
|
||||
|
||||
When the error message doesn't clearly name a root cause:
|
||||
|
||||
```python
|
||||
# 1. Get all action names from definition
|
||||
defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
|
||||
actions = list(defn["properties"]["definition"]["actions"].keys())
|
||||
|
||||
# 2. Check status of each action in the failed run
|
||||
for action in actions:
|
||||
actions_out = mcp("get_live_flow_run_action_outputs",
|
||||
environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID,
|
||||
actionName=action)
|
||||
# Returns an array of action objects
|
||||
item = actions_out[0] if actions_out else {}
|
||||
status = item.get("status", "unknown")
|
||||
print(f"{action}: {status}")
|
||||
|
||||
# 3. Find the boundary between Succeeded and Failed/Skipped
|
||||
# The first Failed action is likely the root cause (unless skipped by design)
|
||||
```
|
||||
|
||||
Actions inside Foreach / Condition branches may appear nested —
|
||||
check the parent action first to confirm the branch ran at all.
|
||||
|
||||
---
|
||||
|
||||
## Post-Fix Verification Checklist
|
||||
|
||||
1. `update_live_flow` returns `error: null` — definition accepted
|
||||
2. `resubmit_live_flow_run` confirms new run started
|
||||
3. Wait for run completion (poll `get_live_flow_runs` every 15 s)
|
||||
4. Confirm new run `status = "Succeeded"`
|
||||
5. If flow has downstream consumers (child flows, emails, SharePoint writes),
|
||||
spot-check those too
|
||||
Reference in New Issue
Block a user