01Summarize a ledger with the Run Python script action in a desktop flowNeeds check

The Run Python script action in Power Automate for desktop documents two versions, Python 2.7 and Python 3.4, with 2.7 as the default. Whatever the script prints lands in PythonScriptOutput, so print results as key=value lines for the flow to use.

Approach and steps
  1. Set Python version to Python 3.4 (the default is Python 2.7). Avoid newer syntax such as f-strings; use str.format
  2. Reference flow variables inside the script with percent notation, such as %LedgerPath%; they are replaced with values at run time
  3. Outside the flow nothing is replaced, so fall back to a sample file when the path does not exist
  4. Print lines such as total=count and open=count, then split PythonScriptOutput by line and by = in the flow
  5. Errors go to ScriptError; if it is not empty, stop the flow and notify the owner
PythonRuns locally
# Run Python script (Power Automate for desktop). Set "Python version" to Python 3.4.
# Keep to old syntax: no f-strings; use str.format.
import csv
import io
import os
from collections import Counter

ledger_path = r"%LedgerPath%"  # replaced by the flow variable at run time
if not os.path.exists(ledger_path):
    ledger_path = "samples/ledger.csv"  # local test outside the flow

with io.open(ledger_path, encoding="utf-8-sig", newline="") as f:
    rows = list(csv.DictReader(f))

status = Counter(r["状態"] for r in rows)
open_rows = [r for r in rows if r["状態"] != "完了"]
print("total={0}".format(len(rows)))
print("open={0}".format(len(open_rows)))
for name, n in sorted(status.items()):
    print("status.{0}={1}".format(name, n))
Output(2026-09-12)
total=40
open=26
status.受付=5
status.完了=14
status.差戻し=11
status.確認中=10
Example instruction for Copilot

Write Python for the Run Python script action in Power Automate for desktop (Python 3.4). Read the CSV at the flow variable %LedgerPath% (UTF-8 with BOM) and print counts per status and the open count as key=value lines. No f-strings; standard library only.

Caution

The page lists the selectable versions but does not name the interpreter or say which packages beyond the standard library are available. Point to external modules with Module folder paths. Percent signs mark flow variables, so format strings with format() instead of %. This code was tested only with Python 3.12 in a venv, not inside Power Automate for desktop.

Availability
Power Automate for desktop (Windows). The licensing page says the Free license covers authoring and running desktop flows locally (attended).
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11)
Source
Microsoft Learn, "Scripting actions reference"
Microsoft Learn, "Types of Power Automate licenses"
Verified
2026-10-25 (v1)
Supporting passages from the sources
To return values from Run Python script actions, use the print function.
Python 2.7, Python 3.4
To use Power Automate variables in scripting actions, use the percentage notation (%) and handle the variables the same way as hardcoded values.
as well as authoring and running desktop flows locally (attended)

02Write an HTTP-triggered Azure Function in Python for a cloud flowVerified

Use @app.route in the Python v2 model to build an HTTP-triggered function that takes JSON rows from a flow, summarizes them and returns JSON. With the function authorization level, callers must present a function key.

Approach and steps
  1. Create func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION); function is also the default when no level is set
  2. Accept requests with @app.route(route="summarize", methods=["POST"]) and summarize the rows from req.get_json()
  3. Keep the logic in summarize_rows() and test it by building an HttpRequest
  4. Return 400 for a body that is not JSON so the flow can treat it as a failure
  5. From the flow's HTTP action, send the key in the x-functions-key header (next tip)
PythonRuns as a service
"""HTTP-triggered function: take ledger rows as JSON and return counts."""
import json
from collections import Counter

import azure.functions as func

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)


def summarize_rows(rows):
    status = Counter(r.get("状態", "") for r in rows)
    open_by_owner = Counter(r.get("担当者", "") for r in rows if r.get("状態") != "完了")
    return {"total": len(rows), "status": dict(status), "open_by_owner": dict(open_by_owner)}


@app.route(route="summarize", methods=["POST"])
def summarize(req: func.HttpRequest) -> func.HttpResponse:
    try:
        rows = req.get_json().get("rows", [])
    except ValueError:
        return func.HttpResponse("JSON body with 'rows' is required", status_code=400)
    body = json.dumps(summarize_rows(rows), ensure_ascii=False)
    return func.HttpResponse(body, mimetype="application/json")
Output(2026-09-12)
{"total": 40, "status": {"完了": 14, "受付": 5, "差戻し": 11, "確認中": 10}, "open_by_owner": {"担当A": 10, "担当B": 7, "担当C": 3, "担当D": 6}}
bad request: 400
Example instruction for Copilot

