How-to
Automating Reporting WorkflowsDeep dive

Create a New Workbook in OneDrive with Microsoft Graph

Create an Excel workbook in OneDrive or SharePoint from Python with Microsoft Graph: build the xlsx in memory, upload it with a simple PUT, control what happens when a file with the same name exists, use an upload session with correctly sized chunks for larger files, turn a range into an Excel table through the workbook API, and verify the requests before pointing the code at a real tenant.

Most Microsoft Graph examples for Excel start from a workbook that already exists in OneDrive or SharePoint. Often, though, the job is to create one: a weekly report that should appear in a team's document library, a fresh tracking workbook for each new project, a file colleagues will edit in Excel for the web. Graph has no "create workbook" call as such — you create the file by uploading an .xlsx, and then, if needed, use the workbook API on it. This page builds the workbook in memory with pandas, uploads it with a single request for ordinary file sizes and an upload session for larger ones, controls what happens when a file of the same name exists, turns its data into an Excel table so later jobs can append rows through Graph, and checks the requests the code sends. It belongs to Automating Excel Online with Microsoft Graph.

Prerequisites

Bash
pip install requests pandas openpyxl

You need an access token for Microsoft Graph with Files.ReadWrite (delegated) or Files.ReadWrite.All / Sites.ReadWrite.All (application), and the drive ID of the target OneDrive or SharePoint document library; getting a token is covered in Authenticate to Microsoft Graph with MSAL for Excel Automation. The outputs on this page were produced against a mocked Graph API (using the responses library) that returns the documented response shapes, so the requests can be shown and checked without a tenant; the code is unchanged against the real service.

Step 1 — Build the workbook in memory

There is no need to write a temporary file: build the workbook into a bytes buffer and upload those bytes:

Python
from io import BytesIO
import pandas as pd

orders = pd.DataFrame({"Order": ["SO-1001", "SO-1002", "SO-1003"], "Customer": ["Oakridge Farms", "Café Señor", "Zenith Logistics"],
                       "Amount": [1200.0, 380.0, 5600.0]})
buf = BytesIO()
with pd.ExcelWriter(buf, engine="openpyxl") as xw:
    orders.to_excel(xw, sheet_name="Orders", index=False)
payload = buf.getvalue()
print(len(payload), "bytes; starts with", payload[:2])

A few kilobytes of .xlsx — a zip file, hence the PK signature. Anything pandas, openpyxl or XlsxWriter can produce can be uploaded this way, including formatting and charts. Building in memory avoids temporary-file clean-up and works in serverless functions with read-only file systems; the pattern is covered in Build an Excel Workbook in Memory with BytesIO.

Step 2 — Upload with one PUT, choosing what happens on a name clash

For files up to 250 MB Graph accepts a single PUT to the item's content, addressed by path. A query parameter decides what happens if a file with that name already exists:

Python
import os
import requests
from urllib.parse import quote

GRAPH = "https://graph.microsoft.com/v1.0"
DRIVE = "b!finance-drive-id"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['GRAPH_TOKEN']}"

