01Pre-check file type, size, and count before handing files to code interpreterVerified
Copilot Studio agents can generate and run Python with code interpreter to analyze CSV and Excel files (preview). Each file can be up to 16 MB, with a maximum of 10 files, and a code interpreter prompt cannot analyze several files at once. Check files locally before you hand them over.
- For files attached in agent chat, check the type (.csv, .xlsx), the size of each file, and the number of files
- For a prompt built in prompt builder, pass one file per run
- Microsoft's extensively tested scenarios are lookups and aggregations over Excel and CSV, so verify results from other file types with extra care
- Split files, or drop columns, that do not meet the limits before sending them
from pathlib import Path
# Limits from the Copilot Studio docs (structured data in agents, preview)
AGENT_MAX_MB = 16
AGENT_MAX_FILES = 10
TESTED_TYPES = {".csv", ".xlsx"} # types in Microsoft's extensively tested scenarios
def precheck(paths, mode="agent"):
paths = [Path(p) for p in paths]
problems = []
if mode == "prompt":
# prompts: one uploaded file per run; no size limit is documented
if len(paths) > 1:
problems.append(f"prompt takes one file, got {len(paths)}")
return problems
if len(paths) > AGENT_MAX_FILES:
problems.append(f"too many files: {len(paths)} > {AGENT_MAX_FILES}")
for p in paths:
size_mb = p.stat().st_size / (1024 * 1024)
if size_mb > AGENT_MAX_MB:
problems.append(f"{p.name}: {size_mb:.1f} MB > {AGENT_MAX_MB} MB")
if p.suffix.lower() not in TESTED_TYPES:
problems.append(f"{p.name}: {p.suffix} is outside the tested CSV/Excel scenarios")
return problems
files = ["samples/ledger.csv", "samples/request_ledger.xlsx", "samples/doc_a.docx"]
for mode in ("agent", "prompt"):
issues = precheck(files, mode)
print(mode, "OK" if not issues else "issues:")
for i in issues:
print(" -", i)Output(2026-09-12)
agent issues:
- doc_a.docx: .docx is outside the tested CSV/Excel scenarios
prompt issues:
- prompt takes one file, got 3Write a Python function that takes a list of files for Copilot Studio code interpreter and prints check results for agent use (per-file size limit, file count limit, warning for types other than CSV and xlsx) and for prompt use (one file only). Keep the limits in constants.
Structured-data analysis is in preview, and limits or behavior can change; the constants reflect the documentation on the day it was fetched, so recheck before use. The fetched pages give no size limit for prompts. Agents that use code interpreter must be configured for user authentication. Verify the analysis against the source data.
Supporting passages from the sources
The size limit for each file that can be analyzed by code interpreter, irrespective of file type, is 16 MB. You can upload a maximum of 10 files.
Analyzing multiple files uploaded in a single prompt isn't supported.
By using code interpreter, Copilot Studio agents can generate and run Python code when they need to respond to user prompts.
Code interpreter must be enabled for each environment before you can use it. The default setting is Off.
02Call a code interpreter prompt through the Predict API and get the executed codeVerified
A prompt with code interpreter turned on can be called through the Dataverse Web API Predict action. Besides the generated content, predictionOutput in the response carries the Python code that ran (code) and its execution log (logs).
- Create the prompt in Copilot Studio or Power Apps and turn on code interpreter (prompts cannot be created through the API)
- Query msdyn_aimodels by msdyn_name and note the row ID of the prompt
- POST to msdyn_aimodels(ID)/Microsoft.Dynamics.CRM.Predict with version "2.0" and the prompt inputs in requestv2
- If overrideHttpStatusCode is 202, poll by sending GET requests to overrideLocation
- Save code and logs from the response and have someone read them before the result is used
import os
import time
import requests
from azure.identity import InteractiveBrowserCredential
ENV_URL = os.environ.get("DATAVERSE_URL", "https://example.crm.dynamics.com")
MODEL_ID = os.environ.get("AI_MODEL_ID", "") # msdyn_aimodelid of the prompt
DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
API = f"{ENV_URL}/api/data/v9.2"
HEADERS = {"Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0"}
def predict_body(inputs):
# Prompt inputs travel in requestv2 as an open type; keys must match the prompt's input names
return {"version": "2.0",
"requestv2": {"@odata.type": "#Microsoft.Dynamics.CRM.expando", **inputs}}
def wait_seconds(value, default=5):
# overrideRetryAfter format is not documented; fall back to a fixed wait
try:
return max(1, int(float(value)))
except (TypeError, ValueError):
return default
def output_of(resp):
v2 = resp["responsev2"]
out = v2["predictionOutput"]
return {"status": v2.get("operationStatus"), "text": out.get("text", ""),
"code": out.get("code", ""), "logs": out.get("logs", ""),
"files": [f.get("file_name") for f in out.get("files", [])]}
def main():
cred = InteractiveBrowserCredential(tenant_id=os.environ["TENANT_ID"], client_id=os.environ["CLIENT_ID"])
token = cred.get_token(f"{ENV_URL}/user_impersonation").token
session = requests.Session()
session.headers.update({**HEADERS, "Authorization": f"Bearer {token}"})
url = f"{API}/msdyn_aimodels({MODEL_ID})/Microsoft.Dynamics.CRM.Predict"
body = predict_body({"question": "Count requests by document type"})
if DRY_RUN:
print("DRY_RUN: would POST", url, body)
return
r = session.post(url, json=body, timeout=120)
r.raise_for_status()
data = r.json()
for _ in range(30):
if str(data.get("overrideHttpStatusCode")) != "202":
break
time.sleep(wait_seconds(data.get("overrideRetryAfter")))
data = session.get(data["overrideLocation"], timeout=60).json()
result = output_of(data)
print(result["status"], result["files"])
print(result["code"])
if __name__ == "__main__":
main()Output(2026-09-12)
Success ['summary.csv']
version: 2.0 | wait: 5 3Write a Python function using requests that calls the Dataverse Predict action with version "2.0" and requestv2 (@odata.type #Microsoft.Dynamics.CRM.expando). If overrideHttpStatusCode is 202, poll overrideLocation, then return text, code, logs, and file names from predictionOutput. Read the environment URL and IDs from environment variables and do not send by default.
Predict calls consume AI Builder capacity. No capacity returns 403 (EntitlementNotAvailable); too many concurrent calls return 500 (MaxConcurrentPlexCallsReachedException) with no Retry-After header, so wait and retry. The format of overrideRetryAfter is not documented, so fall back to a fixed wait. The code field holds the executed source or a placeholder describing it.
Supporting passages from the sources
In case the prediction isn't completed, 202 indicates that a polling is necessary, otherwise null.
The version parameter. The value is always "2.0".
Python Source code or placeholder describing executed code.
use a "<environment-url>/user_impersonation" scope for a public client.
03Save the executed code, logs, and generated files from a Predict responseVerified
predictionOutput in a Predict response holds the Python that ran (code), the execution log (logs), and generated files (base64_content in files). Save them to disk, flag lines that drop or filter rows, and have someone read the code before the result is used.
- Save the Predict response JSON
- Check that responsev2.operationStatus is Success
- Write predictionOutput.code to a .py file and logs to a .txt file
- Decode base64_content for each entry in files and save it under file_name (strip any path)
- Flag lines that change the row count, such as dropna or drop_duplicates, and check that their conditions are intended
import base64
import json
from pathlib import Path
SRC = "samples/excel_predict_response.json" # a saved Predict response
OUT = Path("out_predict")
OUT.mkdir(exist_ok=True)
resp = json.loads(Path(SRC).read_text(encoding="utf-8"))
v2 = resp["responsev2"]
if v2.get("operationStatus") != "Success":
raise SystemExit(f"prediction did not succeed: {v2.get('operationStatus')}")
out = v2["predictionOutput"]
code = out.get("code", "")
(OUT / "executed_code.py").write_text(code, encoding="utf-8")
(OUT / "logs.txt").write_text(out.get("logs", ""), encoding="utf-8")
for f in out.get("files", []):
name = Path(f["file_name"]).name # keep the base name only
(OUT / name).write_bytes(base64.b64decode(f["base64_content"]))
print("file:", name, f.get("content_type"))
print("code lines:", len(code.splitlines()), "| finish:", out.get("finishReason"))
# Lines that change the row count deserve a closer look
WATCH = ("dropna", "fillna", "drop_duplicates", ".query(")
for no, line in enumerate(code.splitlines(), 1):
if any(w in line for w in WATCH):
print(f"review line {no}: {line.strip()}")Output(2026-09-12)
file: summary.csv text/csv
code lines: 7 | finish: stop
review line 3: df = df.dropna(subset=['指摘件数'])Write Python that reads a Dataverse Predict response JSON, saves predictionOutput.code to executed_code.py and logs to logs.txt, decodes files from base64 and saves them, and prints the lines of the code that contain dropna, fillna, drop_duplicates, or query with line numbers.
The code field can be a placeholder describing the executed code rather than the code itself; then check logs and the generated files. Check the content type before opening generated files. The flag list is only a starting point, not a substitute for reading. Responses can include parts of the input data, so restrict access to where you save them.
Supporting passages from the sources
Generated file artifacts with file_name, content_type, and base64_content properties.
Python code execution log output (if provided).
operationStatus: A string value showing whether the operation succeeded. Expected value is Success.
04Recompute Copilot in Excel's answer with pandas and compare the numbersVerified
Copilot in Excel answers with Python-based analysis and leaves the workbook unchanged. Computing the same question yourself in local pandas and lining up the numbers makes it easier to catch a wrong column or filter.
- Note the numbers in the answer and the conditions in the displayed code (columns, filters, aggregation)
- Save the same workbook locally and read it with pandas.read_excel
- Write your own aggregation for the same question without looking at Copilot's code
- Copy the answer's numbers into a dict and list rows that differ or exist on one side only
- Where they differ, compare the filters and missing-value handling in Copilot's code
import pandas as pd
SRC = "samples/request_ledger.xlsx"
# Numbers copied from Copilot's answer to "average findings by department" (example)
copilot = {"営業部": 4.6, "広報部": 4.4, "人事部": 4.1}
df = pd.read_excel(SRC)
mine = df.groupby("依頼部署")["指摘件数"].mean().round(1)
check = pd.DataFrame({"copilot": pd.Series(copilot, dtype=float), "pandas": mine})
check["diff"] = (check["copilot"] - check["pandas"]).round(2)
print(check.to_string())
mismatch = check[(check["diff"].abs() > 0.05) | check.isna().any(axis=1)]
print("rows used:", len(df), "| mismatch:", list(mismatch.index))Output(2026-09-12)
copilot pandas diff
人事部 4.1 4.1 0.0
営業部 4.6 4.6 0.0
広報部 4.4 4.2 0.2
rows used: 40 | mismatch: ['広報部']Write pandas code that reads request_ledger.xlsx, computes the mean findings per department rounded to one decimal, lines it up with values pasted into a dict, and prints departments whose difference exceeds 0.05 or that appear on only one side.
Agreement does not rule out both sides reading the same wrong range, so compare row counts too. Inserted static tables or images do not refresh; recompute after the source changes. Unstructured data is not supported.
Supporting passages from the sources
Copilot does not modify your workbook. Sometimes, it may return a static table or image visualization that you can insert into your sheet, but these aren’t refreshable.
You can expand the code to see how the answer was generated.
05Run advanced analysis code locally with a stand-in for xl()Verified
Code from advanced analysis mode targets Python in Excel cells and reads tables through xl(). Define a local function with the same name that reads the same table from the saved workbook, and you can run the code unchanged and compare it with the result in Excel.
- Insert the advanced analysis code as a Python cell, or copy the code
- Save the workbook and keep a local copy
- Build a dict that maps each reference passed to xl() to a sheet in the saved workbook
- Define xl() with the same name and run the copied code as is
- Import pandas and the other libraries explicitly; Python in Excel imports them by default
import pandas as pd # pre-imported in Python in Excel; explicit here
BOOK = "samples/request_ledger.xlsx"
# Map each reference used in the copied code to a sheet in the saved book
REFS = {"RequestLedger[#All]": "依頼台帳"}
def xl(ref, headers=False):
# Local stand-in for Python in Excel's xl(): reads saved cell values only
return pd.read_excel(BOOK, sheet_name=REFS[ref], header=0 if headers else None)
# ---- code copied from the Python cell (unchanged) ----
df = xl("RequestLedger[#All]", headers=True)
df["依頼月"] = pd.to_datetime(df["依頼日"]).dt.to_period("M")
monthly = df.pivot_table(index="依頼月", columns="状態", values="依頼ID",
aggfunc="count", fill_value=0)
# ---- end of copied code ----
print(monthly.to_string())
print("rows read:", len(df))Output(2026-09-12)
状態 受付 完了 差戻し 確認中
依頼月
2026-07 5 6 5 5
2026-08 0 8 6 5
rows read: 40I want to run Python in Excel code locally. Write a function with the same signature as xl(ref, headers=False) that maps reference names to sheet names with a dict and reads the saved workbook with pandas.read_excel, using the first row as headers when headers=True.
The local xl() only reads cell values; it does not reproduce table boundaries or formula recalculation, and unsaved edits are not included. Local library versions can differ from the Python in Excel environment, so consider version differences when results disagree.
Supporting passages from the sources
This opens a new sheet and runs Python code for deeper, customizable analysis. It’s ideal for users who want to: Explore more complex scenarios Modify and insert underlying Python code as a refreshable Python cell
The following open-source libraries are available with Python in Excel by default. They've been imported with the statements listed.
This means that Python formulas have access to read cell values within the workbook, based on the cell reference, or values from external data sources, through the Power Query connection name.
06List what changed after Copilot writes Python results into a workbookVerified
When editing with Copilot, Python can run and write its results straight into the workbook. Save a copy before the edit, then compare it with the edited file cell by cell in openpyxl to list changed cells, new cells, and new sheets.
- Before asking Copilot to edit, save a copy of the workbook under another name
- Save the edited workbook too, and read both with openpyxl's default mode (formulas stay as text)
- List cells with the same sheet and address whose value or formula differs
- List added or removed sheets and new cells on existing sheets
- Review the list and restore anything unintended from the copy
from openpyxl import load_workbook
BEFORE = "samples/request_ledger.xlsx" # copy saved before the Copilot edit
AFTER = "samples/excel_ledger_after.xlsx" # workbook after the edit
def cells(path):
wb = load_workbook(path) # default mode: formulas as text, nothing recalculated
found = {}
for ws in wb.worksheets:
for row in ws.iter_rows():
for c in row:
if c.value is not None:
found[(ws.title, c.coordinate)] = c.value
return found, wb.sheetnames
a, sheets_a = cells(BEFORE)
b, sheets_b = cells(AFTER)
print("added sheets:", [s for s in sheets_b if s not in sheets_a])
print("removed sheets:", [s for s in sheets_a if s not in sheets_b])
changed = sorted(k for k in a.keys() & b.keys() if a[k] != b[k])
new_in_old_sheets = sorted(k for k in b.keys() - a.keys() if k[0] in sheets_a)
cleared = sorted(a.keys() - b.keys())
for k in changed:
print("changed", k, repr(a[k]), "->", repr(b[k]))
print(f"changed={len(changed)} new_in_existing_sheets={len(new_in_old_sheets)} cleared={len(cleared)}")Output(2026-09-12)
added sheets: ['集計']
removed sheets: []
changed ('依頼台帳', 'G3') '受付' -> '完了'
changed ('依頼台帳', 'H10') 9 -> 12
changed=2 new_in_existing_sheets=1 cleared=0Write Python with openpyxl that reads two xlsx files (before and after an edit) and prints added and removed sheets, cells whose value or formula changed, and counts of new and cleared cells on existing sheets. Do not modify either file.
Only cell values and formula text are compared; formatting, charts, PivotTables, and shapes are not. Formulas are not calculated, so check differences in formula results in Excel. Large workbooks take time and memory. Where change records are required, this list does not replace them.
Supporting passages from the sources
Edit with Copilot helps you work with Python in Excel by executing Python code for advanced analysis, automation, and data transformation with results outputted directly in your workbook.
Copilot updates your workbook using Excel's built-in features. Your content stays editable, and you're in control of everything that's modified.
data_only controls whether cells with formulae have either the formula (default) or the value stored the last time Excel read the sheet.