chore: publish from staged

This commit is contained in:
github-actions[bot]
2026-05-22 01:03:33 +00:00
parent 1d7582717e
commit 5b049e4e19
488 changed files with 101042 additions and 264 deletions
@@ -19,10 +19,10 @@
"governance"
],
"skills": [
"./skills/flowstudio-power-automate-build/",
"./skills/flowstudio-power-automate-debug/",
"./skills/flowstudio-power-automate-governance/",
"./skills/flowstudio-power-automate-mcp/",
"./skills/flowstudio-power-automate-monitoring/"
"./skills/flowstudio-power-automate-build",
"./skills/flowstudio-power-automate-debug",
"./skills/flowstudio-power-automate-governance",
"./skills/flowstudio-power-automate-mcp",
"./skills/flowstudio-power-automate-monitoring"
]
}
@@ -0,0 +1,504 @@
---
name: flowstudio-power-automate-build
description: >-
Build, scaffold, and deploy Power Automate cloud flows using the FlowStudio
MCP server. Your agent constructs flow definitions, wires connections, deploys,
and tests — all via MCP without opening the portal.
Load this skill when asked to: create a flow, build a new flow,
deploy a flow definition, scaffold a Power Automate workflow, construct a flow
JSON, update an existing flow's actions, patch a flow definition, add actions
to a flow, wire up connections, or generate a workflow definition from scratch.
Requires a FlowStudio MCP subscription — see https://mcp.flowstudio.app
---
# Build & Deploy Power Automate Flows with FlowStudio MCP
Step-by-step guide for constructing and deploying Power Automate cloud flows
programmatically 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
Workflow:
1. Load current build tools.
2. Check for an existing flow.
3. Resolve connection references.
4. Build the definition.
5. Deploy.
6. Verify.
7. Test.
---
## Source of Truth
> **Always call `list_skills` / `tool_search` first** to confirm available tool
> names and parameter schemas. Tool names and parameters may change between
> server versions.
> This skill covers response shapes, behavioral notes, and build patterns —
> things tool schemas cannot tell you. If this document disagrees with
> `tool_search` 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
```
---
## 0. Load the Current Build Tools
For a brand-new flow, load the server's `create-flow` bundle. For editing an
existing flow, load `build-flow`. This keeps the agent aligned with the MCP
server's current schema before constructing JSON.
```python
schemas = mcp("tool_search", query="skill:create-flow")
# Includes list_live_environments, list_live_connections,
# describe_live_connector, get_live_dynamic_options, update_live_flow.
```
If you need a tool outside the bundle, load it explicitly:
```python
mcp("tool_search", query="select:get_live_dynamic_properties")
```
---
## 1. Safety Check: Does the Flow Already Exist?
Always look before you build to avoid duplicates:
```python
results = mcp("list_live_flows",
environmentName=ENV,
mode="owner",
search="My New Flow",
top=20)
# list_live_flows returns { "flows": [...], "mode": "...", ... }
matches = [f for f in results["flows"]
if "My New Flow".lower() in f["displayName"].lower()]
if len(matches) > 0:
# Flow exists — modify rather than create
FLOW_ID = matches[0]["id"] # plain UUID from list_live_flows
print(f"Existing flow: {FLOW_ID}")
defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
else:
print("Flow not found — building from scratch")
FLOW_ID = None
```
For very large environments, `list_live_flows` may return a continuation URL.
Pass it back as `continuationUrl` with the same `mode` to retrieve the next
batch. Use `mode="admin"` only when the user needs all environment flows and
the MCP identity has admin rights.
---
## 2. Obtain Connection References
Every connector action needs a `connectionName` that points to a key in the
flow's `connectionReferences` map. That key links to an authenticated connection
in the environment.
> **MANDATORY**: You MUST call `list_live_connections` first — do NOT ask the
> user for connection names or GUIDs. The API returns the exact values you need.
> Only prompt the user if the API confirms that required connections are missing.
### 2a — Find active connections
```python
conns = mcp("list_live_connections", environmentName=ENV)
active = [c for c in conns["connections"]
if c["statuses"][0]["status"] == "Connected"]
conn_map = {c["connectorName"]: c["id"] for c in active}
```
For a known connector, pass `search` to reduce output and get paste-ready
`connectionReferenceTemplate` and `hostTemplate` values:
```python
sp_conns = mcp("list_live_connections",
environmentName=ENV,
search="shared_sharepointonline")
```
### 2b — Determine which connectors the flow needs
Common connector API names: SharePoint `shared_sharepointonline`, Outlook
`shared_office365`, Teams `shared_teams`, Approvals `shared_approvals`,
OneDrive `shared_onedriveforbusiness`, Excel `shared_excelonlinebusiness`,
Dataverse `shared_commondataserviceforapps`, Forms `shared_microsoftforms`.
Flows that need no connectors, such as Recurrence + Compose + HTTP only, can
omit `connectionReferences`.
### 2c — If connections are missing, guide the user
```python
connectors_needed = ["shared_sharepointonline", "shared_office365"] # adjust per flow
missing = [c for c in connectors_needed if c not in conn_map]
if missing:
# STOP: connections require browser OAuth consent.
# Ask the user to create the missing connector connections in the
# selected environment, then re-run list_live_connections.
raise Exception(f"Missing active connections: {missing}")
```
### 2d — Build the connectionReferences block
```python
connection_references = {}
host_templates = {}
for connector in connectors_needed:
c = next(c for c in active if c["connectorName"] == connector)
connection_references[connector] = c.get("connectionReferenceTemplate") or {
"connectionName": c["id"], # the connection id from list_live_connections
"source": "Invoker",
"id": f"/providers/Microsoft.PowerApps/apis/{connector}"
}
host_templates[connector] = c.get("hostTemplate") or {
"connectionName": connector
}
```
In Step 3 action JSON, `inputs.host.connectionName` must be the map key such as
`shared_teams`, not the GUID. The GUID belongs only inside the
`connectionReferences[connector].connectionName` value. If an existing flow uses
the same connectors, you may also copy its `properties.connectionReferences`
from `get_live_flow`.
---
## 3. Build the Flow Definition
Construct the definition object. See [flow-schema.md](references/flow-schema.md)
for the full schema and these action pattern references for copy-paste templates:
- [action-patterns-core.md](references/action-patterns-core.md) — Variables, control flow, expressions
- [action-patterns-data.md](references/action-patterns-data.md) — Array transforms, HTTP, parsing
- [action-patterns-connectors.md](references/action-patterns-connectors.md) — SharePoint, Outlook, Teams, Approvals
```python
definition = {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": { ... }, # see trigger-types.md / build-patterns.md
"actions": { ... } # see ACTION-PATTERNS-*.md / build-patterns.md
}
```
> See [build-patterns.md](references/build-patterns.md) for complete, ready-to-use
> flow definitions covering Recurrence+SharePoint+Teams, HTTP triggers, and more.
### Discover connector operations before guessing JSON
For connector-backed triggers/actions, prefer the live connector describer over
hand-written shapes. It can return authored hints, canonical examples, variant
keys, inputs/outputs, and dynamic metadata pointers.
```python
# Search across connectors when you know the user's intent but not the API.
matches = mcp("describe_live_connector",
environmentName=ENV,
search="send email",
top=5)
# Describe a specific operation before copying an exampleDefinition.
op = mcp("describe_live_connector",
environmentName=ENV,
connectorName="shared_office365",
operationId="SendEmailV2")
print(op.get("hint"))
```
When an operation has multiple authored variants, request the variant the flow
needs:
```python
teams_chat = mcp("describe_live_connector",
environmentName=ENV,
connectorName="shared_teams",
operationId="PostMessageToConversation",
variant="flowbot_chat")
```
When the operation description says a parameter has dynamic options or dynamic
properties, call the indicated next tool:
```python
sp_op = mcp("describe_live_connector",
environmentName=ENV,
connectorName="shared_sharepointonline",
operationId="GetItems")
sites = mcp("get_live_dynamic_options",
environmentName=ENV,
connectorName="shared_sharepointonline",
connectionName=conn_map["shared_sharepointonline"],
operationId="GetItems",
parameterName="dataset",
dynamicMetadata=sp_op["dynamicParameters"]["dataset"])
fields = mcp("get_live_dynamic_properties",
environmentName=ENV,
connectorName="shared_sharepointonline",
connectionName=conn_map["shared_sharepointonline"],
operationId="GetItems",
parameterName="item",
parameters={"dataset": "<site-url>", "table": "<list-id>"},
dynamicMetadata=sp_op["dynamicProperties"]["item"])
```
Use dynamic options for dropdown IDs such as SharePoint sites/lists and Teams
teams/channels. Use dynamic properties for schema/field shapes such as
SharePoint list item columns.
---
## 4. Deploy (Create or Update)
`update_live_flow` handles both creation and updates in a single tool.
### Create a new flow (no existing flow)
Omit `flowName` — the server generates a new GUID and creates via PUT:
```python
definition["description"] = "Weekly SharePoint → Teams notification flow, built by agent"
result = mcp("update_live_flow",
environmentName=ENV,
# flowName omitted → creates a new flow
definition=definition,
connectionReferences=connection_references,
displayName="Overdue Invoice Notifications"
)
if result.get("error") is not None:
print("Create failed:", result["error"])
else:
# Capture the new flow ID for subsequent steps
FLOW_ID = result["created"]
print(f"✅ Flow created: {FLOW_ID}")
```
### Update an existing flow
Provide `flowName` to PATCH:
```python
definition["description"] = (
"Updated by agent on " + __import__('datetime').datetime.utcnow().isoformat()
)
result = mcp("update_live_flow",
environmentName=ENV,
flowName=FLOW_ID,
definition=definition,
connectionReferences=connection_references,
displayName="My Updated Flow"
)
if result.get("error") is not None:
print("Update failed:", result["error"])
else:
print("Update succeeded:", result)
```
> ⚠️ `update_live_flow` always returns an `error` key.
> `null` (Python `None`) means success — do not treat the presence of the key as failure.
>
> ⚠️ Flow description lives at `definition["description"]`. The current server
> appends `#flowstudio-mcp` for usage tracking. Do not pass a top-level
> `description` argument unless `tool_search` shows one in the active schema.
### Common deployment errors
| Error message (contains) | Cause | Fix |
|---|---|---|
| `missing from connectionReferences` | An action's `host.connectionName` references a key that doesn't exist in the `connectionReferences` map | Ensure `host.connectionName` uses the **key** from `connectionReferences` (e.g. `shared_teams`), not the raw GUID |
| `ConnectionAuthorizationFailed` / 403 | The connection GUID belongs to another user or is not authorized | Re-run Step 2a and use a connection owned by the current `x-api-key` user |
| `InvalidTemplate` / `InvalidDefinition` | Syntax error in the definition JSON | Check `runAfter` chains, expression syntax, and action type spelling |
| `ConnectionNotConfigured` | A connector action exists but the connection GUID is invalid or expired | Re-check `list_live_connections` for a fresh GUID |
---
## 5. Verify the Deployment
```python
check = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
# Confirm state
print("State:", check["properties"]["state"]) # Should be "Started"
# If state is "Stopped", use set_live_flow_state — NOT update_live_flow
# mcp("set_live_flow_state", environmentName=ENV, flowName=FLOW_ID, state="Started")
# Confirm the action we added is there
acts = check["properties"]["definition"]["actions"]
print("Actions:", list(acts.keys()))
```
---
## 6. Test the Flow
> **MANDATORY**: Before triggering any test run, **ask the user for confirmation**.
> Running a flow has real side effects — it may send emails, post Teams messages,
> write to SharePoint, start approvals, or call external APIs. Explain what the
> flow will do and wait for explicit approval before calling `trigger_live_flow`
> or `resubmit_live_flow_run`.
### Updated flows (have prior runs) — ANY trigger type
> **Use `resubmit_live_flow_run` first.** It works for EVERY trigger type —
> Recurrence, SharePoint, connector webhooks, Button, and HTTP. It replays
> the original trigger payload. Do NOT ask the user to manually trigger the
> flow or wait for the next scheduled run.
```python
runs = mcp("get_live_flow_runs", environmentName=ENV, flowName=FLOW_ID, top=1)
if runs:
# Works for Recurrence, SharePoint, connector triggers — not just HTTP
result = mcp("resubmit_live_flow_run",
environmentName=ENV, flowName=FLOW_ID, runName=runs[0]["name"])
print(result) # {"resubmitted": true, "triggerName": "..."}
```
### HTTP-triggered flows — custom test payload
Only use `trigger_live_flow` when you need to send a **different** payload
than the original run. For verifying a fix, `resubmit_live_flow_run` is
better because it uses the exact data that caused the failure.
```python
defn = mcp("get_live_flow", environmentName=ENV, flowName=FLOW_ID)
triggers = defn["properties"]["definition"]["triggers"]
manual = next(iter(triggers.values()))
print("Expected body:", manual.get("inputs", {}).get("schema"))
result = mcp("trigger_live_flow",
environmentName=ENV, flowName=FLOW_ID,
body={"name": "Test", "value": 1})
print(f"Status: {result['responseStatus']}")
```
### Brand-new non-HTTP flows (Recurrence, connector triggers, etc.)
A brand-new Recurrence or connector-triggered flow has **no prior runs** to
resubmit and no HTTP endpoint to call. This is the ONLY scenario where you
need the temporary HTTP trigger approach below. **Deploy with a temporary
HTTP trigger first, test the actions, then swap to the production trigger.**
Compact recipe:
```python
production_trigger = definition["triggers"]
definition["triggers"] = {
"manual": {"type": "Request", "kind": "Http", "inputs": {"schema": {}}}
}
result = mcp("update_live_flow",
environmentName=ENV,
flowName=FLOW_ID, # omit if creating new
definition=definition,
connectionReferences=connection_references,
displayName="Overdue Invoice Notifications")
FLOW_ID = FLOW_ID or result["created"]
test = mcp("trigger_live_flow", environmentName=ENV, flowName=FLOW_ID,
body={"sample": "payload"})
runs = mcp("get_live_flow_runs", environmentName=ENV, flowName=FLOW_ID, top=1)
if runs[0]["status"] == "Failed":
err = mcp("get_live_flow_run_error",
environmentName=ENV, flowName=FLOW_ID, runName=runs[0]["name"])
raise Exception(err["failedActions"][-1])
definition["triggers"] = production_trigger
mcp("update_live_flow",
environmentName=ENV,
flowName=FLOW_ID,
definition=definition,
connectionReferences=connection_references)
```
The trigger is only the entry point; testing through HTTP still exercises the
same actions. If actions use `triggerBody()` or `triggerOutputs()`, pass a
representative `trigger_live_flow.body` shaped like the production trigger
payload.
---
## Gotchas
| Mistake | Consequence | Prevention |
|---|---|---|
| Missing `connectionReferences` in deploy | 400 "Supply connectionReferences" | Always call `list_live_connections` first |
| `"operationOptions"` missing on Foreach | Parallel execution, race conditions on writes | Always add `"Sequential"` |
| `union(old_data, new_data)` | Old values override new (first-wins) | Use `union(new_data, old_data)` |
| `split()` on potentially-null string | `InvalidTemplate` crash | Wrap with `coalesce(field, '')` |
| Checking `result["error"]` exists | Always present; true error is `!= null` | Use `result.get("error") is not None` |
| Flow deployed but state is "Stopped" | Flow won't run on schedule | Call `set_live_flow_state` with `state: "Started"` — do **not** use `update_live_flow` for state changes |
| Teams "Chat with Flow bot" recipient as object | 400 `GraphUserDetailNotFound` | Use plain string with trailing semicolon (see below) |
| Copilot/Skills flow not in a solution | Copilot Studio may not discover it as an agent tool | After deploy, call `add_live_flow_to_solution` with the target `solutionId` |
| Button/Skills trigger used for MCP testing | MCP cannot directly fire the production trigger | Test the same actions through a temporary HTTP twin, then swap the trigger back |
| Connector action missing `metadata.operationMetadataId` | Designer/run-only UI can behave inconsistently | Preserve existing IDs; add stable GUIDs for new connector actions |
| Placeholder Excel `scriptId` | Dynamic validation fails at save time | Resolve the real Office Script ID before deploying |
| SharePoint `PatchItem` omits required fields | Save can fail even if the field is not changing | Echo unchanged required fields such as `item/Title` |
| Copilot Studio connector calls a draft agent | Connector invocation can fail or hit stale behavior | Publish the agent before testing/resubmitting the flow |
### Teams `PostMessageToConversation` — Recipient Formats
The `body/recipient` parameter format depends on the `location` value:
| Location | `body/recipient` format | Example |
|---|---|---|
| **Chat with Flow bot** | Plain email string with **trailing semicolon** | `"user@contoso.com;"` |
| **Channel** | Object with `groupId` and `channelId` | `{"groupId": "...", "channelId": "..."}` |
> **Common mistake**: passing `{"to": "user@contoso.com"}` for "Chat with Flow bot"
> returns a 400 `GraphUserDetailNotFound` error. The API expects a plain string.
---
## Reference Files
- [flow-schema.md](references/flow-schema.md) — Full flow definition JSON schema
- [trigger-types.md](references/trigger-types.md) — Trigger type templates
- [action-patterns-core.md](references/action-patterns-core.md) — Variables, control flow, expressions
- [action-patterns-data.md](references/action-patterns-data.md) — Array transforms, HTTP, parsing
- [action-patterns-connectors.md](references/action-patterns-connectors.md) — SharePoint, Outlook, Teams, Approvals
- [build-patterns.md](references/build-patterns.md) — Complete flow definition templates (Recurrence+SP+Teams, HTTP trigger)
## Related Skills
- `flowstudio-power-automate-mcp` — Core connection setup and tool reference
- `flowstudio-power-automate-debug` — Debug failing flows after deployment
@@ -0,0 +1,580 @@
# FlowStudio MCP — Action Patterns: Connectors
SharePoint, Outlook, Teams, and Approvals connector action patterns.
> All examples assume `"runAfter"` is set appropriately.
> Replace `<connectionName>` with the **key** you used in `connectionReferences`
> (e.g. `shared_sharepointonline`, `shared_teams`). This is NOT the connection
> GUID — it is the logical reference name that links the action to its entry in
> the `connectionReferences` map.
---
## SharePoint
### SharePoint — Get Items
```json
"Get_SP_Items": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "GetItems"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList",
"$filter": "Status eq 'Active'",
"$top": 500
}
}
}
```
Result reference: `@outputs('Get_SP_Items')?['body/value']`
> **Dynamic OData filter with string interpolation**: inject a runtime value
> directly into the `$filter` string using `@{...}` syntax:
> ```
> "$filter": "Title eq '@{outputs('ConfirmationCode')}'"
> ```
> Note the single-quotes inside double-quotes — correct OData string literal
> syntax. Avoids a separate variable action.
> **Pagination for large lists**: by default, GetItems stops at `$top`. To auto-paginate
> beyond that, enable the pagination policy on the action. In the flow definition this
> appears as:
> ```json
> "paginationPolicy": { "minimumItemCount": 10000 }
> ```
> Set `minimumItemCount` to the maximum number of items you expect. The connector will
> keep fetching pages until that count is reached or the list is exhausted. Without this,
> flows silently return a capped result on lists with >5,000 items.
---
### SharePoint — Get Item (Single Row by ID)
```json
"Get_SP_Item": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "GetItem"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList",
"id": "@triggerBody()?['ID']"
}
}
}
```
Result reference: `@body('Get_SP_Item')?['FieldName']`
> Use `GetItem` (not `GetItems` with a filter) when you already have the ID.
> Re-fetching after a trigger gives you the **current** row state, not the
> snapshot captured at trigger time — important if another process may have
> modified the item since the flow started.
---
### SharePoint — Create Item
```json
"Create_SP_Item": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "PostItem"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList",
"item/Title": "@variables('myTitle')",
"item/Status": "Active"
}
}
}
```
---
### SharePoint — Update Item
```json
"Update_SP_Item": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "PatchItem"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList",
"id": "@item()?['ID']",
"item/Status": "Processed"
}
}
}
```
> `PatchItem` can validate required SharePoint columns even when you are not
> changing those fields. Echo unchanged required fields from the trigger or a
> prior Get Item action, for example `item/Title`, and use internal field names.
---
### SharePoint — File Upsert (Create or Overwrite in Document Library)
SharePoint's `CreateFile` fails if the file already exists. To upsert (create or overwrite)
without a prior existence check, use `GetFileMetadataByPath` on **both Succeeded and Failed**
from `CreateFile` — if create failed because the file exists, the metadata call still
returns its ID, which `UpdateFile` can then overwrite:
```json
"Create_File": {
"type": "OpenApiConnection",
"inputs": {
"host": { "apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>", "operationId": "CreateFile" },
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"folderPath": "/My Library/Subfolder",
"name": "@{variables('filename')}",
"body": "@outputs('Compose_File_Content')"
}
}
},
"Get_File_Metadata_By_Path": {
"type": "OpenApiConnection",
"runAfter": { "Create_File": ["Succeeded", "Failed"] },
"inputs": {
"host": { "apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>", "operationId": "GetFileMetadataByPath" },
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"path": "/My Library/Subfolder/@{variables('filename')}"
}
}
},
"Update_File": {
"type": "OpenApiConnection",
"runAfter": { "Get_File_Metadata_By_Path": ["Succeeded", "Skipped"] },
"inputs": {
"host": { "apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>", "operationId": "UpdateFile" },
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"id": "@outputs('Get_File_Metadata_By_Path')?['body/{Identifier}']",
"body": "@outputs('Compose_File_Content')"
}
}
}
```
> If `Create_File` succeeds, `Get_File_Metadata_By_Path` is `Skipped` and `Update_File`
> still fires (accepting `Skipped`), harmlessly overwriting the file just created.
> If `Create_File` fails (file exists), the metadata call retrieves the existing file's ID
> and `Update_File` overwrites it. Either way you end with the latest content.
>
> **Document library system properties** — when iterating a file library result (e.g.
> from `ListFolder` or `GetFilesV2`), use curly-brace property names to access
> SharePoint's built-in file metadata. These are different from list field names:
> ```
> @item()?['{Name}'] — filename without path (e.g. "report.csv")
> @item()?['{FilenameWithExtension}'] — same as {Name} in most connectors
> @item()?['{Identifier}'] — internal file ID for use in UpdateFile/DeleteFile
> @item()?['{FullPath}'] — full server-relative path
> @item()?['{IsFolder}'] — boolean, true for folder entries
> ```
---
### SharePoint — GetItemChanges Column Gate
When a SharePoint "item modified" trigger fires, it doesn't tell you WHICH
column changed. Use `GetItemChanges` to get per-column change flags, then gate
downstream logic on specific columns:
```json
"Get_Changes": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "GetItemChanges"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "<list-guid>",
"id": "@triggerBody()?['ID']",
"since": "@triggerBody()?['Modified']",
"includeDrafts": false
}
}
}
```
Gate on a specific column:
```json
"expression": {
"and": [{
"equals": [
"@body('Get_Changes')?['Column']?['hasChanged']",
true
]
}]
}
```
> **New-item detection:** On the very first modification (version 1.0),
> `GetItemChanges` may report no prior version. Check
> `@equals(triggerBody()?['OData__UIVersionString'], '1.0')` to detect
> newly created items and skip change-gate logic for those.
---
### SharePoint — REST MERGE via HttpRequest
For cross-list updates or advanced operations not supported by the standard
Update Item connector (e.g., updating a list in a different site), use the
SharePoint REST API via the `HttpRequest` operation:
```json
"Update_Cross_List_Item": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "HttpRequest"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/target-site",
"parameters/method": "POST",
"parameters/uri": "/_api/web/lists(guid'<list-guid>')/items(@{variables('ItemId')})",
"parameters/headers": {
"Accept": "application/json;odata=nometadata",
"Content-Type": "application/json;odata=nometadata",
"X-HTTP-Method": "MERGE",
"IF-MATCH": "*"
},
"parameters/body": "{ \"Title\": \"@{variables('NewTitle')}\", \"Status\": \"@{variables('NewStatus')}\" }"
}
}
}
```
> **Key headers:**
> - `X-HTTP-Method: MERGE` — tells SharePoint to do a partial update (PATCH semantics)
> - `IF-MATCH: *` — overwrites regardless of current ETag (no conflict check)
>
> The `HttpRequest` operation reuses the existing SharePoint connection — no extra
> authentication needed. Use this when the standard Update Item connector can't
> reach the target list (different site collection, or you need raw REST control).
> Keep the connector-specific parameter names exactly as shown:
> `parameters/method`, `parameters/uri`, `parameters/headers`, and
> `parameters/body`. The body is a JSON string, and `parameters/uri` is relative
> to the SharePoint `dataset`.
---
### SharePoint — File as JSON Database (Read + Parse)
Use a SharePoint document library JSON file as a queryable "database" of
last-known-state records. A separate process (e.g., Power BI dataflow) maintains
the file; the flow downloads and filters it for before/after comparisons.
```json
"Get_File": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "GetFileContent"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"id": "%252fShared%2bDocuments%252fdata.json",
"inferContentType": false
}
}
},
"Parse_JSON_File": {
"type": "Compose",
"runAfter": { "Get_File": ["Succeeded"] },
"inputs": "@json(decodeBase64(body('Get_File')?['$content']))"
},
"Find_Record": {
"type": "Query",
"runAfter": { "Parse_JSON_File": ["Succeeded"] },
"inputs": {
"from": "@outputs('Parse_JSON_File')",
"where": "@equals(item()?['id'], variables('RecordId'))"
}
}
```
> **Decode chain:** `GetFileContent` returns base64-encoded content in
> `body(...)?['$content']`. Apply `decodeBase64()` then `json()` to get a
> usable array. `Filter Array` then acts as a WHERE clause.
>
> **When to use:** When you need a lightweight "before" snapshot to detect field
> changes from a webhook payload (the "after" state). Simpler than maintaining
> a full SharePoint list mirror — works well for up to ~10K records.
>
> **File path encoding:** In the `id` parameter, SharePoint URL-encodes paths
> twice. Spaces become `%2b` (plus sign), slashes become `%252f`.
---
## Excel Online
### Excel — Run Office Script
Office Script actions require real workbook and script identifiers at save time.
Do not deploy placeholder `scriptId` values; `update_live_flow` can fail during
dynamic operation validation even before a test run exists.
Use `describe_live_connector` or `get_live_dynamic_options` when available, or
ask the user for the workbook and script if they are not discoverable. If a real
`scriptId` still cannot be resolved, ask the user to add the Run script action
once in the designer, then read the flow definition and preserve the resolved
parameters.
---
## Outlook
### Outlook — Send Email
```json
"Send_Email": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_office365",
"connectionName": "<connectionName>",
"operationId": "SendEmailV2"
},
"parameters": {
"emailMessage/To": "recipient@contoso.com",
"emailMessage/Subject": "Automated notification",
"emailMessage/Body": "<p>@{outputs('Compose_Message')}</p>",
"emailMessage/IsHtml": true
}
}
}
```
---
### Outlook — Get Emails (Read Template from Folder)
```json
"Get_Email_Template": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_office365",
"connectionName": "<connectionName>",
"operationId": "GetEmailsV3"
},
"parameters": {
"folderPath": "Id::<outlook-folder-id>",
"fetchOnlyUnread": false,
"includeAttachments": false,
"top": 1,
"importance": "Any",
"fetchOnlyWithAttachment": false,
"subjectFilter": "My Email Template Subject"
}
}
}
```
Access subject and body:
```
@first(outputs('Get_Email_Template')?['body/value'])?['subject']
@first(outputs('Get_Email_Template')?['body/value'])?['body']
```
> **Outlook-as-CMS pattern**: store a template email in a dedicated Outlook folder.
> Set `fetchOnlyUnread: false` so the template persists after first use.
> Non-technical users can update subject and body by editing that email —
> no flow changes required. Pass subject and body directly into `SendEmailV2`.
>
> To get a folder ID: in Outlook on the web, right-click the folder → open in
> new tab — the folder GUID is in the URL. Prefix it with `Id::` in `folderPath`.
---
## Teams
### Teams — Post Message
```json
"Post_Teams_Message": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_teams",
"connectionName": "<connectionName>",
"operationId": "PostMessageToConversation"
},
"parameters": {
"poster": "Flow bot",
"location": "Channel",
"body/recipient": {
"groupId": "<team-id>",
"channelId": "<channel-id>"
},
"body/messageBody": "@outputs('Compose_Message')"
}
}
}
```
#### Variant: Group Chat (1:1 or Multi-Person)
To post to a group chat instead of a channel, use `"location": "Group chat"` with
a thread ID as the recipient:
```json
"Post_To_Group_Chat": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_teams",
"connectionName": "<connectionName>",
"operationId": "PostMessageToConversation"
},
"parameters": {
"poster": "Flow bot",
"location": "Group chat",
"body/recipient": "19:<thread-hash>@thread.v2",
"body/messageBody": "@outputs('Compose_Message')"
}
}
}
```
For 1:1 ("Chat with Flow bot"), use `"location": "Chat with Flow bot"` and set
`body/recipient` to the user's email address.
> **Active-user gate:** When sending notifications in a loop, check the recipient's
> Azure AD account is enabled before posting — avoids failed deliveries to departed
> staff:
> ```json
> "Check_User_Active": {
> "type": "OpenApiConnection",
> "inputs": {
> "host": { "apiId": "/providers/Microsoft.PowerApps/apis/shared_office365users",
> "operationId": "UserProfile_V2" },
> "parameters": { "id": "@{item()?['Email']}" }
> }
> }
> ```
> Then gate: `@equals(body('Check_User_Active')?['accountEnabled'], true)`
---
## Copilot Studio
### Copilot Studio — Invoke Agent
When using the Copilot Studio connector, publish the agent before running the
flow. Draft/test agents can exist in the studio canvas but still be unavailable
or stale through the flow connector endpoint.
If a connector action fails with an unavailable-agent or endpoint-style error,
publish the agent, wait briefly for propagation, then resubmit the same flow run
before changing the flow definition.
---
## Approvals
### Split Approval (Create → Wait)
The standard "Start and wait for an approval" is a single blocking action.
For more control (e.g., posting the approval link in Teams, or adding a timeout
scope), split it into two actions: `CreateAnApproval` (fire-and-forget) then
`WaitForAnApproval` (webhook pause).
```json
"Create_Approval": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_approvals",
"connectionName": "<connectionName>",
"operationId": "CreateAnApproval"
},
"parameters": {
"approvalType": "CustomResponse/Result",
"ApprovalCreationInput/title": "Review: @{variables('ItemTitle')}",
"ApprovalCreationInput/assignedTo": "approver@contoso.com",
"ApprovalCreationInput/details": "Please review and select an option.",
"ApprovalCreationInput/responseOptions": ["Approve", "Reject", "Defer"],
"ApprovalCreationInput/enableNotifications": true,
"ApprovalCreationInput/enableReassignment": true
}
}
},
"Wait_For_Approval": {
"type": "OpenApiConnectionWebhook",
"runAfter": { "Create_Approval": ["Succeeded"] },
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_approvals",
"connectionName": "<connectionName>",
"operationId": "WaitForAnApproval"
},
"parameters": {
"approvalName": "@body('Create_Approval')?['name']"
}
}
}
```
> **`approvalType` options:**
> - `"Approve/Reject - First to respond"` — binary, first responder wins
> - `"Approve/Reject - Everyone must approve"` — requires all assignees
> - `"CustomResponse/Result"` — define your own response buttons
>
> After `Wait_For_Approval`, read the outcome:
> ```
> @body('Wait_For_Approval')?['outcome'] → "Approve", "Reject", or custom
> @body('Wait_For_Approval')?['responses'][0]?['responder']?['displayName']
> @body('Wait_For_Approval')?['responses'][0]?['comments']
> ```
>
> The split pattern lets you insert actions between create and wait — e.g.,
> posting the approval link to Teams, starting a timeout scope, or logging
> the pending approval to a tracking list.
@@ -0,0 +1,572 @@
# FlowStudio MCP — Action Patterns: Core
Variables, control flow, and expression patterns for Power Automate flow definitions.
> All examples assume `"runAfter"` is set appropriately.
> Replace `<connectionName>` with the **key** you used in your `connectionReferences` map
> (e.g. `shared_teams`, `shared_office365`) — NOT the connection GUID.
---
## Data & Variables
### Compose (Store a Value)
```json
"Compose_My_Value": {
"type": "Compose",
"runAfter": {},
"inputs": "@variables('myVar')"
}
```
Reference: `@outputs('Compose_My_Value')`
---
### Initialize Variable
```json
"Init_Counter": {
"type": "InitializeVariable",
"runAfter": {},
"inputs": {
"variables": [{
"name": "counter",
"type": "Integer",
"value": 0
}]
}
}
```
Types: `"Integer"`, `"Float"`, `"Boolean"`, `"String"`, `"Array"`, `"Object"`
---
### Set Variable
```json
"Set_Counter": {
"type": "SetVariable",
"runAfter": {},
"inputs": {
"name": "counter",
"value": "@add(variables('counter'), 1)"
}
}
```
---
### Append to Array Variable
```json
"Collect_Item": {
"type": "AppendToArrayVariable",
"runAfter": {},
"inputs": {
"name": "resultArray",
"value": "@item()"
}
}
```
---
### Increment Variable
```json
"Increment_Counter": {
"type": "IncrementVariable",
"runAfter": {},
"inputs": {
"name": "counter",
"value": 1
}
}
```
> Use `IncrementVariable` (not `SetVariable` with `add()`) for counters inside loops —
> it is atomic and avoids expression errors when the variable is used elsewhere in the
> same iteration. `value` can be any integer or expression, e.g. `@mul(item()?['Interval'], 60)`
> to advance a Unix timestamp cursor by N minutes.
---
## Control Flow
### Condition (If/Else)
```json
"Check_Status": {
"type": "If",
"runAfter": {},
"expression": {
"and": [{ "equals": ["@item()?['Status']", "Active"] }]
},
"actions": {
"Handle_Active": {
"type": "Compose",
"runAfter": {},
"inputs": "Active user: @{item()?['Name']}"
}
},
"else": {
"actions": {
"Handle_Inactive": {
"type": "Compose",
"runAfter": {},
"inputs": "Inactive user"
}
}
}
}
```
Comparison operators: `equals`, `not`, `greater`, `greaterOrEquals`, `less`, `lessOrEquals`, `contains`
Logical: `and: [...]`, `or: [...]`
---
### Switch
```json
"Route_By_Type": {
"type": "Switch",
"runAfter": {},
"expression": "@triggerBody()?['type']",
"cases": {
"Case_Email": {
"case": "email",
"actions": { "Process_Email": { "type": "Compose", "runAfter": {}, "inputs": "email" } }
},
"Case_Teams": {
"case": "teams",
"actions": { "Process_Teams": { "type": "Compose", "runAfter": {}, "inputs": "teams" } }
}
},
"default": {
"actions": { "Unknown_Type": { "type": "Compose", "runAfter": {}, "inputs": "unknown" } }
}
}
```
---
### Scope (Grouping / Try-Catch)
Wrap related actions in a Scope to give them a shared name, collapse them in the
designer, and — most importantly — handle their errors as a unit.
```json
"Scope_Get_Customer": {
"type": "Scope",
"runAfter": {},
"actions": {
"HTTP_Get_Customer": {
"type": "Http",
"runAfter": {},
"inputs": {
"method": "GET",
"uri": "https://api.example.com/customers/@{variables('customerId')}"
}
},
"Compose_Email": {
"type": "Compose",
"runAfter": { "HTTP_Get_Customer": ["Succeeded"] },
"inputs": "@outputs('HTTP_Get_Customer')?['body/email']"
}
}
},
"Handle_Scope_Error": {
"type": "Compose",
"runAfter": { "Scope_Get_Customer": ["Failed", "TimedOut"] },
"inputs": "Scope failed: @{result('Scope_Get_Customer')?[0]?['error']?['message']}"
}
```
> Reference scope results: `@result('Scope_Get_Customer')` returns an array of action
> outcomes. Use `runAfter: {"MyScope": ["Failed", "TimedOut"]}` on a follow-up action
> to create try/catch semantics without a Terminate.
---
### Foreach (Sequential)
```json
"Process_Each_Item": {
"type": "Foreach",
"runAfter": {},
"foreach": "@outputs('Get_Items')?['body/value']",
"operationOptions": "Sequential",
"actions": {
"Handle_Item": {
"type": "Compose",
"runAfter": {},
"inputs": "@item()?['Title']"
}
}
}
```
> Always include `"operationOptions": "Sequential"` unless parallel is intentional.
---
### Foreach (Parallel with Concurrency Limit)
```json
"Process_Each_Item_Parallel": {
"type": "Foreach",
"runAfter": {},
"foreach": "@body('Get_SP_Items')?['value']",
"runtimeConfiguration": {
"concurrency": {
"repetitions": 20
}
},
"actions": {
"HTTP_Upsert": {
"type": "Http",
"runAfter": {},
"inputs": {
"method": "POST",
"uri": "https://api.example.com/contacts/@{item()?['Email']}"
}
}
}
}
```
> Set `repetitions` to control how many items are processed simultaneously.
> Practical values: `510` for external API calls (respect rate limits),
> `2050` for internal/fast operations.
> Omit `runtimeConfiguration.concurrency` entirely for the platform default
> (currently 50). Do NOT use `"operationOptions": "Sequential"` and concurrency together.
---
### Wait (Delay)
```json
"Delay_10_Minutes": {
"type": "Wait",
"runAfter": {},
"inputs": {
"interval": {
"count": 10,
"unit": "Minute"
}
}
}
```
Valid `unit` values: `"Second"`, `"Minute"`, `"Hour"`, `"Day"`
> Use a Delay + re-fetch as a deduplication guard: wait for any competing process
> to complete, then re-read the record before acting. This avoids double-processing
> when multiple triggers or manual edits can race on the same item.
---
### Terminate (Success or Failure)
```json
"Terminate_Success": {
"type": "Terminate",
"runAfter": {},
"inputs": {
"runStatus": "Succeeded"
}
},
"Terminate_Failure": {
"type": "Terminate",
"runAfter": { "Risky_Action": ["Failed"] },
"inputs": {
"runStatus": "Failed",
"runError": {
"code": "StepFailed",
"message": "@{outputs('Get_Error_Message')}"
}
}
}
```
---
### Do Until (Loop Until Condition)
Repeats a block of actions until an exit condition becomes true.
Use when the number of iterations is not known upfront (e.g. paginating an API,
walking a time range, polling until a status changes).
```json
"Do_Until_Done": {
"type": "Until",
"runAfter": {},
"expression": "@greaterOrEquals(variables('cursor'), variables('endValue'))",
"limit": {
"count": 5000,
"timeout": "PT5H"
},
"actions": {
"Do_Work": {
"type": "Compose",
"runAfter": {},
"inputs": "@variables('cursor')"
},
"Advance_Cursor": {
"type": "IncrementVariable",
"runAfter": { "Do_Work": ["Succeeded"] },
"inputs": {
"name": "cursor",
"value": 1
}
}
}
}
```
> Always set `limit.count` and `limit.timeout` explicitly — the platform defaults are
> low (60 iterations, 1 hour). For time-range walkers use `limit.count: 5000` and
> `limit.timeout: "PT5H"` (ISO 8601 duration).
>
> The exit condition is evaluated **before** each iteration. Initialise your cursor
> variable before the loop so the condition can evaluate correctly on the first pass.
---
### Agent Retry Loop
When a flow calls an AI or Copilot-style agent until it reaches a terminal
outcome, keep the loop state explicit:
- Initialize variables such as `agentStatus`, `attempt`, and `finalPayload`
before the `Until`.
- Inside the loop, call the agent, validate the response, update the status, and
delay/retry only when the status is non-terminal.
- Put final dispatch actions such as email, SharePoint update, or Teams post
after the loop so retries do not duplicate side effects.
- If the platform rejects a complex `Switch` nested inside `Until`, keep the
loop body to simple validation and state updates, then route with `Switch`
after the loop.
---
### Async Polling with RequestId Correlation
When an API starts a long-running job asynchronously (e.g. Power BI dataset refresh,
report generation, batch export), the trigger call returns a request ID. Capture it
from the **response header**, then poll a status endpoint filtering by that exact ID:
```json
"Start_Job": {
"type": "Http",
"inputs": { "method": "POST", "uri": "https://api.example.com/jobs" }
},
"Capture_Request_ID": {
"type": "Compose",
"runAfter": { "Start_Job": ["Succeeded"] },
"inputs": "@outputs('Start_Job')?['headers/X-Request-Id']"
},
"Initialize_Status": {
"type": "InitializeVariable",
"inputs": { "variables": [{ "name": "jobStatus", "type": "String", "value": "Running" }] }
},
"Poll_Until_Done": {
"type": "Until",
"expression": "@not(equals(variables('jobStatus'), 'Running'))",
"limit": { "count": 60, "timeout": "PT30M" },
"actions": {
"Delay": { "type": "Wait", "inputs": { "interval": { "count": 20, "unit": "Second" } } },
"Get_History": {
"type": "Http",
"runAfter": { "Delay": ["Succeeded"] },
"inputs": { "method": "GET", "uri": "https://api.example.com/jobs/history" }
},
"Filter_This_Job": {
"type": "Query",
"runAfter": { "Get_History": ["Succeeded"] },
"inputs": {
"from": "@outputs('Get_History')?['body/items']",
"where": "@equals(item()?['requestId'], outputs('Capture_Request_ID'))"
}
},
"Set_Status": {
"type": "SetVariable",
"runAfter": { "Filter_This_Job": ["Succeeded"] },
"inputs": {
"name": "jobStatus",
"value": "@first(body('Filter_This_Job'))?['status']"
}
}
}
},
"Handle_Failure": {
"type": "If",
"runAfter": { "Poll_Until_Done": ["Succeeded"] },
"expression": { "equals": ["@variables('jobStatus')", "Failed"] },
"actions": { "Terminate_Failed": { "type": "Terminate", "inputs": { "runStatus": "Failed" } } },
"else": { "actions": {} }
}
```
Access response headers: `@outputs('Start_Job')?['headers/X-Request-Id']`
> **Status variable initialisation**: set a sentinel value (`"Running"`, `"Unknown"`) before
> the loop. The exit condition tests for any value other than the sentinel.
> This way an empty poll result (job not yet in history) leaves the variable unchanged
> and the loop continues — it doesn't accidentally exit on null.
>
> **Filter before extracting**: always `Filter Array` the history to your specific
> request ID before calling `first()`. History endpoints return all jobs; without
> filtering, status from a different concurrent job can corrupt your poll.
---
### runAfter Fallback (Failed → Alternative Action)
Route to a fallback action when a primary action fails — without a Condition block.
Simply set `runAfter` on the fallback to accept `["Failed"]` from the primary:
```json
"HTTP_Get_Hi_Res": {
"type": "Http",
"runAfter": {},
"inputs": { "method": "GET", "uri": "https://api.example.com/data?resolution=hi-res" }
},
"HTTP_Get_Low_Res": {
"type": "Http",
"runAfter": { "HTTP_Get_Hi_Res": ["Failed"] },
"inputs": { "method": "GET", "uri": "https://api.example.com/data?resolution=low-res" }
}
```
> Actions that follow can use `runAfter` accepting both `["Succeeded", "Skipped"]` to
> handle either path — see **Fan-In Join Gate** below.
---
### Fan-In Join Gate (Merge Two Mutually Exclusive Branches)
When two branches are mutually exclusive (only one can succeed per run), use a single
downstream action that accepts `["Succeeded", "Skipped"]` from **both** branches.
The gate fires exactly once regardless of which branch ran:
```json
"Increment_Count": {
"type": "IncrementVariable",
"runAfter": {
"Update_Hi_Res_Metadata": ["Succeeded", "Skipped"],
"Update_Low_Res_Metadata": ["Succeeded", "Skipped"]
},
"inputs": { "name": "LoopCount", "value": 1 }
}
```
> This avoids duplicating the downstream action in each branch. The key insight:
> whichever branch was skipped reports `Skipped` — the gate accepts that state and
> fires once. Only works cleanly when the two branches are truly mutually exclusive
> (e.g. one is `runAfter: [...Failed]` of the other).
---
## Expressions
### Common Expression Patterns
```
Null-safe field access: @item()?['FieldName']
Null guard: @coalesce(item()?['Name'], 'Unknown')
String format: @{variables('firstName')} @{variables('lastName')}
Date today: @utcNow()
Formatted date: @formatDateTime(utcNow(), 'dd/MM/yyyy')
Add days: @addDays(utcNow(), 7)
Array length: @length(variables('myArray'))
Filter array: Use the "Filter array" action (no inline filter expression exists in PA)
Union (new wins): @union(body('New_Data'), outputs('Old_Data'))
Sort: @sort(variables('myArray'), 'Date')
Unix timestamp → date: @formatDateTime(addseconds('1970-1-1', triggerBody()?['created']), 'yyyy-MM-dd')
Date → Unix milliseconds: @div(sub(ticks(startOfDay(item()?['Created'])), ticks(formatDateTime('1970-01-01Z','o'))), 10000)
Date → Unix seconds: @div(sub(ticks(item()?['Start']), ticks('1970-01-01T00:00:00Z')), 10000000)
Unix seconds → datetime: @addSeconds('1970-01-01T00:00:00Z', int(variables('Unix')))
Coalesce as no-else: @coalesce(outputs('Optional_Step'), outputs('Default_Step'))
Flow elapsed minutes: @div(float(sub(ticks(utcNow()), ticks(outputs('Flow_Start')))), 600000000)
HH:mm time string: @formatDateTime(outputs('Local_Datetime'), 'HH:mm')
Response header: @outputs('HTTP_Action')?['headers/X-Request-Id']
Array max (by field): @reverse(sort(body('Select_Items'), 'Date'))[0]
Integer day span: @int(split(dateDifference(outputs('Start'), outputs('End')), '.')[0])
ISO week number: @div(add(dayofyear(addDays(subtractFromTime(date, sub(dayofweek(date),1), 'Day'), 3)), 6), 7)
Join errors to string: @if(equals(length(variables('Errors')),0), null, concat(join(variables('Errors'),', '),' not found.'))
Normalize before compare: @replace(coalesce(outputs('Value'),''),'_',' ')
Robust non-empty check: @greater(length(trim(coalesce(string(outputs('Val')), ''))), 0)
```
### Unsupported / Risky Expression Assumptions
Power Automate expressions are Workflow Definition Language, not JavaScript.
These patterns often look plausible but do not deploy or do not behave as agents
expect:
| Goal | Avoid | Use instead |
|---|---|---|
| Build an object inline | `createObject(...)` | A Compose action with a JSON object literal |
| Transform an array inline | `select(...)` inside an expression | Data Operations `Select` action |
| Filter an array inline | `filter(...)` inside an expression | Data Operations `Filter array` action |
| Find an array item index | `indexOf(array, item)` | Foreach with a counter variable, or build a keyed object map |
### Newlines in Expressions
> **`\n` does NOT produce a newline inside Power Automate expressions.** It is
> treated as a literal backslash + `n` and will either appear verbatim or cause
> a validation error.
Use `decodeUriComponent('%0a')` wherever you need a newline character:
```
Newline (LF): decodeUriComponent('%0a')
CRLF: decodeUriComponent('%0d%0a')
```
Example — multi-line Teams or email body via `concat()`:
```json
"Compose_Message": {
"type": "Compose",
"inputs": "@concat('Hi ', outputs('Get_User')?['body/displayName'], ',', decodeUriComponent('%0a%0a'), 'Your report is ready.', decodeUriComponent('%0a'), '- The Team')"
}
```
Example — `join()` with newline separator:
```json
"Compose_List": {
"type": "Compose",
"inputs": "@join(body('Select_Names'), decodeUriComponent('%0a'))"
}
```
> This is the only reliable way to embed newlines in dynamically built strings
> in Power Automate flow definitions (confirmed against Logic Apps runtime).
---
### Sum an array (XPath trick)
Power Automate has no native `sum()` function. Use XPath on XML instead:
```json
"Prepare_For_Sum": {
"type": "Compose",
"runAfter": {},
"inputs": { "root": { "numbers": "@body('Select_Amounts')" } }
},
"Sum": {
"type": "Compose",
"runAfter": { "Prepare_For_Sum": ["Succeeded"] },
"inputs": "@xpath(xml(outputs('Prepare_For_Sum')), 'sum(/root/numbers)')"
}
```
`Select_Amounts` must output a flat array of numbers (use a **Select** action to extract a single numeric field first). The result is a number you can use directly in conditions or calculations.
> This is the only way to aggregate (sum/min/max) an array without a loop in Power Automate.
@@ -0,0 +1,595 @@
# FlowStudio MCP — Action Patterns: Data Transforms
Array operations, HTTP calls, parsing, and data transformation patterns.
> All examples assume `"runAfter"` is set appropriately.
> `<connectionName>` is the **key** in `connectionReferences` (e.g. `shared_sharepointonline`), not the GUID.
> The GUID goes in the map value's `connectionName` property.
---
## Array Operations
### Select (Reshape / Project an Array)
Transforms each item in an array, keeping only the columns you need or renaming them.
Avoids carrying large objects through the rest of the flow.
```json
"Select_Needed_Columns": {
"type": "Select",
"runAfter": {},
"inputs": {
"from": "@outputs('HTTP_Get_Subscriptions')?['body/data']",
"select": {
"id": "@item()?['id']",
"status": "@item()?['status']",
"trial_end": "@item()?['trial_end']",
"cancel_at": "@item()?['cancel_at']",
"interval": "@item()?['plan']?['interval']"
}
}
}
```
Result reference: `@body('Select_Needed_Columns')` — returns a direct array of reshaped objects.
> Use Select before looping or filtering to reduce payload size and simplify
> downstream expressions. Works on any array — SP results, HTTP responses, variables.
>
> **Tips:**
> - **Single-to-array coercion:** When an API returns a single object but you need
> Select (which requires an array), wrap it: `@array(body('Get_Employee')?['data'])`.
> The output is a 1-element array — access results via `?[0]?['field']`.
> - **Null-normalize optional fields:** Use `@if(empty(item()?['field']), null, item()?['field'])`
> on every optional field to normalize empty strings, missing properties, and empty
> objects to explicit `null`. Ensures consistent downstream `@equals(..., @null)` checks.
> - **Flatten nested objects:** Project nested properties into flat fields:
> ```
> "manager_name": "@if(empty(item()?['manager']?['name']), null, item()?['manager']?['name'])"
> ```
> This enables direct field-level comparison with a flat schema from another source.
---
### Filter Array (Query)
Filters an array to items matching a condition. Use the action form (not the `filter()`
expression) for complex multi-condition logic — it's clearer and easier to maintain.
```json
"Filter_Active_Subscriptions": {
"type": "Query",
"runAfter": {},
"inputs": {
"from": "@body('Select_Needed_Columns')",
"where": "@and(or(equals(item().status, 'trialing'), equals(item().status, 'active')), equals(item().cancel_at, null))"
}
}
```
Result reference: `@body('Filter_Active_Subscriptions')` — direct filtered array.
> Tip: run multiple Filter Array actions on the same source array to create
> named buckets (e.g. active, being-canceled, fully-canceled), then use
> `coalesce(first(body('Filter_A')), first(body('Filter_B')), ...)` to pick
> the highest-priority match without any loops.
---
### Create CSV Table (Array → CSV String)
Converts an array of objects into a CSV-formatted string — no connector call, no code.
Use after a `Select` or `Filter Array` to export data or pass it to a file-write action.
```json
"Create_CSV": {
"type": "Table",
"runAfter": {},
"inputs": {
"from": "@body('Select_Output_Columns')",
"format": "CSV"
}
}
```
Result reference: `@body('Create_CSV')` — a plain string with header row + data rows.
```json
// Custom column order / renamed headers:
"Create_CSV_Custom": {
"type": "Table",
"inputs": {
"from": "@body('Select_Output_Columns')",
"format": "CSV",
"columns": [
{ "header": "Date", "value": "@item()?['transactionDate']" },
{ "header": "Amount", "value": "@item()?['amount']" },
{ "header": "Description", "value": "@item()?['description']" }
]
}
}
```
> Without `columns`, headers are taken from the object property names in the source array.
> With `columns`, you control header names and column order explicitly.
>
> The output is a raw string. Write it to a file with `CreateFile` or `UpdateFile`
> (set `body` to `@body('Create_CSV')`), or store in a variable with `SetVariable`.
>
> If source data came from Power BI's `ExecuteDatasetQuery`, column names will be
> wrapped in square brackets (e.g. `[Amount]`). Strip them before writing:
> `@replace(replace(body('Create_CSV'),'[',''),']','')`
---
### range() + Select for Array Generation
`range(0, N)` produces an integer sequence `[0, 1, 2, …, N-1]`. Pipe it through
a Select action to generate date series, index grids, or any computed array
without a loop:
```json
// Generate 14 consecutive dates starting from a base date
"Generate_Date_Series": {
"type": "Select",
"inputs": {
"from": "@range(0, 14)",
"select": "@addDays(outputs('Base_Date'), item(), 'yyyy-MM-dd')"
}
}
```
Result: `@body('Generate_Date_Series')``["2025-01-06", "2025-01-07", …, "2025-01-19"]`
For Cartesian products, iterate `range(0, mul(rowCount, colCount))` and derive
indexes with `div(item(), colCount)` and `mod(item(), colCount)`.
---
### Dynamic Dictionary via json(concat(join()))
When you need O(1) key→value lookups at runtime and Power Automate has no native
dictionary type, build one from an array using Select + join + json:
```json
"Build_Key_Value_Pairs": {
"type": "Select",
"inputs": {
"from": "@body('Get_Lookup_Items')?['value']",
"select": "@concat('\"', item()?['Key'], '\":\"', item()?['Value'], '\"')"
}
},
"Assemble_Dictionary": {
"type": "Compose",
"inputs": "@json(concat('{', join(body('Build_Key_Value_Pairs'), ','), '}'))"
}
```
Lookup: `@outputs('Assemble_Dictionary')?['myKey']`
> The `json(concat('{', join(...), '}'))` pattern works for string values. For numeric
> or boolean values, omit the inner escaped quotes around the value portion.
> Keys must be unique — duplicate keys silently overwrite earlier ones.
> This replaces deeply nested `if(equals(key,'A'),'X', if(equals(key,'B'),'Y', ...))` chains.
---
### union() for Changed-Field Detection
When you need to find records where *any* of several fields has changed, run one
`Filter Array` per field and `union()` the results. This avoids a complex
multi-condition filter and produces a clean deduplicated set:
```json
"Filter_Name_Changed": {
"type": "Query",
"inputs": { "from": "@body('Existing_Records')",
"where": "@not(equals(item()?['name'], item()?['dest_name']))" }
},
"Filter_Status_Changed": {
"type": "Query",
"inputs": { "from": "@body('Existing_Records')",
"where": "@not(equals(item()?['status'], item()?['dest_status']))" }
},
"All_Changed": {
"type": "Compose",
"inputs": "@union(body('Filter_Name_Changed'), body('Filter_Status_Changed'))"
}
```
Reference: `@outputs('All_Changed')` — deduplicated array of rows where anything changed.
> `union()` deduplicates by object identity, so a row that changed in both fields
> appears once. Add more `Filter_*_Changed` inputs to `union()` as needed:
> `@union(body('F1'), body('F2'), body('F3'))`
---
### File-Content Change Gate
Before running expensive processing on a file or blob, compare its current content
to a stored baseline. Skip entirely if nothing has changed — makes sync flows
idempotent and safe to re-run or schedule aggressively.
```json
"Get_File_From_Source": { ... },
"Get_Stored_Baseline": { ... },
"Condition_File_Changed": {
"type": "If",
"expression": {
"not": {
"equals": [
"@base64(body('Get_File_From_Source'))",
"@body('Get_Stored_Baseline')"
]
}
},
"actions": {
"Update_Baseline": { "...": "overwrite stored copy with new content" },
"Process_File": { "...": "all expensive work goes here" }
},
"else": { "actions": {} }
}
```
> Store the baseline as a file in SharePoint or blob storage — `base64()`-encode the
> live content before comparing so binary and text files are handled uniformly.
> Write the new baseline **before** processing so a re-run after a partial failure
> does not re-process the same file again.
---
### Set-Join for Sync (Update Detection without Nested Loops)
When syncing a source collection into a destination (e.g. API response → SharePoint list,
CSV → database), avoid nested `Apply to each` loops to find changed records.
Instead, **project flat key arrays** and use `contains()` to perform set operations —
zero nested loops, and the final loop only touches changed items.
**Insert/update/delete sync recipe:**
1. `Select_Dest_Keys` from destination rows.
2. `Filter_To_Insert`: source rows whose key is not in destination keys.
3. `Filter_Already_Exists`: source rows whose key is in destination keys.
4. For each compared field, run `Filter_<Field>_Changed`; combine them with
`union()` into `Union_Changed`.
5. `Select_Changed_Keys` from `Union_Changed`, then filter destination rows to
only those keys before updating.
6. `Select_Source_Keys`, then `Filter_To_Delete` destination rows whose key is
not in source keys.
This changes O(n x m) nested loops to O(n + m) set operations and helps avoid
Power Automate's 100k-action run limit.
---
### First-or-Null Single-Row Lookup
Use `first()` on the result array to extract one record without a loop.
Then null-check the output to guard downstream actions.
```json
"Get_First_Match": {
"type": "Compose",
"runAfter": { "Get_SP_Items": ["Succeeded"] },
"inputs": "@first(outputs('Get_SP_Items')?['body/value'])"
}
```
In a Condition, test for no-match with the **`@null` literal** (not `empty()`):
```json
"Condition": {
"type": "If",
"expression": {
"not": {
"equals": [
"@outputs('Get_First_Match')",
"@null"
]
}
}
}
```
Access fields on the matched row: `@outputs('Get_First_Match')?['FieldName']`
> Use this instead of `Apply to each` when you only need one matching record.
> `first()` on an empty array returns `null`; `empty()` is for arrays/strings,
> not scalars — using it on a `first()` result causes a runtime error.
---
## HTTP & Parsing
### HTTP Action (External API)
```json
"Call_External_API": {
"type": "Http",
"runAfter": {},
"inputs": {
"method": "POST",
"uri": "https://api.example.com/endpoint",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer @{variables('apiToken')}"
},
"body": {
"data": "@outputs('Compose_Payload')"
},
"retryPolicy": {
"type": "Fixed",
"count": 3,
"interval": "PT10S"
}
}
}
```
Response reference: `@outputs('Call_External_API')?['body']`
#### Variant: ActiveDirectoryOAuth (Service-to-Service)
For calling APIs that require Azure AD client-credentials (e.g., Microsoft Graph),
use in-line OAuth instead of a Bearer token variable:
```json
"Call_Graph_API": {
"type": "Http",
"runAfter": {},
"inputs": {
"method": "GET",
"uri": "https://graph.microsoft.com/v1.0/users?$search=\"employeeId:@{variables('Code')}\"&$select=id,displayName",
"headers": {
"Content-Type": "application/json",
"ConsistencyLevel": "eventual"
},
"authentication": {
"type": "ActiveDirectoryOAuth",
"authority": "https://login.microsoftonline.com",
"tenant": "<tenant-id>",
"audience": "https://graph.microsoft.com",
"clientId": "<app-registration-id>",
"secret": "@parameters('graphClientSecret')"
}
}
}
```
> **When to use:** Calling Microsoft Graph, Azure Resource Manager, or any
> Azure AD-protected API from a flow without a premium connector.
>
> The `authentication` block handles the entire OAuth client-credentials flow
> transparently — no manual token acquisition step needed.
>
> `ConsistencyLevel: eventual` is required for Graph `$search` queries.
> Without it, `$search` returns 400.
>
> For PATCH/PUT writes, the same `authentication` block works — just change
> `method` and add a `body`.
>
> ⚠️ **Never hardcode `secret` inline.** Use `@parameters('graphClientSecret')`
> and declare it in the flow's `parameters` block (type `securestring`). This
> prevents the secret from appearing in run history or being readable via
> `get_live_flow`. Declare the parameter like:
> ```json
> "parameters": {
> "graphClientSecret": { "type": "securestring", "defaultValue": "" }
> }
> ```
> Then pass the real value via the flow's connections or environment variables
> — never commit it to source control.
---
### HTTP Response (Return to Caller)
Used in HTTP-triggered flows to send a structured reply back to the caller.
Must run before the flow times out (default 2 min for synchronous HTTP).
```json
"Response": {
"type": "Response",
"runAfter": {},
"inputs": {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"status": "success",
"message": "@{outputs('Compose_Result')}"
}
}
}
```
> **PowerApps / low-code caller pattern**: always return `statusCode: 200` with a
> `status` field in the body (`"success"` / `"error"`). PowerApps HTTP actions
> do not handle non-2xx responses gracefully — the caller should inspect
> `body.status` rather than the HTTP status code.
>
> Use multiple Response actions — one per branch — so each path returns
> an appropriate message. Only one will execute per run.
---
### Child Flow Call (Parent→Child via HTTP POST)
Power Automate supports parent→child orchestration by calling a child flow's
HTTP trigger URL directly. The parent sends an HTTP POST and blocks until the
child returns a `Response` action. The child flow uses a `manual` (Request) trigger.
```json
// PARENT — call child flow and wait for its response
"Call_Child_Flow": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://prod-XX.australiasoutheast.logic.azure.com:443/workflows/<workflowId>/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=<SAS>",
"headers": { "Content-Type": "application/json" },
"body": {
"ID": "@triggerBody()?['ID']",
"WeekEnd": "@triggerBody()?['WeekEnd']",
"Payload": "@variables('dataArray')"
},
"retryPolicy": { "type": "none" }
},
"operationOptions": "DisableAsyncPattern",
"runtimeConfiguration": {
"contentTransfer": { "transferMode": "Chunked" }
},
"limit": { "timeout": "PT2H" }
}
```
```json
// CHILD — manual trigger receives the JSON body
// (trigger definition)
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"ID": { "type": "string" },
"WeekEnd": { "type": "string" },
"Payload": { "type": "array" }
}
}
}
}
// CHILD — return result to parent
"Response_Success": {
"type": "Response",
"inputs": {
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"body": { "Result": "Success", "Count": "@length(variables('processed'))" }
}
}
```
> **`retryPolicy: none`** — critical on the parent's HTTP call. Without it, a child
> flow timeout triggers retries, spawning duplicate child runs.
>
> **`DisableAsyncPattern`** — prevents the parent from treating a 202 Accepted as
> completion. The parent will block until the child sends its `Response`.
>
> **`transferMode: Chunked`** — enable when passing large arrays (>100 KB) to the child;
> avoids request-size limits.
>
> **`limit.timeout: PT2H`** — raise the default 2-minute HTTP timeout for long-running
> children. Max is PT24H.
>
> The child flow's trigger URL contains a SAS token (`sig=...`) that authenticates
> the call. Copy it from the child flow's trigger properties panel. The URL changes
> if the trigger is deleted and re-created.
---
### Parse JSON
```json
"Parse_Response": {
"type": "ParseJson",
"runAfter": {},
"inputs": {
"content": "@outputs('Call_External_API')?['body']",
"schema": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"items": {
"type": "array",
"items": { "type": "object" }
}
}
}
}
}
```
Access parsed values: `@body('Parse_Response')?['name']`
---
### Manual CSV → JSON (No Premium Action)
Parse a raw CSV string into an array of objects using only built-in expressions.
Avoids the premium "Parse CSV" connector action.
```json
"Delimiter": { "type": "Compose", "inputs": "," },
"Strip_Quotes": { "type": "Compose", "inputs": "@replace(body('Get_File_Content'), '\"', '')" },
"Detect_Line_Ending": {
"type": "Compose",
"inputs": "@if(equals(indexOf(outputs('Strip_Quotes'), decodeUriComponent('%0D%0A')), -1), if(equals(indexOf(outputs('Strip_Quotes'), decodeUriComponent('%0A')), -1), decodeUriComponent('%0D'), decodeUriComponent('%0A')), decodeUriComponent('%0D%0A'))"
},
"Headers": {
"type": "Compose",
"inputs": "@split(first(split(outputs('Strip_Quotes'), outputs('Detect_Line_Ending'))), outputs('Delimiter'))"
},
"Data_Rows": { "type": "Compose", "inputs": "@skip(split(outputs('Strip_Quotes'), outputs('Detect_Line_Ending')), 1)" },
"Select_CSV_Body": {
"type": "Select",
"inputs": {
"from": "@outputs('Data_Rows')",
"select": {
"@{outputs('Headers')[0]}": "@split(item(), outputs('Delimiter'))[0]",
"@{outputs('Headers')[1]}": "@split(item(), outputs('Delimiter'))[1]",
"@{outputs('Headers')[2]}": "@split(item(), outputs('Delimiter'))[2]"
}
}
},
"Filter_Empty_Rows": {
"type": "Query",
"inputs": {
"from": "@body('Select_CSV_Body')",
"where": "@not(equals(item()?[outputs('Headers')[0]], null))"
}
}
```
Result: `@body('Filter_Empty_Rows')` — array of objects with header names as keys.
Notes: `Detect_Line_Ending` handles CRLF/LF/CR. Dynamic keys in `Select` require
`@{...}` interpolation. This simple pattern does not safely parse quoted fields
with embedded delimiters; for those, use a dedicated parser or custom action.
---
### ConvertTimeZone (Built-in, No Connector)
Converts a timestamp between timezones with no API call or connector licence cost.
Format string `"g"` produces short locale date+time (`M/d/yyyy h:mm tt`).
```json
"Convert_to_Local_Time": {
"type": "Expression",
"kind": "ConvertTimeZone",
"runAfter": {},
"inputs": {
"baseTime": "@{outputs('UTC_Timestamp')}",
"sourceTimeZone": "UTC",
"destinationTimeZone": "Taipei Standard Time",
"formatString": "g"
}
}
```
Result reference: `@body('Convert_to_Local_Time')`**not** `outputs()`, unlike most actions.
Common `formatString` values: `"g"` (short), `"f"` (full), `"yyyy-MM-dd"`, `"HH:mm"`
Common timezone strings: `"UTC"`, `"AUS Eastern Standard Time"`, `"Taipei Standard Time"`,
`"Singapore Standard Time"`, `"GMT Standard Time"`
> This is `type: Expression, kind: ConvertTimeZone` — a built-in Logic Apps action,
> not a connector. No connection reference needed. Reference the output via
> `body()` (not `outputs()`), otherwise the expression returns null.
@@ -0,0 +1,108 @@
# Common Build Patterns
Complete flow definition templates ready to copy and customize.
---
## Pattern: Recurrence + SharePoint list read + Teams notification
```json
{
"triggers": {
"Recurrence": {
"type": "Recurrence",
"recurrence": { "frequency": "Day", "interval": 1,
"startTime": "2026-01-01T08:00:00Z",
"timeZone": "AUS Eastern Standard Time" }
}
},
"actions": {
"Get_SP_Items": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "shared_sharepointonline",
"operationId": "GetItems"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList",
"$filter": "Status eq 'Active'",
"$top": 500
}
}
},
"Apply_To_Each": {
"type": "Foreach",
"runAfter": { "Get_SP_Items": ["Succeeded"] },
"foreach": "@outputs('Get_SP_Items')?['body/value']",
"actions": {
"Post_Teams_Message": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_teams",
"connectionName": "shared_teams",
"operationId": "PostMessageToConversation"
},
"parameters": {
"poster": "Flow bot",
"location": "Channel",
"body/recipient": {
"groupId": "<team-id>",
"channelId": "<channel-id>"
},
"body/messageBody": "Item: @{items('Apply_To_Each')?['Title']}"
}
}
}
},
"operationOptions": "Sequential"
}
}
}
```
---
## Pattern: HTTP trigger (webhook / Power App call)
```json
{
"triggers": {
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "number" }
}
}
}
}
},
"actions": {
"Compose_Response": {
"type": "Compose",
"runAfter": {},
"inputs": "Received: @{triggerBody()?['name']} = @{triggerBody()?['value']}"
},
"Response": {
"type": "Response",
"runAfter": { "Compose_Response": ["Succeeded"] },
"inputs": {
"statusCode": 200,
"body": { "status": "ok", "message": "@{outputs('Compose_Response')}" }
}
}
}
}
```
Access body values: `@triggerBody()?['name']`
@@ -0,0 +1,234 @@
# FlowStudio MCP — Flow Definition Schema
The full JSON structure expected by `update_live_flow` (and returned by `get_live_flow`).
---
## Top-Level Shape
```json
{
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"<TriggerName>": { ... }
},
"actions": {
"<ActionName>": { ... }
},
"outputs": {}
}
```
---
## `triggers`
Exactly one trigger per flow definition. The key name is arbitrary but
conventional names are used (e.g. `Recurrence`, `manual`, `When_a_new_email_arrives`).
See [trigger-types.md](trigger-types.md) for all trigger templates.
---
## `actions`
Dictionary of action definitions keyed by unique action name.
Key names may not contain spaces — use underscores.
Each action must include:
- `type` — action type identifier
- `runAfter` — map of upstream action names → status conditions array
- `inputs` — action-specific input configuration
See [action-patterns-core.md](action-patterns-core.md), [action-patterns-data.md](action-patterns-data.md),
and [action-patterns-connectors.md](action-patterns-connectors.md) for templates.
### Optional Action Properties
Beyond the required `type`, `runAfter`, and `inputs`, actions can include:
| Property | Purpose |
|---|---|
| `runtimeConfiguration` | Pagination, concurrency, secure data, chunked transfer |
| `operationOptions` | `"Sequential"` for Foreach, `"DisableAsyncPattern"` for HTTP |
| `limit` | Timeout override (e.g. `{"timeout": "PT2H"}`) |
| `metadata` | Designer metadata such as `operationMetadataId` |
#### Designer Metadata
For existing connector actions, preserve `metadata.operationMetadataId` when you
edit the definition. For new connector actions or Skills/HTTP response actions,
add a stable GUID and keep it stable across updates. Do not regenerate these IDs
on every deploy; the designer and some run-only surfaces use them to keep action
identity consistent.
#### `runtimeConfiguration` Variants
**Pagination** (SharePoint Get Items with large lists):
```json
"runtimeConfiguration": {
"paginationPolicy": {
"minimumItemCount": 5000
}
}
```
> Without this, Get Items silently caps at 256 results. Set `minimumItemCount`
> to the maximum rows you expect. Required for any SharePoint list over 256 items.
**Concurrency** (parallel Foreach):
```json
"runtimeConfiguration": {
"concurrency": {
"repetitions": 20
}
}
```
**Secure inputs/outputs** (mask values in run history):
```json
"runtimeConfiguration": {
"secureData": {
"properties": ["inputs", "outputs"]
}
}
```
> Use on actions that handle credentials, tokens, or PII. Masked values show
> as `"<redacted>"` in the flow run history UI and API responses.
**Chunked transfer** (large HTTP payloads):
```json
"runtimeConfiguration": {
"contentTransfer": {
"transferMode": "Chunked"
}
}
```
> Enable on HTTP actions sending or receiving bodies >100 KB (e.g. parent→child
> flow calls with large arrays).
---
## `runAfter` Rules
The first action in a branch has `"runAfter": {}` (empty — runs after trigger).
Subsequent actions declare their dependency:
```json
"My_Action": {
"runAfter": {
"Previous_Action": ["Succeeded"]
}
}
```
Multiple upstream dependencies:
```json
"runAfter": {
"Action_A": ["Succeeded"],
"Action_B": ["Succeeded", "Skipped"]
}
```
Error-handling action (runs when upstream failed):
```json
"Log_Error": {
"runAfter": {
"Risky_Action": ["Failed"]
}
}
```
---
## `parameters` (Flow-Level Input Parameters)
Optional. Define reusable values at the flow level:
```json
"parameters": {
"listName": {
"type": "string",
"defaultValue": "MyList"
},
"maxItems": {
"type": "integer",
"defaultValue": 100
}
}
```
Reference: `@parameters('listName')` in expression strings.
---
## `outputs`
Rarely used in cloud flows. Leave as `{}` unless the flow is called
as a child flow and needs to return values.
For child flows that return data:
```json
"outputs": {
"resultData": {
"type": "object",
"value": "@outputs('Compose_Result')"
}
}
```
---
## Scoped Actions (Inside Scope Block)
Actions that need to be grouped for error handling or clarity:
```json
"Scope_Main_Process": {
"type": "Scope",
"runAfter": {},
"actions": {
"Step_One": { ... },
"Step_Two": { "runAfter": { "Step_One": ["Succeeded"] }, ... }
}
}
```
---
## Full Minimal Example
```json
{
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"Recurrence": {
"type": "Recurrence",
"recurrence": {
"frequency": "Week",
"interval": 1,
"schedule": { "weekDays": ["Monday"] },
"startTime": "2026-01-05T09:00:00Z",
"timeZone": "AUS Eastern Standard Time"
}
}
},
"actions": {
"Compose_Greeting": {
"type": "Compose",
"runAfter": {},
"inputs": "Good Monday!"
}
},
"outputs": {}
}
```
@@ -0,0 +1,245 @@
# FlowStudio MCP — Trigger Types
Copy-paste trigger definitions for Power Automate flow definitions.
---
## Recurrence
Run on a schedule.
```json
"Recurrence": {
"type": "Recurrence",
"recurrence": {
"frequency": "Day",
"interval": 1,
"startTime": "2026-01-01T08:00:00Z",
"timeZone": "AUS Eastern Standard Time"
}
}
```
Weekly on specific days:
```json
"Recurrence": {
"type": "Recurrence",
"recurrence": {
"frequency": "Week",
"interval": 1,
"schedule": {
"weekDays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
},
"startTime": "2026-01-05T09:00:00Z",
"timeZone": "AUS Eastern Standard Time"
}
}
```
Common `timeZone` values:
- `"AUS Eastern Standard Time"` — Sydney/Melbourne (UTC+10/+11)
- `"UTC"` — Universal time
- `"E. Australia Standard Time"` — Brisbane (UTC+10 no DST)
- `"New Zealand Standard Time"` — Auckland (UTC+12/+13)
- `"Pacific Standard Time"` — Los Angeles (UTC-8/-7)
- `"GMT Standard Time"` — London (UTC+0/+1)
---
## Manual (HTTP Request / Power Apps)
Receive an HTTP POST with a JSON body.
```json
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "integer" }
},
"required": ["name"]
}
}
}
```
Access values: `@triggerBody()?['name']`
Trigger URL available after saving: `@listCallbackUrl()`
#### No-Schema Variant (Accept Arbitrary JSON)
When the incoming payload structure is unknown or varies, omit the schema
to accept any valid JSON body without validation:
```json
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {}
}
}
```
Access any field dynamically: `@triggerBody()?['anyField']`
> Use this for external webhooks (Stripe, GitHub, Employment Hero, etc.) where the
> payload shape may change or is not fully documented. The flow accepts any
> JSON without returning 400 for unexpected properties.
---
## Manual (Copilot Studio Skills)
Use the Skills trigger when the flow is meant to be called by a Copilot Studio
agent tool. Keep the trigger schema explicit so the agent receives predictable
input names and types.
```json
"manual": {
"type": "Request",
"kind": "Skills",
"inputs": {
"schema": {
"type": "object",
"properties": {
"itemId": { "type": "string" },
"notes": { "type": "string" }
},
"required": ["itemId"]
}
},
"metadata": {
"operationMetadataId": "<stable-guid>"
}
}
```
After deploying a production Skills-triggered flow, call
`add_live_flow_to_solution` with the target `solutionId`; Copilot Studio agent
tool discovery expects the flow to be solution-aware. For MCP-driven testing,
use a temporary HTTP twin with the same actions and payload shape, then restore
the Skills trigger after the actions are verified.
---
## Automated (SharePoint Item Created)
```json
"When_an_item_is_created": {
"type": "OpenApiConnectionNotification",
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "OnNewItem"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList"
},
"subscribe": {
"body": { "notificationUrl": "@listCallbackUrl()" },
"queries": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList"
}
}
}
}
```
Access trigger data: `@triggerBody()?['ID']`, `@triggerBody()?['Title']`, etc.
---
## Automated (SharePoint Item Modified)
```json
"When_an_existing_item_is_modified": {
"type": "OpenApiConnectionNotification",
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "OnUpdatedItem"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList"
},
"subscribe": {
"body": { "notificationUrl": "@listCallbackUrl()" },
"queries": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList"
}
}
}
}
```
---
## Automated (Outlook: When New Email Arrives)
```json
"When_a_new_email_arrives": {
"type": "OpenApiConnectionNotification",
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_office365",
"connectionName": "<connectionName>",
"operationId": "OnNewEmail"
},
"parameters": {
"folderId": "Inbox",
"to": "monitored@contoso.com",
"isHTML": true
},
"subscribe": {
"body": { "notificationUrl": "@listCallbackUrl()" }
}
}
}
```
---
## Child Flow (Called by Another Flow)
```json
"manual": {
"type": "Request",
"kind": "Button",
"inputs": {
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": { "type": "object" }
}
}
}
}
}
```
Access parent-supplied data: `@triggerBody()?['items']`
To return data to the parent, add a `Response` action:
```json
"Respond_to_Parent": {
"type": "Response",
"runAfter": { "Compose_Result": ["Succeeded"] },
"inputs": {
"statusCode": 200,
"body": "@outputs('Compose_Result')"
}
}
```
@@ -0,0 +1,474 @@
---
name: flowstudio-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
---
# 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](https://github.com/ninihen1/power-automate-mcp-skills/blob/main/examples/fix-expression-error.md) |
> [Data entry, not a flow bug](https://github.com/ninihen1/power-automate-mcp-skills/blob/main/examples/data-not-flow.md) |
> [Null value crashes child flow](https://github.com/ninihen1/power-automate-mcp-skills/blob/main/examples/null-child-flow.md)
**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 `list_skills` / `tool_search` first** to confirm available tool
> names and parameter schemas. Tool names and parameters may change between
> server versions.
> This skill covers response shapes, behavioral notes, and diagnostic patterns —
> things tool schemas cannot tell you. If this document disagrees with
> `tool_search` 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
```
---
## 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
> **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.
```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 — 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.**
```python
# 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_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 |
### 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:
```python
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
```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 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:
```python
# 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 list top-level actions when you're not sure
> which action produced the bad data. Once you pick an action inside a foreach,
> pass `iterationIndex` to avoid pulling every repetition into context.
---
## 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
```python
# 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:
```python
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:
```python
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**:
```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 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.
```python
# 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:
```python
# 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](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` — Foundation skill: connection setup, MCP helper, tool discovery
- `flowstudio-power-automate-build` — Build and deploy new flows
@@ -0,0 +1,223 @@
# 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.
---
### Foreach Parent Failed After Handled Inner Failure
**Symptom**: Inner actions have failure handlers, but the parent `Foreach` still
shows `Failed`, and downstream actions such as `Response` are skipped.
**Root cause**: A handled child failure can still mark the loop container as
failed. Downstream `runAfter` that only accepts `Succeeded` will not run.
**Diagnosis**: Inspect the parent foreach with `get_live_flow_run_error`, then
inspect child action outputs for the iteration that failed.
**Fix**: If partial success is acceptable, allow the downstream join/response to
run after `Succeeded` and `Failed`, and include an explicit error summary in the
payload. If the loop must be all-or-nothing, wrap risky inner work in a Scope and
handle success/failure at the Scope boundary.
---
## 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')
```
---
### Null Cascade in Filter Array / Query
**Symptom**: A lookup/filter step returns the wrong record or a later expression
fails on null even though the filter action itself succeeded.
**Root cause**: The lookup key is null or empty. A condition such as
`equals(item()?['Email'], outputs('Lookup_Email'))` can accidentally match rows
where both sides are null, or can pass an empty array downstream.
**Diagnosis**: Inspect the action that creates the lookup key and the filter
output length. Confirm the key is non-empty before trusting the filter result.
**Fix**: Add a non-empty guard before the filter, normalize comparison values
with `trim()`/`toLower()`, and branch explicitly when no match is found.
@@ -0,0 +1,163 @@
# 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
│ │
│ ├── parent action is Foreach / Apply to each
│ │ └── ► Inspect child actions; handled child failures can still fail the parent
│ │
│ └── 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)
├── Filter/Query result unexpectedly matches nulls or returns empty
│ └── Guard lookup keys before the filter; do not compare null-to-null
└── 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
@@ -0,0 +1,332 @@
---
name: flowstudio-power-automate-governance
description: >-
Govern Power Automate flows and Power Apps at scale using the FlowStudio MCP
cached store. Classify flows by business impact, detect orphaned resources,
audit connector usage, enforce compliance standards, manage notification rules,
and compute governance scores — all without Dataverse or the CoE Starter Kit.
Load this skill when asked to: tag or classify flows, set business impact,
assign ownership, detect orphans, audit connectors, check compliance, compute
archive scores, manage notification rules, run a governance review, generate
a compliance report, offboard a maker, or any task that involves writing
governance metadata to flows. Requires a FlowStudio for Teams or MCP Pro+
subscription — see https://mcp.flowstudio.app
---
# Power Automate Governance with FlowStudio MCP
Classify, tag, and govern Power Automate flows at scale through the FlowStudio
MCP **cached store** — without Dataverse, without the CoE Starter Kit, and
without the Power Automate portal.
This skill uses the same `store_*` tool family as `flowstudio-power-automate-monitoring`,
but with a different *intent*: governance writes metadata (`update_store_flow`)
and reads for *audit and classification* outcomes. Monitoring reads the same
tools for *operational health* outcomes. Don't try to memorize which skill
"owns" which tool — pick by what the user is doing. For health checks and
failure-rate dashboards, load `flowstudio-power-automate-monitoring` instead.
> **⚠️ Pro+ subscription required.** This skill calls `store_*` tools that
> only work for FlowStudio for Teams or MCP Pro+ subscribers.
>
> **If the user does not have Pro+ access:** the first `store_*` tool call
> will return a 403/404 error. When that happens:
> 1. STOP calling store tools
> 2. Tell the user governance features require a Pro+ subscription
> 3. Link them to https://mcp.flowstudio.app/pricing
>
> **Discovery:** load tool schemas via the meta-tools rather than `tools/list` —
> call `tool_search` with `query: "skill:governance"` for the canonical bundle,
> or `query: "select:update_store_flow"` for a single tool. This skill covers
> workflow patterns and field semantics — things `tool_search` cannot tell you.
> If this document disagrees with a real API response, the API wins.
---
## Critical: How to Extract Flow IDs
`list_store_flows` returns `id` in format `<environmentId>.<flowId>`. **You must split
on the first `.`** to get `environmentName` and `flowName` for all other tools:
```
id = "Default-<envGuid>.<flowGuid>"
environmentName = "Default-<envGuid>" (everything before first ".")
flowName = "<flowGuid>" (everything after first ".")
```
Also: skip entries that have no `displayName` or have `state=Deleted`
these are sparse records or flows that no longer exist in Power Automate.
If a deleted flow has `monitor=true`, suggest disabling monitoring
(`update_store_flow` with `monitor=false`) to free up a monitoring slot
(standard plan includes 20).
---
## The Write Tool: `update_store_flow`
`update_store_flow` writes governance metadata to the **Flow Studio cache
only** — it does NOT modify the flow in Power Automate. These fields are
not visible via `get_live_flow` or the PA portal. They exist only in the
Flow Studio store and are used by Flow Studio's scanning pipeline and
notification rules.
This means:
- `ownerTeam` / `supportEmail` — sets who Flow Studio considers the
governance contact. Does NOT change the actual PA flow owner.
- `rule_notify_email` — sets who receives Flow Studio failure/missing-run
notifications. Does NOT change Microsoft's built-in flow failure alerts.
- `monitor` / `critical` / `businessImpact` — Flow Studio classification
only. Power Automate has no equivalent fields.
Merge semantics — only fields you provide are updated. Returns the full
updated record (same shape as `get_store_flow`).
Required parameters: `environmentName`, `flowName`. All other fields optional.
### Settable Fields
| Field | Type | Purpose |
|---|---|---|
| `monitor` | bool | Enable run-level scanning (standard plan: 20 flows included) |
| `rule_notify_onfail` | bool | Send email notification on any failed run |
| `rule_notify_onmissingdays` | number | Send notification when flow hasn't run in N days (0 = disabled) |
| `rule_notify_email` | string | Comma-separated notification recipients |
| `description` | string | What the flow does |
| `tags` | string | Classification tags (also auto-extracted from description `#hashtags`) |
| `businessImpact` | string | Low / Medium / High / Critical |
| `businessJustification` | string | Why the flow exists, what process it automates |
| `businessValue` | string | Business value statement |
| `ownerTeam` | string | Accountable team |
| `ownerBusinessUnit` | string | Business unit |
| `supportGroup` | string | Support escalation group |
| `supportEmail` | string | Support contact email |
| `critical` | bool | Designate as business-critical |
| `tier` | string | Standard or Premium |
| `security` | string | Security classification or notes |
> **Caution with `security`:** The `security` field on `get_store_flow`
> contains structured JSON (e.g. `{"triggerRequestAuthenticationType":"All"}`).
> Writing a plain string like `"reviewed"` will overwrite this. To mark a
> flow as security-reviewed, use `tags` instead.
---
## Governance Workflows
### 1. Compliance Detail Review
Identify flows missing required governance metadata.
```
1. Ask the user which compliance fields they require
2. list_store_flows
3. For each active flow: split id, call get_store_flow, check required fields
4. Report non-compliant flows with missing fields listed
5. For updates: ask for values, then update_store_flow(...provided fields)
```
Common compliance fields: `description`, `businessImpact`,
`businessJustification`, `ownerTeam`, `supportEmail`, `monitor`,
`rule_notify_onfail`, `critical`. Ask for the user's policy before flagging.
### 2. Orphaned Resource Detection
Find flows owned by deleted or disabled Azure AD accounts.
```
1. list_store_makers
2. Filter where deleted=true AND ownerFlowCount > 0
3. list_store_flows → collect all flows
4. For each active flow: split id, get_store_flow, parse owners JSON
5. Match owner principalId against orphaned maker id
6. Reassign governance contact or stop/tag for decommission
```
`update_store_flow` does not transfer actual PA ownership; use the admin center
or PowerShell for that. Some orphaned-looking flows are system-generated; tag
them instead of reassigning when appropriate. Store coverage is only as fresh as
the latest scan.
### 3. Archive Score Calculation
Compute an inactivity score (0-7) per flow to identify cleanup candidates.
```
1. list_store_flows
2. For each active flow: split id, get_store_flow
3. Add 1 point each: created≈modified, test/demo/temp/copy name, age >12mo,
stopped/suspended, no owners, no recent runs, complexity.actions < 5
4. Score 5-7: recommend archive; 3-4: tag #archive-review; 0-2: active
5. For confirmed archive: set_live_flow_state(..., "Stopped") and append #archived
```
Archive via MCP means stop the flow and tag it. Deletion requires the portal or
admin PowerShell.
### 4. Connector Audit
Audit which connectors are in use across monitored flows. Useful for DLP
impact analysis and premium license planning.
```
1. list_store_flows(monitor=true)
2. For each active flow: split id, get_store_flow, parse connections JSON
3. Group by apiName; flag Premium tier, HTTP connectors, custom connectors
4. Report inventory to user
```
Scope to monitored flows where possible; each `get_store_flow` call costs time.
`list_store_connections` lists connection instances, not connector usage per
flow. DLP policies are not exposed; ask the user for connector classifications.
### 5. Notification Rule Management
Configure monitoring and alerting for flows at scale.
```
Enable failure alerts on all critical flows:
1. list_store_flows(monitor=true)
2. For each active flow: split id, get_store_flow
3. If critical=true and rule_notify_onfail is false, update_store_flow(...,
rule_notify_onfail=true, rule_notify_email="oncall@contoso.com")
Enable missing-run detection for scheduled flows:
1. list_store_flows(monitor=true)
2. For active Recurrence flows: get_store_flow
3. If rule_notify_onmissingdays is 0/missing, update_store_flow(...,
rule_notify_onmissingdays=2)
```
Check monitoring limits before bulk-enabling `monitor=true`. If no flows have
`critical=true`, report that as a governance gap before configuring alerts.
### 6. Classification and Tagging
Bulk-classify flows by connector type, business function, or risk level.
```
Auto-tag by connector:
1. list_store_flows
2. For each active flow: split id, get_store_flow, parse connections JSON
3. Map apiName values to tags (#sharepoint, #teams, #email, #custom-connector)
4. Read existing store tags, append new tags, update_store_flow(tags=...)
```
Store tags and description hashtags are separate systems. `tags=` overwrites
store tags, so read/append/write. Avoid overriding computed `tier` unless asked.
### 7. Maker Offboarding
When an employee leaves, identify their flows and apps, and reassign
Flow Studio governance contacts and notification recipients.
```
1. get_store_maker(makerKey="<departing-user-aad-oid>")
→ check ownerFlowCount, ownerAppCount, deleted status
2. list_store_flows → collect all flows
3. For each active flow: split id, get_store_flow, parse owners JSON
4. Flag flows whose owner principalId matches the departing user's OID
5. list_store_power_apps → filter ownerId
6. For kept flows: update ownerTeam/supportEmail/rule_notify_email; consider
add_live_flow_to_solution before account deletion
7. For retired flows: set_live_flow_state(..., "Stopped") and tag #decommissioned
8. Report: flows reassigned, flows migrated to solutions, flows stopped,
apps needing manual reassignment
```
This changes Flow Studio governance contacts, not actual PA ownership. Power
Apps ownership changes are manual/admin-center work.
### 8. Security Review
Review flows for potential security concerns using cached store data.
```
1. list_store_flows(monitor=true)
2. For each active flow: split id, get_store_flow
3. Parse security/connections/referencedResources JSON; read sharingType top-level
4. Report findings; for reviewed flows append #security-reviewed tag
```
Security signals: `security.triggerRequestAuthenticationType`, `sharingType`,
`connections`, `referencedResources`, `tier`. Never overwrite the structured
`security` field; tag reviewed flows instead.
### 9. Environment Governance
Audit environments for compliance and sprawl.
```
1. list_store_environments
Skip entries without displayName (tenant-level metadata rows)
2. Flag:
- Developer environments
- Non-managed environments
- Environments where service account lacks admin access (isAdmin=false)
3. list_store_flows → group by environmentName
4. list_store_connections → group by environmentName
```
### 10. Governance Dashboard
Generate a tenant-wide governance summary.
```
Efficient metrics (list calls only):
1. total_flows = len(list_store_flows())
2. monitored = len(list_store_flows(monitor=true))
3. with_onfail = len(list_store_flows(rule_notify_onfail=true))
4. makers/apps/envs/conns = list_store_makers/list_store_power_apps/list_store_environments/list_store_connections
5. Compute monitoring %, notification %, orphan count, high-failure count
Detailed metrics (require get_store_flow per flow — expensive for large tenants):
- Compliance %: flows with businessImpact set / total active flows
- Undocumented count: flows without description
- Tier breakdown: group by tier field
```
---
## Field Reference: `get_store_flow` Fields Used in Governance
All fields below are confirmed present on the `get_store_flow` response.
Fields marked with `*` are also available on `list_store_flows` (cheaper).
| Field | Type | Governance use |
|---|---|---|
| `displayName` * | string | Archive score (test/demo name detection) |
| `state` * | string | Archive score, lifecycle management |
| `tier` | string | License audit (Standard vs Premium) |
| `monitor` * | bool | Is this flow being actively monitored? |
| `critical` | bool | Business-critical designation (settable via update_store_flow) |
| `businessImpact` | string | Compliance classification |
| `businessJustification` | string | Compliance attestation |
| `ownerTeam` | string | Ownership accountability |
| `supportEmail` | string | Escalation contact |
| `rule_notify_onfail` | bool | Failure alerting configured? |
| `rule_notify_onmissingdays` | number | SLA monitoring configured? |
| `rule_notify_email` | string | Alert recipients |
| `description` | string | Documentation completeness |
| `tags` | string | Classification — `list_store_flows` shows description-extracted hashtags only; store tags written by `update_store_flow` require `get_store_flow` to read back |
| `runPeriodTotal` * | number | Activity level |
| `runPeriodFailRate` * | number | Health status |
| `runLast` | ISO string | Last run timestamp |
| `scanned` | ISO string | Data freshness |
| `deleted` | bool | Lifecycle tracking |
| `createdTime` * | ISO string | Archive score (age) |
| `lastModifiedTime` * | ISO string | Archive score (staleness) |
| `owners` | JSON string | Orphan detection, ownership audit — parse with json.loads() |
| `connections` | JSON string | Connector audit, tier — parse with json.loads() |
| `complexity` | JSON string | Archive score (simplicity) — parse with json.loads() |
| `security` | JSON string | Auth type audit — parse with json.loads(), contains `triggerRequestAuthenticationType` |
| `sharingType` | string | Oversharing detection (top-level, NOT inside security) |
| `referencedResources` | JSON string | URL audit — parse with json.loads() |
---
## Related Skills
- `flowstudio-power-automate-monitoring` — Health checks, failure rates, inventory (read-only)
- `flowstudio-power-automate-mcp` — Foundation skill: connection setup, MCP helper, tool discovery
- `flowstudio-power-automate-debug` — Deep diagnosis with action-level inputs/outputs
- `flowstudio-power-automate-build` — Build and deploy flow definitions
@@ -0,0 +1,300 @@
---
name: flowstudio-power-automate-mcp
description: >-
Foundation skill for Power Automate via FlowStudio MCP — auth setup, the
reusable MCP helper (Python + Node.js), tool discovery via `list_skills` /
`tool_search`, and oversized-response handling. Load this skill first when
connecting an agent to Power Automate. For specialized workflows, load
`flowstudio-power-automate-build`, `flowstudio-power-automate-debug`, `flowstudio-power-automate-monitoring`
(Pro+), or `flowstudio-power-automate-governance` (Pro+) — each contains the workflow
narrative, this skill provides the plumbing they all rely on. Requires a
FlowStudio MCP subscription or compatible server — see https://mcp.flowstudio.app
---
# Power Automate via FlowStudio MCP — Foundation
This skill is the **plumbing layer**. It gives an AI agent a reliable way to
talk to a FlowStudio MCP server, discover what tools are available, and handle
the responses cleanly. The actual workflow narratives live in four specialized
skills that all build on this one.
> **Real debugging examples**: [Expression error in child flow](https://github.com/ninihen1/power-automate-mcp-skills/blob/main/examples/fix-expression-error.md) |
> [Data entry, not a flow bug](https://github.com/ninihen1/power-automate-mcp-skills/blob/main/examples/data-not-flow.md) |
> [Null value crashes child flow](https://github.com/ninihen1/power-automate-mcp-skills/blob/main/examples/null-child-flow.md)
> **Requires:** A [FlowStudio](https://mcp.flowstudio.app) MCP subscription (or
> compatible Power Automate MCP server). You will need:
> - MCP endpoint: `https://mcp.flowstudio.app/mcp` (same for all subscribers)
> - API key / JWT token (`x-api-key` header — NOT Bearer)
> - Power Platform environment name (e.g. `Default-<tenant-guid>`)
---
## Which Skill to Use When
Skills are organized by **use-case intent**, not by which tools they call.
Multiple skills reuse the same underlying tools — pick by what the user is
trying to accomplish.
| The user wants to… | Load this skill |
|---|---|
| Make or change a flow (build new, modify existing, fix a bug, deploy) | **`flowstudio-power-automate-build`** |
| Diagnose why a flow failed (root cause analysis on a failing run) | **`flowstudio-power-automate-debug`** |
| See tenant-wide flow health, failure rates, asset inventory | **`flowstudio-power-automate-monitoring`** *(Pro+)* |
| Tag, audit, classify, score, or offboard flows | **`flowstudio-power-automate-governance`** *(Pro+)* |
| Just connect, set up auth, write the helper, parse responses | this skill (foundation) |
**Same tools, different lenses.** `flowstudio-power-automate-build` and `flowstudio-power-automate-debug`
both call `update_live_flow`, `get_live_flow`, and the run-error tools — they
differ in *direction* (forward vs backward) and *intent* (compose vs diagnose).
`flowstudio-power-automate-monitoring` and `flowstudio-power-automate-governance` both call the Store
tools — they differ in *audience* (ops vs compliance) and *outcome* (read
health vs write metadata). Don't try to memorize "which tools belong to which
skill"; pick the skill by what the user is doing.
---
## Source of Truth
| Priority | Source | Covers |
|----------|--------|--------|
| 1 | **Real API response** | Always trust what the server actually returns |
| 2 | **`tool_search` / `list_skills`** | Authoritative tool schemas, parameter names, types, required flags |
| 3 | **SKILL docs & reference files** | Workflow narrative, response shapes, non-obvious behaviors |
If documentation disagrees with a real API response, the API wins. Tool schemas
in this skill (or any other) may lag the server — call `tool_search` to confirm
the current shape before invoking a tool you haven't used recently.
---
## How Agents Discover Tools
The FlowStudio MCP server (v1.1.5+) exposes two **non-billable** meta-tools that
let an agent load only the tools relevant to the current task. Use these in
preference to `tools/list` (which loads all 30+ schemas at once) or guessing
tool names.
| Meta-tool | When to call |
|---|---|
| `list_skills` | Cold start — see the available bundles (`build-flow`, `create-flow`, `debug-flow`, `monitor-flow`, `discover`, `governance`) and pick one |
| `tool_search` with `query: "skill:<name>"` | Load the full schema set for one bundle (e.g. `skill:debug-flow`) |
| `tool_search` with `query: "select:tool1,tool2"` | Load specific tools by name (e.g. when chaining across bundles) |
| `tool_search` with `query: "<keywords>"` | Free-text search when the user request is ambiguous (e.g. `"cancel run"`) |
The server's `tool_search` bundles are intentionally **narrower than this
skill family** — they're starter packs of the most-likely-needed tools per
intent. A workflow skill (e.g. `flowstudio-power-automate-debug`) may pull a bundle and
then call `tool_search` again for additional tools as the workflow progresses.
```python
# Cold start — pick a bundle by intent
skills = mcp("list_skills", {})
# [{"name": "debug-flow", "description": "Investigate why a flow is failing...",
# "tools": ["get_live_flow_runs", "get_live_flow_run_error", ...]}, ...]
# Load schemas for the bundle
debug_tools = mcp("tool_search", {"query": "skill:debug-flow"})
```
Current common bundles:
| Bundle | Use when |
|---|---|
| `create-flow` | Creating a brand-new flow; includes environment/connection discovery, connector description, dynamic options, and `update_live_flow` |
| `build-flow` | Reading or modifying an existing flow definition |
| `debug-flow` | Investigating failed runs and action-level inputs/outputs |
| `monitor-flow` | Starting/stopping, triggering, cancelling, or resubmitting runs |
| `discover` | Enumerating environments, flows, and connections |
| `governance` | Pro+ cached-store tagging, maker audit, and metadata updates |
---
## Recommended Language: Python or Node.js
All examples in this skill family use **Python with `urllib.request`**
(stdlib — no `pip install` needed). **Node.js** is an equally valid choice:
`fetch` is built-in from Node 18+, JSON handling is native, and async/await
maps cleanly onto the request-response pattern of MCP tool calls — making it
a natural fit for teams already working in a JavaScript/TypeScript stack.
| Language | Verdict | Notes |
|---|---|---|
| **Python** | Recommended | Clean JSON handling, no escaping issues, all skill examples use it |
| **Node.js (≥ 18)** | Recommended | Native `fetch` + `JSON.stringify`/`JSON.parse`; no extra packages |
| PowerShell | Avoid for flow operations | `ConvertTo-Json -Depth` silently truncates nested definitions; quoting and escaping break complex payloads. Acceptable for a quick connectivity smoke-test but not for building or updating flows. |
| cURL / Bash | Possible but fragile | Shell-escaping nested JSON is error-prone; no native JSON parser |
> **TL;DR — use the Core MCP Helper (Python or Node.js) below.** Both handle
> JSON-RPC framing, auth, and response parsing in a single reusable function.
---
## Core MCP Helper (Python)
Use this helper throughout all subsequent operations:
```python
import json, urllib.request
TOKEN = "<YOUR_JWT_TOKEN>"
MCP = "https://mcp.flowstudio.app/mcp"
def mcp(tool, args, cid=1):
payload = {"jsonrpc": "2.0", "method": "tools/call", "id": cid,
"params": {"name": tool, "arguments": args}}
req = urllib.request.Request(MCP, data=json.dumps(payload).encode(),
headers={"x-api-key": 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'])}")
text = raw["result"]["content"][0]["text"]
return json.loads(text)
```
> **Common auth errors:**
> - HTTP 401/403 → token is missing, expired, or malformed. Get a fresh JWT from [mcp.flowstudio.app](https://mcp.flowstudio.app).
> - HTTP 400 → malformed JSON-RPC payload. Check `Content-Type: application/json` and body structure.
> - `MCP error: {"code": -32602, ...}` → wrong or missing tool arguments. Call `tool_search` with `select:<toolname>` to confirm the schema.
---
## Core MCP Helper (Node.js)
Equivalent helper for Node.js 18+ (built-in `fetch` — no packages required):
```js
const TOKEN = "<YOUR_JWT_TOKEN>";
const MCP = "https://mcp.flowstudio.app/mcp";
async function mcp(tool, args, cid = 1) {
const payload = {
jsonrpc: "2.0",
method: "tools/call",
id: cid,
params: { name: tool, arguments: args },
};
const res = await fetch(MCP, {
method: "POST",
headers: {
"x-api-key": TOKEN,
"Content-Type": "application/json",
"User-Agent": "FlowStudio-MCP/1.0",
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`MCP HTTP ${res.status}: ${body.slice(0, 200)}`);
}
const raw = await res.json();
if (raw.error) throw new Error(`MCP error: ${JSON.stringify(raw.error)}`);
return JSON.parse(raw.result.content[0].text);
}
```
> Requires Node.js 18+. For older Node, replace `fetch` with `https.request`
> from the stdlib or install `node-fetch`.
---
## Verify the Connection
A 3-line smoke test that confirms the token, endpoint, and helper all work:
```python
skills = mcp("list_skills", {})
print(f"Connected — {len(skills)} skill bundles available:",
[s["name"] for s in skills])
```
Expected output:
```text
Connected — 6 skill bundles available: ['build-flow', 'create-flow', 'debug-flow', 'monitor-flow', 'discover', 'governance']
```
If this fails, see the **Common auth errors** note above. If it succeeds, hand
off to the workflow skill matching the user's intent.
---
## Handling Oversized Responses
Some MCP tool responses are large enough to overflow the agent's context window:
| Tool | Typical size | Cause |
|---|---|---|
| `describe_live_connector` | 100-600 KB | Full Swagger spec for a connector |
| `get_live_dynamic_properties` | 50-500 KB | Dynamic connector field schemas such as SharePoint list columns |
| `get_live_flow_run_action_outputs` (no `actionName`) | 50 KB several MB | Top-level action outputs; with an action in a foreach, every repetition can be returned |
| `get_live_flow` (large flows) | 50-500 KB | Deeply nested branches |
| `list_live_flows` (large tenants) | 50-200 KB | Hundreds of flow records |
### When the harness spills to a file
Agent harnesses (Claude Code, VS Code Copilot, etc.) save oversized responses
to a temp file (e.g. `tool-results/mcp-flowstudio-describe_live_connector-NNNN.txt`)
and return the path instead of the inline JSON. The file is **double-wrapped**
the outer MCP envelope plus the inner JSON-escaped payload:
```text
[{"type":"text","text":"<JSON-escaped payload>"}]
```
Two parses to reach a usable object:
```python
import json
with open(path) as f:
raw = json.loads(f.read())
payload = json.loads(raw[0]["text"])
```
```powershell
$payload = ((Get-Content $path -Raw | ConvertFrom-Json)[0].text) | ConvertFrom-Json
```
### Rules of thumb
1. **Extract, don't echo.** Pull the specific field(s) you need (one `operationId`, one action's outputs) and discard the rest before reasoning about it.
2. **Always pass `actionName` to `get_live_flow_run_action_outputs`.** Omitting it fetches all top-level actions. For actions inside a foreach, passing `actionName` without `iterationIndex` can return every repetition of that action.
3. **Reuse the spill file within a session.** Refetching the same connector swagger costs 30+ seconds and produces another spill — cache the path.
4. **Don't grep the spill file for JSON keys directly.** Strings are JSON-escaped inside the file (`\"OperationId\":`), so a plain grep for `"OperationId":` will not match. Parse first, then filter.
5. **Summarize tool output to the user.** Echo `name + state + trigger` for flow lists and `actionName + status + code` for run errors — not raw JSON, unless asked.
```python
# Good — drill into one operation in a connector swagger
conn = mcp("describe_live_connector", {"environmentName": ENV, "connectorName": "shared_sharepointonline"})
op = conn["properties"]["swagger"]["paths"]["/datasets/{dataset}/tables/{table}/items"]["get"]
print(op["operationId"], "", op.get("summary"))
# Bad — keeping the whole 500 KB swagger in context
print(json.dumps(conn, indent=2)) # don't do this
```
---
## Auth & Connection Notes
| Field | Value |
|---|---|
| Auth header | `x-api-key: <JWT>`**not** `Authorization: Bearer` |
| Token format | Plain JWT — do not strip, alter, or prefix it |
| Timeout | Use ≥ 120 s for `get_live_flow_run_action_outputs` (large outputs) |
| Environment name | `Default-<tenant-guid>` (find it via `list_live_environments` or `list_live_flows` response) |
---
## Reference Files
- [MCP-BOOTSTRAP.md](references/MCP-BOOTSTRAP.md) — endpoint, auth, request/response format (read this first)
- [tool-reference.md](references/tool-reference.md) — response shapes and behavioral notes (parameters are in `tool_search`)
- [action-types.md](references/action-types.md) — Power Automate action type patterns
- [connection-references.md](references/connection-references.md) — connector reference guide
@@ -0,0 +1,78 @@
# MCP Bootstrap — Quick Reference
Everything an agent needs to start calling the FlowStudio MCP server.
```
Endpoint: https://mcp.flowstudio.app/mcp
Protocol: JSON-RPC 2.0 over HTTP POST
Transport: Streamable HTTP — single POST per request, no SSE, no WebSocket
Auth: x-api-key header with JWT token (NOT Bearer)
```
## Required Headers
```
Content-Type: application/json
x-api-key: <token>
User-Agent: FlowStudio-MCP/1.0 ← required, or Cloudflare blocks you
```
## Step 1 — Discover Tool Bundles
Preferred cold-start call:
```json
POST {"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_skills","arguments":{}}}
```
Returns the current bundles (`build-flow`, `create-flow`, `debug-flow`,
`monitor-flow`, `discover`, `governance`) and their member tool names. Free —
not counted against plan limits.
Then load the relevant schemas:
```json
POST {"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"tool_search","arguments":{"query":"skill:create-flow"}}}
```
Use `query:"select:tool1,tool2"` to load exact tools and keyword search such as
`query:"send email"` when the user intent is ambiguous.
Fallback for very low-level MCP clients:
```json
POST {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
```
`tools/list` returns all tools with names, descriptions, and input schemas, but
it is heavier and should not be the first choice for agents that know the
FlowStudio meta-tools.
## Step 2 — Call a Tool
```json
POST {"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"<tool_name>","arguments":{...}}}
```
## Response Shape
```
Success → {"result":{"content":[{"type":"text","text":"<JSON string>"}]}}
Error → {"result":{"content":[{"type":"text","text":"{\"error\":{...}}"}]}}
```
Always parse `result.content[0].text` as JSON to get the actual data.
## Key Tips
- Tool results are JSON strings inside the text field — **double-parse needed**
- `"error"` field in parsed body: `null` = success, object = failure
- `environmentName` is required for most tools, but **not** for:
`list_live_environments`, `list_live_connections`, `list_store_flows`,
`list_store_environments`, `list_store_makers`, `get_store_maker`,
`list_store_power_apps`, `list_store_connections`
- When in doubt, check the `required` array in each tool's schema from
`tool_search` (or `tools/list` as a fallback)
@@ -0,0 +1,79 @@
# FlowStudio MCP — Action Types Reference
Compact lookup for recognising action types returned by `get_live_flow`.
Use this to **read and understand** existing flow definitions.
> For full copy-paste construction patterns, see the `flowstudio-power-automate-build` skill.
---
## How to Read a Flow Definition
Every action has `"type"`, `"runAfter"`, and `"inputs"`. The `runAfter` object
declares dependencies: `{"Previous": ["Succeeded"]}`. Valid statuses:
`Succeeded`, `Failed`, `Skipped`, `TimedOut`.
---
## Action Type Quick Reference
| Type | Purpose | Key fields to inspect | Output reference |
|---|---|---|---|
| `Compose` | Store/transform a value | `inputs` (any expression) | `outputs('Name')` |
| `InitializeVariable` | Declare a variable | `inputs.variables[].{name, type, value}` | `variables('name')` |
| `SetVariable` | Update a variable | `inputs.{name, value}` | `variables('name')` |
| `IncrementVariable` | Increment a numeric variable | `inputs.{name, value}` | `variables('name')` |
| `AppendToArrayVariable` | Push to an array variable | `inputs.{name, value}` | `variables('name')` |
| `If` | Conditional branch | `expression.and/or`, `actions`, `else.actions` | — |
| `Switch` | Multi-way branch | `expression`, `cases.{case, actions}`, `default` | — |
| `Foreach` | Loop over array | `foreach`, `actions`, `operationOptions` | `item()` / `items('Name')` |
| `Until` | Loop until condition | `expression`, `limit.{count, timeout}`, `actions` | — |
| `Wait` | Delay | `inputs.interval.{count, unit}` | — |
| `Scope` | Group / try-catch | `actions` (nested action map) | `result('Name')` |
| `Terminate` | End run | `inputs.{runStatus, runError}` | — |
| `OpenApiConnection` | Connector call (SP, Outlook, Teams…) | `inputs.host.{apiId, connectionName, operationId}`, `inputs.parameters` | `outputs('Name')?['body/...']` |
| `OpenApiConnectionWebhook` | Webhook wait (approvals, adaptive cards) | same as above | `body('Name')?['...']` |
| `Http` | External HTTP call | `inputs.{method, uri, headers, body}` | `outputs('Name')?['body']` |
| `Response` | Return to HTTP caller | `inputs.{statusCode, headers, body}` | — |
| `Query` | Filter array | `inputs.{from, where}` | `body('Name')` (filtered array) |
| `Select` | Reshape/project array | `inputs.{from, select}` | `body('Name')` (projected array) |
| `Table` | Array → CSV/HTML string | `inputs.{from, format, columns}` | `body('Name')` (string) |
| `ParseJson` | Parse JSON with schema | `inputs.{content, schema}` | `body('Name')?['field']` |
| `Expression` | Built-in function (e.g. ConvertTimeZone) | `kind`, `inputs` | `body('Name')` |
---
## Connector Identification
When you see `type: OpenApiConnection`, identify the connector from `host.apiId`:
| apiId suffix | Connector |
|---|---|
| `shared_sharepointonline` | SharePoint |
| `shared_office365` | Outlook / Office 365 |
| `shared_teams` | Microsoft Teams |
| `shared_approvals` | Approvals |
| `shared_office365users` | Office 365 Users |
| `shared_flowmanagement` | Flow Management |
The `operationId` tells you the specific operation (e.g. `GetItems`, `SendEmailV2`,
`PostMessageToConversation`). The `connectionName` maps to a GUID in
`properties.connectionReferences`.
---
## Common Expressions (Reading Cheat Sheet)
| Expression | Meaning |
|---|---|
| `@outputs('X')?['body/value']` | Array result from connector action X |
| `@body('X')` | Direct body of action X (Query, Select, ParseJson) |
| `@item()?['Field']` | Current loop item's field |
| `@triggerBody()?['Field']` | Trigger payload field |
| `@variables('name')` | Variable value |
| `@coalesce(a, b)` | First non-null of a, b |
| `@first(array)` | First element (null if empty) |
| `@length(array)` | Array count |
| `@empty(value)` | True if null/empty string/empty array |
| `@union(a, b)` | Merge arrays — **first wins** on duplicates |
| `@result('Scope')` | Array of action outcomes inside a Scope |
@@ -0,0 +1,143 @@
# FlowStudio MCP — Connection References
Connection references wire a flow's connector actions to real authenticated
connections in the Power Platform. They are required whenever you call
`update_live_flow` with a definition that uses connector actions.
---
## Structure in a Flow Definition
```json
{
"properties": {
"definition": { ... },
"connectionReferences": {
"shared_sharepointonline": {
"connectionName": "shared-sharepointonl-eeeeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
"id": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"displayName": "SharePoint"
},
"shared_office365": {
"connectionName": "shared-office365-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id": "/providers/Microsoft.PowerApps/apis/shared_office365",
"displayName": "Office 365 Outlook"
}
}
}
}
```
Keys are **logical reference names** (e.g. `shared_sharepointonline`).
These match the `connectionName` field inside each action's `host` block.
---
## Finding Connection References
Preferred method: call `list_live_connections` in the target environment. Use
`search` to narrow results to the connector you need; newer MCP server versions
return paste-ready templates.
```python
matches = mcp("list_live_connections",
environmentName=ENV,
search="shared_sharepointonline")
conn = next(c for c in matches["connections"]
if c.get("overallStatus") == "Connected"
or c.get("statuses", [{}])[0].get("status") == "Connected")
conn_refs = {
"shared_sharepointonline": conn.get("connectionReferenceTemplate") or {
"connectionName": conn["id"],
"id": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"source": "Invoker"
}
}
host = conn.get("hostTemplate") or {"connectionName": "shared_sharepointonline"}
```
Use `host` as the action-side `inputs.host`. Use `conn_refs` as
`update_live_flow(connectionReferences=conn_refs)`.
Fallback method: copy from an existing flow.
Call `get_live_flow` on **any existing flow** that uses the same connection
and copy the `connectionReferences` block. The GUID after the connector prefix is
the connection instance owned by the authenticating user.
```python
flow = mcp("get_live_flow", environmentName=ENV, flowName=EXISTING_FLOW_ID)
conn_refs = flow["properties"]["connectionReferences"]
# conn_refs["shared_sharepointonline"]["connectionName"]
# → "shared-sharepointonl-eeeeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
```
> ⚠️ Connection references are **user-scoped**. If a connection is owned
> by another account, `update_live_flow` will return 403
> `ConnectionAuthorizationFailed`. You must use a connection belonging to
> the account whose token is in the `x-api-key` header.
---
## Passing `connectionReferences` to `update_live_flow`
```python
result = mcp("update_live_flow",
environmentName=ENV,
flowName=FLOW_ID,
definition=modified_definition,
connectionReferences={
"shared_sharepointonline": {
"connectionName": "shared-sharepointonl-eeeeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
"id": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline"
}
}
)
```
Only include connections that the definition actually uses.
---
## Common Connector API IDs
| Service | API ID |
|---|---|
| SharePoint Online | `/providers/Microsoft.PowerApps/apis/shared_sharepointonline` |
| Office 365 Outlook | `/providers/Microsoft.PowerApps/apis/shared_office365` |
| Microsoft Teams | `/providers/Microsoft.PowerApps/apis/shared_teams` |
| OneDrive for Business | `/providers/Microsoft.PowerApps/apis/shared_onedriveforbusiness` |
| Azure AD | `/providers/Microsoft.PowerApps/apis/shared_azuread` |
| HTTP with Azure AD | `/providers/Microsoft.PowerApps/apis/shared_webcontents` |
| SQL Server | `/providers/Microsoft.PowerApps/apis/shared_sql` |
| Dataverse | `/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps` |
| Azure Blob Storage | `/providers/Microsoft.PowerApps/apis/shared_azureblob` |
| Approvals | `/providers/Microsoft.PowerApps/apis/shared_approvals` |
| Office 365 Users | `/providers/Microsoft.PowerApps/apis/shared_office365users` |
| Flow Management | `/providers/Microsoft.PowerApps/apis/shared_flowmanagement` |
---
## Teams Adaptive Card Dual-Connection Requirement
Flows that send adaptive cards **and** post follow-up messages require two
separate Teams connections:
```json
"connectionReferences": {
"shared_teams": {
"connectionName": "shared-teams-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id": "/providers/Microsoft.PowerApps/apis/shared_teams"
},
"shared_teams_1": {
"connectionName": "shared-teams-yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"id": "/providers/Microsoft.PowerApps/apis/shared_teams"
}
}
```
Both can point to the **same underlying Teams account** but must be registered
as two distinct connection references. The webhook (`OpenApiConnectionWebhook`)
uses `shared_teams` and subsequent message actions use `shared_teams_1`.
@@ -0,0 +1,601 @@
# FlowStudio MCP — Tool Response Catalog
Response shapes and behavioral notes for the FlowStudio Power Automate MCP server.
> **For tool names and parameters**: Prefer `list_skills` and `tool_search`.
> They return focused, up-to-date schemas without loading every MCP tool at once.
> Use `tools/list` only as a low-level fallback when the meta-tools are not available.
> This document covers what tool schemas do NOT tell you: **response shapes**
> and **non-obvious behaviors** discovered through real usage.
---
## Source of Truth
| Priority | Source | Covers |
|----------|--------|--------|
| 1 | **Real API response** | Always trust what the server actually returns |
| 2 | **`list_skills` / `tool_search`** | Tool names, parameter names, types, required flags |
| 3 | **This document** | Response shapes, behavioral notes, gotchas |
> If this document disagrees with `tool_search`, `tools/list`, or real API
> behavior, the API wins. Update this document accordingly.
---
## Environment & Tenant Discovery
### `list_live_environments`
Response: direct array of environments.
```json
[
{
"id": "Default-26e65220-5561-46ef-9783-ce5f20489241",
"displayName": "FlowStudio (default)",
"sku": "Production",
"location": "australia",
"state": "Enabled",
"isDefault": true,
"isAdmin": true,
"isMember": true,
"createdTime": "2023-08-18T00:41:05Z"
}
]
```
> Use the `id` value as `environmentName` in all other tools.
### `list_store_environments`
Same shape as `list_live_environments` but read from cache (faster).
---
## Connection Discovery
### `list_live_connections`
Response: wrapper object with `connections` array.
```json
{
"connections": [
{
"id": "shared-office365-9f9d2c8e-55f1-49c9-9f9c-1c45d1fbbdce",
"displayName": "user@contoso.com",
"connectorName": "shared_office365",
"environment": "Default-26e65220-...",
"createdBy": "User Name",
"authenticatedUser": "user@contoso.com",
"overallStatus": "Connected",
"statuses": [{"status": "Connected"}],
"createdTime": "2024-03-12T21:23:55.206815Z",
"connectionReferenceTemplate": {
"connectionName": "shared-office365-9f9d2c8e-55f1-49c9-9f9c-1c45d1fbbdce",
"source": "Invoker",
"id": "/providers/Microsoft.PowerApps/apis/shared_office365"
},
"hostTemplate": {
"connectionName": "shared_office365"
}
}
],
"totalCount": 56,
"error": null
}
```
> **Key field**: `id` is the `connectionName` value used in `connectionReferences`.
>
> **Key field**: `connectorName` maps to apiId:
> `"/providers/Microsoft.PowerApps/apis/" + connectorName`
>
> Filter by status: prefer `overallStatus == "Connected"` when present; otherwise
> check `statuses[0].status == "Connected"`.
>
> For build workflows, pass `environmentName` to avoid using a connection from
> the wrong environment. Omit it only when intentionally inventorying connections
> across all environments.
>
> Pass `search=<connector or account>` to narrow output and receive
> `connectionReferenceTemplate` plus `hostTemplate` values that can be copied
> directly into `update_live_flow`.
### `list_store_connections`
Same connection data from cache.
---
## Flow Discovery & Listing
### `list_live_flows`
Response: wrapper object with `flows` array.
```json
{
"mode": "owner",
"flows": [
{
"id": "0757041a-8ef2-cf74-ef06-06881916f371",
"displayName": "My Flow",
"state": "Started",
"triggerType": "Request",
"triggerKind": "Http",
"createdTime": "2023-08-18T01:18:17Z",
"lastModifiedTime": "2023-08-18T12:47:42Z",
"owners": "<aad-object-id>",
"definitionAvailable": true
}
],
"totalCount": 100,
"nextLink": null,
"error": null
}
```
> Access via `result["flows"]`. `id` is a plain UUID --- use directly as `flowName`.
>
> `mode` indicates the access scope used (`"owner"` or `"admin"`).
>
> Parameters added in newer server versions:
> - `search`: filter by display name server-side.
> - `mode`: `owner` for flows owned by the MCP identity; `admin` for all flows
> visible to an admin account.
> - `timeoutSeconds`: return partial results with `nextLink` instead of waiting
> on very large environments.
> - `continuationUrl`: pass the previous `nextLink` to continue the same query.
### `list_store_flows`
Response: **direct array** (no wrapper).
```json
[
{
"id": "3991358a-f603-e49d-b1ed-a9e4f72e2dcb.0757041a-8ef2-cf74-ef06-06881916f371",
"displayName": "Admin | Sync Template v3 (Solutions)",
"state": "Started",
"triggerType": "OpenApiConnectionWebhook",
"environmentName": "3991358a-f603-e49d-b1ed-a9e4f72e2dcb",
"runPeriodTotal": 100,
"createdTime": "2023-08-18T01:18:17Z",
"lastModifiedTime": "2023-08-18T12:47:42Z"
}
]
```
> **`id` format**: `<environmentId>.<flowId>` --- split on the first `.` to extract the flow UUID:
> `flow_id = item["id"].split(".", 1)[1]`
### `get_store_flow`
Response: single flow metadata from cache (selected fields).
```json
{
"id": "<environmentId>.<flowId>",
"displayName": "My Flow",
"state": "Started",
"triggerType": "Recurrence",
"runPeriodTotal": 100,
"runPeriodFailRate": 0.1,
"runPeriodSuccessRate": 0.9,
"runPeriodFails": 10,
"runPeriodSuccess": 90,
"runPeriodDurationAverage": 29410.8,
"runPeriodDurationMax": 158900.0,
"runError": "{\"code\": \"EACCES\", ...}",
"description": "Flow description",
"tier": "Premium",
"complexity": "{...}",
"actions": 42,
"connections": ["sharepointonline", "office365"],
"owners": ["user@contoso.com"],
"createdBy": "user@contoso.com"
}
```
> `runPeriodDurationAverage` / `runPeriodDurationMax` are in **milliseconds** (divide by 1000).
> `runError` is a **JSON string** --- parse with `json.loads()`.
---
## Flow Definition (Live API)
### `get_live_flow`
Response: full flow definition from PA API.
```json
{
"name": "<flow-guid>",
"properties": {
"displayName": "My Flow",
"state": "Started",
"definition": {
"triggers": { "..." },
"actions": { "..." },
"parameters": { "..." }
},
"connectionReferences": { "..." }
}
}
```
### `update_live_flow`
**Create mode**: Omit `flowName` --- creates a new flow. `definition` and `displayName` required.
**Update mode**: Provide `flowName` --- PATCHes existing flow.
Response:
```json
{
"created": false,
"flowKey": "<environmentId>.<flowId>",
"updated": ["definition", "connectionReferences"],
"displayName": "My Flow",
"state": "Started",
"definition": { "...full definition..." },
"error": null
}
```
> `error` is **always present** but may be `null`. Check `result.get("error") is not None`.
>
> On create: `created` is the new flow GUID (string). On update: `created` is `false`.
>
> Required fields can vary by server version. Use `tool_search` with
> `select:update_live_flow` before creating or patching a flow; if a description
> is required, include either the new description or the existing one from
> `get_live_flow`.
>
> The flow description is part of the workflow definition (`definition.description`),
> not a top-level tool argument in current schemas.
### `add_live_flow_to_solution`
Migrates a non-solution flow into a solution. Returns error if already in a solution.
Use this after creating a Copilot Studio Skills-triggered flow that must be
discoverable as an agent tool. Pass `solutionId` for the target solution. If the
server supports omitting `solutionId`, it uses the environment's default solution;
prefer an explicit unmanaged solution for production ALM.
This tool changes solution membership only. It does not validate the trigger
schema, publish a Copilot Studio agent, or prove that the flow is callable by the
agent.
---
## Connector Operation Discovery
### `describe_live_connector`
Describes a connector/API and its operations. Use it before creating connector
actions instead of guessing operation JSON.
Common modes:
| Call shape | Use |
|---|---|
| `search="send email"` without `connectorName` | Search operations across connectors |
| `connectorName="shared_sharepointonline"` | Compact operation catalog for one connector |
| `operationId="GetItems"` | Expanded schema for one operation |
| `variant="flowbot_chat"` | Authored example for one operation variant |
The operation detail can include:
- `hint`: authored guidance from the connector hints table.
- `exampleDefinition`: copy-ready action/trigger shape when available.
- Dynamic metadata with `nextTool=get_live_dynamic_options` or
`nextTool=get_live_dynamic_properties`.
### `get_live_dynamic_options`
Resolves live dropdown/list options for connector parameters. Use this for
IDs selected from lists, such as SharePoint sites/lists, Teams teams/channels,
or other `x-ms-dynamic-list` / `x-ms-dynamic-values` parameters.
Pass the `dynamicMetadata` object returned by `describe_live_connector`, the
connection id from `list_live_connections`, and any already-resolved dependent
parameters.
### `get_live_dynamic_properties`
Resolves live schema/field properties for connector parameters. Use this for
dynamic field sets such as SharePoint list item columns after the site and list
are known.
Useful parameters:
- `parameters`: dependent values, for example `{ "dataset": "<site-url>",
"table": "<list-id>" }`.
- `propertyName`: request one field after inspecting the compact response.
- `includeRaw`: include raw connector schema only when needed; it can be large.
---
## Run History & Monitoring
### `get_live_flow_runs`
Response: direct array of runs (newest first).
```json
[{
"name": "<run-id>",
"status": "Succeeded|Failed|Running|Cancelled",
"startTime": "2026-02-25T06:13:38Z",
"endTime": "2026-02-25T06:14:02Z",
"triggerName": "Recurrence",
"error": null
}]
```
> `top` defaults to **30** and auto-paginates for higher values. Set `top: 300`
> for 24-hour coverage on flows running every 5 minutes.
>
> Run ID field is **`name`** (not `runName`). Use this value as the `runName`
> parameter in other tools.
### `get_live_flow_run_error`
Response: structured error breakdown for a failed run.
```json
{
"runName": "08584296068667933411438594643CU15",
"failedActions": [
{
"actionName": "Apply_to_each_prepare_workers",
"status": "Failed",
"error": {"code": "ActionFailed", "message": "An action failed."},
"code": "ActionFailed",
"startTime": "2026-02-25T06:13:52Z",
"endTime": "2026-02-25T06:15:24Z"
},
{
"actionName": "HTTP_find_AD_User_by_Name",
"status": "Failed",
"code": "NotSpecified",
"startTime": "2026-02-25T06:14:01Z",
"endTime": "2026-02-25T06:14:05Z"
}
],
"allActions": [
{"actionName": "Apply_to_each", "status": "Skipped"},
{"actionName": "Compose_WeekEnd", "status": "Succeeded"},
{"actionName": "HTTP_find_AD_User_by_Name", "status": "Failed"}
]
}
```
> `failedActions` is ordered outer-to-inner --- the **last entry is the root cause**.
> Use `failedActions[-1]["actionName"]` as the starting point for diagnosis.
### `get_live_flow_run_action_outputs`
Response: array of action detail objects.
```json
[
{
"actionName": "Compose_WeekEnd_now",
"status": "Succeeded",
"startTime": "2026-02-25T06:13:52Z",
"endTime": "2026-02-25T06:13:52Z",
"error": null,
"inputs": "Mon, 25 Feb 2026 06:13:52 GMT",
"outputs": "Mon, 25 Feb 2026 06:13:52 GMT"
}
]
```
> **`actionName` is optional**: omit it to return top-level actions in the run.
> Provide it for a specific action. If that action runs inside a foreach, the
> tool can return every repetition of that action across iterations; pass
> `iterationIndex` to pin to one zero-based iteration.
>
> Outputs can be very large (50 MB+) for bulk-data actions. Use 120s+ timeout.
---
## Run Control
### `resubmit_live_flow_run`
Response: `{ flowKey, resubmitted: true, runName, triggerName }`
### `cancel_live_flow_run`
Cancels a `Running` flow run.
> Do NOT cancel runs waiting for an adaptive card response --- status `Running`
> is normal while a Teams card is awaiting user input.
---
## HTTP Trigger Tools
### `get_live_flow_http_schema`
Deprecated. Prefer `get_live_flow` and inspect the `Request` trigger's
`inputs.schema` plus any `Response` actions directly from the definition.
Response keys:
```
flowKey - Flow GUID
displayName - Flow display name
triggerName - Trigger action name (e.g. "manual")
triggerType - Trigger type (e.g. "Request")
triggerKind - Trigger kind (e.g. "Http")
requestMethod - HTTP method (e.g. "POST")
relativePath - Relative path configured on the trigger (if any)
requestSchema - JSON schema the trigger expects as POST body
requestHeaders - Headers the trigger expects
responseSchemas - Array of JSON schemas defined on Response action(s)
responseSchemaCount - Number of Response actions that define output schemas
```
> The request body schema is in `requestSchema` (not `triggerSchema`).
### `get_live_flow_trigger_url`
Deprecated. Prefer `trigger_live_flow` when you need to invoke an HTTP-triggered
flow; it fetches the current callback URL internally.
Returns the signed callback URL for HTTP-triggered flows. Response includes
`flowKey`, `triggerName`, `triggerType`, `triggerKind`, `triggerMethod`, `triggerUrl`.
### `trigger_live_flow`
Response keys: `flowKey`, `triggerName`, `triggerUrl`, `requiresAadAuth`, `authType`,
`responseStatus`, `responseBody`.
> **Only works for `Request` (HTTP) triggers.** Returns an error for Recurrence
> and other trigger types: `"only HTTP Request triggers can be invoked via this tool"`.
> `Button`-kind triggers return `ListCallbackUrlOperationBlocked`.
>
> `responseStatus` + `responseBody` contain the flow's Response action output.
> AAD-authenticated triggers are handled automatically.
>
> **Content-type note**: The body is sent as `application/octet-stream` (raw),
> not `application/json`. Flows with a trigger schema that has `required` fields
> will reject the request with `InvalidRequestContent` (400) because PA validates
> `Content-Type` before parsing against the schema. Flows without a schema, or
> flows designed to accept raw input (e.g. Baker-pattern flows that parse the body
> internally), will work fine. The flow receives the JSON as base64-encoded
> `$content` with `$content-type: application/octet-stream`.
---
## Flow State Management
### `set_live_flow_state`
Start or stop a Power Automate flow via the live PA API. Does **not** require
a Power Clarity workspace — works for any flow the impersonated account can access.
Reads the current state first and only issues the start/stop call if a change is
actually needed.
Parameters: `environmentName`, `flowName`, `state` (`"Started"` | `"Stopped"`) — all required.
Response:
```json
{
"flowName": "6321ab25-7eb0-42df-b977-e97d34bcb272",
"environmentName": "Default-26e65220-...",
"requestedState": "Started",
"actualState": "Started"
}
```
> **Use this tool** — not `update_live_flow` — to start or stop a flow.
> `update_live_flow` only changes displayName/definition; the PA API ignores
> state passed through that endpoint.
### `set_store_flow_state`
Start or stop a flow via the live PA API **and** persist the updated state back
to the Power Clarity cache. Same parameters as `set_live_flow_state` but requires
a Power Clarity workspace.
Response (different shape from `set_live_flow_state`):
```json
{
"flowKey": "<environmentId>.<flowId>",
"requestedState": "Stopped",
"currentState": "Stopped",
"flow": { /* full gFlows record, same shape as get_store_flow */ }
}
```
> Prefer `set_live_flow_state` when you only need to toggle state — it's
> simpler and has no subscription requirement.
>
> Use `set_store_flow_state` when you need the cache updated immediately
> (without waiting for the next daily scan) AND want the full updated
> governance record back in the same call — useful for workflows that
> stop a flow and immediately tag or inspect it.
---
## Store Tools --- FlowStudio for Teams Only
### `get_store_flow_summary`
Response: aggregated run statistics.
```json
{
"totalRuns": 100,
"failRuns": 10,
"failRate": 0.1,
"averageDurationSeconds": 29.4,
"maxDurationSeconds": 158.9,
"firstFailRunRemediation": "<hint or null>"
}
```
### `get_store_flow_runs`
Cached run history for the last N days with duration and remediation hints.
### `get_store_flow_errors`
Cached failed-only runs with failed action names and remediation hints.
### `get_store_flow_trigger_url`
Trigger URL from cache (instant, no PA API call).
### `update_store_flow`
Update governance metadata (description, tags, monitor flag, notification rules, business impact).
### `list_store_makers` / `get_store_maker`
Maker (citizen developer) discovery and detail.
### `list_store_power_apps`
List all Power Apps canvas apps from the cache.
---
## Behavioral Notes
Non-obvious behaviors discovered through real API usage. These are things
tool schemas cannot tell you.
### `get_live_flow_run_action_outputs`
- **`actionName` is optional**: omit to get top-level actions, provide to get one
action. For actions inside foreach loops, a named action may return multiple
repetitions; use `iterationIndex` to pin to one iteration.
- Outputs can be 50 MB+ for bulk-data actions --- always use 120s+ timeout.
### `update_live_flow`
- Required fields can vary by server version; confirm with `tool_search`
(`select:update_live_flow`) before create/update. If `description` is required,
preserve the existing description when patching.
- `error` key is **always present** in response --- `null` means success.
Do NOT check `if "error" in result`; check `result.get("error") is not None`.
- On create, `created` = new flow GUID (string). On update, `created` = `false`.
- **Cannot change flow state.** Only updates displayName, definition, and
connectionReferences. Use `set_live_flow_state` to start/stop a flow.
### `trigger_live_flow`
- **Only works for HTTP Request triggers.** Returns error for Recurrence, connector,
and other trigger types.
- AAD-authenticated triggers are handled automatically (impersonated Bearer token).
### `get_live_flow_runs`
- `top` defaults to **30** with automatic pagination for higher values.
- Run ID field is `name`, not `runName`. Use this value as `runName` in other tools.
- Runs are returned newest-first.
### Teams `PostMessageToConversation` (via `update_live_flow`)
- **"Chat with Flow bot"**: `body/recipient` = `"user@domain.com;"` (string with trailing semicolon).
- **"Channel"**: `body/recipient` = `{"groupId": "...", "channelId": "..."}` (object).
- `poster`: `"Flow bot"` for Workflows bot identity, `"User"` for user identity.
### `list_live_connections`
- For build workflows, pass `environmentName`; omitting it inventories
connections across environments.
- Use `search=<connector/account>` to get smaller output and paste-ready
`connectionReferenceTemplate` / `hostTemplate` values.
- `id` is the value you need for `connectionName` in `connectionReferences`.
- `connectorName` maps to apiId: `"/providers/Microsoft.PowerApps/apis/" + connectorName`.
@@ -0,0 +1,373 @@
---
name: flowstudio-power-automate-monitoring
description: >-
Pro+ subscription required. Tenant-wide Power Automate monitoring using the
FlowStudio MCP cached store: failure rates, run-health trends, maker/app
inventory, inactive owners, and compliance/health reports. Use only for
aggregated tenant views. For one environment, one flow, run control, or
root-cause debugging, use flowstudio-power-automate-mcp, flowstudio-power-automate-debug, or the
server monitor-flow bundle. Requires FlowStudio for Teams or MCP Pro+.
---
# Power Automate Monitoring with FlowStudio MCP
Monitor flow health, track failure rates, and inventory tenant assets through
the FlowStudio MCP **cached store** — fast reads, no PA API rate limits, and
enriched with governance metadata and remediation hints.
> **⚠️ Pro+ subscription required.** This skill calls `store_*` tools that
> only work for FlowStudio for Teams or MCP Pro+ subscribers.
>
> **If the user does not have Pro+ access:** the first `store_*` tool call
> will return a 403/404 error. When that happens:
> 1. STOP calling store tools
> 2. Tell the user this feature requires a Pro+ subscription
> 3. Link them to https://mcp.flowstudio.app/pricing
> 4. If their question can be answered with live tools (e.g. "list flows in
> one environment"), offer to use the `flowstudio-power-automate-mcp` skill instead
>
> **Discovery:** load tool schemas via `tool_search` rather than `tools/list`
> call with `query: "select:list_store_flows,get_store_flow_summary"` for the
> common monitoring tools, or load the full set with `query: "skill:governance"`
> (the server's governance bundle covers most monitoring reads too — this skill
> and `flowstudio-power-automate-governance` share the underlying tool family). This skill
> covers response shapes, behavioral notes, and workflow patterns — things
> `tool_search` cannot tell you. If this document disagrees with a real API
> response, the API wins.
---
## How Monitoring Works
Flow Studio scans the Power Automate API daily for each subscriber and caches
the results. There are two levels:
- **All flows** get metadata scanned: definition, connections, owners, trigger
type, and aggregate run statistics (`runPeriodTotal`, `runPeriodFailRate`,
etc.). Environments, apps, connections, and makers are also scanned.
- **Monitored flows** (`monitor: true`) additionally get per-run detail:
individual run records with status, duration, failed action names, and
remediation hints. This is what populates `get_store_flow_runs` and
`get_store_flow_summary`.
**Data freshness:** Check the `scanned` field on `get_store_flow` to see when
a flow was last scanned. If stale, the scanning pipeline may not be running.
**Enabling monitoring:** Set `monitor: true` via `update_store_flow` or the
Flow Studio for Teams app
([how to select flows](https://learn.flowstudio.app/teams-monitoring)).
**Designating critical flows:** Use `update_store_flow` with `critical=true`
on business-critical flows. This enables the governance skill's notification
rule management to auto-configure failure alerts on critical flows.
---
## Tools
| Tool | Purpose |
|---|---|
| `list_store_flows` | List flows with failure rates and monitoring filters |
| `get_store_flow` | Full cached record: run stats, owners, tier, connections, definition (`triggerUrl` field included) |
| `get_store_flow_summary` | Aggregated run stats: success/fail rate, avg/max duration |
| `get_store_flow_runs` | Per-run history with duration, status, failed actions, remediation (filter `status="Failed"` for errors-only view) |
| `update_store_flow` | Set monitor flag, notification rules, tags, governance metadata |
| `list_store_environments` | All Power Platform environments |
| `list_store_connections` | All connections |
| `list_store_makers` | All makers (citizen developers) |
| `get_store_maker` | Maker detail: flow/app counts, licenses, account status |
| `list_store_power_apps` | All Power Apps canvas apps |
> For start/stop, use `set_live_flow_state` from the `monitor-flow` bundle
> (`tool_search query: "select:set_live_flow_state"`) — the cache resyncs on
> the next scan. The previous `set_store_flow_state` convenience wrapper is
> deprecated.
---
## Store vs Live
| Question | Use Store | Use Live |
|---|---|---|
| How many flows are failing? | `list_store_flows` | — |
| What's the fail rate over 30 days? | `get_store_flow_summary` | — |
| Show error history for a flow | `get_store_flow_runs` (filter `status="Failed"`) | — |
| Who built this flow? | `get_store_flow` → parse `owners` | — |
| Read the full flow definition | `get_store_flow` has it (JSON string) | `get_live_flow` (structured) |
| Inspect action inputs/outputs from a run | — | `get_live_flow_run_action_outputs` |
| Resubmit a failed run | — | `resubmit_live_flow_run` |
> Store tools answer "what happened?" and "how healthy is it?"
> Live tools answer "what exactly went wrong?" and "fix it now."
> If `get_store_flow_runs` or `get_store_flow_summary` return empty results,
> check: (1) is `monitor: true` on the flow? and (2) is the `scanned` field
> recent? Use `get_store_flow` to verify both.
---
## Response Shapes
### `list_store_flows`
Direct array. Filters: `monitor` (bool), `rule_notify_onfail` (bool),
`rule_notify_onmissingdays` (bool).
```json
[
{
"id": "Default-<envGuid>.<flowGuid>",
"displayName": "Stripe subscription updated",
"state": "Started",
"triggerType": "Request",
"triggerUrl": "https://...",
"tags": ["#operations", "#sensitive"],
"environmentName": "Default-aaaaaaaa-...",
"monitor": true,
"runPeriodFailRate": 0.012,
"runPeriodTotal": 82,
"createdTime": "2025-06-24T01:20:53Z",
"lastModifiedTime": "2025-06-24T03:51:03Z"
}
]
```
> `id` format: `Default-<envGuid>.<flowGuid>`. Split on first `.` to get
> `environmentName` and `flowName`.
>
> `triggerUrl` and `tags` are optional. Some entries are sparse (just `id` +
> `monitor`) — skip entries without `displayName`.
>
> Tags on `list_store_flows` are auto-extracted from the flow's `description`
> field (maker hashtags like `#operations`). Tags written via
> `update_store_flow(tags=...)` are stored separately and only visible on
> `get_store_flow` — they do NOT appear in the list response.
### `get_store_flow`
Full cached record. Key fields:
| Category | Fields |
|---|---|
| Identity | `name`, `displayName`, `environmentName`, `state`, `triggerType`, `triggerKind`, `tier`, `sharingType` |
| Run stats | `runPeriodTotal`, `runPeriodFails`, `runPeriodSuccess`, `runPeriodFailRate`, `runPeriodSuccessRate`, `runPeriodDurationAverage`/`Max`/`Min` (milliseconds), `runTotal`, `runFails`, `runFirst`, `runLast`, `runToday` |
| Governance | `monitor` (bool), `rule_notify_onfail` (bool), `rule_notify_onmissingdays` (number), `rule_notify_email` (string), `log_notify_onfail` (ISO), `description`, `tags` |
| Freshness | `scanned` (ISO), `nextScan` (ISO) |
| Lifecycle | `deleted` (bool), `deletedTime` (ISO) |
| JSON strings | `actions`, `connections`, `owners`, `complexity`, `definition`, `createdBy`, `security`, `triggers`, `referencedResources`, `runError` — all require `json.loads()` to parse |
> Duration fields (`runPeriodDurationAverage`, `Max`, `Min`) are in
> **milliseconds**. Divide by 1000 for seconds.
>
> `runError` contains the last run error as a JSON string. Parse it:
> `json.loads(record["runError"])` — returns `{}` when no error.
### `get_store_flow_summary`
Aggregated stats over a time window (default: last 7 days).
```json
{
"flowKey": "Default-<envGuid>.<flowGuid>",
"windowStart": null,
"windowEnd": null,
"totalRuns": 82,
"successRuns": 81,
"failRuns": 1,
"successRate": 0.988,
"failRate": 0.012,
"averageDurationSeconds": 2.877,
"maxDurationSeconds": 9.433,
"firstFailRunRemediation": null,
"firstFailRunUrl": null
}
```
> Returns all zeros when no run data exists for this flow in the window.
> Use `startTime` and `endTime` (ISO 8601) parameters to change the window.
### `get_store_flow_runs`
Direct array of cached run records. Parameters: `startTime`, `endTime`,
`status` (array — pass `["Failed"]` for an errors-only view, `["Succeeded"]`,
or omit for all).
> Returns `[]` when no run data exists in the window.
### Trigger URL
Read the `triggerUrl` field directly from `get_store_flow` (cached) or
`get_live_flow` (live). It is `null` for non-HTTP triggers.
### Starting / stopping a flow
Use `set_live_flow_state` from the `monitor-flow` server bundle. The cache
catches up on the next daily scan; if you need cache freshness sooner, call
`get_live_flow` after the state change to confirm and let the next scan sync.
### `update_store_flow`
Updates governance metadata. Only provided fields are updated (merge).
Returns the full updated record (same shape as `get_store_flow`).
Settable fields: `monitor` (bool), `rule_notify_onfail` (bool),
`rule_notify_onmissingdays` (number, 0=disabled),
`rule_notify_email` (comma-separated), `description`, `tags`,
`businessImpact`, `businessJustification`, `businessValue`,
`ownerTeam`, `ownerBusinessUnit`, `supportGroup`, `supportEmail`,
`critical` (bool), `tier`, `security`.
### `list_store_environments`
Direct array.
```json
[
{
"id": "Default-aaaaaaaa-...",
"displayName": "Flow Studio (default)",
"sku": "Default",
"type": "NotSpecified",
"location": "australia",
"isDefault": true,
"isAdmin": true,
"isManagedEnvironment": false,
"createdTime": "2017-01-18T01:06:46Z"
}
]
```
> `sku` values: `Default`, `Production`, `Developer`, `Sandbox`, `Teams`.
### `list_store_connections`
Direct array. Can be very large (1500+ items).
```json
[
{
"id": "<environmentId>.<connectionId>",
"displayName": "user@contoso.com",
"createdBy": "{\"id\":\"...\",\"displayName\":\"...\",\"email\":\"...\"}",
"environmentName": "...",
"statuses": "[{\"status\":\"Connected\"}]"
}
]
```
> `createdBy` and `statuses` are **JSON strings** — parse with `json.loads()`.
### `list_store_makers`
Direct array.
```json
[
{
"id": "09dbe02f-...",
"displayName": "Sample Maker",
"mail": "maker@contoso.com",
"deleted": false,
"ownerFlowCount": 199,
"ownerAppCount": 209,
"userIsServicePrinciple": false
}
]
```
> Deleted makers have `deleted: true` and no `displayName`/`mail` fields.
### `get_store_maker`
Full maker record. Key fields: `displayName`, `mail`, `userPrincipalName`,
`ownerFlowCount`, `ownerAppCount`, `accountEnabled`, `deleted`, `country`,
`firstFlow`, `firstFlowCreatedTime`, `lastFlowCreatedTime`,
`firstPowerApp`, `lastPowerAppCreatedTime`,
`licenses` (JSON string of M365 SKUs).
### `list_store_power_apps`
Direct array.
```json
[
{
"id": "<environmentId>.<appId>",
"displayName": "My App",
"environmentName": "...",
"ownerId": "09dbe02f-...",
"ownerName": "Catherine Han",
"appType": "Canvas",
"sharedUsersCount": 0,
"createdTime": "2023-08-18T01:06:22Z",
"lastModifiedTime": "2023-08-18T01:06:22Z",
"lastPublishTime": "2023-08-18T01:06:22Z"
}
]
```
---
## Common Workflows
### Find unhealthy flows
```
1. list_store_flows
2. Filter where runPeriodFailRate > 0.1 and runPeriodTotal >= 5
3. Sort by runPeriodFailRate descending
4. For each: get_store_flow for full detail
```
### Check a specific flow's health
```
1. get_store_flow → check scanned (freshness), runPeriodFailRate, runPeriodTotal
2. get_store_flow_summary → aggregated stats with optional time window
3. get_store_flow_runs(status=["Failed"]) → per-run failure detail with remediation hints
4. If deeper diagnosis needed → switch to live tools:
get_live_flow_runs → get_live_flow_run_action_outputs
```
### Enable monitoring on a flow
```
1. update_store_flow with monitor=true
2. Optionally set rule_notify_onfail=true, rule_notify_email="user@domain.com"
3. Run data will appear after the next daily scan
```
### Daily health check
```
1. list_store_flows
2. Flag flows with runPeriodFailRate > 0.2 and runPeriodTotal >= 3
3. Flag monitored flows with state="Stopped" (may indicate auto-suspension)
4. For critical failures → get_store_flow_runs(status=["Failed"]) for remediation hints
```
### Maker audit
```
1. list_store_makers
2. Identify deleted accounts still owning flows (deleted=true, ownerFlowCount > 0)
3. get_store_maker for full detail on specific users
```
### Inventory
```
1. list_store_environments → environment count, SKUs, locations
2. list_store_flows → flow count by state, trigger type, fail rate
3. list_store_power_apps → app count, owners, sharing
4. list_store_connections → connection count per environment
```
---
## Related Skills
- `flowstudio-power-automate-mcp` — Foundation skill: connection setup, MCP helper, tool discovery
- `flowstudio-power-automate-debug` — Deep diagnosis with action-level inputs/outputs (live API)
- `flowstudio-power-automate-build` — Build and deploy flow definitions
- `flowstudio-power-automate-governance` — Governance metadata, tagging, notification rules, CoE patterns