🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Power Automate Debug

by @ninihen1

Debug failing Power Automate cloud flows using the FlowStudio MCP server. The Graph API only shows top-level status codes. This skill gives your agent action...

TERMINAL
clawhub install power-automate-debug

πŸ“– About This Skill


name: power-automate-debug description: >- Debug failing Power Automate cloud flows using the FlowStudio MCP server. The Graph API only shows top-level status codes. This skill gives your agent action-level inputs and outputs to find the actual root cause. 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 metadata: openclaw: requires: env: - FLOWSTUDIO_MCP_TOKEN primaryEnv: FLOWSTUDIO_MCP_TOKEN homepage: 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.

> Real debugging examples: Expression error in child flow | > Data entry, not a flow bug | > Null value crashes child flow

Prerequisite: A FlowStudio MCP server must be reachable with a valid JWT. See the 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

import json, urllib.request

MCP_URL = "https://mcp.flowstudio.app/mcp" MCP_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 = "" # e.g. Default-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx


Step 1 β€” Locate the Flow

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

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

> CRITICAL: get_live_flow_run_error tells you which action failed. > get_live_flow_run_action_outputs tells you why. You must call BOTH. > Never stop at the error alone β€” error codes like ActionFailed, > NotSpecified, and InternalServerError are generic wrappers. The actual > root cause (wrong field, null value, HTTP 500 body, stack trace) is only > visible in the action's inputs and outputs.

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 β€” Inspect the Failing Action's Inputs and Outputs

> This is the most important step. get_live_flow_run_error only gives > you a generic error code. The actual error detail β€” HTTP status codes, > response bodies, stack traces, null values β€” lives in the action's runtime > inputs and outputs. **Always inspect the failing action immediately after > identifying it.**

# Get the root failing action's full inputs and outputs
root_action = err["failedActions"][-1]["actionName"]
detail = mcp("get_live_flow_run_action_outputs",
    environmentName=ENV,
    flowName=FLOW_ID,
    runName=RUN_ID,
    actionName=root_action)

out = detail[0] if detail else {} print(f"Action: {out.get('actionName')}") print(f"Status: {out.get('status')}")

For HTTP actions, the real error is in outputs.body

if isinstance(out.get("outputs"), dict): status_code = out["outputs"].get("statusCode") body = out["outputs"].get("body", {}) print(f"HTTP {status_code}") print(json.dumps(body, indent=2)[:500])

# Error bodies are often nested JSON strings β€” parse them if isinstance(body, dict) and "error" in body: err_detail = body["error"] if isinstance(err_detail, str): err_detail = json.loads(err_detail) print(f"Error: {err_detail.get('message', err_detail)}")

For expression errors, the error is in the error field

if out.get("error"): print(f"Error: {out['error']}")

Also check inputs β€” they show what expression/URL/body was used

if out.get("inputs"): print(f"Inputs: {json.dumps(out['inputs'], indent=2)[:500]}")

