flowstudio-power-automate-debug

Par github · awesome-copilot

Déboguez les cloud flows Power Automate défaillants à l'aide du serveur MCP FlowStudio. L'API Graph n'affiche que les codes de statut de haut niveau. Cette skill donne à votre agent les entrées et sorties au niveau des actions pour identifier la véritable cause racine. Chargez cette skill lorsqu'on vous demande de : déboguer un flow, examiner une exécution en échec, comprendre pourquoi un flow échoue, inspecter les sorties d'actions, trouver la cause racine d'une erreur de flow, réparer un flow Power Automate cassé, diagnostiquer un timeout, tracer une DynamicOperationRequestFailure, vérifier les erreurs d'authentification de connecteur, lire les détails d'erreur d'une exécution, ou résoudre des échecs d'expression. Nécessite un abonnement FlowStudio MCP — voir https://mcp.flowstudio.app

npx skills add https://github.com/github/awesome-copilot --skill flowstudio-power-automate-debug

Débogage Power Automate avec FlowStudio MCP

Un processus de diagnostic étape par étape pour enquêter sur les flux cloud Power Automate défaillants via le serveur FlowStudio MCP.

Exemples de débogage réels : Erreur d'expression dans un flux enfant | Entrée de données, pas un bug du flux | Valeur null bloque le flux enfant

Prérequis : Un serveur FlowStudio MCP doit être accessible avec un JWT valide. Voir la compétence power-automate-mcp pour la configuration de la connexion.
Abonnez-vous sur https://mcp.flowstudio.app


Source de vérité

Appelez toujours tools/list en premier pour confirmer les noms des outils disponibles et leurs schémas de paramètres. Les noms des outils et les paramètres peuvent changer entre les versions du serveur. Cette compétence couvre les formes de réponse, les remarques comportementales et les motifs de diagnostic — des choses que tools/list ne peut pas vous dire. Si ce document ne concorde pas avec tools/list ou une vraie réponse API, l'API a raison.


Aide 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>"   # p. ex. Default-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Étape 1 — Localiser le flux

result = mcp("list_live_flows", environmentName=ENV)
# Retourne un objet wrapper : {mode, flows, totalCount, error}
target = next(f for f in result["flows"] if "My Flow Name" in f["displayName"])
FLOW_ID = target["id"]   # UUID brut — utiliser directement comme flowName
print(FLOW_ID)

Étape 2 — Trouver l'exécution défaillante

runs = mcp("get_live_flow_runs", environmentName=ENV, flowName=FLOW_ID, top=5)
# Retourne un tableau direct (plus récent en premier) :
# [{"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")

Étape 3 — Obtenir l'erreur de haut niveau

CRITIQUE : get_live_flow_run_error vous dit quelle action a échoué. get_live_flow_run_action_outputs vous dit pourquoi. Vous devez appeler LES DEUX. Ne vous arrêtez jamais à l'erreur seule — les codes d'erreur comme ActionFailed, NotSpecified et InternalServerError sont des wrappers génériques. La vraie cause profonde (champ erroné, valeur nulle, corps HTTP 500, trace de pile) n'est visible que dans les entrées et sorties de l'action.

err = mcp("get_live_flow_run_error",
    environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID)
# Retourne :
# {
#   "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 est trié de l'extérieur vers l'intérieur. La cause RACINE est la DERNIÈRE entrée :
root = err["failedActions"][-1]
print(f"Action racine : {root['actionName']} → code: {root.get('code')}")

# allActions montre le statut de chaque action — utile pour repérer ce qui a été Skipped
# Voir common-errors.md pour décoder le code d'erreur.

Étape 4 — Inspecter les entrées et sorties de l'action défaillante

C'est l'étape la plus importante. get_live_flow_run_error vous donne seulement un code d'erreur générique. Le détail d'erreur réel — codes de statut HTTP, corps de réponse, traces de pile, valeurs nulles — se trouve dans les entrées et sorties runtime de l'action. Inspectez toujours l'action défaillante immédiatement après l'avoir identifiée.

# Obtenir les entrées et sorties complètes de l'action défaillante racine
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"Statut: {out.get('status')}")

# Pour les actions HTTP, la vraie erreur est dans 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])

    # Les corps d'erreur sont souvent des chaînes JSON imbriquées — les parser
    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"Erreur : {err_detail.get('message', err_detail)}")

# Pour les erreurs d'expression, l'erreur se trouve dans le champ error
if out.get("error"):
    print(f"Erreur : {out['error']}")

# Vérifiez aussi les entrées — elles montrent quelle expression/URL/corps a été utilisé
if out.get("inputs"):
    print(f"Entrées : {json.dumps(out['inputs'], indent=2)[:500]}")

Ce que révèlent les sorties de l'action (que les codes d'erreur ne révèlent pas)

