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.
- Set Python version to Python 3.4 (the default is Python 2.7). Avoid newer syntax such as f-strings; use str.format
- Reference flow variables inside the script with percent notation, such as %LedgerPath%; they are replaced with values at run time
- Outside the flow nothing is replaced, so fall back to a sample file when the path does not exist
- Print lines such as total=count and open=count, then split PythonScriptOutput by line and by = in the flow
- Errors go to ScriptError; if it is not empty, stop the flow and notify the owner
# 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.確認中=10Write 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.
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.
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.
- Create func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION); function is also the default when no level is set
- Accept requests with @app.route(route="summarize", methods=["POST"]) and summarize the rows from req.get_json()
- Keep the logic in summarize_rows() and test it by building an HttpRequest
- Return 400 for a body that is not JSON so the flow can treat it as a failure
- From the flow's HTTP action, send the key in the x-functions-key header (next tip)
"""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: 400Write 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.
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.
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.
- Set the HTTP action's Method to POST and URI to https://<APP_NAME>.azurewebsites.net/api/summarize
- The key can go in the code query string or the x-functions-key header; use the header so the URL carries no key
- Send rows as JSON in Body and use the returned JSON in later actions
- Synchronous requests time out after 120 seconds; use an asynchronous polling pattern or an Until loop for long work
- HTTP actions count toward the daily action limit like other actions
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"}]}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.
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.
- 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)
- The flow URL carries a sig= signature, so read it from the FLOW_URL environment variable instead of writing it in code
- 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
- Send only when DRY_RUN=0 and print the response status code
- If the URL leaks, regenerate the SAS key; the sig= value changes, which confirms the regeneration worked
"""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(架空)", "状態": "受付"}]}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.
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.
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.
- Put in-workbook work (reading and writing tables, totaling columns) in the Office Script main(workbook, ...) and return the result to the flow
- Call it from the flow with the Excel Online (Business) connector's Run script or Run script from SharePoint library action
- Keep calls to outside services out of the script; call a Python function from the flow's HTTP action instead
- Run script is limited to 1,600 calls per user per day, 120 seconds for synchronous operations, and 30,000,000 bytes of parameters
- Office Scripts can reach only the workbook, not the machine hosting it; keep work on local files on the Python side
// 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;
}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.
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.
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.
- Set swagger to 2.0, host to the function app's host name and basePath to /api
- In securityDefinitions, set type apiKey, in header and name x-functions-key; keep the key value out of the definition
- Describe POST /summarize under paths; the name in operationId is what shows up as the action
- In Power Automate choose Import an OpenAPI file and check the host and base URL on the General page
- Connectors created in Power Automate are also available in Power Apps and Copilot Studio
{
"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"
}
}
}
}
}
}
}
}
}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.
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.
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.