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://mp-staging.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-data.md
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'sconnectionNameproperty.
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 pick the highest-priority match without any loops.
Create CSV Table (Array → CSV String)
Converts an array of objects into a CSV-formatted string — no connector call, no code.
Use after a Select or Filter Array to export data or pass it to a file-write action.
"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. Withcolumns, you control header names and column order explicitly.The output is a raw string. Write it to a file with
CreateFileorUpdateFile(setbodyto@body('Create_CSV')), or store in a variable withSetVariable.If source data came from Power BI's
ExecuteDatasetQuery, column names will be wrapped in square brackets (e.g.[Amount]). Strip them before writing:@replace(replace(body('Create_CSV'),'[',''),']','')
range() + Select for Array Generation
range(0, N) produces an integer sequence [0, 1, 2, …, N-1]. Pipe it through
a Select action to generate date series, index grids, or any computed array
without a loop:
// 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 numeric or boolean values, omit the inner escaped quotes around the value portion. Keys must be unique — duplicate keys silently overwrite earlier ones. This replaces deeply nestedif(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 fields appears 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 the live content before comparing so binary and text files are handled uniformly. Write the new baseline before processing so a re-run after a partial failure does not re-process the same file again.
Set-Join for Sync (Update Detection without Nested Loops)
When syncing a source collection into a destination (e.g. API response → SharePoint list,
CSV → database), avoid nested Apply to each loops to find changed records.
Instead, project flat key arrays and use contains() to perform set operations —
zero nested loops, and the final loop only touches changed items.
Insert/update/delete sync recipe:
Select_Dest_Keysfrom destination rows.Filter_To_Insert: source rows whose key is not in destination keys.Filter_Already_Exists: source rows whose key is in destination keys.For each compared field, run
Filter_<Field>_Changed; combine them withunion()intoUnion_Changed.Select_Changed_KeysfromUnion_Changed, then filter destination rows to only those keys before updating.Select_Source_Keys, thenFilter_To_Deletedestination 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 eachwhen you only need one matching record.first()on an empty array returnsnull;empty()is for arrays/strings, not scalars — using it on afirst()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
authenticationblock handles the entire OAuth client-credentials flow transparently — no manual token acquisition step needed.
ConsistencyLevel: eventualis required for Graph$searchqueries. Without it,$searchreturns 400.For PATCH/PUT writes, the same
authenticationblock works — just changemethodand add abody.⚠️ Never hardcode
secretinline. Use@parameters('graphClientSecret')and declare it in the flow'sparametersblock (typesecurestring). This prevents the secret from appearing in run history or being readable viaget_live_flow. Declare the parameter like:"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: 200with astatusfield in the body ("success"/"error"). PowerApps HTTP actions do not handle non-2xx responses gracefully — the caller should inspectbody.statusrather 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 itsResponse.
transferMode: Chunked— enable when passing large arrays (>100 KB) to the child; avoids request-size limits.
limit.timeout: PT2H— raise the default 2-minute HTTP timeout for long-running children. Max is PT24H.The child flow's trigger URL contains a SAS token (
sig=...) that authenticates the call. Copy it from the child flow's trigger properties panel. The URL changes if the trigger is deleted and re-created.
Parse JSON
"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 viabody()(notoutputs()), otherwise the expression returns null.
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