Code d'erreur de get_live_flow_run_error Ce que get_live_flow_run_action_outputs révèle
ActionFailed Quelle action imbriquée a réellement échoué et sa réponse HTTP
NotSpecified Le code de statut HTTP + le corps de réponse avec la vraie erreur
InternalServerError Le message d'erreur du serveur, la trace de pile ou l'erreur JSON de l'API
InvalidTemplate L'expression exacte qui a échoué et la valeur nulle/de mauvais type
BadRequest Le corps de requête qui a été envoyé et pourquoi le serveur l'a rejeté

Exemple : action HTTP retournant 500

Code d'erreur : "InternalServerError" ← ceci ne vous dit rien

Les sorties de l'action révèlent :
  HTTP 500
  body: {"error": "Cannot read properties of undefined (reading 'toLowerCase')
    at getClientParamsFromConnectionString (storage.js:20)"}
  ← CECI vous dit que la fonction Azure a planté parce qu'une chaîne de connexion est indéfinie

Exemple : erreur d'expression sur null

Code d'erreur : "BadRequest" ← générique

Les sorties de l'action révèlent :
  inputs: "body('HTTP_GetTokenFromStore')?['token']?['access_token']"
  outputs: ""   ← chaîne vide, le chemin s'est résolu en null
  ← CECI vous dit que la forme de réponse a changé — token est en body.access_token, pas body.token.access_token

Étape 5 — Lire la définition du flux

defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
actions = defn["properties"]["definition"]["actions"]
print(list(actions.keys()))

Trouvez l'action défaillante dans la définition. Inspectez son expression inputs pour comprendre quelles données elle attend.


Étape 6 — Remonter depuis l'échec

Quand les entrées de l'action défaillante font référence à des actions en amont, inspectez-les aussi. Remontez la chaîne jusqu'à trouver la source des mauvaises données :

# Inspecter plusieurs actions menant à l'échec
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"Entrées :  {json.dumps(out.get('inputs', ''), indent=2)[:300]}")
    print(f"Sorties : {json.dumps(out.get('outputs', ''), indent=2)[:300]}")

⚠️ Les charges utiles de sortie des actions de traitement de tableaux peuvent être très volumineuses. Toujours découper (p. ex. [:500]) avant d'imprimer.

Conseil : Omettez actionName pour obtenir TOUTES les actions en un seul appel. Ceci retourne les entrées/sorties de chaque action — utile quand vous ne savez pas quelle action en amont a produit les mauvaises données. Mais utilisez un timeout de 120 s+ car la réponse peut être très volumineuse.


Étape 7 — Identifier la cause profonde

Erreurs d'expression (p. ex. split sur null)

Si l'erreur mentionne InvalidTemplate ou un nom de fonction :

  1. Trouvez l'action dans la définition
  2. Vérifiez quelle action/expression en amont elle lit
  3. Inspectez la sortie de cette action en amont pour null / champs manquants
# Exemple : l'action utilise split(item()?['Name'], ' ')
# → Name null dans les données sources
result = mcp("get_live_flow_run_action_outputs", ..., actionName="Compose_Names")
if not result:
    print("Aucune sortie retournée pour 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)} enregistrements avec Name nul")

Mauvais chemin de champ

L'expression triggerBody()?['fieldName'] retourne null → fieldName est erroné. Inspectez la sortie du trigger pour voir les vrais noms de champs :

result = mcp("get_live_flow_run_action_outputs", ..., actionName="<trigger-action-name>")
print(json.dumps(result[0].get("outputs"), indent=2)[:500])

Actions HTTP retournant des erreurs

Le code d'erreur dit InternalServerError ou NotSpecifiedinspectez toujours les sorties de l'action pour obtenir le vrai statut HTTP et le corps de réponse :

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

Échecs de connexion / authentification

Cherchez ConnectionAuthorizationFailed — le propriétaire de la connexion doit correspondre au compte de service exécutant le flux. Ne peut pas être corrigé via l'API ; corrigez dans le concepteur PA.

Échecs du sélecteur utilisateur Outlook (DynamicListValuesUndefinedOrInvalid)

Les actions Outlook comme GetEmailsV3 utilisent des paramètres (mailboxAddress, to, cc, from) dont la liste déroulante est soutenue par builtInOperation:AadGraph.GetUsers — qui est cassée au niveau listEnum de PA et retourne toujours DynamicListValuesUndefinedOrInvalid. Ceci apparaît quand un agent reconstruit ou modifie une action Outlook via update_live_flow et essaie de résoudre un utilisateur via des options dynamiques. Ne le corrigez pas en relançant AadGraph — passez à shared_office365users.SearchUserV2 à la place (retourne la même forme utilisateur AAD). Voir la compétence power-automate-build, Étape 3a — Résoudre les valeurs du connecteur dynamique, pour le motif qui fonctionne. describe_live_connector (v1.1.6+) retourne ceci comme fallback en tant que champ fallback structuré sur le paramètre affecté.