Write an HTTP-triggered Azure Function (Python v2 model) that accepts POST JSON {"rows": [...]} and returns counts per status and open counts per owner as JSON. Use the FUNCTION auth level, keep the summary logic in its own function, and return 400 for non-JSON bodies.

Caution

The key can also go in the code query string, but URLs tend to end up in shared notes and logs, so prefer the header. Synchronous calls from a flow have a time limit, so split heavy work. If rows include personal names, do not write request bodies to the function logs.

Availability
Azure Functions (Python v2 programming model). Definition and logic tested with azure-functions 1.25.0.
Requires
azure-functions 1.25.0
Tested
definitions loaded (not started as a service); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, azure-functions 1.25.0)
Source
Microsoft Learn, "Azure Functions HTTP trigger"
Verified
2026-10-25 (v1)
Supporting passages from the sources
When a level isn't explicitly set, authorization defaults to the function level.
It can also be included in an x-functions-key HTTP header.

03Call the function from a flow's HTTP action: key header and time limitsNeeds check

In a cloud flow, POST to the function URL from an HTTP action and pass the key in the x-functions-key header. Synchronous outbound requests from a flow time out after 120 seconds, so move long work to an asynchronous polling pattern or an Until loop.

Approach and steps
  1. Set the HTTP action's Method to POST and URI to https://<APP_NAME>.azurewebsites.net/api/summarize
  2. The key can go in the code query string or the x-functions-key header; use the header so the URL carries no key
  3. Send rows as JSON in Body and use the returned JSON in later actions
  4. Synchronous requests time out after 120 seconds; use an asynchronous polling pattern or an Until loop for long work
  5. HTTP actions count toward the daily action limit like other actions
HTTP
POST /api/summarize HTTP/1.1
Host: <APP_NAME>.azurewebsites.net
Content-Type: application/json
x-functions-key: <FUNCTION_KEY>

{"rows": [{"依頼ID": "R-2026-002", "状態": "受付", "担当者": "担当A"},
          {"依頼ID": "R-2026-004", "状態": "完了", "担当者": "担当A"}]}
Caution

Do not type the key directly into the flow; supply it from the secure store your organization uses. The saved official pages did not settle how the HTTP action is licensed (standard or premium), so confirm with your admin before rollout.

Availability
Power Automate cloud flows. Timeout values are from the Power Automate limits page.
Tested
not executable (configuration, HTTP or formula) (2026-09-12)
Source
Microsoft Learn, "Azure Functions HTTP trigger"
Microsoft Learn, "Limits of automated, scheduled, and instant flows"
Microsoft Learn, "Power Automate licensing FAQ"
Verified
2026-10-25 (v1)
Supporting passages from the sources
It can also be included in an x-functions-key HTTP header.
For longer-running operations, use an asynchronous polling pattern or an "Until" loop.
count toward your daily action limit

04Start a flow from Python through the When an HTTP request is received triggerVerified

Keep the URL of a flow that starts on an HTTP request in an environment variable and POST JSON to it with requests. DRY_RUN is the default, printing the payload without sending. When the trigger is limited to users in your tenant, attach a bearer token.

Approach and steps
  1. The trigger has three authentication modes: Any user in my tenant (the default for new flows), Specific users in my tenant, and Anyone (a legacy setting; anyone with the URL can start the flow)
  2. The flow URL carries a sig= signature, so read it from the FLOW_URL environment variable instead of writing it in code
  3. When the trigger is limited to your tenant, send a token with the required claims such as aud in the Authorization header; the aud value must match exactly, trailing slash included
  4. Send only when DRY_RUN=0 and print the response status code
  5. If the URL leaks, regenerate the SAS key; the sig= value changes, which confirms the regeneration worked
PythonRuns locally
"""Send JSON to a flow that starts with 'When an HTTP request is received' (DRY_RUN by default)."""
import json
import os

import requests

DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
FLOW_URL = os.environ.get("FLOW_URL", "")  # carries a SAS signature: keep it out of code and logs
BEARER = os.environ.get("FLOW_BEARER_TOKEN", "")  # needed when the trigger allows tenant users only


def build_request(rows):
    headers = {"Content-Type": "application/json"}
    if BEARER:
        headers["Authorization"] = "Bearer " + BEARER
    return headers, {"source": "python", "rows": rows}