What the action outputs reveal (that error codes don't)

| Error code from get_live_flow_run_error | What get_live_flow_run_action_outputs reveals | |---|---| | ActionFailed | Which nested action actually failed and its HTTP response | | NotSpecified | The HTTP status code + response body with the real error | | InternalServerError | The server's error message, stack trace, or API error JSON | | InvalidTemplate | The exact expression that failed and the null/wrong-type value | | BadRequest | The request body that was sent and why the server rejected it |

Example: HTTP action returning 500

Error code: "InternalServerError" ← this tells you nothing

Action outputs reveal: HTTP 500 body: {"error": "Cannot read properties of undefined (reading 'toLowerCase') at getClientParamsFromConnectionString (storage.js:20)"} ← THIS tells you the Azure Function crashed because a connection string is undefined

Example: Expression error on null

Error code: "BadRequest" ← generic

Action outputs reveal: inputs: "body('HTTP_GetTokenFromStore')?['token']?['access_token']" outputs: "" ← empty string, the path resolved to null ← THIS tells you the response shape changed β€” token is at body.access_token, not body.token.access_token


Step 5 β€” Read the Flow Definition

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 6 β€” Walk Back from the Failure

When the failing action's inputs reference upstream actions, inspect those too. Walk backward through the chain until you find the source of the bad data:

# Inspect multiple actions leading up to the failure
for action_name in [root_action, "Compose_WeekEnd", "HTTP_Get_Data"]:
    result = mcp("get_live_flow_run_action_outputs",
        environmentName=ENV,
        flowName=FLOW_ID,
        runName=RUN_ID,
        actionName=action_name)
    out = result[0] if result else {}
    print(f"\n--- {action_name} ({out.get('status')}) ---")
    print(f"Inputs:  {json.dumps(out.get('inputs', ''), indent=2)[:300]}")
    print(f"Outputs: {json.dumps(out.get('outputs', ''), indent=2)[:300]}")

> ⚠️ Output payloads from array-processing actions can be very large. > Always slice (e.g. [:500]) before printing.

> Tip: Omit actionName to get ALL actions in a single call. > This returns every action's inputs/outputs β€” useful when you're not sure > which upstream action produced the bad data. But use 120s+ timeout as > the response can be very large.


Step 7 β€” 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

# Example: action uses split(item()?['Name'], ' ')

β†’ null Name in the source data

result = mcp("get_live_flow_run_action_outputs", ..., actionName="Compose_Names") 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. Inspect the trigger output to see the actual field names:
result = mcp("get_live_flow_run_action_outputs", ..., actionName="")
print(json.dumps(result[0].get("outputs"), indent=2)[:500])

HTTP Actions Returning Errors

The error code says InternalServerError or NotSpecified β€” **always inspect the action outputs** to get the actual HTTP status and response body:
result = mcp("get_live_flow_run_action_outputs", ..., actionName="HTTP_Get_Data")
out = result[0]
print(f"HTTP {out['outputs']['statusCode']}")
print(json.dumps(out['outputs']['body'], indent=2)[:500])

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.

Outlook user-picker failures (DynamicListValuesUndefinedOrInvalid)

Outlook actions like GetEmailsV3 use parameters (mailboxAddress, to, cc, from) whose dropdown is backed by builtInOperation:AadGraph.GetUsers β€” which is broken at the PA listEnum layer and always returns DynamicListValuesUndefinedOrInvalid. This shows up when an agent rebuilds or modifies an Outlook action via update_live_flow and tries to resolve a user through dynamic options. Don't fix it by retrying AadGraph β€” switch to shared_office365users.SearchUserV2 instead (returns the same AAD user shape). See the power-automate-build skill, **Step 3a β€” Resolving Dynamic Connector Values**, for the working pattern. describe_live_connector (v1.1.6+) returns this fallback as a structured fallback field on the affected parameter.


Step 8 β€” Apply the Fix

For expression/data issues:

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 9 β€” Verify the Fix

> Use resubmit_live_flow_run to test ANY flow β€” not just HTTP triggers. > resubmit_live_flow_run replays a previous run using its original trigger > payload. This works for every trigger type: Recurrence, SharePoint > "When an item is created", connector webhooks, Button triggers, and HTTP > triggers. You do NOT need to ask the user to manually trigger the flow or > wait for the next scheduled run. > > The only case where resubmit is not available is a **brand-new flow that > has never run** β€” it has no prior run to replay.

# Resubmit the failed run β€” works for ANY trigger type
resubmit = mcp("resubmit_live_flow_run",
    environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID)
print(resubmit)   # {"resubmitted": true, "triggerName": "..."}

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

When to use resubmit vs trigger

| Scenario | Use | Why | |---|---|---| | Testing a fix on any flow | resubmit_live_flow_run | Replays the exact trigger payload that caused the failure β€” best way to verify | | Recurrence / scheduled flow | resubmit_live_flow_run | Cannot be triggered on demand any other way | | SharePoint / connector trigger | resubmit_live_flow_run | Cannot be triggered without creating a real SP item | | HTTP trigger with custom test payload | trigger_live_flow | When you need to send different data than the original run | | Brand-new flow, never run | trigger_live_flow (HTTP only) | No prior run exists to resubmit |

Testing HTTP-Triggered Flows with custom payloads

For flows with a Request (HTTP) trigger, use trigger_live_flow when you need to send a different payload than the original run:

# First inspect what the trigger expects β€” read directly from the flow definition
defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
triggers = defn["properties"]["definition"]["triggers"]
manual = next(iter(triggers.values()))   # usually the only trigger on HTTP flows
request_schema = manual.get("inputs", {}).get("schema")
print("Expected body schema:", request_schema)

Response schemas live on Response action(s) in the actions block

for name, act in defn["properties"]["definition"]["actions"].items(): if act.get("type") == "Response": print(f"Response {name}:", act.get("inputs", {}).get("schema"))

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['responseStatus']}, Body: {result.get('responseBody')}")

> 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 | Then ALWAYS Call | What to Look For | |---|---|---|---| | Flow shows as Failed | get_live_flow_run_error | get_live_flow_run_action_outputs on the failing action | HTTP status + response body in outputs | | Error code is generic (ActionFailed, NotSpecified) | β€” | get_live_flow_run_action_outputs | The outputs.body contains the real error message, stack trace, or API error | | HTTP action returns 500 | β€” | get_live_flow_run_action_outputs | outputs.statusCode + outputs.body with server error detail | | 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 |

> Rule: never diagnose from error codes alone. get_live_flow_run_error > identifies the failing action. get_live_flow_run_action_outputs reveals > the actual cause. Always call both.


Reference Files

  • common-errors.md β€” Error codes, likely causes, and fixes
  • debug-workflow.md β€” Full decision tree for complex failures
  • Related Skills

  • power-automate-mcp β€” Foundation skill: connection setup, MCP helper, tool discovery
  • power-automate-build β€” Build and deploy new flows