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/listen 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 quetools/listne peut pas vous dire. Si ce document ne concorde pas avectools/listou 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_errorvous dit quelle action a échoué.get_live_flow_run_action_outputsvous dit pourquoi. Vous devez appeler LES DEUX. Ne vous arrêtez jamais à l'erreur seule — les codes d'erreur commeActionFailed,NotSpecifiedetInternalServerErrorsont 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_errorvous 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
actionNamepour 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 :
- Trouvez l'action dans la définition
- Vérifiez quelle action/expression en amont elle lit
- 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 NotSpecified — inspectez 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_flowretourne toujours une cléerror. Une valeur denull(PythonNone) signifie succès.
Étape 9 — Vérifier la correction
Utilisez
resubmit_live_flow_runpour tester N'IMPORTE QUEL flux — pas seulement les triggers HTTP.resubmit_live_flow_runrejoue 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ù
resubmitn'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_flowgère automatiquement les triggers authentifiés par AAD. Fonctionne seulement pour les flux avec un type de triggerRequest(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_erroridentifie l'action défaillante.get_live_flow_run_action_outputsrévèle la vraie cause. Appelez toujours les deux.
Fichiers de référence
- common-errors.md — Codes d'erreur, causes probables et corrections
- debug-workflow.md — Arbre de décision complet pour les échecs complexes
Compétences associées
power-automate-mcp— Compétence fondatrice : configuration de la connexion, aide MCP, découverte des outilspower-automate-build— Construire et déployer de nouveaux flux