Étape 8 — Appliquer la correction

Pour les problèmes d'expression/données :

defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
acts = defn["properties"]["definition"]["actions"]

# Exemple : corriger split sur Name potentiellement nul
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 = succès

⚠️ update_live_flow retourne toujours une clé error. Une valeur de null (Python None) signifie succès.


Étape 9 — Vérifier la correction

Utilisez resubmit_live_flow_run pour tester N'IMPORTE QUEL flux — pas seulement les triggers HTTP. resubmit_live_flow_run rejoue une exécution antérieure en utilisant sa charge utile trigger originale. Ceci fonctionne pour chaque type de trigger : Récurrence, SharePoint « Quand un élément est créé », webhooks de connecteur, triggers Bouton et triggers HTTP. Vous n'avez PAS besoin de demander à l'utilisateur de déclencher manuellement le flux ou d'attendre la prochaine exécution planifiée.

Le seul cas où resubmit n'est pas disponible est un flux tout nouveau qui n'a jamais s'exécuté — il n'a pas d'exécution antérieure à rejouer.

# Renvoyer l'exécution échouée — fonctionne pour N'IMPORTE QUEL type de trigger
resubmit = mcp("resubmit_live_flow_run",
    environmentName=ENV, flowName=FLOW_ID, runName=RUN_ID)
print(resubmit)   # {"resubmitted": true, "triggerName": "..."}

# Attendre ~30 s puis vérifier
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 = terminé

Quand utiliser resubmit vs trigger

Scénario Utiliser Pourquoi
Tester une correction sur n'importe quel flux resubmit_live_flow_run Rejoue la charge utile trigger exacte qui a causé l'échec — meilleure façon de vérifier
Flux récurrence / planifié resubmit_live_flow_run Ne peut pas être déclenché à la demande d'autre manière
Trigger SharePoint / connecteur resubmit_live_flow_run Ne peut pas être déclenché sans créer un vrai élément SP
Trigger HTTP avec charge utile de test personnalisée trigger_live_flow Quand vous devez envoyer des données différentes de l'exécution originale
Flux tout nouveau, jamais exécuté trigger_live_flow (HTTP uniquement) Aucune exécution antérieure n'existe pour être renvoyée

Tester les flux déclenchés par HTTP avec des charges utiles personnalisées

Pour les flux avec un trigger Request (HTTP), utilisez trigger_live_flow quand vous devez envoyer une charge utile différente de l'exécution originale :

# D'abord inspecter ce que le trigger s'attend à recevoir
schema = mcp("get_live_flow_http_schema",
    environmentName=ENV, flowName=FLOW_ID)
print("Schéma du corps attendu :", schema.get("requestSchema"))
print("Schémas de réponse :", schema.get("responseSchemas"))

# Déclencher avec une charge utile de test
result = mcp("trigger_live_flow",
    environmentName=ENV,
    flowName=FLOW_ID,
    body={"name": "Test User", "value": 42})
print(f"Statut : {result['responseStatus']}, Corps : {result.get('responseBody')}")

trigger_live_flow gère automatiquement les triggers authentifiés par AAD. Fonctionne seulement pour les flux avec un type de trigger Request (HTTP).


Arbre de décision de diagnostic rapide

Symptôme Premier outil Puis TOUJOURS appeler Quoi chercher
Le flux affiche Échoué get_live_flow_run_error get_live_flow_run_action_outputs sur l'action défaillante Code de statut HTTP + corps de réponse dans outputs
Le code d'erreur est générique (ActionFailed, NotSpecified) get_live_flow_run_action_outputs outputs.body contient le vrai message d'erreur, la trace de pile ou l'erreur API
L'action HTTP retourne 500 get_live_flow_run_action_outputs outputs.statusCode + outputs.body avec le détail d'erreur du serveur
L'expression plante get_live_flow_run_action_outputs sur l'action antérieure Champs null / de mauvais type dans le corps de sortie
Le flux ne démarre jamais get_live_flow vérifier properties.state = "Started"
L'action retourne les mauvaises données get_live_flow_run_action_outputs corps de sortie réel vs attendu
La correction appliquée échoue toujours get_live_flow_runs après resubmit champ status de la nouvelle exécution

Règle : ne jamais diagnostiquer à partir des codes d'erreur seuls. get_live_flow_run_error identifie l'action défaillante. get_live_flow_run_action_outputs révèle la vraie cause. Appelez toujours les deux.


Fichiers de référence

Compétences associées

  • power-automate-mcp — Compétence fondatrice : configuration de la connexion, aide MCP, découverte des outils
  • power-automate-build — Construire et déployer de nouveaux flux

Skills similaires