Flowstudio Power Automate Build
GitHubOfficialFirst committed by ninihen1Distributed by GitHub
- Skill
- Workflow
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured workflow for constructing and managing Power Automate cloud flows entirely through the FlowStudio MCP server. It enables developers to scaffold flow definitions, manage connection references, and perform deployments and testing without needing to access the Power Automate web portal. The skill emphasizes programmatic interaction, ensuring that flow definitions are built, validated, and deployed using consistent patterns.
What it does
- Scaffolds new Power Automate flow definitions from scratch.
- Manages and wires connection references for various connectors.
- Updates and patches existing flow definitions.
- Automates the deployment of flows to specific environments.
- Verifies flow state and performs testing via run resubmission or custom triggers.
When to use it
- When you need to automate the creation or modification of Power Automate workflows.
- When you want to manage flow deployments programmatically via an MCP server.
- When you need to update existing flows without using the web-based designer.
- When you require a repeatable process for testing and verifying flow logic.
Advantages
- Enables infrastructure-as-code practices for Power Automate flows.
- Reduces manual effort by eliminating the need to use the web portal.
- Provides a consistent, programmatic approach to flow construction and deployment.
- Supports automated testing and validation of flow logic.
Usage examples
- “Create a new Power Automate flow that triggers on a SharePoint item creation and posts a message to Teams.”
- The agent will list available connections, construct the flow definition with the appropriate SharePoint and Teams actions, and deploy it to the specified environment.
- “Update the existing flow 'Overdue Invoice Notifications' to add a new logging step.”
- The agent will retrieve the current flow definition, patch it with the new action, and redeploy the updated version.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/flowstudio-power-automate-build/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip flowstudio-power-automate-build.zip -d .agents/skills
Documentation by ninihen1
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:
Load current build tools.
Check for an existing flow.
Resolve connection references.
Build the definition.
Deploy.
Verify.
Test.
Source of Truth
Always call
list_skills/tool_searchfirst 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 withtool_searchor a real API response, the API wins.
Python Helper
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-xxxxxxxxxxxx0. 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.
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")1. Safety Check: Does the Flow Already Exist?
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 = NoneFor 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_connectionsfirst — 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
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")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
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
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 for the full schema and these action pattern references for copy-paste templates:
action-patterns-core.md — Variables, control flow, expressions
action-patterns-data.md — Array transforms, HTTP, parsing
action-patterns-connectors.md — SharePoint, Outlook, Teams, Approvals
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.
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.
# 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.
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:
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:
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_flowalways returns anerrorkey.null(PythonNone) means success — do not treat the presence of the key as failure.⚠️ Flow description lives at
definition["description"]. The current server appends#flowstudio-mcpfor usage tracking. Do not pass a top-leveldescriptionargument unlesstool_searchshows 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
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_floworresubmit_live_flow_run.
Updated flows (have prior runs) — ANY trigger type
Use
resubmit_live_flow_runfirst. 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": "..."}HTTP, Button, and PowerApps 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.
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 nullBrand-new non-HTTP flows
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:
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"])
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 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 | 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 |
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 | "[email protected];" |
| Channel | Object with groupId and channelId | {"groupId": "...", "channelId": "..."} |
Common mistake: passing
{"to": "[email protected]"}for "Chat with Flow bot" returns a 400GraphUserDetailNotFounderror. The API expects a plain string.
Reference Files
flow-schema.md — Full flow definition JSON schema
trigger-types.md — Trigger type templates
action-patterns-core.md — Variables, control flow, expressions
action-patterns-data.md — Array transforms, HTTP, parsing
action-patterns-connectors.md — SharePoint, Outlook, Teams, Approvals
build-patterns.md — Complete flow definition templates (Recurrence+SP+Teams, HTTP trigger)
Related Skills
flowstudio-power-automate-mcp— Core connection setup and tool referenceflowstudio-power-automate-debug— Debug failing flows after deployment
Files
- ATTRIBUTION.mdAdded by AutoGineer895 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md19.5 KiB
- references/action-patterns-connectors.md18.0 KiB
- references/action-patterns-core.md16.2 KiB
- references/action-patterns-data.md18.6 KiB
- references/build-patterns.md2.7 KiB
- references/flow-schema.md5.1 KiB
- references/trigger-types.md5.3 KiB
references/action-patterns-core.md
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 yourconnectionReferencesmap (e.g.shared_teams,shared_office365) — NOT the connection GUID.
Data & Variables
Compose (Store a Value)
"Compose_My_Value": {
"type": "Compose",
"runAfter": {},
"inputs": "@variables('myVar')"
}Reference: @outputs('Compose_My_Value')
Initialize Variable
"Init_Counter": {
"type": "InitializeVariable",
"runAfter": {},
"inputs": {
"variables": [{
"name": "counter",
"type": "Integer",
"value": 0
}]
}
}Types: "Integer", "Float", "Boolean", "String", "Array", "Object"
Set Variable
"Set_Counter": {
"type": "SetVariable",
"runAfter": {},
"inputs": {
"name": "counter",
"value": "@add(variables('counter'), 1)"
}
}Append to Array Variable
"Collect_Item": {
"type": "AppendToArrayVariable",
"runAfter": {},
"inputs": {
"name": "resultArray",
"value": "@item()"
}
}Increment Variable
"Increment_Counter": {
"type": "IncrementVariable",
"runAfter": {},
"inputs": {
"name": "counter",
"value": 1
}
}Use
IncrementVariable(notSetVariablewithadd()) for counters inside loops — it is atomic and avoids expression errors when the variable is used elsewhere in the same iteration.valuecan 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)
"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
"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.
"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. UserunAfter: {"MyScope": ["Failed", "TimedOut"]}on a follow-up action to create try/catch semantics without a Terminate.
Foreach (Sequential)
"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)
"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
repetitionsto control how many items are processed simultaneously. Practical values:5–10for external API calls (respect rate limits),20–50for internal/fast operations. OmitruntimeConfiguration.concurrencyentirely for the platform default (currently 50). Do NOT use"operationOptions": "Sequential"and concurrency together.
Wait (Delay)
"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)
"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).
"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.countandlimit.timeoutexplicitly — the platform defaults are low (60 iterations, 1 hour). For time-range walkers uselimit.count: 5000andlimit.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, andfinalPayloadbefore theUntil.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
Switchnested insideUntil, keep the loop body to simple validation and state updates, then route withSwitchafter 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:
"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 Arraythe history to your specific request ID before callingfirst(). 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:
"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
runAfteraccepting 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:
"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 isrunAfter: [...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
\ndoes NOT produce a newline inside Power Automate expressions. It is treated as a literal backslash +nand 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():
"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:
"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:
"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.
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an MCP server, Needs Python
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
ninihen1
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/flowstudio-power-automate-build · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- ninihen1
AutoGineer is not affiliated with or endorsed by ninihen1 or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download flowstudio-power-automate-build.zip