
Flowstudio Power Automate Build
- 1.8k installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
flowstudio-power-automate-build is an agent skill that >-.
About
>- --- 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.
- Build & Deploy Power Automate Flows with FlowStudio MCP
- Load current build tools.
- Check for an existing flow.
- Resolve connection references.
- Build the definition.
Flowstudio Power Automate Build by the numbers
- 1,813 all-time installs (skills.sh)
- +28 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #307 of 1,881 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
flowstudio-power-automate-build capabilities & compatibility
- Capabilities
- build & deploy power automate flows with flowstu · load current build tools. · check for an existing flow. · resolve connection references. · build the definition.
- Use cases
- documentation
What flowstudio-power-automate-build says it does
--- 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.
**Prerequisite**: A FlowStudio MCP server must be reachable with a valid JWT.
See the `flowstudio-power-automate-mcp` skill for connection setup.
npx skills add https://github.com/github/awesome-copilot --skill flowstudio-power-automate-buildAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
What problem does flowstudio-power-automate-build solve for developers using this skill?
>-
Who is it for?
Developers who need flowstudio-power-automate-build patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
>-
What you get
Actionable workflows and conventions from SKILL.md for flowstudio-power-automate-build.
- Power Automate action JSON
- connectionReferences mappings
By the numbers
- Covers 4 Microsoft 365 connector families: SharePoint, Teams, Outlook, Approvals
Files
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
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.
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_connections first — do NOT ask theuser 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 serverappends #flowstudio-mcp for 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_flowor 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.
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.
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:
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 — 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
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 inconnectionReferences
(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
"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$filterstring 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-paginatebeyond 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 willkeep 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)
"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']
UseGetItem(notGetItemswith 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
"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
"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 notchanging 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:
"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')"
}
}
}IfCreate_Filesucceeds,Get_File_Metadata_By_PathisSkippedandUpdate_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 IDand Update_File overwrites it. Either way you end with the latest content.>
Document library system properties — when iterating a file library result (e.g.
fromListFolderorGetFilesV2), 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:
"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:
"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 detectnewly 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:
"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 extraauthentication 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, andparameters/uriis 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.
"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 inbody(...)?['$content']. ApplydecodeBase64()thenjson()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 pathstwice. 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
"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)
"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 withId::infolderPath.
---
Teams
Teams — Post Message
"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:
"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).
"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.
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
}
}UseIncrementVariable(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 actionoutcomes. Use runAfter: {"MyScope": ["Failed", "TimedOut"]} on a follow-up actionto 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 repetitions to control how many items are processed simultaneously.Practical values: 5–10 for external API calls (respect rate limits),20–50 for internal/fast operations.Omit runtimeConfiguration.concurrency entirely 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 setlimit.countandlimit.timeoutexplicitly — the platform defaults are
low (60 iterations, 1 hour). For time-range walkers use limit.count: 5000 andlimit.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, andfinalPayload
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
Switchnested insideUntil, 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:
"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 specificrequest ID before calling first(). History endpoints return all jobs; withoutfiltering, 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 userunAfteraccepting 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 andfires 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 causea 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.
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 inconnectionReferences(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.
"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 explicitnull. 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.
"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 pickthe 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.
"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.
// 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 withCreateFileorUpdateFile
(setbodyto@body('Create_CSV')), or store in a variable withSetVariable.
>
If source data came from Power BI's ExecuteDatasetQuery, column names will bewrapped 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:
// 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:
"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 numericor 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:
"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 fieldsappears once. Add moreFilter_*_Changedinputs tounion()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.
"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 thelive 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.
"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()):
"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 returnsnull;empty()is for arrays/strings,
not scalars — using it on a first() result causes a runtime error.---
HTTP & Parsing
HTTP Action (External API)
"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:
"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 flowtransparently — no manual token acquisition step needed.
>
ConsistencyLevel: eventualis required for Graph$searchqueries.
Without it, $search returns 400.>
For PATCH/PUT writes, the same authentication block works — just changemethodand add abody.
>
⚠️ Never hardcode `secret` inline. Use @parameters('graphClientSecret')and declare it in the flow'sparametersblock (typesecurestring). 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).
"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 astatusfield 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.
// 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" }
}// 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 authenticatesthe call. Copy it from the child flow's trigger properties panel. The URL changes
if the trigger is deleted and re-created.
---
Parse 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.
"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).
"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()(notoutputs()), otherwise the expression returns null.
Common Build Patterns
Complete flow definition templates ready to copy and customize.
---
Pattern: Recurrence + SharePoint list read + Teams notification
{
"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)
{
"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']
FlowStudio MCP — Flow Definition Schema
The full JSON structure expected by update_live_flow (and returned by get_live_flow).
---
Top-Level Shape
{
"$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 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 identifierrunAfter— map of upstream action names → status conditions arrayinputs— action-specific input configuration
See action-patterns-core.md, action-patterns-data.md, and 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):
"runtimeConfiguration": {
"paginationPolicy": {
"minimumItemCount": 5000
}
}Without this, Get Items silently caps at 256 results. Set minimumItemCountto the maximum rows you expect. Required for any SharePoint list over 256 items.
Concurrency (parallel Foreach):
"runtimeConfiguration": {
"concurrency": {
"repetitions": 20
}
}Secure inputs/outputs (mask values in run history):
"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):
"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:
"My_Action": {
"runAfter": {
"Previous_Action": ["Succeeded"]
}
}Multiple upstream dependencies:
"runAfter": {
"Action_A": ["Succeeded"],
"Action_B": ["Succeeded", "Skipped"]
}Error-handling action (runs when upstream failed):
"Log_Error": {
"runAfter": {
"Risky_Action": ["Failed"]
}
}---
parameters (Flow-Level Input Parameters)
Optional. Define reusable values at the flow level:
"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:
"outputs": {
"resultData": {
"type": "object",
"value": "@outputs('Compose_Result')"
}
}---
Scoped Actions (Inside Scope Block)
Actions that need to be grouped for error handling or clarity:
"Scope_Main_Process": {
"type": "Scope",
"runAfter": {},
"actions": {
"Step_One": { ... },
"Step_Two": { "runAfter": { "Step_One": ["Succeeded"] }, ... }
}
}---
Full Minimal Example
{
"$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": {}
}FlowStudio MCP — Trigger Types
Copy-paste trigger definitions for Power Automate flow definitions.
---
Recurrence
Run on a schedule.
"Recurrence": {
"type": "Recurrence",
"recurrence": {
"frequency": "Day",
"interval": 1,
"startTime": "2026-01-01T08:00:00Z",
"timeZone": "AUS Eastern Standard Time"
}
}Weekly on specific days:
"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.
"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:
"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.
"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)
"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)
"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)
"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)
"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:
"Respond_to_Parent": {
"type": "Response",
"runAfter": { "Compose_Result": ["Succeeded"] },
"inputs": {
"statusCode": 200,
"body": "@outputs('Compose_Result')"
}
}Related skills
FAQ
What does flowstudio-power-automate-build do?
>-
When should I use flowstudio-power-automate-build?
>-
Is flowstudio-power-automate-build safe to install?
Review the Security Audits panel on this page before installing in production.