def main():
    rows = [{"依頼ID": "R-2026-041", "文書名": "文書41(架空)", "状態": "受付"}]
    headers, body = build_request(rows)
    if DRY_RUN or not FLOW_URL:
        print("DRY_RUN: would POST", len(rows), "row(s); bearer token:", "Authorization" in headers)
        print(json.dumps(body, ensure_ascii=False))
        return
    r = requests.post(FLOW_URL, headers=headers, json=body, timeout=30)
    print("status:", r.status_code)


if __name__ == "__main__":
    main()
Output(2026-09-12)
DRY_RUN: would POST 1 row(s); bearer token: False
{"source": "python", "rows": [{"依頼ID": "R-2026-041", "文書名": "文書41(架空)", "状態": "受付"}]}
Example instruction for Copilot

Write Python that POSTs JSON with requests to the Power Automate flow (When an HTTP request is received) at the FLOW_URL environment variable. Default to DRY_RUN, printing the payload without sending, add an Authorization header when FLOW_BEARER_TOKEN is set, and never log the URL.

Caution

Avoid the Anyone setting, since the URL alone starts the flow. The docs note that the OAuth option is still rolling out and may not be available in every region. Getting the token is out of scope here (see the security page). Inbound requests also have a time limit, so have the flow respond early.

Availability
Power Automate cloud flows. The trigger authentication setting is described as rolling out (checked 2026-09-12).
Requires
requests 2.34.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, requests 2.34.2)
Source
Microsoft Learn, "Add OAuth authentication for HTTP request triggers"
Microsoft Learn, "Regenerate the SAS key used in HTTP trigger flows"
Verified
2026-11-01 (v1)
Supporting passages from the sources
Anyone can trigger this workflow if they have access to the URL and the associated JSON schema.
Ensures that any user in the same tenant as the maker can trigger this workflow. This setting is the default for any new flows.
Audience values must be an exact match, including trailing slashes.
Once the key is regenerated, this value changes and serves as a confirmation that the execution of the following steps was successful.

05Office Scripts for the workbook, Python for external callsVerified

Office Scripts can be called from Power Automate with Run script, reading and writing workbook tables and returning a value to the flow. When run from a flow, however, external API calls fail, so hand external calls and heavy processing to a Python function and connect the two in the flow.

Approach and steps
  1. Put in-workbook work (reading and writing tables, totaling columns) in the Office Script main(workbook, ...) and return the result to the flow
  2. Call it from the flow with the Excel Online (Business) connector's Run script or Run script from SharePoint library action
  3. Keep calls to outside services out of the script; call a Python function from the flow's HTTP action instead
  4. Run script is limited to 1,600 calls per user per day, 120 seconds for synchronous operations, and 30,000,000 bytes of parameters
  5. Office Scripts can reach only the workbook, not the machine hosting it; keep work on local files on the Python side
Text
// Office Script (TypeScript), called from Power Automate "Run script".
// Counts rows whose status is not "完了" and returns the number to the flow.
function main(workbook: ExcelScript.Workbook, statusColumn: string): number {
  const table = workbook.getTable("RequestLedger");
  const values = table.getColumnByName(statusColumn).getRangeBetweenHeaderAndTotal().getValues();
  let open = 0;
  values.forEach((row) => {
    if (row[0] !== "完了") {
      open += 1;
    }
  });
  return open;
}
Example instruction for Copilot

Write an Office Scripts main function in TypeScript that counts rows in the RequestLedger table whose status column is not 完了 and returns the number. It is called from Power Automate Run script, takes the column name as a parameter, and must not use fetch.

Caution

Using Office Scripts in Power Automate requires a Microsoft 365 business license. Office 365 Enterprise E1 and Office 365 F3 can use scripts with Power Automate but lack the Power Automate integration inside Excel. Workbooks in the ISO strict format do not work with Run script. Admins can turn off the Excel Online connector or Office Scripts.

Availability
Excel on the web, Excel for Windows (Version 2210 or higher) or Excel for Mac, with OneDrive for Business and a supported Microsoft 365 license.
Tested
not executable (configuration, HTTP or formula) (2026-09-12)
Source
Microsoft Learn, "Platform limits and requirements with Office Scripts"
Microsoft Learn, "Run Office Scripts with Power Automate"
Microsoft Learn, "Differences between Office Scripts and VBA macros"
Verified
2026-11-01 (v1)
Supporting passages from the sources
External API calls fail when a script is run through Power Automate.
Each user is limited to 1,600 calls to the Run script action per day.
Run script from SharePoint library. This is the action to use when scripts are stored in your team's SharePoint site.
Office Scripts only have access to the workbook, not the machine hosting the workbook.

06Describe the function in OpenAPI 2.0 and import it as a custom connectorVerified