def upload_small(path, data, conflict="replace"):
    url = f"{GRAPH}/drives/{DRIVE}/root:/{quote(path)}:/content"
    r = session.put(url, data=data, params={"@microsoft.graph.conflictBehavior": conflict},
                    headers={"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, timeout=120)
    if r.status_code >= 400:
        raise RuntimeError(f"{r.status_code}: {r.json()['error']['message']}")
    return r.json()

item = upload_small("Reports/2026/orders_2026-09.xlsx", payload)
print(item["id"], item["name"], item["webUrl"])
try:
    upload_small("Reports/existing.xlsx", payload, conflict="fail")
except RuntimeError as e:
    print("with conflictBehavior=fail:", e)

The path after root:/ is relative to the drive's root; folders in it must exist (create them with a POST to the parent's children with a folder facet). @microsoft.graph.conflictBehavior takes replace (overwrite — the default for this call), fail (refuse with HTTP 409, as shown) or rename (keep both, adding a number to the new file's name). For scheduled reports, replace with a dated file name is usually right; for one-off project workbooks, fail protects a file someone may already have edited. The response is a driveItem with the new file's id — needed for the workbook API — and webUrl, the link people open in the browser. Graph's documentation recommends the simple upload only for small files (a few megabytes); larger ones should use an upload session.

What happens when the file name already exists With conflictBehavior replace the existing file is overwritten and keeps its sharing. With fail the upload is refused with HTTP 409. With rename the new file is saved alongside with a number added to its name. A file with this name exists… replace overwrite it scheduled reports, dated names fail HTTP 409, nothing changed protect edited workbooks rename save as 'name 1.xlsx' keep both versions

Step 3 — Larger files: an upload session in chunks

For larger workbooks, create an upload session and send the bytes in chunks. Each chunk's size must be a multiple of 320 KiB (327,680 bytes), except the last:

Python
CHUNK = 320 * 1024 * 10                                   # 3.2 MiB, a multiple of 320 KiB

def upload_large(path, data, conflict="replace"):
    url = f"{GRAPH}/drives/{DRIVE}/root:/{quote(path)}:/createUploadSession"
    s = session.post(url, json={"item": {"@microsoft.graph.conflictBehavior": conflict}}, timeout=60).json()
    upload_url, total = s["uploadUrl"], len(data)
    for start in range(0, total, CHUNK):
        end = min(start + CHUNK, total) - 1
        r = requests.put(upload_url, data=data[start:end + 1], timeout=300,
                         headers={"Content-Range": f"bytes {start}-{end}/{total}", "Content-Length": str(end - start + 1)})
        r.raise_for_status()
    return r.json()

big = payload * 2000                                      # stand-in for a larger workbook's bytes
done = upload_large("Reports/2026/large.xlsx", big)
print(done["name"], done["size"], "bytes in", len(chunks), "chunks:", chunks[0], "…", chunks[-1])

createUploadSession returns a pre-authenticated uploadUrl; chunks are sent there without the Graph bearer token, which is why the code uses requests.put rather than the authorised session — sending the token to the upload URL can make the request fail. Each response before the last returns 202 with the next expected byte range; the final chunk returns the created driveItem. Chunks of a few megabytes are a good balance between the number of requests and the cost of retrying one after a network error; the session also lets an interrupted upload be resumed from the next expected range rather than restarted.

An upload session in chunks A POST to createUploadSession returns an upload URL. The file is sent in chunks whose sizes are multiples of 320 KiB, each with a Content-Range header and without the Graph token. Intermediate chunks return 202 with the next expected range and the last returns the created file. createUploadSession POST, conflictBehavior PUT chunk Content-Range bytes a-b/total 202 Accepted nextExpectedRanges 201 Created driveItem with id

Step 4 — Turn the data into an Excel table

Later jobs that append rows through Graph need an Excel table. With the new item's ID, add one over the data range and give it a stable name:

Python
BOOK = f"{GRAPH}/drives/{DRIVE}/items/{item['id']}/workbook"
table = session.post(f"{BOOK}/tables/add", json={"address": f"Orders!A1:C{len(orders) + 1}", "hasHeaders": True}, timeout=60).json()
renamed = session.patch(f"{BOOK}/tables/{table['id']}", json={"name": "Orders"}, timeout=60).json()
print("table created:", table["name"], "→ renamed to", renamed["name"])

tables/add turns a range — addressed as Sheet!A1:C4 — into a table with its first row as headers; Graph names it Table1, and a PATCH renames it to something a later job can rely on. Alternatively, create the table in the file before uploading, with openpyxl's Table or XlsxWriter's add_table — one request fewer, and the table's style is under your control. Appending rows to it later is covered in Append Rows to an Excel Online Table with Microsoft Graph.

From upload to a named table The script uploads the workbook bytes and receives a drive item with an id. It then posts to tables/add on that item's workbook, receives Table1, and patches the table's name to Orders so later jobs can find it. Script Graph drive Workbook API 1. PUT root:/path:/content 2. 201 driveItem id 3. POST tables/add A1:C4 4. Table1 5. PATCH name=Orders

Step 5 — Check what was sent

Because every call above went through requests, a test can inspect the exact requests — here against the mock, in your own tests with the same technique:

Python
for call in mock.calls[:3]:
    req = call.request
    print(req.method, req.url.split("?")[0].replace(GRAPH, ""), "| conflict:", "fail" if "fail" in req.url else "replace" if "replace" in req.url else "-",
          "| auth:", "Authorization" in req.headers)
print("upload-session chunks carried no token:", all("Authorization" not in c.request.headers for c in mock.calls if "upload.example.com" in c.request.url))

The simple uploads went to /drives/{id}/root:/{path}:/content with the chosen conflict behaviour and the bearer token; the chunk uploads went to the session URL without it. Checks like these — that paths are quoted, the conflict behaviour is what you intended and tokens go only where they should — catch the mistakes that otherwise surface as a confusing 401 or a silently overwritten file in production. Uploading to SharePoint document libraries by site and path is covered in Upload an Excel Report to SharePoint with Python.

Common pitfalls

  • Uploading to a folder that does not exist. Create missing folders first.
  • Overwriting by accident. Set conflictBehavior deliberately; replace is the default.
  • Large files in one PUT. Use an upload session with chunks in multiples of 320 KiB.
  • Sending the bearer token to the upload URL. The session URL is pre-authenticated.
  • Relying on the default table name. Rename it so later jobs can find it.

Conclusion

To create a workbook in OneDrive or SharePoint with Microsoft Graph, build it in memory and upload the bytes: a single PUT to root:/{path}:/content for small files, an upload session with chunks in multiples of 320 KiB for larger ones, with @microsoft.graph.conflictBehavior set to replace, fail or rename as the situation needs. Use the returned item ID for the workbook API — for example to add and name a table — and test the requests your code sends before running it against a real tenant.

Frequently asked questions

How do I create an Excel file in OneDrive with Python? Build the xlsx in memory with pandas or openpyxl and upload the bytes with a PUT request to the drive item's content endpoint, addressed by path under root, using a Microsoft Graph access token.

How do I stop Graph overwriting an existing file? Set the conflictBehavior query parameter to fail, which makes the upload fail with HTTP 409 if the name exists, or use rename to keep both files.

How do I upload a large Excel file with Microsoft Graph? Create an upload session with createUploadSession and send the file in chunks whose sizes are multiples of 320 KiB, each with a Content-Range header, to the returned upload URL.

How do I add a table to a workbook through Graph? POST to /workbook/tables/add on the item with the range address and hasHeaders, then PATCH the table to give it a meaningful name.

More on excel online & microsoft graph

More on excel online & microsoft graph →