{"kind":"Skill","metadata":{"namespace":"community","name":"flowstudio-power-automate-build","version":"0.1.0"},"spec":{"description":"\u003e-","files":{"SKILL.md":"---\nname: flowstudio-power-automate-build\ndescription: \u003e-\n  Build, scaffold, and deploy Power Automate cloud flows using the FlowStudio\n  MCP server. Your agent constructs flow definitions, wires connections, deploys,\n  and tests — all via MCP without opening the portal.\n  Load this skill when asked to: create a flow, build a new flow,\n  deploy a flow definition, scaffold a Power Automate workflow, construct a flow\n  JSON, update an existing flow's actions, patch a flow definition, add actions\n  to a flow, wire up connections, or generate a workflow definition from scratch.\n  Requires a FlowStudio MCP subscription — see https://mcp.flowstudio.app\n---\n\n# Build \u0026 Deploy Power Automate Flows with FlowStudio MCP\n\nStep-by-step guide for constructing and deploying Power Automate cloud flows\nprogrammatically through the FlowStudio MCP server.\n\n**Prerequisite**: A FlowStudio MCP server must be reachable with a valid JWT.\nSee the `flowstudio-power-automate-mcp` skill for connection setup.\nSubscribe at https://mcp.flowstudio.app\n\nWorkflow:\n1. Load current build tools.\n2. Check for an existing flow.\n3. Resolve connection references.\n4. Build the definition.\n5. Deploy.\n6. Verify.\n7. Test.\n\n---\n\n## Source of Truth\n\n\u003e **Always call `list_skills` / `tool_search` first** to confirm available tool\n\u003e names and parameter schemas. Tool names and parameters may change between\n\u003e server versions.\n\u003e This skill covers response shapes, behavioral notes, and build patterns —\n\u003e things tool schemas cannot tell you. If this document disagrees with\n\u003e `tool_search` or a real API response, the API wins.\n\n---\n\n## Python Helper\n\n```python\nimport json, urllib.request\n\nMCP_URL   = \"https://mcp.flowstudio.app/mcp\"\nMCP_TOKEN = \"\u003cYOUR_JWT_TOKEN\u003e\"\n\ndef mcp(tool, **kwargs):\n    payload = json.dumps({\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\",\n                          \"params\": {\"name\": tool, \"arguments\": kwargs}}).encode()\n    req = urllib.request.Request(MCP_URL, data=payload,\n        headers={\"x-api-key\": MCP_TOKEN, \"Content-Type\": \"application/json\",\n                 \"User-Agent\": \"FlowStudio-MCP/1.0\"})\n    try:\n        resp = urllib.request.urlopen(req, timeout=120)\n    except urllib.error.HTTPError as e:\n        body = e.read().decode(\"utf-8\", errors=\"replace\")\n        raise RuntimeError(f\"MCP HTTP {e.code}: {body[:200]}\") from e\n    raw = json.loads(resp.read())\n    if \"error\" in raw:\n        raise RuntimeError(f\"MCP error: {json.dumps(raw['error'])}\")\n    return json.loads(raw[\"result\"][\"content\"][0][\"text\"])\n\nENV = \"\u003cenvironment-id\u003e\"  # e.g. Default-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n---\n\n## 0. Load the Current Build Tools\n\nFor a brand-new flow, load the server's `create-flow` bundle. For editing an\nexisting flow, load `build-flow`. This keeps the agent aligned with the MCP\nserver's current schema before constructing JSON.\n\n```python\nschemas = mcp(\"tool_search\", query=\"skill:create-flow\")\n# Includes list_live_environments, list_live_connections,\n# describe_live_connector, get_live_dynamic_options, update_live_flow.\n```\n\nIf you need a tool outside the bundle, load it explicitly:\n\n```python\nmcp(\"tool_search\", query=\"select:get_live_dynamic_properties\")\n```\n\n---\n\n## 1. Safety Check: Does the Flow Already Exist?\n\nAlways look before you build to avoid duplicates:\n\n```python\nresults = mcp(\"list_live_flows\",\n    environmentName=ENV,\n    mode=\"owner\",\n    search=\"My New Flow\",\n    top=20)\n\n# list_live_flows returns { \"flows\": [...], \"mode\": \"...\", ... }\nmatches = [f for f in results[\"flows\"]\n           if \"My New Flow\".lower() in f[\"displayName\"].lower()]\n\nif len(matches) \u003e 0:\n    # Flow exists — modify rather than create\n    FLOW_ID = matches[0][\"id\"]   # plain UUID from list_live_flows\n    print(f\"Existing flow: {FLOW_ID}\")\n    defn = mcp(\"get_live_flow\", environmentName=ENV, flowName=FLOW_ID)\nelse:\n    print(\"Flow not found — building from scratch\")\n    FLOW_ID = None\n```\n\nFor very large environments, `list_live_flows` may return a continuation URL.\nPass it back as `continuationUrl` with the same `mode` to retrieve the next\nbatch. Use `mode=\"admin\"` only when the user needs all environment flows and\nthe MCP identity has admin rights.\n\n---\n\n## 2. Obtain Connection References\n\nEvery connector action needs a `connectionName` that points to a key in the\nflow's `connectionReferences` map. That key links to an authenticated connection\nin the environment.\n\n\u003e **MANDATORY**: You MUST call `list_live_connections` first — do NOT ask the\n\u003e user for connection names or GUIDs. The API returns the exact values you need.\n\u003e Only prompt the user if the API confirms that required connections are missing.\n\n### 2a — Find active connections\n\n```python\nconns = mcp(\"list_live_connections\", environmentName=ENV)\nactive = [c for c in conns[\"connections\"]\n          if c[\"statuses\"][0][\"status\"] == \"Connected\"]\nconn_map = {c[\"connectorName\"]: c[\"id\"] for c in active}\n```\n\nFor a known connector, pass `search` to reduce output and get paste-ready\n`connectionReferenceTemplate` and `hostTemplate` values:\n\n```python\nsp_conns = mcp(\"list_live_connections\",\n    environmentName=ENV,\n    search=\"shared_sharepointonline\")\n```\n\n### 2b — Determine which connectors the flow needs\n\nCommon connector API names: SharePoint `shared_sharepointonline`, Outlook\n`shared_office365`, Teams `shared_teams`, Approvals `shared_approvals`,\nOneDrive `shared_onedriveforbusiness`, Excel `shared_excelonlinebusiness`,\nDataverse `shared_commondataserviceforapps`, Forms `shared_microsoftforms`.\n\nFlows that need no connectors, such as Recurrence + Compose + HTTP only, can\nomit `connectionReferences`.\n\n### 2c — If connections are missing, guide the user\n\n```python\nconnectors_needed = [\"shared_sharepointonline\", \"shared_office365\"]  # adjust per flow\nmissing = [c for c in connectors_needed if c not in conn_map]\nif missing:\n    # STOP: connections require browser OAuth consent.\n    # Ask the user to create the missing connector connections in the\n    # selected environment, then re-run list_live_connections.\n    raise Exception(f\"Missing active connections: {missing}\")\n```\n\n### 2d — Build the connectionReferences block\n\n```python\nconnection_references = {}\nhost_templates = {}\nfor connector in connectors_needed:\n    c = next(c for c in active if c[\"connectorName\"] == connector)\n    connection_references[connector] = c.get(\"connectionReferenceTemplate\") or {\n        \"connectionName\": c[\"id\"],   # the connection id from list_live_connections\n        \"source\": \"Invoker\",\n        \"id\": f\"/providers/Microsoft.PowerApps/apis/{connector}\"\n    }\n    host_templates[connector] = c.get(\"hostTemplate\") or {\n        \"connectionName\": connector\n    }\n```\n\nIn Step 3 action JSON, `inputs.host.connectionName` must be the map key such as\n`shared_teams`, not the GUID. The GUID belongs only inside the\n`connectionReferences[connector].connectionName` value. If an existing flow uses\nthe same connectors, you may also copy its `properties.connectionReferences`\nfrom `get_live_flow`.\n\n---\n\n## 3. Build the Flow Definition\n\nConstruct the definition object. See [flow-schema.md](references/flow-schema.md)\nfor the full schema and these action pattern references for copy-paste templates:\n- [action-patterns-core.md](references/action-patterns-core.md) — Variables, control flow, expressions\n- [action-patterns-data.md](references/action-patterns-data.md) — Array transforms, HTTP, parsing\n- [action-patterns-connectors.md](references/action-patterns-connectors.md) — SharePoint, Outlook, Teams, Approvals\n\n```python\ndefinition = {\n    \"$schema\": \"https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#\",\n    \"contentVersion\": \"1.0.0.0\",\n    \"triggers\": { ... },   # see trigger-types.md / build-patterns.md\n    \"actions\": { ... }     # see ACTION-PATTERNS-*.md / build-patterns.md\n}\n```\n\n\u003e See [build-patterns.md](references/build-patterns.md) for complete, ready-to-use\n\u003e flow definitions covering Recurrence+SharePoint+Teams, HTTP triggers, and more.\n\n### Discover connector operations before guessing JSON\n\nFor connector-backed triggers/actions, prefer the live connector describer over\nhand-written shapes. It can return authored hints, canonical examples, variant\nkeys, inputs/outputs, and dynamic metadata pointers.\n\n```python\n# Search across connectors when you know the user's intent but not the API.\nmatches = mcp(\"describe_live_connector\",\n    environmentName=ENV,\n    search=\"send email\",\n    top=5)\n\n# Describe a specific operation before copying an exampleDefinition.\nop = mcp(\"describe_live_connector\",\n    environmentName=ENV,\n    connectorName=\"shared_office365\",\n    operationId=\"SendEmailV2\")\nprint(op.get(\"hint\"))\n```\n\nWhen an operation has multiple authored variants, request the variant the flow\nneeds:\n\n```python\nteams_chat = mcp(\"describe_live_connector\",\n    environmentName=ENV,\n    connectorName=\"shared_teams\",\n    operationId=\"PostMessageToConversation\",\n    variant=\"flowbot_chat\")\n```\n\nWhen the operation description says a parameter has dynamic options or dynamic\nproperties, call the indicated next tool:\n\n```python\nsp_op = mcp(\"describe_live_connector\",\n    environmentName=ENV,\n    connectorName=\"shared_sharepointonline\",\n    operationId=\"GetItems\")\n\nsites = mcp(\"get_live_dynamic_options\",\n    environmentName=ENV,\n    connectorName=\"shared_sharepointonline\",\n    connectionName=conn_map[\"shared_sharepointonline\"],\n    operationId=\"GetItems\",\n    parameterName=\"dataset\",\n    dynamicMetadata=sp_op[\"dynamicParameters\"][\"dataset\"])\n\nfields = mcp(\"get_live_dynamic_properties\",\n    environmentName=ENV,\n    connectorName=\"shared_sharepointonline\",\n    connectionName=conn_map[\"shared_sharepointonline\"],\n    operationId=\"GetItems\",\n    parameterName=\"item\",\n    parameters={\"dataset\": \"\u003csite-url\u003e\", \"table\": \"\u003clist-id\u003e\"},\n    dynamicMetadata=sp_op[\"dynamicProperties\"][\"item\"])\n```\n\nUse dynamic options for dropdown IDs such as SharePoint sites/lists and Teams\nteams/channels. Use dynamic properties for schema/field shapes such as\nSharePoint list item columns.\n\n---\n\n## 4. Deploy (Create or Update)\n\n`update_live_flow` handles both creation and updates in a single tool.\n\n### Create a new flow (no existing flow)\n\nOmit `flowName` — the server generates a new GUID and creates via PUT:\n\n```python\ndefinition[\"description\"] = \"Weekly SharePoint → Teams notification flow, built by agent\"\n\nresult = mcp(\"update_live_flow\",\n    environmentName=ENV,\n    # flowName omitted → creates a new flow\n    definition=definition,\n    connectionReferences=connection_references,\n    displayName=\"Overdue Invoice Notifications\"\n)\n\nif result.get(\"error\") is not None:\n    print(\"Create failed:\", result[\"error\"])\nelse:\n    # Capture the new flow ID for subsequent steps\n    FLOW_ID = result[\"created\"]\n    print(f\"✅ Flow created: {FLOW_ID}\")\n```\n\n### Update an existing flow\n\nProvide `flowName` to PATCH:\n\n```python\ndefinition[\"description\"] = (\n    \"Updated by agent on \" + __import__('datetime').datetime.utcnow().isoformat()\n)\n\nresult = mcp(\"update_live_flow\",\n    environmentName=ENV,\n    flowName=FLOW_ID,\n    definition=definition,\n    connectionReferences=connection_references,\n    displayName=\"My Updated Flow\"\n)\n\nif result.get(\"error\") is not None:\n    print(\"Update failed:\", result[\"error\"])\nelse:\n    print(\"Update succeeded:\", result)\n```\n\n\u003e ⚠️ `update_live_flow` always returns an `error` key.\n\u003e `null` (Python `None`) means success — do not treat the presence of the key as failure.\n\u003e\n\u003e ⚠️ Flow description lives at `definition[\"description\"]`. The current server\n\u003e appends `#flowstudio-mcp` for usage tracking. Do not pass a top-level\n\u003e `description` argument unless `tool_search` shows one in the active schema.\n\n### Common deployment errors\n\n| Error message (contains) | Cause | Fix |\n|---|---|---|\n| `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 |\n| `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 |\n| `InvalidTemplate` / `InvalidDefinition` | Syntax error in the definition JSON | Check `runAfter` chains, expression syntax, and action type spelling |\n| `ConnectionNotConfigured` | A connector action exists but the connection GUID is invalid or expired | Re-check `list_live_connections` for a fresh GUID |\n\n---\n\n## 5. Verify the Deployment\n\n```python\ncheck = mcp(\"get_live_flow\", environmentName=ENV, flowName=FLOW_ID)\n\n# Confirm state\nprint(\"State:\", check[\"properties\"][\"state\"])  # Should be \"Started\"\n# If state is \"Stopped\", use set_live_flow_state — NOT update_live_flow\n# mcp(\"set_live_flow_state\", environmentName=ENV, flowName=FLOW_ID, state=\"Started\")\n\n# Confirm the action we added is there\nacts = check[\"properties\"][\"definition\"][\"actions\"]\nprint(\"Actions:\", list(acts.keys()))\n```\n\n---\n\n## 6. Test the Flow\n\n\u003e **MANDATORY**: Before triggering any test run, **ask the user for confirmation**.\n\u003e Running a flow has real side effects — it may send emails, post Teams messages,\n\u003e write to SharePoint, start approvals, or call external APIs. Explain what the\n\u003e flow will do and wait for explicit approval before calling `trigger_live_flow`\n\u003e or `resubmit_live_flow_run`.\n\n### Updated flows (have prior runs) — ANY trigger type\n\n\u003e **Use `resubmit_live_flow_run` first.** It works for EVERY trigger type —\n\u003e Recurrence, SharePoint, connector webhooks, Button, and HTTP. It replays\n\u003e the original trigger payload. Do NOT ask the user to manually trigger the\n\u003e flow or wait for the next scheduled run.\n\n```python\nruns = mcp(\"get_live_flow_runs\", environmentName=ENV, flowName=FLOW_ID, top=1)\nif runs:\n    # Works for Recurrence, SharePoint, connector triggers — not just HTTP\n    result = mcp(\"resubmit_live_flow_run\",\n        environmentName=ENV, flowName=FLOW_ID, runName=runs[0][\"name\"])\n    print(result)   # {\"resubmitted\": true, \"triggerName\": \"...\"}\n```\n\n### HTTP-triggered flows — custom test payload\n\nOnly use `trigger_live_flow` when you need to send a **different** payload\nthan the original run. For verifying a fix, `resubmit_live_flow_run` is\nbetter because it uses the exact data that caused the failure.\n\n```python\ndefn = mcp(\"get_live_flow\", environmentName=ENV, flowName=FLOW_ID)\ntriggers = defn[\"properties\"][\"definition\"][\"triggers\"]\nmanual = next(iter(triggers.values()))\nprint(\"Expected body:\", manual.get(\"inputs\", {}).get(\"schema\"))\n\nresult = mcp(\"trigger_live_flow\",\n    environmentName=ENV, flowName=FLOW_ID,\n    body={\"name\": \"Test\", \"value\": 1})\nprint(f\"Status: {result['responseStatus']}\")\n```\n\n### Brand-new non-HTTP flows (Recurrence, connector triggers, etc.)\n\nA brand-new Recurrence or connector-triggered flow has **no prior runs** to\nresubmit and no HTTP endpoint to call. This is the ONLY scenario where you\nneed the temporary HTTP trigger approach below. **Deploy with a temporary\nHTTP trigger first, test the actions, then swap to the production trigger.**\n\nCompact recipe:\n\n```python\nproduction_trigger = definition[\"triggers\"]\ndefinition[\"triggers\"] = {\n    \"manual\": {\"type\": \"Request\", \"kind\": \"Http\", \"inputs\": {\"schema\": {}}}\n}\n\nresult = mcp(\"update_live_flow\",\n    environmentName=ENV,\n    flowName=FLOW_ID,       # omit if creating new\n    definition=definition,\n    connectionReferences=connection_references,\n    displayName=\"Overdue Invoice Notifications\")\nFLOW_ID = FLOW_ID or result[\"created\"]\n\ntest = mcp(\"trigger_live_flow\", environmentName=ENV, flowName=FLOW_ID,\n           body={\"sample\": \"payload\"})\nruns = mcp(\"get_live_flow_runs\", environmentName=ENV, flowName=FLOW_ID, top=1)\n\nif runs[0][\"status\"] == \"Failed\":\n    err = mcp(\"get_live_flow_run_error\",\n        environmentName=ENV, flowName=FLOW_ID, runName=runs[0][\"name\"])\n    raise Exception(err[\"failedActions\"][-1])\n\ndefinition[\"triggers\"] = production_trigger\nmcp(\"update_live_flow\",\n    environmentName=ENV,\n    flowName=FLOW_ID,\n    definition=definition,\n    connectionReferences=connection_references)\n```\n\nThe trigger is only the entry point; testing through HTTP still exercises the\nsame actions. If actions use `triggerBody()` or `triggerOutputs()`, pass a\nrepresentative `trigger_live_flow.body` shaped like the production trigger\npayload.\n\n---\n\n## Gotchas\n\n| Mistake | Consequence | Prevention |\n|---|---|---|\n| Missing `connectionReferences` in deploy | 400 \"Supply connectionReferences\" | Always call `list_live_connections` first |\n| `\"operationOptions\"` missing on Foreach | Parallel execution, race conditions on writes | Always add `\"Sequential\"` |\n| `union(old_data, new_data)` | Old values override new (first-wins) | Use `union(new_data, old_data)` |\n| `split()` on potentially-null string | `InvalidTemplate` crash | Wrap with `coalesce(field, '')` |\n| Checking `result[\"error\"]` exists | Always present; true error is `!= null` | Use `result.get(\"error\") is not None` |\n| 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 |\n| Teams \"Chat with Flow bot\" recipient as object | 400 `GraphUserDetailNotFound` | Use plain string with trailing semicolon (see below) |\n| 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` |\n| 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 |\n| Connector action missing `metadata.operationMetadataId` | Designer/run-only UI can behave inconsistently | Preserve existing IDs; add stable GUIDs for new connector actions |\n| Placeholder Excel `scriptId` | Dynamic validation fails at save time | Resolve the real Office Script ID before deploying |\n| SharePoint `PatchItem` omits required fields | Save can fail even if the field is not changing | Echo unchanged required fields such as `item/Title` |\n| Copilot Studio connector calls a draft agent | Connector invocation can fail or hit stale behavior | Publish the agent before testing/resubmitting the flow |\n\n### Teams `PostMessageToConversation` — Recipient Formats\n\nThe `body/recipient` parameter format depends on the `location` value:\n\n| Location | `body/recipient` format | Example |\n|---|---|---|\n| **Chat with Flow bot** | Plain email string with **trailing semicolon** | `\"user@contoso.com;\"` |\n| **Channel** | Object with `groupId` and `channelId` | `{\"groupId\": \"...\", \"channelId\": \"...\"}` |\n\n\u003e **Common mistake**: passing `{\"to\": \"user@contoso.com\"}` for \"Chat with Flow bot\"\n\u003e returns a 400 `GraphUserDetailNotFound` error. The API expects a plain string.\n\n---\n\n## Reference Files\n\n- [flow-schema.md](references/flow-schema.md) — Full flow definition JSON schema\n- [trigger-types.md](references/trigger-types.md) — Trigger type templates\n- [action-patterns-core.md](references/action-patterns-core.md) — Variables, control flow, expressions\n- [action-patterns-data.md](references/action-patterns-data.md) — Array transforms, HTTP, parsing\n- [action-patterns-connectors.md](references/action-patterns-connectors.md) — SharePoint, Outlook, Teams, Approvals\n- [build-patterns.md](references/build-patterns.md) — Complete flow definition templates (Recurrence+SP+Teams, HTTP trigger)\n\n## Related Skills\n\n- `flowstudio-power-automate-mcp` — Core connection setup and tool reference\n- `flowstudio-power-automate-debug` — Debug failing flows after deployment\n","references/action-patterns-connectors.md":"# FlowStudio MCP — Action Patterns: Connectors\n\nSharePoint, Outlook, Teams, and Approvals connector action patterns.\n\n\u003e All examples assume `\"runAfter\"` is set appropriately.\n\u003e Replace `\u003cconnectionName\u003e` with the **key** you used in `connectionReferences`\n\u003e (e.g. `shared_sharepointonline`, `shared_teams`). This is NOT the connection\n\u003e GUID — it is the logical reference name that links the action to its entry in\n\u003e the `connectionReferences` map.\n\n---\n\n## SharePoint\n\n### SharePoint — Get Items\n\n```json\n\"Get_SP_Items\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"GetItems\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"MyList\",\n      \"$filter\": \"Status eq 'Active'\",\n      \"$top\": 500\n    }\n  }\n}\n```\n\nResult reference: `@outputs('Get_SP_Items')?['body/value']`\n\n\u003e **Dynamic OData filter with string interpolation**: inject a runtime value\n\u003e directly into the `$filter` string using `@{...}` syntax:\n\u003e ```\n\u003e \"$filter\": \"Title eq '@{outputs('ConfirmationCode')}'\"  \n\u003e ```\n\u003e Note the single-quotes inside double-quotes — correct OData string literal\n\u003e syntax. Avoids a separate variable action.\n\n\u003e **Pagination for large lists**: by default, GetItems stops at `$top`. To auto-paginate\n\u003e beyond that, enable the pagination policy on the action. In the flow definition this\n\u003e appears as:\n\u003e ```json\n\u003e \"paginationPolicy\": { \"minimumItemCount\": 10000 }\n\u003e ```\n\u003e Set `minimumItemCount` to the maximum number of items you expect. The connector will\n\u003e keep fetching pages until that count is reached or the list is exhausted. Without this,\n\u003e flows silently return a capped result on lists with \u003e5,000 items.\n\n---\n\n### SharePoint — Get Item (Single Row by ID)\n\n```json\n\"Get_SP_Item\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"GetItem\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"MyList\",\n      \"id\": \"@triggerBody()?['ID']\"\n    }\n  }\n}\n```\n\nResult reference: `@body('Get_SP_Item')?['FieldName']`\n\n\u003e Use `GetItem` (not `GetItems` with a filter) when you already have the ID.\n\u003e Re-fetching after a trigger gives you the **current** row state, not the\n\u003e snapshot captured at trigger time — important if another process may have\n\u003e modified the item since the flow started.\n\n---\n\n### SharePoint — Create Item\n\n```json\n\"Create_SP_Item\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"PostItem\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"MyList\",\n      \"item/Title\": \"@variables('myTitle')\",\n      \"item/Status\": \"Active\"\n    }\n  }\n}\n```\n\n---\n\n### SharePoint — Update Item\n\n```json\n\"Update_SP_Item\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"PatchItem\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"MyList\",\n      \"id\": \"@item()?['ID']\",\n      \"item/Status\": \"Processed\"\n    }\n  }\n}\n```\n\n\u003e `PatchItem` can validate required SharePoint columns even when you are not\n\u003e changing those fields. Echo unchanged required fields from the trigger or a\n\u003e prior Get Item action, for example `item/Title`, and use internal field names.\n\n---\n\n### SharePoint — File Upsert (Create or Overwrite in Document Library)\n\nSharePoint's `CreateFile` fails if the file already exists. To upsert (create or overwrite)\nwithout a prior existence check, use `GetFileMetadataByPath` on **both Succeeded and Failed**\nfrom `CreateFile` — if create failed because the file exists, the metadata call still\nreturns its ID, which `UpdateFile` can then overwrite:\n\n```json\n\"Create_File\": {\n  \"type\": \"OpenApiConnection\",\n  \"inputs\": {\n    \"host\": { \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n              \"connectionName\": \"\u003cconnectionName\u003e\", \"operationId\": \"CreateFile\" },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"folderPath\": \"/My Library/Subfolder\",\n      \"name\": \"@{variables('filename')}\",\n      \"body\": \"@outputs('Compose_File_Content')\"\n    }\n  }\n},\n\"Get_File_Metadata_By_Path\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": { \"Create_File\": [\"Succeeded\", \"Failed\"] },\n  \"inputs\": {\n    \"host\": { \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n              \"connectionName\": \"\u003cconnectionName\u003e\", \"operationId\": \"GetFileMetadataByPath\" },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"path\": \"/My Library/Subfolder/@{variables('filename')}\"\n    }\n  }\n},\n\"Update_File\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": { \"Get_File_Metadata_By_Path\": [\"Succeeded\", \"Skipped\"] },\n  \"inputs\": {\n    \"host\": { \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n              \"connectionName\": \"\u003cconnectionName\u003e\", \"operationId\": \"UpdateFile\" },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"id\": \"@outputs('Get_File_Metadata_By_Path')?['body/{Identifier}']\",\n      \"body\": \"@outputs('Compose_File_Content')\"\n    }\n  }\n}\n```\n\n\u003e If `Create_File` succeeds, `Get_File_Metadata_By_Path` is `Skipped` and `Update_File`\n\u003e still fires (accepting `Skipped`), harmlessly overwriting the file just created.\n\u003e If `Create_File` fails (file exists), the metadata call retrieves the existing file's ID\n\u003e and `Update_File` overwrites it. Either way you end with the latest content.\n\u003e\n\u003e **Document library system properties** — when iterating a file library result (e.g.\n\u003e from `ListFolder` or `GetFilesV2`), use curly-brace property names to access\n\u003e SharePoint's built-in file metadata. These are different from list field names:\n\u003e ```\n\u003e @item()?['{Name}']                  — filename without path (e.g. \"report.csv\")\n\u003e @item()?['{FilenameWithExtension}'] — same as {Name} in most connectors\n\u003e @item()?['{Identifier}']            — internal file ID for use in UpdateFile/DeleteFile\n\u003e @item()?['{FullPath}']              — full server-relative path\n\u003e @item()?['{IsFolder}']             — boolean, true for folder entries\n\u003e ```\n\n---\n\n### SharePoint — GetItemChanges Column Gate\n\nWhen a SharePoint \"item modified\" trigger fires, it doesn't tell you WHICH\ncolumn changed. Use `GetItemChanges` to get per-column change flags, then gate\ndownstream logic on specific columns:\n\n```json\n\"Get_Changes\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"GetItemChanges\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"\u003clist-guid\u003e\",\n      \"id\": \"@triggerBody()?['ID']\",\n      \"since\": \"@triggerBody()?['Modified']\",\n      \"includeDrafts\": false\n    }\n  }\n}\n```\n\nGate on a specific column:\n\n```json\n\"expression\": {\n  \"and\": [{\n    \"equals\": [\n      \"@body('Get_Changes')?['Column']?['hasChanged']\",\n      true\n    ]\n  }]\n}\n```\n\n\u003e **New-item detection:** On the very first modification (version 1.0),\n\u003e `GetItemChanges` may report no prior version. Check\n\u003e `@equals(triggerBody()?['OData__UIVersionString'], '1.0')` to detect\n\u003e newly created items and skip change-gate logic for those.\n\n---\n\n### SharePoint — REST MERGE via HttpRequest\n\nFor cross-list updates or advanced operations not supported by the standard\nUpdate Item connector (e.g., updating a list in a different site), use the\nSharePoint REST API via the `HttpRequest` operation:\n\n```json\n\"Update_Cross_List_Item\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"HttpRequest\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/target-site\",\n      \"parameters/method\": \"POST\",\n      \"parameters/uri\": \"/_api/web/lists(guid'\u003clist-guid\u003e')/items(@{variables('ItemId')})\",\n      \"parameters/headers\": {\n        \"Accept\": \"application/json;odata=nometadata\",\n        \"Content-Type\": \"application/json;odata=nometadata\",\n        \"X-HTTP-Method\": \"MERGE\",\n        \"IF-MATCH\": \"*\"\n      },\n      \"parameters/body\": \"{ \\\"Title\\\": \\\"@{variables('NewTitle')}\\\", \\\"Status\\\": \\\"@{variables('NewStatus')}\\\" }\"\n    }\n  }\n}\n```\n\n\u003e **Key headers:**\n\u003e - `X-HTTP-Method: MERGE` — tells SharePoint to do a partial update (PATCH semantics)\n\u003e - `IF-MATCH: *` — overwrites regardless of current ETag (no conflict check)\n\u003e\n\u003e The `HttpRequest` operation reuses the existing SharePoint connection — no extra\n\u003e authentication needed. Use this when the standard Update Item connector can't\n\u003e reach the target list (different site collection, or you need raw REST control).\n\u003e Keep the connector-specific parameter names exactly as shown:\n\u003e `parameters/method`, `parameters/uri`, `parameters/headers`, and\n\u003e `parameters/body`. The body is a JSON string, and `parameters/uri` is relative\n\u003e to the SharePoint `dataset`.\n\n---\n\n### SharePoint — File as JSON Database (Read + Parse)\n\nUse a SharePoint document library JSON file as a queryable \"database\" of\nlast-known-state records. A separate process (e.g., Power BI dataflow) maintains\nthe file; the flow downloads and filters it for before/after comparisons.\n\n```json\n\"Get_File\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"GetFileContent\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"id\": \"%252fShared%2bDocuments%252fdata.json\",\n      \"inferContentType\": false\n    }\n  }\n},\n\"Parse_JSON_File\": {\n  \"type\": \"Compose\",\n  \"runAfter\": { \"Get_File\": [\"Succeeded\"] },\n  \"inputs\": \"@json(decodeBase64(body('Get_File')?['$content']))\"\n},\n\"Find_Record\": {\n  \"type\": \"Query\",\n  \"runAfter\": { \"Parse_JSON_File\": [\"Succeeded\"] },\n  \"inputs\": {\n    \"from\": \"@outputs('Parse_JSON_File')\",\n    \"where\": \"@equals(item()?['id'], variables('RecordId'))\"\n  }\n}\n```\n\n\u003e **Decode chain:** `GetFileContent` returns base64-encoded content in\n\u003e `body(...)?['$content']`. Apply `decodeBase64()` then `json()` to get a\n\u003e usable array. `Filter Array` then acts as a WHERE clause.\n\u003e\n\u003e **When to use:** When you need a lightweight \"before\" snapshot to detect field\n\u003e changes from a webhook payload (the \"after\" state). Simpler than maintaining\n\u003e a full SharePoint list mirror — works well for up to ~10K records.\n\u003e\n\u003e **File path encoding:** In the `id` parameter, SharePoint URL-encodes paths\n\u003e twice. Spaces become `%2b` (plus sign), slashes become `%252f`.\n\n---\n\n## Excel Online\n\n### Excel — Run Office Script\n\nOffice Script actions require real workbook and script identifiers at save time.\nDo not deploy placeholder `scriptId` values; `update_live_flow` can fail during\ndynamic operation validation even before a test run exists.\n\nUse `describe_live_connector` or `get_live_dynamic_options` when available, or\nask the user for the workbook and script if they are not discoverable. If a real\n`scriptId` still cannot be resolved, ask the user to add the Run script action\nonce in the designer, then read the flow definition and preserve the resolved\nparameters.\n\n---\n\n## Outlook\n\n### Outlook — Send Email\n\n```json\n\"Send_Email\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_office365\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"SendEmailV2\"\n    },\n    \"parameters\": {\n      \"emailMessage/To\": \"recipient@contoso.com\",\n      \"emailMessage/Subject\": \"Automated notification\",\n      \"emailMessage/Body\": \"\u003cp\u003e@{outputs('Compose_Message')}\u003c/p\u003e\",\n      \"emailMessage/IsHtml\": true\n    }\n  }\n}\n```\n\n---\n\n### Outlook — Get Emails (Read Template from Folder)\n\n```json\n\"Get_Email_Template\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_office365\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"GetEmailsV3\"\n    },\n    \"parameters\": {\n      \"folderPath\": \"Id::\u003coutlook-folder-id\u003e\",\n      \"fetchOnlyUnread\": false,\n      \"includeAttachments\": false,\n      \"top\": 1,\n      \"importance\": \"Any\",\n      \"fetchOnlyWithAttachment\": false,\n      \"subjectFilter\": \"My Email Template Subject\"\n    }\n  }\n}\n```\n\nAccess subject and body:\n```\n@first(outputs('Get_Email_Template')?['body/value'])?['subject']\n@first(outputs('Get_Email_Template')?['body/value'])?['body']\n```\n\n\u003e **Outlook-as-CMS pattern**: store a template email in a dedicated Outlook folder.\n\u003e Set `fetchOnlyUnread: false` so the template persists after first use.\n\u003e Non-technical users can update subject and body by editing that email —\n\u003e no flow changes required. Pass subject and body directly into `SendEmailV2`.\n\u003e\n\u003e To get a folder ID: in Outlook on the web, right-click the folder → open in\n\u003e new tab — the folder GUID is in the URL. Prefix it with `Id::` in `folderPath`.\n\n---\n\n## Teams\n\n### Teams — Post Message\n\n```json\n\"Post_Teams_Message\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_teams\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"PostMessageToConversation\"\n    },\n    \"parameters\": {\n      \"poster\": \"Flow bot\",\n      \"location\": \"Channel\",\n      \"body/recipient\": {\n        \"groupId\": \"\u003cteam-id\u003e\",\n        \"channelId\": \"\u003cchannel-id\u003e\"\n      },\n      \"body/messageBody\": \"@outputs('Compose_Message')\"\n    }\n  }\n}\n```\n\n#### Variant: Group Chat (1:1 or Multi-Person)\n\nTo post to a group chat instead of a channel, use `\"location\": \"Group chat\"` with\na thread ID as the recipient:\n\n```json\n\"Post_To_Group_Chat\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_teams\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"PostMessageToConversation\"\n    },\n    \"parameters\": {\n      \"poster\": \"Flow bot\",\n      \"location\": \"Group chat\",\n      \"body/recipient\": \"19:\u003cthread-hash\u003e@thread.v2\",\n      \"body/messageBody\": \"@outputs('Compose_Message')\"\n    }\n  }\n}\n```\n\nFor 1:1 (\"Chat with Flow bot\"), use `\"location\": \"Chat with Flow bot\"` and set\n`body/recipient` to the user's email address.\n\n\u003e **Active-user gate:** When sending notifications in a loop, check the recipient's\n\u003e Azure AD account is enabled before posting — avoids failed deliveries to departed\n\u003e staff:\n\u003e ```json\n\u003e \"Check_User_Active\": {\n\u003e   \"type\": \"OpenApiConnection\",\n\u003e   \"inputs\": {\n\u003e     \"host\": { \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_office365users\",\n\u003e               \"operationId\": \"UserProfile_V2\" },\n\u003e     \"parameters\": { \"id\": \"@{item()?['Email']}\" }\n\u003e   }\n\u003e }\n\u003e ```\n\u003e Then gate: `@equals(body('Check_User_Active')?['accountEnabled'], true)`\n\n---\n\n## Copilot Studio\n\n### Copilot Studio — Invoke Agent\n\nWhen using the Copilot Studio connector, publish the agent before running the\nflow. Draft/test agents can exist in the studio canvas but still be unavailable\nor stale through the flow connector endpoint.\n\nIf a connector action fails with an unavailable-agent or endpoint-style error,\npublish the agent, wait briefly for propagation, then resubmit the same flow run\nbefore changing the flow definition.\n\n---\n\n## Approvals\n\n### Split Approval (Create → Wait)\n\nThe standard \"Start and wait for an approval\" is a single blocking action.\nFor more control (e.g., posting the approval link in Teams, or adding a timeout\nscope), split it into two actions: `CreateAnApproval` (fire-and-forget) then\n`WaitForAnApproval` (webhook pause).\n\n```json\n\"Create_Approval\": {\n  \"type\": \"OpenApiConnection\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_approvals\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"CreateAnApproval\"\n    },\n    \"parameters\": {\n      \"approvalType\": \"CustomResponse/Result\",\n      \"ApprovalCreationInput/title\": \"Review: @{variables('ItemTitle')}\",\n      \"ApprovalCreationInput/assignedTo\": \"approver@contoso.com\",\n      \"ApprovalCreationInput/details\": \"Please review and select an option.\",\n      \"ApprovalCreationInput/responseOptions\": [\"Approve\", \"Reject\", \"Defer\"],\n      \"ApprovalCreationInput/enableNotifications\": true,\n      \"ApprovalCreationInput/enableReassignment\": true\n    }\n  }\n},\n\"Wait_For_Approval\": {\n  \"type\": \"OpenApiConnectionWebhook\",\n  \"runAfter\": { \"Create_Approval\": [\"Succeeded\"] },\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_approvals\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"WaitForAnApproval\"\n    },\n    \"parameters\": {\n      \"approvalName\": \"@body('Create_Approval')?['name']\"\n    }\n  }\n}\n```\n\n\u003e **`approvalType` options:**\n\u003e - `\"Approve/Reject - First to respond\"` — binary, first responder wins\n\u003e - `\"Approve/Reject - Everyone must approve\"` — requires all assignees\n\u003e - `\"CustomResponse/Result\"` — define your own response buttons\n\u003e\n\u003e After `Wait_For_Approval`, read the outcome:\n\u003e ```\n\u003e @body('Wait_For_Approval')?['outcome']          → \"Approve\", \"Reject\", or custom\n\u003e @body('Wait_For_Approval')?['responses'][0]?['responder']?['displayName']\n\u003e @body('Wait_For_Approval')?['responses'][0]?['comments']\n\u003e ```\n\u003e\n\u003e The split pattern lets you insert actions between create and wait — e.g.,\n\u003e posting the approval link to Teams, starting a timeout scope, or logging\n\u003e the pending approval to a tracking list.\n","references/action-patterns-core.md":"# FlowStudio MCP — Action Patterns: Core\n\nVariables, control flow, and expression patterns for Power Automate flow definitions.\n\n\u003e All examples assume `\"runAfter\"` is set appropriately.\n\u003e Replace `\u003cconnectionName\u003e` with the **key** you used in your `connectionReferences` map\n\u003e (e.g. `shared_teams`, `shared_office365`) — NOT the connection GUID.\n\n---\n\n## Data \u0026 Variables\n\n### Compose (Store a Value)\n\n```json\n\"Compose_My_Value\": {\n  \"type\": \"Compose\",\n  \"runAfter\": {},\n  \"inputs\": \"@variables('myVar')\"\n}\n```\n\nReference: `@outputs('Compose_My_Value')`\n\n---\n\n### Initialize Variable\n\n```json\n\"Init_Counter\": {\n  \"type\": \"InitializeVariable\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"variables\": [{\n      \"name\": \"counter\",\n      \"type\": \"Integer\",\n      \"value\": 0\n    }]\n  }\n}\n```\n\nTypes: `\"Integer\"`, `\"Float\"`, `\"Boolean\"`, `\"String\"`, `\"Array\"`, `\"Object\"`\n\n---\n\n### Set Variable\n\n```json\n\"Set_Counter\": {\n  \"type\": \"SetVariable\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"name\": \"counter\",\n    \"value\": \"@add(variables('counter'), 1)\"\n  }\n}\n```\n\n---\n\n### Append to Array Variable\n\n```json\n\"Collect_Item\": {\n  \"type\": \"AppendToArrayVariable\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"name\": \"resultArray\",\n    \"value\": \"@item()\"\n  }\n}\n```\n\n---\n\n### Increment Variable\n\n```json\n\"Increment_Counter\": {\n  \"type\": \"IncrementVariable\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"name\": \"counter\",\n    \"value\": 1\n  }\n}\n```\n\n\u003e Use `IncrementVariable` (not `SetVariable` with `add()`) for counters inside loops —\n\u003e it is atomic and avoids expression errors when the variable is used elsewhere in the\n\u003e same iteration. `value` can be any integer or expression, e.g. `@mul(item()?['Interval'], 60)`\n\u003e to advance a Unix timestamp cursor by N minutes.\n\n---\n\n## Control Flow\n\n### Condition (If/Else)\n\n```json\n\"Check_Status\": {\n  \"type\": \"If\",\n  \"runAfter\": {},\n  \"expression\": {\n    \"and\": [{ \"equals\": [\"@item()?['Status']\", \"Active\"] }]\n  },\n  \"actions\": {\n    \"Handle_Active\": {\n      \"type\": \"Compose\",\n      \"runAfter\": {},\n      \"inputs\": \"Active user: @{item()?['Name']}\"\n    }\n  },\n  \"else\": {\n    \"actions\": {\n      \"Handle_Inactive\": {\n        \"type\": \"Compose\",\n        \"runAfter\": {},\n        \"inputs\": \"Inactive user\"\n      }\n    }\n  }\n}\n```\n\nComparison operators: `equals`, `not`, `greater`, `greaterOrEquals`, `less`, `lessOrEquals`, `contains`  \nLogical: `and: [...]`, `or: [...]`\n\n---\n\n### Switch\n\n```json\n\"Route_By_Type\": {\n  \"type\": \"Switch\",\n  \"runAfter\": {},\n  \"expression\": \"@triggerBody()?['type']\",\n  \"cases\": {\n    \"Case_Email\": {\n      \"case\": \"email\",\n      \"actions\": { \"Process_Email\": { \"type\": \"Compose\", \"runAfter\": {}, \"inputs\": \"email\" } }\n    },\n    \"Case_Teams\": {\n      \"case\": \"teams\",\n      \"actions\": { \"Process_Teams\": { \"type\": \"Compose\", \"runAfter\": {}, \"inputs\": \"teams\" } }\n    }\n  },\n  \"default\": {\n    \"actions\": { \"Unknown_Type\": { \"type\": \"Compose\", \"runAfter\": {}, \"inputs\": \"unknown\" } }\n  }\n}\n```\n\n---\n\n### Scope (Grouping / Try-Catch)\n\nWrap related actions in a Scope to give them a shared name, collapse them in the\ndesigner, and — most importantly — handle their errors as a unit.\n\n```json\n\"Scope_Get_Customer\": {\n  \"type\": \"Scope\",\n  \"runAfter\": {},\n  \"actions\": {\n    \"HTTP_Get_Customer\": {\n      \"type\": \"Http\",\n      \"runAfter\": {},\n      \"inputs\": {\n        \"method\": \"GET\",\n        \"uri\": \"https://api.example.com/customers/@{variables('customerId')}\"\n      }\n    },\n    \"Compose_Email\": {\n      \"type\": \"Compose\",\n      \"runAfter\": { \"HTTP_Get_Customer\": [\"Succeeded\"] },\n      \"inputs\": \"@outputs('HTTP_Get_Customer')?['body/email']\"\n    }\n  }\n},\n\"Handle_Scope_Error\": {\n  \"type\": \"Compose\",\n  \"runAfter\": { \"Scope_Get_Customer\": [\"Failed\", \"TimedOut\"] },\n  \"inputs\": \"Scope failed: @{result('Scope_Get_Customer')?[0]?['error']?['message']}\"\n}\n```\n\n\u003e Reference scope results: `@result('Scope_Get_Customer')` returns an array of action\n\u003e outcomes. Use `runAfter: {\"MyScope\": [\"Failed\", \"TimedOut\"]}` on a follow-up action\n\u003e to create try/catch semantics without a Terminate.\n\n---\n\n### Foreach (Sequential)\n\n```json\n\"Process_Each_Item\": {\n  \"type\": \"Foreach\",\n  \"runAfter\": {},\n  \"foreach\": \"@outputs('Get_Items')?['body/value']\",\n  \"operationOptions\": \"Sequential\",\n  \"actions\": {\n    \"Handle_Item\": {\n      \"type\": \"Compose\",\n      \"runAfter\": {},\n      \"inputs\": \"@item()?['Title']\"\n    }\n  }\n}\n```\n\n\u003e Always include `\"operationOptions\": \"Sequential\"` unless parallel is intentional.\n\n---\n\n### Foreach (Parallel with Concurrency Limit)\n\n```json\n\"Process_Each_Item_Parallel\": {\n  \"type\": \"Foreach\",\n  \"runAfter\": {},\n  \"foreach\": \"@body('Get_SP_Items')?['value']\",\n  \"runtimeConfiguration\": {\n    \"concurrency\": {\n      \"repetitions\": 20\n    }\n  },\n  \"actions\": {\n    \"HTTP_Upsert\": {\n      \"type\": \"Http\",\n      \"runAfter\": {},\n      \"inputs\": {\n        \"method\": \"POST\",\n        \"uri\": \"https://api.example.com/contacts/@{item()?['Email']}\"\n      }\n    }\n  }\n}\n```\n\n\u003e Set `repetitions` to control how many items are processed simultaneously.\n\u003e Practical values: `5–10` for external API calls (respect rate limits),\n\u003e `20–50` for internal/fast operations.\n\u003e Omit `runtimeConfiguration.concurrency` entirely for the platform default\n\u003e (currently 50). Do NOT use `\"operationOptions\": \"Sequential\"` and concurrency together.\n\n---\n\n### Wait (Delay)\n\n```json\n\"Delay_10_Minutes\": {\n  \"type\": \"Wait\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"interval\": {\n      \"count\": 10,\n      \"unit\": \"Minute\"\n    }\n  }\n}\n```\n\nValid `unit` values: `\"Second\"`, `\"Minute\"`, `\"Hour\"`, `\"Day\"`\n\n\u003e Use a Delay + re-fetch as a deduplication guard: wait for any competing process\n\u003e to complete, then re-read the record before acting. This avoids double-processing\n\u003e when multiple triggers or manual edits can race on the same item.\n\n---\n\n### Terminate (Success or Failure)\n\n```json\n\"Terminate_Success\": {\n  \"type\": \"Terminate\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"runStatus\": \"Succeeded\"\n  }\n},\n\"Terminate_Failure\": {\n  \"type\": \"Terminate\",\n  \"runAfter\": { \"Risky_Action\": [\"Failed\"] },\n  \"inputs\": {\n    \"runStatus\": \"Failed\",\n    \"runError\": {\n      \"code\": \"StepFailed\",\n      \"message\": \"@{outputs('Get_Error_Message')}\"\n    }\n  }\n}\n```\n\n---\n\n### Do Until (Loop Until Condition)\n\nRepeats a block of actions until an exit condition becomes true.\nUse when the number of iterations is not known upfront (e.g. paginating an API,\nwalking a time range, polling until a status changes).\n\n```json\n\"Do_Until_Done\": {\n  \"type\": \"Until\",\n  \"runAfter\": {},\n  \"expression\": \"@greaterOrEquals(variables('cursor'), variables('endValue'))\",\n  \"limit\": {\n    \"count\": 5000,\n    \"timeout\": \"PT5H\"\n  },\n  \"actions\": {\n    \"Do_Work\": {\n      \"type\": \"Compose\",\n      \"runAfter\": {},\n      \"inputs\": \"@variables('cursor')\"\n    },\n    \"Advance_Cursor\": {\n      \"type\": \"IncrementVariable\",\n      \"runAfter\": { \"Do_Work\": [\"Succeeded\"] },\n      \"inputs\": {\n        \"name\": \"cursor\",\n        \"value\": 1\n      }\n    }\n  }\n}\n```\n\n\u003e Always set `limit.count` and `limit.timeout` explicitly — the platform defaults are\n\u003e low (60 iterations, 1 hour). For time-range walkers use `limit.count: 5000` and\n\u003e `limit.timeout: \"PT5H\"` (ISO 8601 duration).\n\u003e\n\u003e The exit condition is evaluated **before** each iteration. Initialise your cursor\n\u003e variable before the loop so the condition can evaluate correctly on the first pass.\n\n---\n\n### Agent Retry Loop\n\nWhen a flow calls an AI or Copilot-style agent until it reaches a terminal\noutcome, keep the loop state explicit:\n\n- Initialize variables such as `agentStatus`, `attempt`, and `finalPayload`\n  before the `Until`.\n- Inside the loop, call the agent, validate the response, update the status, and\n  delay/retry only when the status is non-terminal.\n- Put final dispatch actions such as email, SharePoint update, or Teams post\n  after the loop so retries do not duplicate side effects.\n- If the platform rejects a complex `Switch` nested inside `Until`, keep the\n  loop body to simple validation and state updates, then route with `Switch`\n  after the loop.\n\n---\n\n### Async Polling with RequestId Correlation\n\nWhen an API starts a long-running job asynchronously (e.g. Power BI dataset refresh,\nreport generation, batch export), the trigger call returns a request ID. Capture it\nfrom the **response header**, then poll a status endpoint filtering by that exact ID:\n\n```json\n\"Start_Job\": {\n  \"type\": \"Http\",\n  \"inputs\": { \"method\": \"POST\", \"uri\": \"https://api.example.com/jobs\" }\n},\n\"Capture_Request_ID\": {\n  \"type\": \"Compose\",\n  \"runAfter\": { \"Start_Job\": [\"Succeeded\"] },\n  \"inputs\": \"@outputs('Start_Job')?['headers/X-Request-Id']\"\n},\n\"Initialize_Status\": {\n  \"type\": \"InitializeVariable\",\n  \"inputs\": { \"variables\": [{ \"name\": \"jobStatus\", \"type\": \"String\", \"value\": \"Running\" }] }\n},\n\"Poll_Until_Done\": {\n  \"type\": \"Until\",\n  \"expression\": \"@not(equals(variables('jobStatus'), 'Running'))\",\n  \"limit\": { \"count\": 60, \"timeout\": \"PT30M\" },\n  \"actions\": {\n    \"Delay\": { \"type\": \"Wait\", \"inputs\": { \"interval\": { \"count\": 20, \"unit\": \"Second\" } } },\n    \"Get_History\": {\n      \"type\": \"Http\",\n      \"runAfter\": { \"Delay\": [\"Succeeded\"] },\n      \"inputs\": { \"method\": \"GET\", \"uri\": \"https://api.example.com/jobs/history\" }\n    },\n    \"Filter_This_Job\": {\n      \"type\": \"Query\",\n      \"runAfter\": { \"Get_History\": [\"Succeeded\"] },\n      \"inputs\": {\n        \"from\": \"@outputs('Get_History')?['body/items']\",\n        \"where\": \"@equals(item()?['requestId'], outputs('Capture_Request_ID'))\"\n      }\n    },\n    \"Set_Status\": {\n      \"type\": \"SetVariable\",\n      \"runAfter\": { \"Filter_This_Job\": [\"Succeeded\"] },\n      \"inputs\": {\n        \"name\": \"jobStatus\",\n        \"value\": \"@first(body('Filter_This_Job'))?['status']\"\n      }\n    }\n  }\n},\n\"Handle_Failure\": {\n  \"type\": \"If\",\n  \"runAfter\": { \"Poll_Until_Done\": [\"Succeeded\"] },\n  \"expression\": { \"equals\": [\"@variables('jobStatus')\", \"Failed\"] },\n  \"actions\": { \"Terminate_Failed\": { \"type\": \"Terminate\", \"inputs\": { \"runStatus\": \"Failed\" } } },\n  \"else\": { \"actions\": {} }\n}\n```\n\nAccess response headers: `@outputs('Start_Job')?['headers/X-Request-Id']`\n\n\u003e **Status variable initialisation**: set a sentinel value (`\"Running\"`, `\"Unknown\"`) before\n\u003e the loop. The exit condition tests for any value other than the sentinel.\n\u003e This way an empty poll result (job not yet in history) leaves the variable unchanged\n\u003e and the loop continues — it doesn't accidentally exit on null.\n\u003e\n\u003e **Filter before extracting**: always `Filter Array` the history to your specific\n\u003e request ID before calling `first()`. History endpoints return all jobs; without\n\u003e filtering, status from a different concurrent job can corrupt your poll.\n\n---\n\n### runAfter Fallback (Failed → Alternative Action)\n\nRoute to a fallback action when a primary action fails — without a Condition block.\nSimply set `runAfter` on the fallback to accept `[\"Failed\"]` from the primary:\n\n```json\n\"HTTP_Get_Hi_Res\": {\n  \"type\": \"Http\",\n  \"runAfter\": {},\n  \"inputs\": { \"method\": \"GET\", \"uri\": \"https://api.example.com/data?resolution=hi-res\" }\n},\n\"HTTP_Get_Low_Res\": {\n  \"type\": \"Http\",\n  \"runAfter\": { \"HTTP_Get_Hi_Res\": [\"Failed\"] },\n  \"inputs\": { \"method\": \"GET\", \"uri\": \"https://api.example.com/data?resolution=low-res\" }\n}\n```\n\n\u003e Actions that follow can use `runAfter` accepting both `[\"Succeeded\", \"Skipped\"]` to\n\u003e handle either path — see **Fan-In Join Gate** below.\n\n---\n\n### Fan-In Join Gate (Merge Two Mutually Exclusive Branches)\n\nWhen two branches are mutually exclusive (only one can succeed per run), use a single\ndownstream action that accepts `[\"Succeeded\", \"Skipped\"]` from **both** branches.\nThe gate fires exactly once regardless of which branch ran:\n\n```json\n\"Increment_Count\": {\n  \"type\": \"IncrementVariable\",\n  \"runAfter\": {\n    \"Update_Hi_Res_Metadata\":  [\"Succeeded\", \"Skipped\"],\n    \"Update_Low_Res_Metadata\": [\"Succeeded\", \"Skipped\"]\n  },\n  \"inputs\": { \"name\": \"LoopCount\", \"value\": 1 }\n}\n```\n\n\u003e This avoids duplicating the downstream action in each branch. The key insight:\n\u003e whichever branch was skipped reports `Skipped` — the gate accepts that state and\n\u003e fires once. Only works cleanly when the two branches are truly mutually exclusive\n\u003e (e.g. one is `runAfter: [...Failed]` of the other).\n\n---\n\n## Expressions\n\n### Common Expression Patterns\n\n```\nNull-safe field access:    @item()?['FieldName']\nNull guard:                @coalesce(item()?['Name'], 'Unknown')\nString format:             @{variables('firstName')} @{variables('lastName')}\nDate today:                @utcNow()\nFormatted date:            @formatDateTime(utcNow(), 'dd/MM/yyyy')\nAdd days:                  @addDays(utcNow(), 7)\nArray length:              @length(variables('myArray'))\nFilter array:              Use the \"Filter array\" action (no inline filter expression exists in PA)\nUnion (new wins):          @union(body('New_Data'), outputs('Old_Data'))\nSort:                      @sort(variables('myArray'), 'Date')\nUnix timestamp → date:     @formatDateTime(addseconds('1970-1-1', triggerBody()?['created']), 'yyyy-MM-dd')\nDate → Unix milliseconds:  @div(sub(ticks(startOfDay(item()?['Created'])), ticks(formatDateTime('1970-01-01Z','o'))), 10000)\nDate → Unix seconds:       @div(sub(ticks(item()?['Start']), ticks('1970-01-01T00:00:00Z')), 10000000)\nUnix seconds → datetime:   @addSeconds('1970-01-01T00:00:00Z', int(variables('Unix')))\nCoalesce as no-else:       @coalesce(outputs('Optional_Step'), outputs('Default_Step'))\nFlow elapsed minutes:      @div(float(sub(ticks(utcNow()), ticks(outputs('Flow_Start')))), 600000000)\nHH:mm time string:         @formatDateTime(outputs('Local_Datetime'), 'HH:mm')\nResponse header:           @outputs('HTTP_Action')?['headers/X-Request-Id']\nArray max (by field):      @reverse(sort(body('Select_Items'), 'Date'))[0]\nInteger day span:          @int(split(dateDifference(outputs('Start'), outputs('End')), '.')[0])\nISO week number:           @div(add(dayofyear(addDays(subtractFromTime(date, sub(dayofweek(date),1), 'Day'), 3)), 6), 7)\nJoin errors to string:     @if(equals(length(variables('Errors')),0), null, concat(join(variables('Errors'),', '),' not found.'))\nNormalize before compare:  @replace(coalesce(outputs('Value'),''),'_',' ')\nRobust non-empty check:    @greater(length(trim(coalesce(string(outputs('Val')), ''))), 0)\n```\n\n### Unsupported / Risky Expression Assumptions\n\nPower Automate expressions are Workflow Definition Language, not JavaScript.\nThese patterns often look plausible but do not deploy or do not behave as agents\nexpect:\n\n| Goal | Avoid | Use instead |\n|---|---|---|\n| Build an object inline | `createObject(...)` | A Compose action with a JSON object literal |\n| Transform an array inline | `select(...)` inside an expression | Data Operations `Select` action |\n| Filter an array inline | `filter(...)` inside an expression | Data Operations `Filter array` action |\n| Find an array item index | `indexOf(array, item)` | Foreach with a counter variable, or build a keyed object map |\n\n### Newlines in Expressions\n\n\u003e **`\\n` does NOT produce a newline inside Power Automate expressions.** It is\n\u003e treated as a literal backslash + `n` and will either appear verbatim or cause\n\u003e a validation error.\n\nUse `decodeUriComponent('%0a')` wherever you need a newline character:\n\n```\nNewline (LF):   decodeUriComponent('%0a')\nCRLF:           decodeUriComponent('%0d%0a')\n```\n\nExample — multi-line Teams or email body via `concat()`:\n```json\n\"Compose_Message\": {\n  \"type\": \"Compose\",\n  \"inputs\": \"@concat('Hi ', outputs('Get_User')?['body/displayName'], ',', decodeUriComponent('%0a%0a'), 'Your report is ready.', decodeUriComponent('%0a'), '- The Team')\"\n}\n```\n\nExample — `join()` with newline separator:\n```json\n\"Compose_List\": {\n  \"type\": \"Compose\",\n  \"inputs\": \"@join(body('Select_Names'), decodeUriComponent('%0a'))\"\n}\n```\n\n\u003e This is the only reliable way to embed newlines in dynamically built strings\n\u003e in Power Automate flow definitions (confirmed against Logic Apps runtime).\n\n---\n\n### Sum an array (XPath trick)\n\nPower Automate has no native `sum()` function. Use XPath on XML instead:\n\n```json\n\"Prepare_For_Sum\": {\n  \"type\": \"Compose\",\n  \"runAfter\": {},\n  \"inputs\": { \"root\": { \"numbers\": \"@body('Select_Amounts')\" } }\n},\n\"Sum\": {\n  \"type\": \"Compose\",\n  \"runAfter\": { \"Prepare_For_Sum\": [\"Succeeded\"] },\n  \"inputs\": \"@xpath(xml(outputs('Prepare_For_Sum')), 'sum(/root/numbers)')\"\n}\n```\n\n`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.\n\n\u003e This is the only way to aggregate (sum/min/max) an array without a loop in Power Automate.\n","references/action-patterns-data.md":"# FlowStudio MCP — Action Patterns: Data Transforms\n\nArray operations, HTTP calls, parsing, and data transformation patterns.\n\n\u003e All examples assume `\"runAfter\"` is set appropriately.\n\u003e `\u003cconnectionName\u003e` is the **key** in `connectionReferences` (e.g. `shared_sharepointonline`), not the GUID.\n\u003e The GUID goes in the map value's `connectionName` property.\n\n---\n\n## Array Operations\n\n### Select (Reshape / Project an Array)\n\nTransforms each item in an array, keeping only the columns you need or renaming them.\nAvoids carrying large objects through the rest of the flow.\n\n```json\n\"Select_Needed_Columns\": {\n  \"type\": \"Select\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"from\": \"@outputs('HTTP_Get_Subscriptions')?['body/data']\",\n    \"select\": {\n      \"id\":           \"@item()?['id']\",\n      \"status\":       \"@item()?['status']\",\n      \"trial_end\":    \"@item()?['trial_end']\",\n      \"cancel_at\":    \"@item()?['cancel_at']\",\n      \"interval\":     \"@item()?['plan']?['interval']\"\n    }\n  }\n}\n```\n\nResult reference: `@body('Select_Needed_Columns')` — returns a direct array of reshaped objects.\n\n\u003e Use Select before looping or filtering to reduce payload size and simplify\n\u003e downstream expressions. Works on any array — SP results, HTTP responses, variables.\n\u003e\n\u003e **Tips:**\n\u003e - **Single-to-array coercion:** When an API returns a single object but you need\n\u003e   Select (which requires an array), wrap it: `@array(body('Get_Employee')?['data'])`.\n\u003e   The output is a 1-element array — access results via `?[0]?['field']`.\n\u003e - **Null-normalize optional fields:** Use `@if(empty(item()?['field']), null, item()?['field'])`\n\u003e   on every optional field to normalize empty strings, missing properties, and empty\n\u003e   objects to explicit `null`. Ensures consistent downstream `@equals(..., @null)` checks.\n\u003e - **Flatten nested objects:** Project nested properties into flat fields:\n\u003e   ```\n\u003e   \"manager_name\": \"@if(empty(item()?['manager']?['name']), null, item()?['manager']?['name'])\"\n\u003e   ```\n\u003e   This enables direct field-level comparison with a flat schema from another source.\n\n---\n\n### Filter Array (Query)\n\nFilters an array to items matching a condition. Use the action form (not the `filter()`\nexpression) for complex multi-condition logic — it's clearer and easier to maintain.\n\n```json\n\"Filter_Active_Subscriptions\": {\n  \"type\": \"Query\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"from\": \"@body('Select_Needed_Columns')\",\n    \"where\": \"@and(or(equals(item().status, 'trialing'), equals(item().status, 'active')), equals(item().cancel_at, null))\"\n  }\n}\n```\n\nResult reference: `@body('Filter_Active_Subscriptions')` — direct filtered array.\n\n\u003e Tip: run multiple Filter Array actions on the same source array to create\n\u003e named buckets (e.g. active, being-canceled, fully-canceled), then use\n\u003e `coalesce(first(body('Filter_A')), first(body('Filter_B')), ...)` to pick\n\u003e the highest-priority match without any loops.\n\n---\n\n### Create CSV Table (Array → CSV String)\n\nConverts an array of objects into a CSV-formatted string — no connector call, no code.\nUse after a `Select` or `Filter Array` to export data or pass it to a file-write action.\n\n```json\n\"Create_CSV\": {\n  \"type\": \"Table\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"from\": \"@body('Select_Output_Columns')\",\n    \"format\": \"CSV\"\n  }\n}\n```\n\nResult reference: `@body('Create_CSV')` — a plain string with header row + data rows.\n\n```json\n// Custom column order / renamed headers:\n\"Create_CSV_Custom\": {\n  \"type\": \"Table\",\n  \"inputs\": {\n    \"from\": \"@body('Select_Output_Columns')\",\n    \"format\": \"CSV\",\n    \"columns\": [\n      { \"header\": \"Date\",        \"value\": \"@item()?['transactionDate']\" },\n      { \"header\": \"Amount\",      \"value\": \"@item()?['amount']\" },\n      { \"header\": \"Description\", \"value\": \"@item()?['description']\" }\n    ]\n  }\n}\n```\n\n\u003e Without `columns`, headers are taken from the object property names in the source array.\n\u003e With `columns`, you control header names and column order explicitly.\n\u003e\n\u003e The output is a raw string. Write it to a file with `CreateFile` or `UpdateFile`\n\u003e (set `body` to `@body('Create_CSV')`), or store in a variable with `SetVariable`.\n\u003e\n\u003e If source data came from Power BI's `ExecuteDatasetQuery`, column names will be\n\u003e wrapped in square brackets (e.g. `[Amount]`). Strip them before writing:\n\u003e `@replace(replace(body('Create_CSV'),'[',''),']','')`\n\n---\n\n### range() + Select for Array Generation\n\n`range(0, N)` produces an integer sequence `[0, 1, 2, …, N-1]`. Pipe it through\na Select action to generate date series, index grids, or any computed array\nwithout a loop:\n\n```json\n// Generate 14 consecutive dates starting from a base date\n\"Generate_Date_Series\": {\n  \"type\": \"Select\",\n  \"inputs\": {\n    \"from\": \"@range(0, 14)\",\n    \"select\": \"@addDays(outputs('Base_Date'), item(), 'yyyy-MM-dd')\"\n  }\n}\n```\n\nResult: `@body('Generate_Date_Series')` → `[\"2025-01-06\", \"2025-01-07\", …, \"2025-01-19\"]`\n\nFor Cartesian products, iterate `range(0, mul(rowCount, colCount))` and derive\nindexes with `div(item(), colCount)` and `mod(item(), colCount)`.\n\n---\n\n### Dynamic Dictionary via json(concat(join()))\n\nWhen you need O(1) key→value lookups at runtime and Power Automate has no native\ndictionary type, build one from an array using Select + join + json:\n\n```json\n\"Build_Key_Value_Pairs\": {\n  \"type\": \"Select\",\n  \"inputs\": {\n    \"from\": \"@body('Get_Lookup_Items')?['value']\",\n    \"select\": \"@concat('\\\"', item()?['Key'], '\\\":\\\"', item()?['Value'], '\\\"')\"\n  }\n},\n\"Assemble_Dictionary\": {\n  \"type\": \"Compose\",\n  \"inputs\": \"@json(concat('{', join(body('Build_Key_Value_Pairs'), ','), '}'))\"\n}\n```\n\nLookup: `@outputs('Assemble_Dictionary')?['myKey']`\n\n\u003e The `json(concat('{', join(...), '}'))` pattern works for string values. For numeric\n\u003e or boolean values, omit the inner escaped quotes around the value portion.\n\u003e Keys must be unique — duplicate keys silently overwrite earlier ones.\n\u003e This replaces deeply nested `if(equals(key,'A'),'X', if(equals(key,'B'),'Y', ...))` chains.\n\n---\n\n### union() for Changed-Field Detection\n\nWhen you need to find records where *any* of several fields has changed, run one\n`Filter Array` per field and `union()` the results. This avoids a complex\nmulti-condition filter and produces a clean deduplicated set:\n\n```json\n\"Filter_Name_Changed\": {\n  \"type\": \"Query\",\n  \"inputs\": { \"from\": \"@body('Existing_Records')\",\n              \"where\": \"@not(equals(item()?['name'], item()?['dest_name']))\" }\n},\n\"Filter_Status_Changed\": {\n  \"type\": \"Query\",\n  \"inputs\": { \"from\": \"@body('Existing_Records')\",\n              \"where\": \"@not(equals(item()?['status'], item()?['dest_status']))\" }\n},\n\"All_Changed\": {\n  \"type\": \"Compose\",\n  \"inputs\": \"@union(body('Filter_Name_Changed'), body('Filter_Status_Changed'))\"\n}\n```\n\nReference: `@outputs('All_Changed')` — deduplicated array of rows where anything changed.\n\n\u003e `union()` deduplicates by object identity, so a row that changed in both fields\n\u003e appears once. Add more `Filter_*_Changed` inputs to `union()` as needed:\n\u003e `@union(body('F1'), body('F2'), body('F3'))`\n\n---\n\n### File-Content Change Gate\n\nBefore running expensive processing on a file or blob, compare its current content\nto a stored baseline. Skip entirely if nothing has changed — makes sync flows\nidempotent and safe to re-run or schedule aggressively.\n\n```json\n\"Get_File_From_Source\": { ... },\n\"Get_Stored_Baseline\": { ... },\n\"Condition_File_Changed\": {\n  \"type\": \"If\",\n  \"expression\": {\n    \"not\": {\n      \"equals\": [\n        \"@base64(body('Get_File_From_Source'))\",\n        \"@body('Get_Stored_Baseline')\"\n      ]\n    }\n  },\n  \"actions\": {\n    \"Update_Baseline\": { \"...\": \"overwrite stored copy with new content\" },\n    \"Process_File\":    { \"...\": \"all expensive work goes here\" }\n  },\n  \"else\": { \"actions\": {} }\n}\n```\n\n\u003e Store the baseline as a file in SharePoint or blob storage — `base64()`-encode the\n\u003e live content before comparing so binary and text files are handled uniformly.\n\u003e Write the new baseline **before** processing so a re-run after a partial failure\n\u003e does not re-process the same file again.\n\n---\n\n### Set-Join for Sync (Update Detection without Nested Loops)\n\nWhen syncing a source collection into a destination (e.g. API response → SharePoint list,\nCSV → database), avoid nested `Apply to each` loops to find changed records.\nInstead, **project flat key arrays** and use `contains()` to perform set operations —\nzero nested loops, and the final loop only touches changed items.\n\n**Insert/update/delete sync recipe:**\n\n1. `Select_Dest_Keys` from destination rows.\n2. `Filter_To_Insert`: source rows whose key is not in destination keys.\n3. `Filter_Already_Exists`: source rows whose key is in destination keys.\n4. For each compared field, run `Filter_\u003cField\u003e_Changed`; combine them with\n   `union()` into `Union_Changed`.\n5. `Select_Changed_Keys` from `Union_Changed`, then filter destination rows to\n   only those keys before updating.\n6. `Select_Source_Keys`, then `Filter_To_Delete` destination rows whose key is\n   not in source keys.\n\nThis changes O(n x m) nested loops to O(n + m) set operations and helps avoid\nPower Automate's 100k-action run limit.\n\n---\n\n### First-or-Null Single-Row Lookup\n\nUse `first()` on the result array to extract one record without a loop.\nThen null-check the output to guard downstream actions.\n\n```json\n\"Get_First_Match\": {\n  \"type\": \"Compose\",\n  \"runAfter\": { \"Get_SP_Items\": [\"Succeeded\"] },\n  \"inputs\": \"@first(outputs('Get_SP_Items')?['body/value'])\"\n}\n```\n\nIn a Condition, test for no-match with the **`@null` literal** (not `empty()`):\n\n```json\n\"Condition\": {\n  \"type\": \"If\",\n  \"expression\": {\n    \"not\": {\n      \"equals\": [\n        \"@outputs('Get_First_Match')\",\n        \"@null\"\n      ]\n    }\n  }\n}\n```\n\nAccess fields on the matched row: `@outputs('Get_First_Match')?['FieldName']`\n\n\u003e Use this instead of `Apply to each` when you only need one matching record.\n\u003e `first()` on an empty array returns `null`; `empty()` is for arrays/strings,\n\u003e not scalars — using it on a `first()` result causes a runtime error.\n\n---\n\n## HTTP \u0026 Parsing\n\n### HTTP Action (External API)\n\n```json\n\"Call_External_API\": {\n  \"type\": \"Http\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"method\": \"POST\",\n    \"uri\": \"https://api.example.com/endpoint\",\n    \"headers\": {\n      \"Content-Type\": \"application/json\",\n      \"Authorization\": \"Bearer @{variables('apiToken')}\"\n    },\n    \"body\": {\n      \"data\": \"@outputs('Compose_Payload')\"\n    },\n    \"retryPolicy\": {\n      \"type\": \"Fixed\",\n      \"count\": 3,\n      \"interval\": \"PT10S\"\n    }\n  }\n}\n```\n\nResponse reference: `@outputs('Call_External_API')?['body']`\n\n#### Variant: ActiveDirectoryOAuth (Service-to-Service)\n\nFor calling APIs that require Azure AD client-credentials (e.g., Microsoft Graph),\nuse in-line OAuth instead of a Bearer token variable:\n\n```json\n\"Call_Graph_API\": {\n  \"type\": \"Http\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"method\": \"GET\",\n    \"uri\": \"https://graph.microsoft.com/v1.0/users?$search=\\\"employeeId:@{variables('Code')}\\\"\u0026$select=id,displayName\",\n    \"headers\": {\n      \"Content-Type\": \"application/json\",\n      \"ConsistencyLevel\": \"eventual\"\n    },\n    \"authentication\": {\n      \"type\": \"ActiveDirectoryOAuth\",\n      \"authority\": \"https://login.microsoftonline.com\",\n      \"tenant\": \"\u003ctenant-id\u003e\",\n      \"audience\": \"https://graph.microsoft.com\",\n      \"clientId\": \"\u003capp-registration-id\u003e\",\n      \"secret\": \"@parameters('graphClientSecret')\"\n    }\n  }\n}\n```\n\n\u003e **When to use:** Calling Microsoft Graph, Azure Resource Manager, or any\n\u003e Azure AD-protected API from a flow without a premium connector.\n\u003e\n\u003e The `authentication` block handles the entire OAuth client-credentials flow\n\u003e transparently — no manual token acquisition step needed.\n\u003e\n\u003e `ConsistencyLevel: eventual` is required for Graph `$search` queries.\n\u003e Without it, `$search` returns 400.\n\u003e\n\u003e For PATCH/PUT writes, the same `authentication` block works — just change\n\u003e `method` and add a `body`.\n\u003e\n\u003e ⚠️ **Never hardcode `secret` inline.** Use `@parameters('graphClientSecret')`\n\u003e and declare it in the flow's `parameters` block (type `securestring`). This\n\u003e prevents the secret from appearing in run history or being readable via\n\u003e `get_live_flow`. Declare the parameter like:\n\u003e ```json\n\u003e \"parameters\": {\n\u003e   \"graphClientSecret\": { \"type\": \"securestring\", \"defaultValue\": \"\" }\n\u003e }\n\u003e ```\n\u003e Then pass the real value via the flow's connections or environment variables\n\u003e — never commit it to source control.\n\n---\n\n### HTTP Response (Return to Caller)\n\nUsed in HTTP-triggered flows to send a structured reply back to the caller.\nMust run before the flow times out (default 2 min for synchronous HTTP).\n\n```json\n\"Response\": {\n  \"type\": \"Response\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"statusCode\": 200,\n    \"headers\": {\n      \"Content-Type\": \"application/json\"\n    },\n    \"body\": {\n      \"status\": \"success\",\n      \"message\": \"@{outputs('Compose_Result')}\"\n    }\n  }\n}\n```\n\n\u003e **PowerApps / low-code caller pattern**: always return `statusCode: 200` with a\n\u003e `status` field in the body (`\"success\"` / `\"error\"`). PowerApps HTTP actions\n\u003e do not handle non-2xx responses gracefully — the caller should inspect\n\u003e `body.status` rather than the HTTP status code.\n\u003e\n\u003e Use multiple Response actions — one per branch — so each path returns\n\u003e an appropriate message. Only one will execute per run.\n\n---\n\n### Child Flow Call (Parent→Child via HTTP POST)\n\nPower Automate supports parent→child orchestration by calling a child flow's\nHTTP trigger URL directly. The parent sends an HTTP POST and blocks until the\nchild returns a `Response` action. The child flow uses a `manual` (Request) trigger.\n\n```json\n// PARENT — call child flow and wait for its response\n\"Call_Child_Flow\": {\n  \"type\": \"Http\",\n  \"inputs\": {\n    \"method\": \"POST\",\n    \"uri\": \"https://prod-XX.australiasoutheast.logic.azure.com:443/workflows/\u003cworkflowId\u003e/triggers/manual/paths/invoke?api-version=2016-06-01\u0026sp=%2Ftriggers%2Fmanual%2Frun\u0026sv=1.0\u0026sig=\u003cSAS\u003e\",\n    \"headers\": { \"Content-Type\": \"application/json\" },\n    \"body\": {\n      \"ID\": \"@triggerBody()?['ID']\",\n      \"WeekEnd\": \"@triggerBody()?['WeekEnd']\",\n      \"Payload\": \"@variables('dataArray')\"\n    },\n    \"retryPolicy\": { \"type\": \"none\" }\n  },\n  \"operationOptions\": \"DisableAsyncPattern\",\n  \"runtimeConfiguration\": {\n    \"contentTransfer\": { \"transferMode\": \"Chunked\" }\n  },\n  \"limit\": { \"timeout\": \"PT2H\" }\n}\n```\n\n```json\n// CHILD — manual trigger receives the JSON body\n// (trigger definition)\n\"manual\": {\n  \"type\": \"Request\",\n  \"kind\": \"Http\",\n  \"inputs\": {\n    \"schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"ID\": { \"type\": \"string\" },\n        \"WeekEnd\": { \"type\": \"string\" },\n        \"Payload\": { \"type\": \"array\" }\n      }\n    }\n  }\n}\n\n// CHILD — return result to parent\n\"Response_Success\": {\n  \"type\": \"Response\",\n  \"inputs\": {\n    \"statusCode\": 200,\n    \"headers\": { \"Content-Type\": \"application/json\" },\n    \"body\": { \"Result\": \"Success\", \"Count\": \"@length(variables('processed'))\" }\n  }\n}\n```\n\n\u003e **`retryPolicy: none`** — critical on the parent's HTTP call. Without it, a child\n\u003e flow timeout triggers retries, spawning duplicate child runs.\n\u003e\n\u003e **`DisableAsyncPattern`** — prevents the parent from treating a 202 Accepted as\n\u003e completion. The parent will block until the child sends its `Response`.\n\u003e\n\u003e **`transferMode: Chunked`** — enable when passing large arrays (\u003e100 KB) to the child;\n\u003e avoids request-size limits.\n\u003e\n\u003e **`limit.timeout: PT2H`** — raise the default 2-minute HTTP timeout for long-running\n\u003e children. Max is PT24H.\n\u003e\n\u003e The child flow's trigger URL contains a SAS token (`sig=...`) that authenticates\n\u003e the call. Copy it from the child flow's trigger properties panel. The URL changes\n\u003e if the trigger is deleted and re-created.\n\n---\n\n### Parse JSON\n\n```json\n\"Parse_Response\": {\n  \"type\": \"ParseJson\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"content\": \"@outputs('Call_External_API')?['body']\",\n    \"schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"id\": { \"type\": \"integer\" },\n        \"name\": { \"type\": \"string\" },\n        \"items\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"object\" }\n        }\n      }\n    }\n  }\n}\n```\n\nAccess parsed values: `@body('Parse_Response')?['name']`\n\n---\n\n### Manual CSV → JSON (No Premium Action)\n\nParse a raw CSV string into an array of objects using only built-in expressions.\nAvoids the premium \"Parse CSV\" connector action.\n\n```json\n\"Delimiter\": { \"type\": \"Compose\", \"inputs\": \",\" },\n\"Strip_Quotes\": { \"type\": \"Compose\", \"inputs\": \"@replace(body('Get_File_Content'), '\\\"', '')\" },\n\"Detect_Line_Ending\": {\n  \"type\": \"Compose\",\n  \"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'))\"\n},\n\"Headers\": {\n  \"type\": \"Compose\",\n  \"inputs\": \"@split(first(split(outputs('Strip_Quotes'), outputs('Detect_Line_Ending'))), outputs('Delimiter'))\"\n},\n\"Data_Rows\": { \"type\": \"Compose\", \"inputs\": \"@skip(split(outputs('Strip_Quotes'), outputs('Detect_Line_Ending')), 1)\" },\n\"Select_CSV_Body\": {\n  \"type\": \"Select\",\n  \"inputs\": {\n    \"from\": \"@outputs('Data_Rows')\",\n    \"select\": {\n      \"@{outputs('Headers')[0]}\": \"@split(item(), outputs('Delimiter'))[0]\",\n      \"@{outputs('Headers')[1]}\": \"@split(item(), outputs('Delimiter'))[1]\",\n      \"@{outputs('Headers')[2]}\": \"@split(item(), outputs('Delimiter'))[2]\"\n    }\n  }\n},\n\"Filter_Empty_Rows\": {\n  \"type\": \"Query\",\n  \"inputs\": {\n    \"from\": \"@body('Select_CSV_Body')\",\n    \"where\": \"@not(equals(item()?[outputs('Headers')[0]], null))\"\n  }\n}\n```\n\nResult: `@body('Filter_Empty_Rows')` — array of objects with header names as keys.\n\nNotes: `Detect_Line_Ending` handles CRLF/LF/CR. Dynamic keys in `Select` require\n`@{...}` interpolation. This simple pattern does not safely parse quoted fields\nwith embedded delimiters; for those, use a dedicated parser or custom action.\n\n---\n\n### ConvertTimeZone (Built-in, No Connector)\n\nConverts a timestamp between timezones with no API call or connector licence cost.\nFormat string `\"g\"` produces short locale date+time (`M/d/yyyy h:mm tt`).\n\n```json\n\"Convert_to_Local_Time\": {\n  \"type\": \"Expression\",\n  \"kind\": \"ConvertTimeZone\",\n  \"runAfter\": {},\n  \"inputs\": {\n    \"baseTime\": \"@{outputs('UTC_Timestamp')}\",\n    \"sourceTimeZone\": \"UTC\",\n    \"destinationTimeZone\": \"Taipei Standard Time\",\n    \"formatString\": \"g\"\n  }\n}\n```\n\nResult reference: `@body('Convert_to_Local_Time')` — **not** `outputs()`, unlike most actions.\n\nCommon `formatString` values: `\"g\"` (short), `\"f\"` (full), `\"yyyy-MM-dd\"`, `\"HH:mm\"`\n\nCommon timezone strings: `\"UTC\"`, `\"AUS Eastern Standard Time\"`, `\"Taipei Standard Time\"`,\n`\"Singapore Standard Time\"`, `\"GMT Standard Time\"`\n\n\u003e This is `type: Expression, kind: ConvertTimeZone` — a built-in Logic Apps action,\n\u003e not a connector. No connection reference needed. Reference the output via\n\u003e `body()` (not `outputs()`), otherwise the expression returns null.\n","references/build-patterns.md":"# Common Build Patterns\n\nComplete flow definition templates ready to copy and customize.\n\n---\n\n## Pattern: Recurrence + SharePoint list read + Teams notification\n\n```json\n{\n  \"triggers\": {\n    \"Recurrence\": {\n      \"type\": \"Recurrence\",\n      \"recurrence\": { \"frequency\": \"Day\", \"interval\": 1,\n                       \"startTime\": \"2026-01-01T08:00:00Z\",\n                       \"timeZone\": \"AUS Eastern Standard Time\" }\n    }\n  },\n  \"actions\": {\n    \"Get_SP_Items\": {\n      \"type\": \"OpenApiConnection\",\n      \"runAfter\": {},\n      \"inputs\": {\n        \"host\": {\n          \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n          \"connectionName\": \"shared_sharepointonline\",\n          \"operationId\": \"GetItems\"\n        },\n        \"parameters\": {\n          \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n          \"table\": \"MyList\",\n          \"$filter\": \"Status eq 'Active'\",\n          \"$top\": 500\n        }\n      }\n    },\n    \"Apply_To_Each\": {\n      \"type\": \"Foreach\",\n      \"runAfter\": { \"Get_SP_Items\": [\"Succeeded\"] },\n      \"foreach\": \"@outputs('Get_SP_Items')?['body/value']\",\n      \"actions\": {\n        \"Post_Teams_Message\": {\n          \"type\": \"OpenApiConnection\",\n          \"runAfter\": {},\n          \"inputs\": {\n            \"host\": {\n              \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_teams\",\n              \"connectionName\": \"shared_teams\",\n              \"operationId\": \"PostMessageToConversation\"\n            },\n            \"parameters\": {\n              \"poster\": \"Flow bot\",\n              \"location\": \"Channel\",\n              \"body/recipient\": {\n                \"groupId\": \"\u003cteam-id\u003e\",\n                \"channelId\": \"\u003cchannel-id\u003e\"\n              },\n              \"body/messageBody\": \"Item: @{items('Apply_To_Each')?['Title']}\"\n            }\n          }\n        }\n      },\n      \"operationOptions\": \"Sequential\"\n    }\n  }\n}\n```\n\n---\n\n## Pattern: HTTP trigger (webhook / Power App call)\n\n```json\n{\n  \"triggers\": {\n    \"manual\": {\n      \"type\": \"Request\",\n      \"kind\": \"Http\",\n      \"inputs\": {\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"name\": { \"type\": \"string\" },\n            \"value\": { \"type\": \"number\" }\n          }\n        }\n      }\n    }\n  },\n  \"actions\": {\n    \"Compose_Response\": {\n      \"type\": \"Compose\",\n      \"runAfter\": {},\n      \"inputs\": \"Received: @{triggerBody()?['name']} = @{triggerBody()?['value']}\"\n    },\n    \"Response\": {\n      \"type\": \"Response\",\n      \"runAfter\": { \"Compose_Response\": [\"Succeeded\"] },\n      \"inputs\": {\n        \"statusCode\": 200,\n        \"body\": { \"status\": \"ok\", \"message\": \"@{outputs('Compose_Response')}\" }\n      }\n    }\n  }\n}\n```\n\nAccess body values: `@triggerBody()?['name']`\n","references/flow-schema.md":"# FlowStudio MCP — Flow Definition Schema\n\nThe full JSON structure expected by `update_live_flow` (and returned by `get_live_flow`).\n\n---\n\n## Top-Level Shape\n\n```json\n{\n  \"$schema\": \"https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#\",\n  \"contentVersion\": \"1.0.0.0\",\n  \"parameters\": {\n    \"$connections\": {\n      \"defaultValue\": {},\n      \"type\": \"Object\"\n    }\n  },\n  \"triggers\": {\n    \"\u003cTriggerName\u003e\": { ... }\n  },\n  \"actions\": {\n    \"\u003cActionName\u003e\": { ... }\n  },\n  \"outputs\": {}\n}\n```\n\n---\n\n## `triggers`\n\nExactly one trigger per flow definition. The key name is arbitrary but\nconventional names are used (e.g. `Recurrence`, `manual`, `When_a_new_email_arrives`).\n\nSee [trigger-types.md](trigger-types.md) for all trigger templates.\n\n---\n\n## `actions`\n\nDictionary of action definitions keyed by unique action name.\nKey names may not contain spaces — use underscores.\n\nEach action must include:\n- `type` — action type identifier\n- `runAfter` — map of upstream action names → status conditions array\n- `inputs` — action-specific input configuration\n\nSee [action-patterns-core.md](action-patterns-core.md), [action-patterns-data.md](action-patterns-data.md),\nand [action-patterns-connectors.md](action-patterns-connectors.md) for templates.\n\n### Optional Action Properties\n\nBeyond the required `type`, `runAfter`, and `inputs`, actions can include:\n\n| Property | Purpose |\n|---|---|\n| `runtimeConfiguration` | Pagination, concurrency, secure data, chunked transfer |\n| `operationOptions` | `\"Sequential\"` for Foreach, `\"DisableAsyncPattern\"` for HTTP |\n| `limit` | Timeout override (e.g. `{\"timeout\": \"PT2H\"}`) |\n| `metadata` | Designer metadata such as `operationMetadataId` |\n\n#### Designer Metadata\n\nFor existing connector actions, preserve `metadata.operationMetadataId` when you\nedit the definition. For new connector actions or Skills/HTTP response actions,\nadd a stable GUID and keep it stable across updates. Do not regenerate these IDs\non every deploy; the designer and some run-only surfaces use them to keep action\nidentity consistent.\n\n#### `runtimeConfiguration` Variants\n\n**Pagination** (SharePoint Get Items with large lists):\n```json\n\"runtimeConfiguration\": {\n  \"paginationPolicy\": {\n    \"minimumItemCount\": 5000\n  }\n}\n```\n\u003e Without this, Get Items silently caps at 256 results. Set `minimumItemCount`\n\u003e to the maximum rows you expect. Required for any SharePoint list over 256 items.\n\n**Concurrency** (parallel Foreach):\n```json\n\"runtimeConfiguration\": {\n  \"concurrency\": {\n    \"repetitions\": 20\n  }\n}\n```\n\n**Secure inputs/outputs** (mask values in run history):\n```json\n\"runtimeConfiguration\": {\n  \"secureData\": {\n    \"properties\": [\"inputs\", \"outputs\"]\n  }\n}\n```\n\u003e Use on actions that handle credentials, tokens, or PII. Masked values show\n\u003e as `\"\u003credacted\u003e\"` in the flow run history UI and API responses.\n\n**Chunked transfer** (large HTTP payloads):\n```json\n\"runtimeConfiguration\": {\n  \"contentTransfer\": {\n    \"transferMode\": \"Chunked\"\n  }\n}\n```\n\u003e Enable on HTTP actions sending or receiving bodies \u003e100 KB (e.g. parent→child\n\u003e flow calls with large arrays).\n\n---\n\n## `runAfter` Rules\n\nThe first action in a branch has `\"runAfter\": {}` (empty — runs after trigger).\n\nSubsequent actions declare their dependency:\n\n```json\n\"My_Action\": {\n  \"runAfter\": {\n    \"Previous_Action\": [\"Succeeded\"]\n  }\n}\n```\n\nMultiple upstream dependencies:\n```json\n\"runAfter\": {\n  \"Action_A\": [\"Succeeded\"],\n  \"Action_B\": [\"Succeeded\", \"Skipped\"]\n}\n```\n\nError-handling action (runs when upstream failed):\n```json\n\"Log_Error\": {\n  \"runAfter\": {\n    \"Risky_Action\": [\"Failed\"]\n  }\n}\n```\n\n---\n\n## `parameters` (Flow-Level Input Parameters)\n\nOptional. Define reusable values at the flow level:\n\n```json\n\"parameters\": {\n  \"listName\": {\n    \"type\": \"string\",\n    \"defaultValue\": \"MyList\"\n  },\n  \"maxItems\": {\n    \"type\": \"integer\",\n    \"defaultValue\": 100\n  }\n}\n```\n\nReference: `@parameters('listName')` in expression strings.\n\n---\n\n## `outputs`\n\nRarely used in cloud flows. Leave as `{}` unless the flow is called\nas a child flow and needs to return values.\n\nFor child flows that return data:\n\n```json\n\"outputs\": {\n  \"resultData\": {\n    \"type\": \"object\",\n    \"value\": \"@outputs('Compose_Result')\"\n  }\n}\n```\n\n---\n\n## Scoped Actions (Inside Scope Block)\n\nActions that need to be grouped for error handling or clarity:\n\n```json\n\"Scope_Main_Process\": {\n  \"type\": \"Scope\",\n  \"runAfter\": {},\n  \"actions\": {\n    \"Step_One\": { ... },\n    \"Step_Two\": { \"runAfter\": { \"Step_One\": [\"Succeeded\"] }, ... }\n  }\n}\n```\n\n---\n\n## Full Minimal Example\n\n```json\n{\n  \"$schema\": \"https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#\",\n  \"contentVersion\": \"1.0.0.0\",\n  \"triggers\": {\n    \"Recurrence\": {\n      \"type\": \"Recurrence\",\n      \"recurrence\": {\n        \"frequency\": \"Week\",\n        \"interval\": 1,\n        \"schedule\": { \"weekDays\": [\"Monday\"] },\n        \"startTime\": \"2026-01-05T09:00:00Z\",\n        \"timeZone\": \"AUS Eastern Standard Time\"\n      }\n    }\n  },\n  \"actions\": {\n    \"Compose_Greeting\": {\n      \"type\": \"Compose\",\n      \"runAfter\": {},\n      \"inputs\": \"Good Monday!\"\n    }\n  },\n  \"outputs\": {}\n}\n```\n","references/trigger-types.md":"# FlowStudio MCP — Trigger Types\n\nCopy-paste trigger definitions for Power Automate flow definitions.\n\n---\n\n## Recurrence\n\nRun on a schedule.\n\n```json\n\"Recurrence\": {\n  \"type\": \"Recurrence\",\n  \"recurrence\": {\n    \"frequency\": \"Day\",\n    \"interval\": 1,\n    \"startTime\": \"2026-01-01T08:00:00Z\",\n    \"timeZone\": \"AUS Eastern Standard Time\"\n  }\n}\n```\n\nWeekly on specific days:\n```json\n\"Recurrence\": {\n  \"type\": \"Recurrence\",\n  \"recurrence\": {\n    \"frequency\": \"Week\",\n    \"interval\": 1,\n    \"schedule\": {\n      \"weekDays\": [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"]\n    },\n    \"startTime\": \"2026-01-05T09:00:00Z\",\n    \"timeZone\": \"AUS Eastern Standard Time\"\n  }\n}\n```\n\nCommon `timeZone` values:\n- `\"AUS Eastern Standard Time\"` — Sydney/Melbourne (UTC+10/+11)\n- `\"UTC\"` — Universal time\n- `\"E. Australia Standard Time\"` — Brisbane (UTC+10 no DST)\n- `\"New Zealand Standard Time\"` — Auckland (UTC+12/+13)\n- `\"Pacific Standard Time\"` — Los Angeles (UTC-8/-7)\n- `\"GMT Standard Time\"` — London (UTC+0/+1)\n\n---\n\n## Manual (HTTP Request / Power Apps)\n\nReceive an HTTP POST with a JSON body.\n\n```json\n\"manual\": {\n  \"type\": \"Request\",\n  \"kind\": \"Http\",\n  \"inputs\": {\n    \"schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"name\": { \"type\": \"string\" },\n        \"value\": { \"type\": \"integer\" }\n      },\n      \"required\": [\"name\"]\n    }\n  }\n}\n```\n\nAccess values: `@triggerBody()?['name']`  \nTrigger URL available after saving: `@listCallbackUrl()`\n\n#### No-Schema Variant (Accept Arbitrary JSON)\n\nWhen the incoming payload structure is unknown or varies, omit the schema\nto accept any valid JSON body without validation:\n\n```json\n\"manual\": {\n  \"type\": \"Request\",\n  \"kind\": \"Http\",\n  \"inputs\": {\n    \"schema\": {}\n  }\n}\n```\n\nAccess any field dynamically: `@triggerBody()?['anyField']`\n\n\u003e Use this for external webhooks (Stripe, GitHub, Employment Hero, etc.) where the\n\u003e payload shape may change or is not fully documented. The flow accepts any\n\u003e JSON without returning 400 for unexpected properties.\n\n---\n\n## Manual (Copilot Studio Skills)\n\nUse the Skills trigger when the flow is meant to be called by a Copilot Studio\nagent tool. Keep the trigger schema explicit so the agent receives predictable\ninput names and types.\n\n```json\n\"manual\": {\n  \"type\": \"Request\",\n  \"kind\": \"Skills\",\n  \"inputs\": {\n    \"schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"itemId\": { \"type\": \"string\" },\n        \"notes\": { \"type\": \"string\" }\n      },\n      \"required\": [\"itemId\"]\n    }\n  },\n  \"metadata\": {\n    \"operationMetadataId\": \"\u003cstable-guid\u003e\"\n  }\n}\n```\n\nAfter deploying a production Skills-triggered flow, call\n`add_live_flow_to_solution` with the target `solutionId`; Copilot Studio agent\ntool discovery expects the flow to be solution-aware. For MCP-driven testing,\nuse a temporary HTTP twin with the same actions and payload shape, then restore\nthe Skills trigger after the actions are verified.\n\n---\n\n## Automated (SharePoint Item Created)\n\n```json\n\"When_an_item_is_created\": {\n  \"type\": \"OpenApiConnectionNotification\",\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"OnNewItem\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"MyList\"\n    },\n    \"subscribe\": {\n      \"body\": { \"notificationUrl\": \"@listCallbackUrl()\" },\n      \"queries\": {\n        \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n        \"table\": \"MyList\"\n      }\n    }\n  }\n}\n```\n\nAccess trigger data: `@triggerBody()?['ID']`, `@triggerBody()?['Title']`, etc.\n\n---\n\n## Automated (SharePoint Item Modified)\n\n```json\n\"When_an_existing_item_is_modified\": {\n  \"type\": \"OpenApiConnectionNotification\",\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_sharepointonline\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"OnUpdatedItem\"\n    },\n    \"parameters\": {\n      \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n      \"table\": \"MyList\"\n    },\n    \"subscribe\": {\n      \"body\": { \"notificationUrl\": \"@listCallbackUrl()\" },\n      \"queries\": {\n        \"dataset\": \"https://mytenant.sharepoint.com/sites/mysite\",\n        \"table\": \"MyList\"\n      }\n    }\n  }\n}\n```\n\n---\n\n## Automated (Outlook: When New Email Arrives)\n\n```json\n\"When_a_new_email_arrives\": {\n  \"type\": \"OpenApiConnectionNotification\",\n  \"inputs\": {\n    \"host\": {\n      \"apiId\": \"/providers/Microsoft.PowerApps/apis/shared_office365\",\n      \"connectionName\": \"\u003cconnectionName\u003e\",\n      \"operationId\": \"OnNewEmail\"\n    },\n    \"parameters\": {\n      \"folderId\": \"Inbox\",\n      \"to\": \"monitored@contoso.com\",\n      \"isHTML\": true\n    },\n    \"subscribe\": {\n      \"body\": { \"notificationUrl\": \"@listCallbackUrl()\" }\n    }\n  }\n}\n```\n\n---\n\n## Child Flow (Called by Another Flow)\n\n```json\n\"manual\": {\n  \"type\": \"Request\",\n  \"kind\": \"Button\",\n  \"inputs\": {\n    \"schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"items\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"object\" }\n        }\n      }\n    }\n  }\n}\n```\n\nAccess parent-supplied data: `@triggerBody()?['items']`\n\nTo return data to the parent, add a `Response` action:\n```json\n\"Respond_to_Parent\": {\n  \"type\": \"Response\",\n  \"runAfter\": { \"Compose_Result\": [\"Succeeded\"] },\n  \"inputs\": {\n    \"statusCode\": 200,\n    \"body\": \"@outputs('Compose_Result')\"\n  }\n}\n```\n"},"import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/tree/541b7819d8c3545c6df122491af4fa1eae415779/plugins/flowstudio-power-automate/skills/flowstudio-power-automate-build"}},"content_hash":[59,174,210,172,191,177,73,180,128,48,231,121,145,212,9,187,84,204,156,17,208,107,186,4,188,202,28,71,113,142,1,169],"trust_level":"unsigned","yanked":false}