Write the function's inputs and outputs as an OpenAPI 2.0 (formerly Swagger) JSON file and bring it in with Import an OpenAPI file in Power Automate to use it as a flow action. Keep the definition under 1 MB; OpenAPI 3.0 files are not supported.

Approach and steps
  1. Set swagger to 2.0, host to the function app's host name and basePath to /api
  2. In securityDefinitions, set type apiKey, in header and name x-functions-key; keep the key value out of the definition
  3. Describe POST /summarize under paths; the name in operationId is what shows up as the action
  4. In Power Automate choose Import an OpenAPI file and check the host and base URL on the General page
  5. Connectors created in Power Automate are also available in Power Apps and Copilot Studio
JSON
{
  "swagger": "2.0",
  "info": {
    "version": "1.0.0",
    "title": "LedgerSummary",
    "description": "Summarize request-ledger rows with an Azure Function"
  },
  "host": "example-func.azurewebsites.net",
  "basePath": "/api",
  "schemes": [
    "https"
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "securityDefinitions": {
    "function_key": {
      "type": "apiKey",
      "in": "header",
      "name": "x-functions-key"
    }
  },
  "security": [
    {
      "function_key": []
    }
  ],
  "paths": {
    "/summarize": {
      "post": {
        "summary": "Count ledger rows by status",
        "operationId": "SummarizeLedger",
        "parameters": [
          {
            "in": "body",
            "name": "body",
            "schema": {
              "type": "object",
              "properties": {
                "rows": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  },
                  "x-ms-summary": "rows"
                }
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "200",
            "schema": {
              "type": "object",
              "properties": {
                "total": {
                  "type": "integer",
                  "x-ms-summary": "total"
                },
                "status": {
                  "type": "object",
                  "x-ms-summary": "status"
                },
                "open_by_owner": {
                  "type": "object",
                  "x-ms-summary": "open by owner"
                }
              }
            }
          }
        }
      }
    }
  }
}
Example instruction for Copilot

Write an OpenAPI 2.0 JSON definition, importable as a Power Automate custom connector, for the Azure Function POST /api/summarize (body {"rows": [...]}, response total, status and open_by_owner). Use apiKey authentication in the x-functions-key header and leave the key value out.

Caution

Definitions written in OpenAPI 3.0 cannot be imported, so check what format your generator produces. The Free license is limited to standard connectors, so custom connectors need a license such as Power Automate Premium. Host names and keys differ per environment, so check the connection settings after import.

Availability
Custom connectors in Power Automate, Power Apps, Copilot Studio and Logic Apps. The licensing page says Power Automate Premium covers standard, premium and custom connectors.
Tested
not executable (configuration, HTTP or formula) (2026-09-12)
Source
Microsoft Learn, "Create a custom connector from an OpenAPI definition"
Microsoft Learn, "Custom connectors overview"
Microsoft Learn, "Types of Power Automate licenses"
Verified
2026-11-01 (v1)
Supporting passages from the sources
OpenAPI definitions that are in OpenAPI 3.0 format are not supported.
When creating a custom connector, the OpenAPI definition must be less than 1 MB.
In this case, the DetectSentiment action from the OpenAPI definition is displayed.
Connectors created in Power Automate are available in Power Apps and Copilot Studio
Connector usage is limited to standard connectors only.

07Call an Agent from a Durable Functions orchestrator with context.call_agentVerified

Insert one Agent reasoning step into a long-running process. The starter returns 202 and the caller reads the result from statusQueryGetUri.

Approach and steps
  1. Change the package line in requirements.txt to azurefunctions-agents-extensions-agent-framework[durable,mcp].
  2. Do input preparation in an activity_trigger function. Keep validation, calculation and data minimization out of the orchestrator.
  3. Call context.call_agent() from the orchestrator. It takes the logical Agent name and a JSON-compatible input.
  4. The result is recorded in orchestration history and reused on replay instead of calling the model again.
  5. The starter calls client.start_new to begin the orchestrator and returns 202 with statusQueryGetUri.
  6. Check statusQueryGetUri until runtimeStatus becomes Completed, then read risk_assessment from the output.
PythonRuns locally
@app.activity_trigger(input_name="order")
def prepare_order_activity(order: dict) -> dict:
    # Validation and data minimization belong in an activity
    return {
        "order_id": order["order_id"],
        "customer_id": order["customer"]["id"],
        "items": order["items"],
    }


@app.orchestration_trigger(context_name="context")
def order_orchestrator(context: DurableAgentContext):
    prepared_order = yield context.call_activity(
        "prepare_order_activity",
        context.get_input(),
    )
    # The model call is scheduled as an extension-managed activity
    assessment = yield context.call_agent(
        "order-fulfillment",
        {"order": prepared_order, "task": "assess fulfillment risk"},
    )
    return {
        "order_id": prepared_order["order_id"],
        "risk_assessment": assessment,
    }
Example instruction for Copilot

Write a Durable Functions Python orchestrator. Reduce the input in an activity, call the Agent named order-fulfillment with context.call_agent, and return order_id and risk_assessment.

Caution

This is a preview feature. Do not call models or the network directly inside the orchestrator. Send minimal data to the model and have a person check the output. Put the order ID in the request body, not the route. Try it with small data.

Availability
Preview. Only Microsoft Agent Framework is supported and Python 3.13 or later is required. Install the durable extra for Durable Functions.
Requires
azurefunctions-agents-extensions-agent-framework[durable,mcp], agent-framework-foundry, azure-identity
Tested
syntax-checked only (not run) (2026-10-07)
Source
Azure SDK Blog, "Bring agentic reasoning to Python function apps with Azure Functions Agent bindings (preview)"
Verified
2026-10-07 (v1)
Supporting passages from the sources
context.call_agent() takes the logical Agent name and a JSON-compatible input.

08Write a @workflow_tool for Azure Functions Hosted Skills workflowsNeeds check

Write a tool for Dynamic Workflows in Hosted Skills as a function that takes one dict and returns a JSON-serializable value.

Approach and steps
  1. Set workflows.enabled: true in the front matter of .agent.md.
  2. Place a function decorated with @workflow_tool in the tools/ folder.
  3. The function takes one dict and returns a JSON-serializable value.
  4. Keep it stateless and safe to repeat with the same input.
PythonRuns locally
# tools/incident_tools.py
from typing import Any
from azure_functions_agents import workflow_tool

@workflow_tool(description="Fetch recent log lines for a service.")
async def fetch_logs(args: dict[str, Any]) -> dict[str, Any]:
    service = args["service"]
    return {"service": service, "lines": ["..."]}
Example instruction for Copilot

Write a @workflow_tool function in tools/ that takes a service name and returns log lines. One dict argument, JSON-serializable dict result.

Caution

This is a preview feature and the API can change. Keep tools stateless and idempotent. Try small data and check results yourself.

Availability
Dynamic Workflows in Azure Functions Hosted Skills is in public preview. Requires Python 3.13 or later, Azure Functions Core Tools v4, and Azurite.
Requires
azurefunctions-agents-runtime
Tested
syntax-checked only (not run) (2026-10-09)
Source
Azure SDK Blog, "Dynamic Workflows in Azure Functions Hosted Skills: Durable, AI-Led Work for Event-Driven Apps"
Verified
2026-10-09 (v1)
Supporting passages from the sources
Dynamic Workflows is a preview feature of Azure Functions Hosted Skills (public preview).

09Draft a Run Python script with Copilot natural language to script (preview)Verified

Ask Copilot in Power Automate for desktop to generate the code of a Run Python script action, then pass flow variables with percentage notation and return the result with print.

Approach and steps
  1. Drag a Run Python script action into the designer and select Generate script with Copilot.
  2. On the Create prompt screen, type a prompt that states the input and the output, then select Generate. To recreate it, change the prompt and select Regenerate.
  3. If the result is acceptable, select Use this script, then modify the code in the action window and add any necessary variables.
  4. Write flow variables with percentage notation (%); they are handled the same way as hardcoded values.
  5. Return the result with print. The returned value is stored in PythonScriptOutput and errors are stored in ScriptError.
  6. Always review the generated code before use.
PythonRuns locally
# Power Automate replaces %...% with the variable value before the script runs
variableName = "%LedgerText%"
rowCount = len(variableName.splitlines())
# Return the result to the flow with print
print(rowCount)
Example instruction for Copilot

Write Python code. The variable %LedgerText% holds the text of a request ledger. Count the lines and return only that number with print. Do not use external libraries.

Caution

Natural language to script is a preview feature and is not meant for production use. It is available only to work or school accounts in environments located in the listed regions. Always review the generated code and test with a small amount of data first.

Availability
Preview. Copilot in Power Automate for desktop; work or school accounts only; environments located in the supported regions.
Tested
syntax-checked only (not run) (2026-10-30)
Source
Microsoft Learn, "Scripting actions (Power Automate for desktop)"
Verified
2026-10-30 (v1)
Supporting passages from the sources
Python scripts don't require any special notation to declare new variables. To return values from Run Python script actions, use the print function.