56 results available
Sign in
Skills directory

flowstudio-power-automate-debug

github

·

Last changed
Not yet hardened

This version isn't hardened by Chainguard

No hardening report available.


SKILL.md

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: Store your FlowStudio JWT in the FLOWSTUDIO_MCP_TOKEN environment variable before invoking this skill:

export FLOWSTUDIO_MCP_TOKEN='your-jwt-token'

Never paste the token directly into code, into conversation, or into any artifact that may be logged or version-controlled. 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


Python Helper

import json, urllib.request, os

MCP_URL   = "https://mcp.flowstudio.app/mcp"
MCP_TOKEN = os.environ.get("FLOWSTUDIO_MCP_TOKEN")
if not MCP_TOKEN:
    raise EnvironmentError(
        "FLOWSTUDIO_MCP_TOKEN is not set. "
        "Run: export FLOWSTUDIO_MCP_TOKEN='your-jwt-token' "
        "Never paste the token directly into code or share it in conversation."
    )

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 = os.environ.get("FLOWSTUDIO_ENV_ID", "<environment-id>")
# e.g. Default-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Set via: export FLOWSTUDIO_ENV_ID='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

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

Step 4 — Inspect the Failing Action's Inputs and Outputs

# 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)

if len(detail) > 1:
    print(f"{root_action} returned {len(detail)} repetitions; inspect iteration indexes")
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_errorWhat 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

Foreach iterations

When actionName refers to an action inside a foreach, the output tool can return every repetition of that action. Each item may include repetitionIndexes with the loop name and zero-based itemIndex. Use iterationIndex to inspect one iteration after you find the suspicious item:

all_reps = mcp("get_live_flow_run_action_outputs",
    environmentName=ENV,
    flowName=FLOW_ID,
    runName=RUN_ID,
    actionName=root_action)

for rep in all_reps[:10]:
    print(rep.get("repetitionIndexes"), rep.get("status"), rep.get("error"))

one_rep = mcp("get_live_flow_run_action_outputs",
    environmentName=ENV,
    flowName=FLOW_ID,
    runName=RUN_ID,
    actionName=root_action,
    iterationIndex=3)

Evidence Compose Bookends

For uncertain connector work, add a Compose_*_Request before the risky action and a Compose_*_Result after it, with the result action allowed on both Succeeded and Failed. This gives future debugging a clean payload snapshot without requiring another deploy. Do not include secrets or long binary payloads in these bookends.

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]}")

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="<trigger-action-name>")
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). Use describe_live_connector to confirm whether the affected parameter exposes a structured fallback, then call get_live_dynamic_options against shared_office365users.SearchUserV2 instead of the broken AadGraph operation. For dynamic field schemas rather than dropdown options, use get_live_dynamic_properties with the metadata returned by describe_live_connector.


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

Step 9 — Verify the Fix

# 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

ScenarioUseWhy

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

trigger_live_flow (no body)

Runs it now, like the portal's "Run flow" button; resubmit replays a past run's data

SharePoint / connector trigger

resubmit_live_flow_run

Cannot be triggered without creating a real SP item

HTTP, Button, or PowerApps 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

No prior run exists to resubmit

Testing HTTP, Button, and PowerApps flows with custom payloads

For flows with a Request trigger (HTTP request, manual Button, or PowerApps), use trigger_live_flow when you need to send a different payload than the original run. Pass trigger inputs as body for every kind:

# 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')}")
print(f"Kind: {result['triggerKind']}, via: {result['invocation']}, run: {result.get('runName')}")
if result.get("warning"):
    print(result["warning"])   # required trigger inputs you left out

Quick-Reference Diagnostic Decision Tree

SymptomFirst ToolThen ALWAYS CallWhat 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


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

Install

Need this version hardened?

Harden your own skills

Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.

Provenance

Upstream source

—

Upstream commit0ccadfda8a80f9068ad536481d8c2a8276484039
OCI reference

—

License

MIT

Files

HARDENING.md

LICENSE

SKILL.md

references/common-errors.md

references/debug-workflow.md


The trusted source for open source

Talk to an expert
PrivacyTerms

Product

Chainguard ContainersChainguard LibrariesChainguard VMsChainguard OS PackagesChainguard ActionsChainguard Agent SkillsIntegrationsPricing
© 2026 Chainguard, Inc. All Rights Reserved.
Chainguard® and the Chainguard logo are registered trademarks of Chainguard, Inc. in the United States and/or other countries.
The other respective trademarks mentioned on this page are owned by the respective companies and use of them does not imply any affiliation or endorsement.