* feat: add flowstudio-power-automate-debug and flowstudio-power-automate-build skills Two companion skills for the FlowStudio Power Automate MCP server: - flowstudio-power-automate-debug: Debug workflow for failed Power Automate cloud flow runs - flowstudio-power-automate-build: Build & deploy flows from natural language descriptions Both require a FlowStudio MCP subscription: https://flowstudio.app These complement the existing flowstudio-power-automate-mcp skill (merged in PR #896). * fix: address all review comments — README, cross-refs, response shapes, step numbering - Add skills to docs/README.skills.md (fixes validate-readme CI check) - Update cross-skill references to use flowstudio- prefix (#1, #4, #7, #9) - Fix get_live_flow_run_action_outputs: returns array, index [0] (#2, #3) - Renumber Step 6→5, Step 7→6 — remove gap in build workflow (#8) - Fix connectionName note: it's the key, not the GUID (#10) - Remove invalid arrow function from Filter array expression (#11) * feat: add flowstudio-power-automate plugin bundling all 3 skills Plugin bundles: - flowstudio-power-automate-mcp (core connection & CRUD) - flowstudio-power-automate-debug (debug failed runs) - flowstudio-power-automate-build (build & deploy flows) Install: copilot plugin install flowstudio-power-automate@awesome-copilot Per @aaronpowell's suggestion in review.
16 KiB
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 theconnectionReferencesmap.
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-paginate beyond that, enable the pagination policy on the action. In the flow definition this appears as:"paginationPolicy": { "minimumItemCount": 10000 }Set
minimumItemCountto the maximum number of items you expect. The connector will keep fetching pages until that count is reached or the list is exhausted. Without this, flows silently return a capped result on lists with >5,000 items.
SharePoint — Get Item (Single Row by ID)
"Get_SP_Item": {
"type": "OpenApiConnection",
"runAfter": {},
"inputs": {
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionName": "<connectionName>",
"operationId": "GetItem"
},
"parameters": {
"dataset": "https://mytenant.sharepoint.com/sites/mysite",
"table": "MyList",
"id": "@triggerBody()?['ID']"
}
}
}
Result reference: @body('Get_SP_Item')?['FieldName']
Use
GetItem(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"
}
}
}
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')"
}
}
}
If
Create_Filesucceeds,Get_File_Metadata_By_PathisSkippedandUpdate_Filestill fires (acceptingSkipped), harmlessly overwriting the file just created. IfCreate_Filefails (file exists), the metadata call retrieves the existing file's ID andUpdate_Fileoverwrites it. Either way you end with the latest content.Document library system properties — when iterating a file library result (e.g. from
ListFolderorGetFilesV2), 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),
GetItemChangesmay report no prior version. Check@equals(triggerBody()?['OData__UIVersionString'], '1.0')to detect newly created items and skip change-gate logic for those.
SharePoint — REST MERGE via HttpRequest
For cross-list updates or advanced operations not supported by the standard
Update Item connector (e.g., updating a list in a different site), use the
SharePoint REST API via the HttpRequest operation:
"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
HttpRequestoperation reuses the existing SharePoint connection — no extra authentication needed. Use this when the standard Update Item connector can't reach the target list (different site collection, or you need raw REST control).
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:
GetFileContentreturns base64-encoded content inbody(...)?['$content']. ApplydecodeBase64()thenjson()to get a usable array.Filter Arraythen 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
idparameter, SharePoint URL-encodes paths twice. Spaces become%2b(plus sign), slashes become%252f.
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: falseso 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 intoSendEmailV2.To get a folder ID: in Outlook on the web, right-click the folder → open in new tab — the folder GUID is in the URL. Prefix it with
Id::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:
"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)
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']"
}
}
}
approvalTypeoptions:
"Approve/Reject - First to respond"— binary, first responder wins"Approve/Reject - Everyone must approve"— requires all assignees"CustomResponse/Result"— define your own response buttonsAfter
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.