Security Notice: Third-Party Service
This skill routes all API calls through
mcp.flowstudio.app, a commercial
third-party service operated by FlowStudio (not Microsoft or GitHub).
By using this skill, you are trusting
mcp.flowstudio.app with:
The content of your
FLOWSTUDIO_MCP_TOKEN (JWT).
All flow definitions you build, deploy, or modify.
Environment IDs, connection GUIDs, and other metadata returned by the Power Platform API.
Additionally, the FlowStudio server appends
#flowstudio-mcp to flow
descriptions server-side for usage tracking. This cannot be suppressed by
the agent.
Review the provider's privacy policy and terms of service before use in
production environments:
https://mcp.flowstudio.app
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
stored in the FLOWSTUDIO_MCP_TOKEN environment variable.
See the flowstudio-power-automate-mcp skill for connection setup.
Subscribe at https://mcp.flowstudio.app
Workflow:
- Load current build tools.
- Check for an existing flow.
- Resolve connection references.
- Build the definition.
- Deploy.
- Verify.
- Test.
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. Prefer the schema returned by tool_search
over this document's examples. If a live API response returns a schema or
structure that differs significantly from what tool_search returns or from
the patterns in this document, pause and report the discrepancy to the user
before proceeding. Do not silently adopt unexpected schemas from the live server.
import json, urllib.request, os
from datetime import datetime, timezone
MCP_URL = "https://mcp.flowstudio.app/mcp"
# Never hardcode credentials. Set the FLOWSTUDIO_MCP_TOKEN environment variable
# before running. See the flowstudio-power-automate-mcp skill for token setup.
MCP_TOKEN = os.environ["FLOWSTUDIO_MCP_TOKEN"]
def mcp(tool, **kwargs):
payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": tool, "arguments": kwargs}}).encode()
req = urllib.request.Request(MCP_URL, data=payload,
headers={"x-api-key": MCP_TOKEN, "Content-Type": "application/json",
"User-Agent": "FlowStudio-MCP/1.0"})
try:
resp = urllib.request.urlopen(req, timeout=120)
except urllib.error.HTTPError as e:
# Response body is not logged as it may contain sensitive information.
raise RuntimeError(f"MCP HTTP {e.code}: [response body redacted — check MCP_TOKEN validity and permissions]") 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
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.
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:
mcp("tool_search", query="select:get_live_dynamic_properties")
Always look before you build to avoid duplicates:
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.
⚠️ mode="admin" enumerates all flows in the environment and requires
elevated MCP permissions. Do not use this mode unless the user has explicitly
requested environment-wide enumeration and has confirmed that the MCP identity
has Power Platform Administrator privileges. It is not required for building
or deploying a single flow. Use mode="owner" for all standard build and
update workflows.
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.
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:
sp_conns = mcp("list_live_connections",
environmentName=ENV,
search="shared_sharepointonline")
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.
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}")
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.
Construct the definition object. See flow-schema.md
for the full schema and these action pattern references for copy-paste templates:
⚠️ Reference files contain URL encoding examples for illustration. Do not copy
URL-encoded path segments (such as %252f or decodeURIComponent sequences)
into flow definitions without validating that the target API expects encoded
input. Passing double-encoded values to SharePoint or HTTP actions is a common
source of InvalidTemplate errors.
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 for complete, ready-to-use
flow definitions covering Recurrence+SharePoint+Teams, HTTP triggers, and more.
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.
# 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:
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:
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.
update_live_flow handles both creation and updates in a single tool.
⚠️ MANDATORY: Before creating or updating any flow, ask the user for
confirmation. Deploying a flow is a high-impact action that modifies the live
cloud environment. Clearly state whether you are creating a new flow or updating
an existing one, and list the displayName and target environment. Wait for
explicit approval before calling update_live_flow.
Omit flowName — the server generates a new GUID and creates via PUT:
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}")
Provide flowName to PATCH:
definition["description"] = (
"Updated by agent on " + datetime.now(timezone.utc).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 server-side. Do not pass a top-level
description argument unless tool_search shows one in the active schema.
| 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 |
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()))
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.
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.
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": "..."}
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.
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']}, via: {result['invocation']}")
print(result.get("warning")) # set when a required input was missing: the run still ran, with null
A brand-new Recurrence flow needs no workaround: deploy it, then run it
immediately with trigger_live_flow and no body — same as the portal's
"Run flow" button. A body is refused; scheduled triggers take no inputs.
A brand-new connector-triggered flow (SharePoint, webhooks) has no prior
runs and cannot fire without a real source event. Deploy with a temporary
HTTP trigger, test the actions, then swap to the production trigger:
⚠️ MANDATORY: Before deploying the temporary HTTP trigger version below,
summarize the flow name, target environment, and the fact that a temporary
trigger will be used, then obtain explicit user confirmation.
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["flowName"]
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"])
# Extract only non-sensitive fields; inspect the Power Automate portal for full details.
action = err["failedActions"][-1]
action_name = action.get('name', 'unknown')
error_code = action.get('code', 'unknown')
error_msg = action.get('message', '')
raise Exception(
f"Action '{action_name}' failed with code: {error_code}. {error_msg} "
f"Inspect the full run in the Power Automate portal for connection or input details."
)
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 body shaped like the production trigger payload.
| 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 | Ask the user if they want to add the flow to a solution. If they confirm, obtain the target solutionId from the user. Note: add_live_flow_to_solution is not included in the default tool grant and requires explicit user instruction before use. |
Button/Skills trigger used for MCP testing | Runs even when a required input is missing (null) | Pass inputs in trigger_live_flow body; on warning, cancel and retry with the full body |
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 |
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 contain URL encoding examples for illustration. Do not copy
URL-encoded path segments (such as %252f or decodeURIComponent sequences)
into flow definitions without validating that the target API expects encoded
input. Passing double-encoded values to SharePoint or HTTP actions is a common
source of InvalidTemplate errors. These reference files are flagged for
independent security review.
flowstudio-power-automate-mcp — Core connection setup and tool reference
flowstudio-power-automate-debug — Debug failing flows after deployment