[{"data":1,"prerenderedAt":2603},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph":3,"surround:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph":2573,"ancestors:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph":2581,"children:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph":2587,"related:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph":2588},{"id":4,"title":5,"body":6,"dateModified":2539,"datePublished":2539,"description":2540,"extension":2541,"faq":2542,"keyword":2551,"kind":2552,"meta":2553,"navigation":278,"path":2562,"seo":2563,"slug":2566,"stem":2567,"topics":2568,"type":2571,"__hash__":2572},"docs\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph\u002Findex.md","Append Rows to an Excel Online Table with Microsoft Graph",{"type":7,"value":8,"toc":2526},"minimark",[9,24,142,147,177,213,217,220,603,610,614,623,963,974,1065,1155,1159,1166,1534,1544,1548,1554,1813,1819,1929,1933,1948,2314,2319,2323,2426,2430,2441,2445,2454,2458,2464,2470,2476,2482,2486,2522],[10,11,12,13,17,18,23],"p",{},"A shared tracker in SharePoint is often the least painful place for data to land: people already open it, it\nhas filters and a pivot built on it, and Power BI or Power Automate may already read from it. What it lacks\nis a feed. Every morning somebody pastes yesterday's orders underneath the table and hopes the formulas\nfollowed. The Graph ",[14,15,16],"code",{},"rows\u002Fadd"," endpoint does that paste properly — the table grows, calculated columns and\nformatting extend to the new rows, and nobody has to close the file first. This guide, part of\n",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002F","Automating Excel Online with Microsoft Graph",",\nbuilds a reliable append job around it.",[25,26,34,35,34,39,34,43,34,50,34,59,34,66,34,71,34,78,34,83,34,87,34,92,34,96,34,100,34,103,34,106,34,110,34,113,34,117,34,121,34,124,34,129,34,133,34,136],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 156","img",[30,31],"tbl-append-t","tbl-append-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"An idempotent append to an Excel Online table",[40,41,42],"desc",{"id":31},"Read the table header to learn the column order, read existing key values, drop incoming rows that are already present, reorder the remaining columns to match the header, and add them to the table in batches.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","156","#ffffff",[44,51],{"x":52,"y":52,"width":53,"height":54,"rx":55,"fill":56,"stroke":57,"style":58},"20","150","86","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[60,61,65],"text",{"x":62,"y":63,"style":64},"95","50","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","header",[60,67,70],{"x":62,"y":68,"style":69},"72","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","headerRowRange",[72,73],"line",{"x1":74,"y1":75,"x2":76,"y2":75,"stroke":77,"style":58},"175","63","204","var(--brand,#5b5cf0)",[79,80],"polygon",{"points":81,"fill":82},"207,63 198,58 198,68","#5b5cf0",[44,84],{"x":85,"y":52,"width":53,"height":54,"rx":55,"fill":86,"stroke":77,"style":58},"210","#f0f4ff",[60,88,91],{"x":89,"y":63,"style":90},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","existing keys",[60,93,95],{"x":89,"y":68,"style":94},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","read one column",[72,97],{"x1":98,"y1":75,"x2":99,"y2":75,"stroke":77,"style":58},"365","394",[79,101],{"points":102,"fill":82},"397,63 388,58 388,68",[44,104],{"x":105,"y":52,"width":53,"height":54,"rx":55,"fill":86,"stroke":77,"style":58},"400",[60,107,109],{"x":108,"y":63,"style":90},"475","filter + align",[60,111,112],{"x":108,"y":68,"style":94},"new rows only,",[60,114,116],{"x":108,"y":115,"style":94},"92","header order",[72,118],{"x1":119,"y1":75,"x2":120,"y2":75,"stroke":77,"style":58},"555","584",[79,122],{"points":123,"fill":82},"587,63 578,58 578,68",[44,125],{"x":126,"y":52,"width":53,"height":54,"rx":55,"fill":127,"stroke":128,"style":58},"590","#d9f4f1","var(--teal,#0f9488)",[60,130,16],{"x":131,"y":63,"style":132},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[60,134,135],{"x":131,"y":68,"style":94},"in batches",[60,137,141],{"x":138,"y":139,"style":140},"380","140","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","running the job twice must not add the same rows twice",[143,144,146],"h2",{"id":145},"prerequisites","Prerequisites",[148,149,154],"pre",{"className":150,"code":151,"language":152,"meta":153,"style":153},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install msal requests pandas\n","bash","",[14,155,156],{"__ignoreMap":153},[157,158,160,164,168,171,174],"span",{"class":72,"line":159},1,[157,161,163],{"class":162},"sMTad","pip",[157,165,167],{"class":166},"srMev"," install",[157,169,170],{"class":166}," msal",[157,172,173],{"class":166}," requests",[157,175,176],{"class":166}," pandas\n",[178,179,180,198],"ul",{},[181,182,183,184,188,189,192,193,197],"li",{},"A ",[185,186,187],"strong",{},"delegated"," token with ",[14,190,191],{},"Files.ReadWrite"," — the table endpoints do not accept application permissions. See\n",[19,194,196],{"href":195},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002F","Authenticate to Microsoft Graph with MSAL for Excel Automation",".",[181,199,200,201,204,205,208,209,212],{},"An ",[14,202,203],{},".xlsx"," workbook in OneDrive or SharePoint containing an Excel table (created with ",[185,206,207],{},"Insert → Table",") with a name such as\n",[14,210,211],{},"Orders",", and its drive and item IDs.",[143,214,216],{"id":215},"step-1-set-up-the-client","Step 1 — Set up the client",[10,218,219],{},"The same small helper used throughout this section: a session carrying the token and a function that raises with Graph's error text.",[148,221,225],{"className":222,"code":223,"language":224,"meta":153,"style":153},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\nfrom urllib.parse import quote\nimport pandas as pd\nimport requests\n\nGRAPH = \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\nBOOK = f\"{GRAPH}\u002Fdrives\u002F{os.environ['GRAPH_DRIVE_ID']}\u002Fitems\u002F{os.environ['GRAPH_ITEM_ID']}\u002Fworkbook\"\nTABLE = f\"{BOOK}\u002Ftables\u002F{quote('Orders')}\"\n\nsession = requests.Session()\nsession.headers[\"Authorization\"] = \"Bearer \" + os.environ.get(\"GRAPH_DELEGATED_TOKEN\", \"token\")\n\ndef call(method: str, url: str, **kwargs) -> dict:\n    response = session.request(method, url, timeout=120, **kwargs)\n    if response.status_code >= 400:\n        raise RuntimeError(f\"{method} {url} -> {response.status_code}: {response.text[:400]}\")\n    return response.json() if response.content else {}\n","python",[14,226,227,237,251,265,273,280,293,349,386,391,403,438,443,478,505,522,582],{"__ignoreMap":153},[157,228,229,233],{"class":72,"line":159},[157,230,232],{"class":231},"s-kum","import",[157,234,236],{"class":235},"skGVy"," os\n",[157,238,240,243,246,248],{"class":72,"line":239},2,[157,241,242],{"class":231},"from",[157,244,245],{"class":235}," urllib.parse ",[157,247,232],{"class":231},[157,249,250],{"class":235}," quote\n",[157,252,254,256,259,262],{"class":72,"line":253},3,[157,255,232],{"class":231},[157,257,258],{"class":235}," pandas ",[157,260,261],{"class":231},"as",[157,263,264],{"class":235}," pd\n",[157,266,268,270],{"class":72,"line":267},4,[157,269,232],{"class":231},[157,271,272],{"class":235}," requests\n",[157,274,276],{"class":72,"line":275},5,[157,277,279],{"emptyLinePlaceholder":278},true,"\n",[157,281,283,287,290],{"class":72,"line":282},6,[157,284,286],{"class":285},"sP0c6","GRAPH",[157,288,289],{"class":231}," =",[157,291,292],{"class":166}," \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n",[157,294,296,299,301,304,307,311,313,316,319,321,324,327,330,332,335,337,339,342,344,346],{"class":72,"line":295},7,[157,297,298],{"class":285},"BOOK",[157,300,289],{"class":231},[157,302,303],{"class":231}," f",[157,305,306],{"class":166},"\"",[157,308,310],{"class":309},"sSjpA","{",[157,312,286],{"class":285},[157,314,315],{"class":309},"}",[157,317,318],{"class":166},"\u002Fdrives\u002F",[157,320,310],{"class":309},[157,322,323],{"class":235},"os.environ[",[157,325,326],{"class":166},"'GRAPH_DRIVE_ID'",[157,328,329],{"class":235},"]",[157,331,315],{"class":309},[157,333,334],{"class":166},"\u002Fitems\u002F",[157,336,310],{"class":309},[157,338,323],{"class":235},[157,340,341],{"class":166},"'GRAPH_ITEM_ID'",[157,343,329],{"class":235},[157,345,315],{"class":309},[157,347,348],{"class":166},"\u002Fworkbook\"\n",[157,350,352,355,357,359,361,363,365,367,370,372,375,378,381,383],{"class":72,"line":351},8,[157,353,354],{"class":285},"TABLE",[157,356,289],{"class":231},[157,358,303],{"class":231},[157,360,306],{"class":166},[157,362,310],{"class":309},[157,364,298],{"class":285},[157,366,315],{"class":309},[157,368,369],{"class":166},"\u002Ftables\u002F",[157,371,310],{"class":309},[157,373,374],{"class":235},"quote(",[157,376,377],{"class":166},"'Orders'",[157,379,380],{"class":235},")",[157,382,315],{"class":309},[157,384,385],{"class":166},"\"\n",[157,387,389],{"class":72,"line":388},9,[157,390,279],{"emptyLinePlaceholder":278},[157,392,394,397,400],{"class":72,"line":393},10,[157,395,396],{"class":235},"session ",[157,398,399],{"class":231},"=",[157,401,402],{"class":235}," requests.Session()\n",[157,404,406,409,412,415,417,420,423,426,429,432,435],{"class":72,"line":405},11,[157,407,408],{"class":235},"session.headers[",[157,410,411],{"class":166},"\"Authorization\"",[157,413,414],{"class":235},"] ",[157,416,399],{"class":231},[157,418,419],{"class":166}," \"Bearer \"",[157,421,422],{"class":231}," +",[157,424,425],{"class":235}," os.environ.get(",[157,427,428],{"class":166},"\"GRAPH_DELEGATED_TOKEN\"",[157,430,431],{"class":235},", ",[157,433,434],{"class":166},"\"token\"",[157,436,437],{"class":235},")\n",[157,439,441],{"class":72,"line":440},12,[157,442,279],{"emptyLinePlaceholder":278},[157,444,446,449,453,456,459,462,464,466,469,472,475],{"class":72,"line":445},13,[157,447,448],{"class":231},"def",[157,450,452],{"class":451},"s_Opv"," call",[157,454,455],{"class":235},"(method: ",[157,457,458],{"class":285},"str",[157,460,461],{"class":235},", url: ",[157,463,458],{"class":285},[157,465,431],{"class":235},[157,467,468],{"class":231},"**",[157,470,471],{"class":235},"kwargs) -> ",[157,473,474],{"class":285},"dict",[157,476,477],{"class":235},":\n",[157,479,481,484,486,489,493,495,498,500,502],{"class":72,"line":480},14,[157,482,483],{"class":235},"    response ",[157,485,399],{"class":231},[157,487,488],{"class":235}," session.request(method, url, ",[157,490,492],{"class":491},"sa561","timeout",[157,494,399],{"class":231},[157,496,497],{"class":285},"120",[157,499,431],{"class":235},[157,501,468],{"class":231},[157,503,504],{"class":235},"kwargs)\n",[157,506,508,511,514,517,520],{"class":72,"line":507},15,[157,509,510],{"class":231},"    if",[157,512,513],{"class":235}," response.status_code ",[157,515,516],{"class":231},">=",[157,518,519],{"class":285}," 400",[157,521,477],{"class":235},[157,523,525,528,531,534,537,539,541,544,546,549,552,554,557,559,562,564,567,569,572,574,576,578,580],{"class":72,"line":524},16,[157,526,527],{"class":231},"        raise",[157,529,530],{"class":285}," RuntimeError",[157,532,533],{"class":235},"(",[157,535,536],{"class":231},"f",[157,538,306],{"class":166},[157,540,310],{"class":309},[157,542,543],{"class":235},"method",[157,545,315],{"class":309},[157,547,548],{"class":309}," {",[157,550,551],{"class":235},"url",[157,553,315],{"class":309},[157,555,556],{"class":166}," -> ",[157,558,310],{"class":309},[157,560,561],{"class":235},"response.status_code",[157,563,315],{"class":309},[157,565,566],{"class":166},": ",[157,568,310],{"class":309},[157,570,571],{"class":235},"response.text[:",[157,573,105],{"class":285},[157,575,329],{"class":235},[157,577,315],{"class":309},[157,579,306],{"class":166},[157,581,437],{"class":235},[157,583,585,588,591,594,597,600],{"class":72,"line":584},17,[157,586,587],{"class":231},"    return",[157,589,590],{"class":235}," response.json() ",[157,592,593],{"class":231},"if",[157,595,596],{"class":235}," response.content ",[157,598,599],{"class":231},"else",[157,601,602],{"class":235}," {}\n",[10,604,605,606,609],{},"Addressing the table by name through ",[14,607,608],{},"\u002Fworkbook\u002Ftables\u002F{name}"," works regardless of which worksheet it sits on, and it keeps working\nwhen somebody moves the table or renames the sheet.",[143,611,613],{"id":612},"step-2-align-your-columns-to-the-table-header","Step 2 — Align your columns to the table header",[10,615,616,618,619,622],{},[14,617,16],{}," takes positional arrays. It has no idea that your DataFrame's third column is ",[14,620,621],{},"Amount","; it writes whatever is third into the\ntable's third column. Read the header and reorder the frame to match it, failing loudly if a column is missing.",[148,624,626],{"className":222,"code":625,"language":224,"meta":153,"style":153},"def table_header() -> list[str]:\n    return call(\"GET\", f\"{TABLE}\u002FheaderRowRange\")[\"values\"][0]\n\ndef align_to_table(frame: pd.DataFrame, header: list[str]) -> pd.DataFrame:\n    missing = [c for c in header if c not in frame.columns]\n    if missing:\n        raise ValueError(f\"incoming data lacks table columns {missing}\")\n    extra = [c for c in frame.columns if c not in header]\n    if extra:\n        print(\"ignoring columns not in the table:\", extra)\n    return frame[header]\n\nheader = table_header()\nincoming = pd.DataFrame({\n    \"Amount\": [1320.0, 540.5, 1200.0],\n    \"Rep\": [\"Cara\", \"Dev\", \"Ana\"],\n    \"Region\": [\"West\", \"South\", \"North\"],\n    \"Source\": [\"crm\", \"crm\", \"crm\"],\n})\naligned = align_to_table(incoming, header)\nprint(header, aligned.columns.tolist())\n",[14,627,628,643,682,686,701,736,743,768,797,804,817,824,828,838,848,872,894,916,937,943,954],{"__ignoreMap":153},[157,629,630,632,635,638,640],{"class":72,"line":159},[157,631,448],{"class":231},[157,633,634],{"class":451}," table_header",[157,636,637],{"class":235},"() -> list[",[157,639,458],{"class":285},[157,641,642],{"class":235},"]:\n",[157,644,645,647,650,653,655,657,659,661,663,665,668,671,674,677,679],{"class":72,"line":239},[157,646,587],{"class":231},[157,648,649],{"class":235}," call(",[157,651,652],{"class":166},"\"GET\"",[157,654,431],{"class":235},[157,656,536],{"class":231},[157,658,306],{"class":166},[157,660,310],{"class":309},[157,662,354],{"class":285},[157,664,315],{"class":309},[157,666,667],{"class":166},"\u002FheaderRowRange\"",[157,669,670],{"class":235},")[",[157,672,673],{"class":166},"\"values\"",[157,675,676],{"class":235},"][",[157,678,46],{"class":285},[157,680,681],{"class":235},"]\n",[157,683,684],{"class":72,"line":253},[157,685,279],{"emptyLinePlaceholder":278},[157,687,688,690,693,696,698],{"class":72,"line":267},[157,689,448],{"class":231},[157,691,692],{"class":451}," align_to_table",[157,694,695],{"class":235},"(frame: pd.DataFrame, header: list[",[157,697,458],{"class":285},[157,699,700],{"class":235},"]) -> pd.DataFrame:\n",[157,702,703,706,708,711,714,717,720,723,725,727,730,733],{"class":72,"line":275},[157,704,705],{"class":235},"    missing ",[157,707,399],{"class":231},[157,709,710],{"class":235}," [c ",[157,712,713],{"class":231},"for",[157,715,716],{"class":235}," c ",[157,718,719],{"class":231},"in",[157,721,722],{"class":235}," header ",[157,724,593],{"class":231},[157,726,716],{"class":235},[157,728,729],{"class":231},"not",[157,731,732],{"class":231}," in",[157,734,735],{"class":235}," frame.columns]\n",[157,737,738,740],{"class":72,"line":282},[157,739,510],{"class":231},[157,741,742],{"class":235}," missing:\n",[157,744,745,747,750,752,754,757,759,762,764,766],{"class":72,"line":295},[157,746,527],{"class":231},[157,748,749],{"class":285}," ValueError",[157,751,533],{"class":235},[157,753,536],{"class":231},[157,755,756],{"class":166},"\"incoming data lacks table columns ",[157,758,310],{"class":309},[157,760,761],{"class":235},"missing",[157,763,315],{"class":309},[157,765,306],{"class":166},[157,767,437],{"class":235},[157,769,770,773,775,777,779,781,783,786,788,790,792,794],{"class":72,"line":351},[157,771,772],{"class":235},"    extra ",[157,774,399],{"class":231},[157,776,710],{"class":235},[157,778,713],{"class":231},[157,780,716],{"class":235},[157,782,719],{"class":231},[157,784,785],{"class":235}," frame.columns ",[157,787,593],{"class":231},[157,789,716],{"class":235},[157,791,729],{"class":231},[157,793,732],{"class":231},[157,795,796],{"class":235}," header]\n",[157,798,799,801],{"class":72,"line":388},[157,800,510],{"class":231},[157,802,803],{"class":235}," extra:\n",[157,805,806,809,811,814],{"class":72,"line":393},[157,807,808],{"class":285},"        print",[157,810,533],{"class":235},[157,812,813],{"class":166},"\"ignoring columns not in the table:\"",[157,815,816],{"class":235},", extra)\n",[157,818,819,821],{"class":72,"line":405},[157,820,587],{"class":231},[157,822,823],{"class":235}," frame[header]\n",[157,825,826],{"class":72,"line":440},[157,827,279],{"emptyLinePlaceholder":278},[157,829,830,833,835],{"class":72,"line":445},[157,831,832],{"class":235},"header ",[157,834,399],{"class":231},[157,836,837],{"class":235}," table_header()\n",[157,839,840,843,845],{"class":72,"line":480},[157,841,842],{"class":235},"incoming ",[157,844,399],{"class":231},[157,846,847],{"class":235}," pd.DataFrame({\n",[157,849,850,853,856,859,861,864,866,869],{"class":72,"line":507},[157,851,852],{"class":166},"    \"Amount\"",[157,854,855],{"class":235},": [",[157,857,858],{"class":285},"1320.0",[157,860,431],{"class":235},[157,862,863],{"class":285},"540.5",[157,865,431],{"class":235},[157,867,868],{"class":285},"1200.0",[157,870,871],{"class":235},"],\n",[157,873,874,877,879,882,884,887,889,892],{"class":72,"line":524},[157,875,876],{"class":166},"    \"Rep\"",[157,878,855],{"class":235},[157,880,881],{"class":166},"\"Cara\"",[157,883,431],{"class":235},[157,885,886],{"class":166},"\"Dev\"",[157,888,431],{"class":235},[157,890,891],{"class":166},"\"Ana\"",[157,893,871],{"class":235},[157,895,896,899,901,904,906,909,911,914],{"class":72,"line":584},[157,897,898],{"class":166},"    \"Region\"",[157,900,855],{"class":235},[157,902,903],{"class":166},"\"West\"",[157,905,431],{"class":235},[157,907,908],{"class":166},"\"South\"",[157,910,431],{"class":235},[157,912,913],{"class":166},"\"North\"",[157,915,871],{"class":235},[157,917,919,922,924,927,929,931,933,935],{"class":72,"line":918},18,[157,920,921],{"class":166},"    \"Source\"",[157,923,855],{"class":235},[157,925,926],{"class":166},"\"crm\"",[157,928,431],{"class":235},[157,930,926],{"class":166},[157,932,431],{"class":235},[157,934,926],{"class":166},[157,936,871],{"class":235},[157,938,940],{"class":72,"line":939},19,[157,941,942],{"class":235},"})\n",[157,944,946,949,951],{"class":72,"line":945},20,[157,947,948],{"class":235},"aligned ",[157,950,399],{"class":231},[157,952,953],{"class":235}," align_to_table(incoming, header)\n",[157,955,957,960],{"class":72,"line":956},21,[157,958,959],{"class":285},"print",[157,961,962],{"class":235},"(header, aligned.columns.tolist())\n",[10,964,965,966,969,970,973],{},"Tables often include ",[185,967,968],{},"calculated columns"," — a ",[14,971,972],{},"Margin"," column defined by a formula, for example. You cannot simply leave them out,\nbecause every row array must cover every column of the table. Add them to the frame with an empty string instead, and Excel fills the\ncolumn formula in for each new row, exactly as it does when a person types below the table.",[148,975,977],{"className":222,"code":976,"language":224,"meta":153,"style":153},"def add_calculated_placeholders(frame: pd.DataFrame, header: list[str], calculated: set[str]) -> pd.DataFrame:\n    for column in calculated & set(header):\n        frame = frame.assign(**{column: \"\"})\n    return frame\n\naligned = align_to_table(add_calculated_placeholders(incoming, header, {\"Margin\"}), header)\n",[14,978,979,997,1019,1039,1046,1050],{"__ignoreMap":153},[157,980,981,983,986,988,990,993,995],{"class":72,"line":159},[157,982,448],{"class":231},[157,984,985],{"class":451}," add_calculated_placeholders",[157,987,695],{"class":235},[157,989,458],{"class":285},[157,991,992],{"class":235},"], calculated: set[",[157,994,458],{"class":285},[157,996,700],{"class":235},[157,998,999,1002,1005,1007,1010,1013,1016],{"class":72,"line":239},[157,1000,1001],{"class":231},"    for",[157,1003,1004],{"class":235}," column ",[157,1006,719],{"class":231},[157,1008,1009],{"class":235}," calculated ",[157,1011,1012],{"class":231},"&",[157,1014,1015],{"class":285}," set",[157,1017,1018],{"class":235},"(header):\n",[157,1020,1021,1024,1026,1029,1031,1034,1037],{"class":72,"line":253},[157,1022,1023],{"class":235},"        frame ",[157,1025,399],{"class":231},[157,1027,1028],{"class":235}," frame.assign(",[157,1030,468],{"class":231},[157,1032,1033],{"class":235},"{column: ",[157,1035,1036],{"class":166},"\"\"",[157,1038,942],{"class":235},[157,1040,1041,1043],{"class":72,"line":267},[157,1042,587],{"class":231},[157,1044,1045],{"class":235}," frame\n",[157,1047,1048],{"class":72,"line":275},[157,1049,279],{"emptyLinePlaceholder":278},[157,1051,1052,1054,1056,1059,1062],{"class":72,"line":282},[157,1053,948],{"class":235},[157,1055,399],{"class":231},[157,1057,1058],{"class":235}," align_to_table(add_calculated_placeholders(incoming, header, {",[157,1060,1061],{"class":166},"\"Margin\"",[157,1063,1064],{"class":235},"}), header)\n",[25,1066,34,1071,34,1074,34,1077,34,1080,34,1087,34,1093,34,1099,34,1103,34,1107,34,1111,34,1114,34,1118,34,1122,34,1125,34,1128,34,1131,34,1134,34,1139,34,1143,34,1145,34,1148,34,1151],{"viewBox":1067,"role":28,"ariaLabelledBy":1068,"xmlns":32,"style":33},"0 0 760 191",[1069,1070],"tbl-align-t","tbl-align-d",[36,1072,1073],{"id":1069},"Why incoming columns must be reordered before appending",[40,1075,1076],{"id":1070},"The incoming DataFrame has columns Amount, Rep, Region and Source. The table header is Region, Rep, Amount. Without alignment the amount would land in the Region column; after selecting columns in header order each value lands correctly and Source is ignored.",[44,1078],{"x":46,"y":46,"width":47,"height":1079,"fill":49},"191",[44,1081],{"x":52,"y":52,"width":1082,"height":1083,"rx":1084,"fill":1085,"stroke":1086,"style":58},"226.7","127","14","#fee8f2","var(--accent,#f43f8f)",[60,1088,1092],{"x":1089,"y":1090,"style":1091},"133.3","46","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","incoming frame",[72,1094],{"x1":1095,"y1":1096,"x2":1097,"y2":1096,"stroke":1086,"style":1098},"36","56","230.7","stroke-width:1px",[60,1100,1102],{"x":1089,"y":1101,"style":94},"78","Amount, Rep, Region, Source",[60,1104,1106],{"x":1089,"y":1105,"style":94},"101","order from the CRM export",[60,1108,1110],{"x":1089,"y":1109,"style":94},"124","one extra column",[44,1112],{"x":1113,"y":52,"width":1082,"height":1083,"rx":1084,"fill":86,"stroke":77,"style":58},"266.7",[60,1115,1117],{"x":138,"y":1090,"style":1116},"font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","table header",[72,1119],{"x1":1120,"y1":1096,"x2":1121,"y2":1096,"stroke":77,"style":1098},"282.7","477.3",[60,1123,1124],{"x":138,"y":1101,"style":94},"Region, Rep, Amount",[60,1126,1127],{"x":138,"y":1105,"style":94},"order in the workbook",[60,1129,1130],{"x":138,"y":1109,"style":94},"positional arrays follow it",[44,1132],{"x":1133,"y":52,"width":1082,"height":1083,"rx":1084,"fill":127,"stroke":128,"style":58},"513.3",[60,1135,1138],{"x":1136,"y":1090,"style":1137},"626.7","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","frame[header]",[72,1140],{"x1":1141,"y1":1096,"x2":1142,"y2":1096,"stroke":128,"style":1098},"529.3","724",[60,1144,1124],{"x":1136,"y":1101,"style":94},[60,1146,1147],{"x":1136,"y":1105,"style":94},"values land correctly",[60,1149,1150],{"x":1136,"y":1109,"style":94},"Source dropped, logged",[60,1152,1154],{"x":138,"y":1153,"style":140},"177","rows\u002Fadd is positional; the header decides the order",[143,1156,1158],{"id":1157},"step-3-skip-rows-that-are-already-in-the-table","Step 3 — Skip rows that are already in the table",[10,1160,1161,1162,1165],{},"A scheduled job will eventually run twice for the same day — a retry after a timeout, a manual rerun after a fix. Without a check, the\ntable gets duplicate orders. Read the key column from the table and drop incoming rows whose key already exists. The rows endpoint returns\neach row's ",[14,1163,1164],{},"values",", which is enough to rebuild the keys.",[148,1167,1169],{"className":222,"code":1168,"language":224,"meta":153,"style":153},"def existing_rows() -> pd.DataFrame:\n    rows = []\n    url = f\"{TABLE}\u002Frows\"\n    while url:\n        page = call(\"GET\", url)\n        rows.extend(item[\"values\"][0] for item in page.get(\"value\", []))\n        url = page.get(\"@odata.nextLink\")\n    return pd.DataFrame(rows, columns=header)\n\ncurrent = existing_rows()\nKEY = [\"Region\", \"Rep\", \"Amount\"]           # use a real order ID column when the table has one\n\ndef keys(frame: pd.DataFrame) -> pd.Series:\n    normalised = frame[KEY].copy()\n    for column in KEY:\n        numeric = pd.to_numeric(normalised[column], errors=\"coerce\")\n        text = normalised[column].astype(str).str.strip()\n        normalised[column] = numeric.map(\"{:.10g}\".format).where(numeric.notna(), text)\n    return normalised.apply(tuple, axis=1)\n\nis_new = ~keys(aligned).isin(set(keys(current)))\nto_add = aligned[is_new]\nprint(f\"{len(aligned)} incoming, {len(to_add)} new\")      # 3 incoming, 2 new\n",[14,1170,1171,1181,1191,1211,1219,1233,1262,1276,1291,1295,1305,1335,1339,1349,1364,1377,1397,1412,1436,1458,1462,1481,1492],{"__ignoreMap":153},[157,1172,1173,1175,1178],{"class":72,"line":159},[157,1174,448],{"class":231},[157,1176,1177],{"class":451}," existing_rows",[157,1179,1180],{"class":235},"() -> pd.DataFrame:\n",[157,1182,1183,1186,1188],{"class":72,"line":239},[157,1184,1185],{"class":235},"    rows ",[157,1187,399],{"class":231},[157,1189,1190],{"class":235}," []\n",[157,1192,1193,1196,1198,1200,1202,1204,1206,1208],{"class":72,"line":253},[157,1194,1195],{"class":235},"    url ",[157,1197,399],{"class":231},[157,1199,303],{"class":231},[157,1201,306],{"class":166},[157,1203,310],{"class":309},[157,1205,354],{"class":285},[157,1207,315],{"class":309},[157,1209,1210],{"class":166},"\u002Frows\"\n",[157,1212,1213,1216],{"class":72,"line":267},[157,1214,1215],{"class":231},"    while",[157,1217,1218],{"class":235}," url:\n",[157,1220,1221,1224,1226,1228,1230],{"class":72,"line":275},[157,1222,1223],{"class":235},"        page ",[157,1225,399],{"class":231},[157,1227,649],{"class":235},[157,1229,652],{"class":166},[157,1231,1232],{"class":235},", url)\n",[157,1234,1235,1238,1240,1242,1244,1246,1248,1251,1253,1256,1259],{"class":72,"line":282},[157,1236,1237],{"class":235},"        rows.extend(item[",[157,1239,673],{"class":166},[157,1241,676],{"class":235},[157,1243,46],{"class":285},[157,1245,414],{"class":235},[157,1247,713],{"class":231},[157,1249,1250],{"class":235}," item ",[157,1252,719],{"class":231},[157,1254,1255],{"class":235}," page.get(",[157,1257,1258],{"class":166},"\"value\"",[157,1260,1261],{"class":235},", []))\n",[157,1263,1264,1267,1269,1271,1274],{"class":72,"line":295},[157,1265,1266],{"class":235},"        url ",[157,1268,399],{"class":231},[157,1270,1255],{"class":235},[157,1272,1273],{"class":166},"\"@odata.nextLink\"",[157,1275,437],{"class":235},[157,1277,1278,1280,1283,1286,1288],{"class":72,"line":351},[157,1279,587],{"class":231},[157,1281,1282],{"class":235}," pd.DataFrame(rows, ",[157,1284,1285],{"class":491},"columns",[157,1287,399],{"class":231},[157,1289,1290],{"class":235},"header)\n",[157,1292,1293],{"class":72,"line":388},[157,1294,279],{"emptyLinePlaceholder":278},[157,1296,1297,1300,1302],{"class":72,"line":393},[157,1298,1299],{"class":235},"current ",[157,1301,399],{"class":231},[157,1303,1304],{"class":235}," existing_rows()\n",[157,1306,1307,1310,1312,1315,1318,1320,1323,1325,1328,1331],{"class":72,"line":405},[157,1308,1309],{"class":285},"KEY",[157,1311,289],{"class":231},[157,1313,1314],{"class":235}," [",[157,1316,1317],{"class":166},"\"Region\"",[157,1319,431],{"class":235},[157,1321,1322],{"class":166},"\"Rep\"",[157,1324,431],{"class":235},[157,1326,1327],{"class":166},"\"Amount\"",[157,1329,1330],{"class":235},"]           ",[157,1332,1334],{"class":1333},"s-wDw","# use a real order ID column when the table has one\n",[157,1336,1337],{"class":72,"line":440},[157,1338,279],{"emptyLinePlaceholder":278},[157,1340,1341,1343,1346],{"class":72,"line":445},[157,1342,448],{"class":231},[157,1344,1345],{"class":451}," keys",[157,1347,1348],{"class":235},"(frame: pd.DataFrame) -> pd.Series:\n",[157,1350,1351,1354,1356,1359,1361],{"class":72,"line":480},[157,1352,1353],{"class":235},"    normalised ",[157,1355,399],{"class":231},[157,1357,1358],{"class":235}," frame[",[157,1360,1309],{"class":285},[157,1362,1363],{"class":235},"].copy()\n",[157,1365,1366,1368,1370,1372,1375],{"class":72,"line":507},[157,1367,1001],{"class":231},[157,1369,1004],{"class":235},[157,1371,719],{"class":231},[157,1373,1374],{"class":285}," KEY",[157,1376,477],{"class":235},[157,1378,1379,1382,1384,1387,1390,1392,1395],{"class":72,"line":524},[157,1380,1381],{"class":235},"        numeric ",[157,1383,399],{"class":231},[157,1385,1386],{"class":235}," pd.to_numeric(normalised[column], ",[157,1388,1389],{"class":491},"errors",[157,1391,399],{"class":231},[157,1393,1394],{"class":166},"\"coerce\"",[157,1396,437],{"class":235},[157,1398,1399,1402,1404,1407,1409],{"class":72,"line":584},[157,1400,1401],{"class":235},"        text ",[157,1403,399],{"class":231},[157,1405,1406],{"class":235}," normalised[column].astype(",[157,1408,458],{"class":285},[157,1410,1411],{"class":235},").str.strip()\n",[157,1413,1414,1417,1419,1422,1424,1426,1429,1431,1433],{"class":72,"line":918},[157,1415,1416],{"class":235},"        normalised[column] ",[157,1418,399],{"class":231},[157,1420,1421],{"class":235}," numeric.map(",[157,1423,306],{"class":166},[157,1425,310],{"class":309},[157,1427,1428],{"class":231},":.10g",[157,1430,315],{"class":309},[157,1432,306],{"class":166},[157,1434,1435],{"class":235},".format).where(numeric.notna(), text)\n",[157,1437,1438,1440,1443,1446,1448,1451,1453,1456],{"class":72,"line":939},[157,1439,587],{"class":231},[157,1441,1442],{"class":235}," normalised.apply(",[157,1444,1445],{"class":285},"tuple",[157,1447,431],{"class":235},[157,1449,1450],{"class":491},"axis",[157,1452,399],{"class":231},[157,1454,1455],{"class":285},"1",[157,1457,437],{"class":235},[157,1459,1460],{"class":72,"line":945},[157,1461,279],{"emptyLinePlaceholder":278},[157,1463,1464,1467,1469,1472,1475,1478],{"class":72,"line":956},[157,1465,1466],{"class":235},"is_new ",[157,1468,399],{"class":231},[157,1470,1471],{"class":231}," ~",[157,1473,1474],{"class":235},"keys(aligned).isin(",[157,1476,1477],{"class":285},"set",[157,1479,1480],{"class":235},"(keys(current)))\n",[157,1482,1484,1487,1489],{"class":72,"line":1483},22,[157,1485,1486],{"class":235},"to_add ",[157,1488,399],{"class":231},[157,1490,1491],{"class":235}," aligned[is_new]\n",[157,1493,1495,1497,1499,1501,1503,1505,1508,1511,1513,1516,1518,1520,1523,1525,1528,1531],{"class":72,"line":1494},23,[157,1496,959],{"class":285},[157,1498,533],{"class":235},[157,1500,536],{"class":231},[157,1502,306],{"class":166},[157,1504,310],{"class":309},[157,1506,1507],{"class":285},"len",[157,1509,1510],{"class":235},"(aligned)",[157,1512,315],{"class":309},[157,1514,1515],{"class":166}," incoming, ",[157,1517,310],{"class":309},[157,1519,1507],{"class":285},[157,1521,1522],{"class":235},"(to_add)",[157,1524,315],{"class":309},[157,1526,1527],{"class":166}," new\"",[157,1529,1530],{"class":235},")      ",[157,1532,1533],{"class":1333},"# 3 incoming, 2 new\n",[10,1535,1536,1537,1540,1541,1543],{},"The composite key here is only for the example; a real feed should carry a unique identifier — an order number, an invoice ID — and use that\nsingle column. The normalisation step is not optional, though. Excel returns ",[14,1538,1539],{},"1200"," for a whole number while pandas holds ",[14,1542,868],{},", and a naive\nstring comparison treats those as different keys and appends the row again. Formatting every numeric key the same way, and trimming text keys,\nmakes the comparison reflect what a person would call the same record.",[143,1545,1547],{"id":1546},"step-4-send-rows-in-batches","Step 4 — Send rows in batches",[10,1549,1550,1551,1553],{},"Microsoft's guidance for ",[14,1552,16],{}," is explicit: adding one row per call degrades performance, so collect rows and send many in each request.\nA few hundred to a couple of thousand rows per call is a good starting range; adjust from there based on how long each request takes.",[148,1555,1557],{"className":222,"code":1556,"language":224,"meta":153,"style":153},"def to_json_value(value):\n    if pd.isna(value):\n        return \"\"\n    return value.item() if hasattr(value, \"item\") else value\n\ndef append_rows(frame: pd.DataFrame, batch_size: int = 500) -> int:\n    added = 0\n    for start in range(0, len(frame), batch_size):\n        chunk = frame.iloc[start:start + batch_size]\n        values = [[to_json_value(v) for v in row] for row in chunk.itertuples(index=False)]\n        call(\"POST\", f\"{TABLE}\u002Frows\u002Fadd\", json={\"index\": None, \"values\": values})\n        added += len(values)\n    return added\n\nprint(\"added\", append_rows(to_add))\n",[14,1558,1559,1569,1576,1584,1610,1614,1639,1649,1672,1688,1729,1776,1790,1797,1801],{"__ignoreMap":153},[157,1560,1561,1563,1566],{"class":72,"line":159},[157,1562,448],{"class":231},[157,1564,1565],{"class":451}," to_json_value",[157,1567,1568],{"class":235},"(value):\n",[157,1570,1571,1573],{"class":72,"line":239},[157,1572,510],{"class":231},[157,1574,1575],{"class":235}," pd.isna(value):\n",[157,1577,1578,1581],{"class":72,"line":253},[157,1579,1580],{"class":231},"        return",[157,1582,1583],{"class":166}," \"\"\n",[157,1585,1586,1588,1591,1593,1596,1599,1602,1605,1607],{"class":72,"line":267},[157,1587,587],{"class":231},[157,1589,1590],{"class":235}," value.item() ",[157,1592,593],{"class":231},[157,1594,1595],{"class":285}," hasattr",[157,1597,1598],{"class":235},"(value, ",[157,1600,1601],{"class":166},"\"item\"",[157,1603,1604],{"class":235},") ",[157,1606,599],{"class":231},[157,1608,1609],{"class":235}," value\n",[157,1611,1612],{"class":72,"line":275},[157,1613,279],{"emptyLinePlaceholder":278},[157,1615,1616,1618,1621,1624,1627,1629,1632,1635,1637],{"class":72,"line":282},[157,1617,448],{"class":231},[157,1619,1620],{"class":451}," append_rows",[157,1622,1623],{"class":235},"(frame: pd.DataFrame, batch_size: ",[157,1625,1626],{"class":285},"int",[157,1628,289],{"class":231},[157,1630,1631],{"class":285}," 500",[157,1633,1634],{"class":235},") -> ",[157,1636,1626],{"class":285},[157,1638,477],{"class":235},[157,1640,1641,1644,1646],{"class":72,"line":295},[157,1642,1643],{"class":235},"    added ",[157,1645,399],{"class":231},[157,1647,1648],{"class":285}," 0\n",[157,1650,1651,1653,1656,1658,1661,1663,1665,1667,1669],{"class":72,"line":351},[157,1652,1001],{"class":231},[157,1654,1655],{"class":235}," start ",[157,1657,719],{"class":231},[157,1659,1660],{"class":285}," range",[157,1662,533],{"class":235},[157,1664,46],{"class":285},[157,1666,431],{"class":235},[157,1668,1507],{"class":285},[157,1670,1671],{"class":235},"(frame), batch_size):\n",[157,1673,1674,1677,1679,1682,1685],{"class":72,"line":388},[157,1675,1676],{"class":235},"        chunk ",[157,1678,399],{"class":231},[157,1680,1681],{"class":235}," frame.iloc[start:start ",[157,1683,1684],{"class":231},"+",[157,1686,1687],{"class":235}," batch_size]\n",[157,1689,1690,1693,1695,1698,1700,1703,1705,1708,1710,1713,1715,1718,1721,1723,1726],{"class":72,"line":393},[157,1691,1692],{"class":235},"        values ",[157,1694,399],{"class":231},[157,1696,1697],{"class":235}," [[to_json_value(v) ",[157,1699,713],{"class":231},[157,1701,1702],{"class":235}," v ",[157,1704,719],{"class":231},[157,1706,1707],{"class":235}," row] ",[157,1709,713],{"class":231},[157,1711,1712],{"class":235}," row ",[157,1714,719],{"class":231},[157,1716,1717],{"class":235}," chunk.itertuples(",[157,1719,1720],{"class":491},"index",[157,1722,399],{"class":231},[157,1724,1725],{"class":285},"False",[157,1727,1728],{"class":235},")]\n",[157,1730,1731,1734,1737,1739,1741,1743,1745,1747,1749,1752,1754,1757,1759,1761,1764,1766,1769,1771,1773],{"class":72,"line":405},[157,1732,1733],{"class":235},"        call(",[157,1735,1736],{"class":166},"\"POST\"",[157,1738,431],{"class":235},[157,1740,536],{"class":231},[157,1742,306],{"class":166},[157,1744,310],{"class":309},[157,1746,354],{"class":285},[157,1748,315],{"class":309},[157,1750,1751],{"class":166},"\u002Frows\u002Fadd\"",[157,1753,431],{"class":235},[157,1755,1756],{"class":491},"json",[157,1758,399],{"class":231},[157,1760,310],{"class":235},[157,1762,1763],{"class":166},"\"index\"",[157,1765,566],{"class":235},[157,1767,1768],{"class":285},"None",[157,1770,431],{"class":235},[157,1772,673],{"class":166},[157,1774,1775],{"class":235},": values})\n",[157,1777,1778,1781,1784,1787],{"class":72,"line":440},[157,1779,1780],{"class":235},"        added ",[157,1782,1783],{"class":231},"+=",[157,1785,1786],{"class":285}," len",[157,1788,1789],{"class":235},"(values)\n",[157,1791,1792,1794],{"class":72,"line":445},[157,1793,587],{"class":231},[157,1795,1796],{"class":235}," added\n",[157,1798,1799],{"class":72,"line":480},[157,1800,279],{"emptyLinePlaceholder":278},[157,1802,1803,1805,1807,1810],{"class":72,"line":507},[157,1804,959],{"class":285},[157,1806,533],{"class":235},[157,1808,1809],{"class":166},"\"added\"",[157,1811,1812],{"class":235},", append_rows(to_add))\n",[10,1814,1815,1818],{},[14,1816,1817],{},"\"index\": None"," appends at the end; an integer inserts at that zero-based position and shifts later rows down. Each batch is its own request, so\nif batch three fails, batches one and two are already in the table — which is exactly why the duplicate check in Step 3 runs before every attempt,\nnot just the first.",[25,1820,34,1826,34,1829,34,1832,34,1836,34,1841,34,1846,34,1852,34,1857,34,1864,34,1869,34,1872,34,1876,34,1879,34,1884,34,1888,34,1891,34,1895,34,1898,34,1902,34,1906,34,1909,34,1913,34,1916,34,1920,34,1924],{"viewBox":1821,"role":28,"ariaLabelledBy":1822,"xmlns":32,"style":1825},"0 0 720 256",[1823,1824],"tbl-batch-t","tbl-batch-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,1827,1828],{"id":1823},"Requests needed to append 2,000 rows at different batch sizes",[40,1830,1831],{"id":1824},"Appending one row per call needs 2,000 requests. Batches of 100 need 20, batches of 500 need 4, and one batch of 2,000 needs a single request.",[44,1833],{"x":46,"y":46,"width":1834,"height":1835,"fill":49},"720","256",[60,1837,1840],{"x":52,"y":1838,"style":1839},"30","font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","requests to append 2,000 rows",[44,1842],{"x":52,"y":1090,"width":1843,"height":1844,"rx":1845,"fill":49,"stroke":57,"style":1098},"170","28","6",[60,1847,1851],{"x":1848,"y":1849,"style":1850},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle","1 row per call",[44,1853],{"x":76,"y":1854,"width":1855,"height":1856,"rx":1845,"fill":56,"stroke":57,"style":1098},"47","330","26",[44,1858],{"x":1859,"y":1860,"width":1861,"height":1862,"rx":1863,"fill":1085,"stroke":1086,"style":1098},"205","48","311.5","24","5",[60,1865,1868],{"x":1866,"y":1849,"style":1867},"546","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","2,000 requests",[44,1870],{"x":52,"y":1871,"width":1843,"height":1844,"rx":1845,"fill":49,"stroke":57,"style":1098},"90",[60,1873,1875],{"x":1848,"y":1874,"style":1850},"109","100 per call",[44,1877],{"x":76,"y":1878,"width":1855,"height":1856,"rx":1845,"fill":56,"stroke":57,"style":1098},"91",[44,1880],{"x":1859,"y":115,"width":1881,"height":1862,"rx":1863,"fill":1882,"stroke":1883,"style":1098},"113.5","#fdefd8","var(--gold,#b4740a)",[60,1885,1887],{"x":1866,"y":1874,"style":1886},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","20 requests",[44,1889],{"x":52,"y":1890,"width":1843,"height":1844,"rx":1845,"fill":49,"stroke":57,"style":1098},"134",[60,1892,1894],{"x":1848,"y":1893,"style":1850},"153","500 per call",[44,1896],{"x":76,"y":1897,"width":1855,"height":1856,"rx":1845,"fill":56,"stroke":57,"style":1098},"135",[44,1899],{"x":1859,"y":1900,"width":1901,"height":1862,"rx":1863,"fill":127,"stroke":128,"style":1098},"136","37.6",[60,1903,1905],{"x":1866,"y":1893,"style":1904},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","4 requests",[44,1907],{"x":52,"y":1908,"width":1843,"height":1844,"rx":1845,"fill":49,"stroke":57,"style":1098},"178",[60,1910,1912],{"x":1848,"y":1911,"style":1850},"197","2,000 per call",[44,1914],{"x":76,"y":1915,"width":1855,"height":1856,"rx":1845,"fill":56,"stroke":57,"style":1098},"179",[44,1917],{"x":1859,"y":1918,"width":1919,"height":1862,"rx":1863,"fill":86,"stroke":77,"style":1098},"180","14.5",[60,1921,1923],{"x":1866,"y":1911,"style":1922},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","1 request",[60,1925,1928],{"x":1926,"y":1927,"style":140},"360","238","fewer, larger calls; tune the size by response time",[143,1930,1932],{"id":1931},"step-5-retry-throttling-and-gateway-timeouts","Step 5 — Retry throttling and gateway timeouts",[10,1934,1935,1936,1939,1940,1943,1944,1947],{},"The table endpoint can return ",[14,1937,1938],{},"504 Gateway Timeout"," occasionally, and Microsoft's reference says the right response is to repeat the request.\nHeavy use also brings ",[14,1941,1942],{},"429 Too Many Requests"," with a ",[14,1945,1946],{},"Retry-After"," header. Wrap the POST in a retry that honours that header and backs off otherwise.",[148,1949,1951],{"className":222,"code":1950,"language":224,"meta":153,"style":153},"import time\n\ndef call_with_retry(method: str, url: str, attempts: int = 5, **kwargs) -> dict:\n    for attempt in range(1, attempts + 1):\n        response = session.request(method, url, timeout=120, **kwargs)\n        if response.status_code in (429, 503, 504) and attempt \u003C attempts:\n            wait = float(response.headers.get(\"Retry-After\", 2 ** attempt))\n            print(f\"{response.status_code}; retrying in {wait:.0f}s\")\n            time.sleep(wait)\n            continue\n        if response.status_code >= 400:\n            raise RuntimeError(f\"{method} {url} -> {response.status_code}: {response.text[:400]}\")\n        return response.json() if response.content else {}\n    raise RuntimeError(\"unreachable\")\n\nresult = call_with_retry(\"POST\", f\"{TABLE}\u002Frows\u002Fadd\", json={\"values\": [[\"North\", \"Ana\", 99.0]]})\nprint(result.get(\"index\"))\n",[14,1952,1953,1960,1964,1999,2025,2046,2084,2111,2146,2151,2156,2168,2217,2231,2245,2249,2302],{"__ignoreMap":153},[157,1954,1955,1957],{"class":72,"line":159},[157,1956,232],{"class":231},[157,1958,1959],{"class":235}," time\n",[157,1961,1962],{"class":72,"line":239},[157,1963,279],{"emptyLinePlaceholder":278},[157,1965,1966,1968,1971,1973,1975,1977,1979,1982,1984,1986,1989,1991,1993,1995,1997],{"class":72,"line":253},[157,1967,448],{"class":231},[157,1969,1970],{"class":451}," call_with_retry",[157,1972,455],{"class":235},[157,1974,458],{"class":285},[157,1976,461],{"class":235},[157,1978,458],{"class":285},[157,1980,1981],{"class":235},", attempts: ",[157,1983,1626],{"class":285},[157,1985,289],{"class":231},[157,1987,1988],{"class":285}," 5",[157,1990,431],{"class":235},[157,1992,468],{"class":231},[157,1994,471],{"class":235},[157,1996,474],{"class":285},[157,1998,477],{"class":235},[157,2000,2001,2003,2006,2008,2010,2012,2014,2017,2019,2022],{"class":72,"line":267},[157,2002,1001],{"class":231},[157,2004,2005],{"class":235}," attempt ",[157,2007,719],{"class":231},[157,2009,1660],{"class":285},[157,2011,533],{"class":235},[157,2013,1455],{"class":285},[157,2015,2016],{"class":235},", attempts ",[157,2018,1684],{"class":231},[157,2020,2021],{"class":285}," 1",[157,2023,2024],{"class":235},"):\n",[157,2026,2027,2030,2032,2034,2036,2038,2040,2042,2044],{"class":72,"line":275},[157,2028,2029],{"class":235},"        response ",[157,2031,399],{"class":231},[157,2033,488],{"class":235},[157,2035,492],{"class":491},[157,2037,399],{"class":231},[157,2039,497],{"class":285},[157,2041,431],{"class":235},[157,2043,468],{"class":231},[157,2045,504],{"class":235},[157,2047,2048,2051,2053,2055,2058,2061,2063,2066,2068,2071,2073,2076,2078,2081],{"class":72,"line":282},[157,2049,2050],{"class":231},"        if",[157,2052,513],{"class":235},[157,2054,719],{"class":231},[157,2056,2057],{"class":235}," (",[157,2059,2060],{"class":285},"429",[157,2062,431],{"class":235},[157,2064,2065],{"class":285},"503",[157,2067,431],{"class":235},[157,2069,2070],{"class":285},"504",[157,2072,1604],{"class":235},[157,2074,2075],{"class":231},"and",[157,2077,2005],{"class":235},[157,2079,2080],{"class":231},"\u003C",[157,2082,2083],{"class":235}," attempts:\n",[157,2085,2086,2089,2091,2094,2097,2100,2102,2105,2108],{"class":72,"line":295},[157,2087,2088],{"class":235},"            wait ",[157,2090,399],{"class":231},[157,2092,2093],{"class":285}," float",[157,2095,2096],{"class":235},"(response.headers.get(",[157,2098,2099],{"class":166},"\"Retry-After\"",[157,2101,431],{"class":235},[157,2103,2104],{"class":285},"2",[157,2106,2107],{"class":231}," **",[157,2109,2110],{"class":235}," attempt))\n",[157,2112,2113,2116,2118,2120,2122,2124,2126,2128,2131,2133,2136,2139,2141,2144],{"class":72,"line":351},[157,2114,2115],{"class":285},"            print",[157,2117,533],{"class":235},[157,2119,536],{"class":231},[157,2121,306],{"class":166},[157,2123,310],{"class":309},[157,2125,561],{"class":235},[157,2127,315],{"class":309},[157,2129,2130],{"class":166},"; retrying in ",[157,2132,310],{"class":309},[157,2134,2135],{"class":235},"wait",[157,2137,2138],{"class":231},":.0f",[157,2140,315],{"class":309},[157,2142,2143],{"class":166},"s\"",[157,2145,437],{"class":235},[157,2147,2148],{"class":72,"line":388},[157,2149,2150],{"class":235},"            time.sleep(wait)\n",[157,2152,2153],{"class":72,"line":393},[157,2154,2155],{"class":231},"            continue\n",[157,2157,2158,2160,2162,2164,2166],{"class":72,"line":405},[157,2159,2050],{"class":231},[157,2161,513],{"class":235},[157,2163,516],{"class":231},[157,2165,519],{"class":285},[157,2167,477],{"class":235},[157,2169,2170,2173,2175,2177,2179,2181,2183,2185,2187,2189,2191,2193,2195,2197,2199,2201,2203,2205,2207,2209,2211,2213,2215],{"class":72,"line":440},[157,2171,2172],{"class":231},"            raise",[157,2174,530],{"class":285},[157,2176,533],{"class":235},[157,2178,536],{"class":231},[157,2180,306],{"class":166},[157,2182,310],{"class":309},[157,2184,543],{"class":235},[157,2186,315],{"class":309},[157,2188,548],{"class":309},[157,2190,551],{"class":235},[157,2192,315],{"class":309},[157,2194,556],{"class":166},[157,2196,310],{"class":309},[157,2198,561],{"class":235},[157,2200,315],{"class":309},[157,2202,566],{"class":166},[157,2204,310],{"class":309},[157,2206,571],{"class":235},[157,2208,105],{"class":285},[157,2210,329],{"class":235},[157,2212,315],{"class":309},[157,2214,306],{"class":166},[157,2216,437],{"class":235},[157,2218,2219,2221,2223,2225,2227,2229],{"class":72,"line":445},[157,2220,1580],{"class":231},[157,2222,590],{"class":235},[157,2224,593],{"class":231},[157,2226,596],{"class":235},[157,2228,599],{"class":231},[157,2230,602],{"class":235},[157,2232,2233,2236,2238,2240,2243],{"class":72,"line":480},[157,2234,2235],{"class":231},"    raise",[157,2237,530],{"class":285},[157,2239,533],{"class":235},[157,2241,2242],{"class":166},"\"unreachable\"",[157,2244,437],{"class":235},[157,2246,2247],{"class":72,"line":507},[157,2248,279],{"emptyLinePlaceholder":278},[157,2250,2251,2254,2256,2259,2261,2263,2265,2267,2269,2271,2273,2275,2277,2279,2281,2283,2285,2288,2290,2292,2294,2296,2299],{"class":72,"line":524},[157,2252,2253],{"class":235},"result ",[157,2255,399],{"class":231},[157,2257,2258],{"class":235}," call_with_retry(",[157,2260,1736],{"class":166},[157,2262,431],{"class":235},[157,2264,536],{"class":231},[157,2266,306],{"class":166},[157,2268,310],{"class":309},[157,2270,354],{"class":285},[157,2272,315],{"class":309},[157,2274,1751],{"class":166},[157,2276,431],{"class":235},[157,2278,1756],{"class":491},[157,2280,399],{"class":231},[157,2282,310],{"class":235},[157,2284,673],{"class":166},[157,2286,2287],{"class":235},": [[",[157,2289,913],{"class":166},[157,2291,431],{"class":235},[157,2293,891],{"class":166},[157,2295,431],{"class":235},[157,2297,2298],{"class":285},"99.0",[157,2300,2301],{"class":235},"]]})\n",[157,2303,2304,2306,2309,2311],{"class":72,"line":584},[157,2305,959],{"class":285},[157,2307,2308],{"class":235},"(result.get(",[157,2310,1763],{"class":166},[157,2312,2313],{"class":235},"))\n",[10,2315,183,2316,2318],{},[14,2317,2070],{}," is ambiguous: the rows may or may not have been added before the gateway gave up. Re-reading the keys before retrying a batch — or accepting that\nthe next scheduled run's duplicate check will catch it — keeps an ambiguous timeout from becoming duplicate data. Keep requests to one workbook sequential;\nMicrosoft recommends against parallel writes to the same file, which cause throttling and merge conflicts rather than speed.",[143,2320,2322],{"id":2321},"common-pitfalls","Common pitfalls",[2324,2325,2326,2342],"table",{},[2327,2328,2329],"thead",{},[2330,2331,2332,2336,2339],"tr",{},[2333,2334,2335],"th",{},"Symptom",[2333,2337,2338],{},"Cause",[2333,2340,2341],{},"Fix",[2343,2344,2345,2360,2377,2388,2399,2413],"tbody",{},[2330,2346,2347,2351,2354],{},[2348,2349,2350],"td",{},"Values in the wrong columns",[2348,2352,2353],{},"Frame column order differs from the table",[2348,2355,2356,2357,2359],{},"Select columns in ",[14,2358,70],{}," order",[2330,2361,2362,2368,2371],{},[2348,2363,2364,2367],{},[14,2365,2366],{},"InvalidArgument"," on rows\u002Fadd",[2348,2369,2370],{},"Row width differs from the table's column count",[2348,2372,2373,2374,2376],{},"Include every table column; ",[14,2375,1036],{}," for calculated ones",[2330,2378,2379,2382,2385],{},[2348,2380,2381],{},"Duplicate rows after a rerun",[2348,2383,2384],{},"No idempotency check",[2348,2386,2387],{},"Compare keys with existing rows before appending",[2330,2389,2390,2393,2396],{},[2348,2391,2392],{},"Slow appends",[2348,2394,2395],{},"One row per call",[2348,2397,2398],{},"Batch hundreds of rows per request",[2330,2400,2401,2407,2410],{},[2348,2402,2403,2406],{},[14,2404,2405],{},"ItemNotFound"," for the table",[2348,2408,2409],{},"Wrong table name, or the range is not an Excel table",[2348,2411,2412],{},"Check the name on the Table Design tab",[2330,2414,2415,2418,2421],{},[2348,2416,2417],{},"Throttling during large loads",[2348,2419,2420],{},"Many or parallel requests",[2348,2422,2423,2424],{},"Sequential batches; honour ",[14,2425,1946],{},[143,2427,2429],{"id":2428},"performance-and-scale-notes","Performance and scale notes",[10,2431,2432,2433,2436,2437,197],{},"Reading every existing row to build the key set is the expensive part for a large table. When the table is big, read only the key column through its\nrange — ",[14,2434,2435],{},"\u002Ftables\u002F{name}\u002Fcolumns\u002F{column}\u002Frange"," — or keep a local record of keys already sent. Use a workbook session for the whole job so the service\nkeeps the workbook open between the header read, the key read and the appends. And keep perspective: an Excel table in SharePoint is a comfortable home\nfor thousands or tens of thousands of rows; beyond that, a database with the workbook as a view is the more durable design, as in\n",[19,2438,2440],{"href":2439},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule\u002F","Refresh an Excel Report from a Database on a Schedule",[143,2442,2444],{"id":2443},"conclusion","Conclusion",[10,2446,2447,2448,2450,2451,2453],{},"Appending to an Excel Online table is a positional operation, so align the DataFrame to the table's header first. Make the job idempotent by removing\nrows whose keys already exist, send rows in batches rather than one at a time, and retry ",[14,2449,2060],{}," and ",[14,2452,2070],{}," responses with a delay — re-checking keys before\na retried batch. Use a delegated token and a workbook session, keep writes to one workbook sequential, and the shared tracker gains a feed that people can\nkeep using exactly as before.",[143,2455,2457],{"id":2456},"frequently-asked-questions","Frequently asked questions",[10,2459,2460,2463],{},[185,2461,2462],{},"Do calculated columns fill in for rows added through Graph?","\nYes. Send an empty value for the calculated column and Excel applies the column formula to each new row, just as it does when a person types below the table.",[10,2465,2466,2469],{},[185,2467,2468],{},"Can I insert rows at the top instead of the end?","\nYes. Set index to 0 in the request body. Rows below are shifted down, which is slower on large tables than appending at the end.",[10,2471,2472,2475],{},[185,2473,2474],{},"Can I update or delete an existing table row?","\nRows can be addressed by index under the table's rows collection, updated through their range and deleted. Because indexes shift as rows change, locate rows by key immediately before changing them.",[10,2477,2478,2481],{},[185,2479,2480],{},"Does this work with Power Automate reading the same table?","\nYes. Rows added through Graph are ordinary table rows, so flows and Power BI datasets reading the table see them on their next run.",[143,2483,2485],{"id":2484},"related","Related",[178,2487,2488,2494,2501,2508,2515],{},[181,2489,2490,2491,2493],{},"Up one level: ",[19,2492,22],{"href":21}," — reading, editing and recalculating workbooks in Microsoft 365.",[181,2495,2496,2500],{},[19,2497,2499],{"href":2498},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-and-write-excel-online-ranges-with-microsoft-graph\u002F","Read and Write Excel Online Ranges with Microsoft Graph"," — range updates, dates and blanks.",[181,2502,2503,2507],{},[19,2504,2506],{"href":2505},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frecalculate-excel-online-workbooks-with-graph-sessions\u002F","Recalculate Excel Online Workbooks with Graph Sessions"," — sessions that make multi-call jobs faster.",[181,2509,2510,2514],{},[19,2511,2513],{"href":2512},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fcreate-an-excel-table-with-python\u002F","Create an Excel Table with Python"," — building the table in the first place.",[181,2516,2517,2521],{},[19,2518,2520],{"href":2519},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","Retry a Failed Excel Report Job in Python"," — retry strategies for whole jobs.",[2523,2524,2525],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":153,"searchDepth":239,"depth":239,"links":2527},[2528,2529,2530,2531,2532,2533,2534,2535,2536,2537,2538],{"id":145,"depth":239,"text":146},{"id":215,"depth":239,"text":216},{"id":612,"depth":239,"text":613},{"id":1157,"depth":239,"text":1158},{"id":1546,"depth":239,"text":1547},{"id":1931,"depth":239,"text":1932},{"id":2321,"depth":239,"text":2322},{"id":2428,"depth":239,"text":2429},{"id":2443,"depth":239,"text":2444},{"id":2456,"depth":239,"text":2457},{"id":2484,"depth":239,"text":2485},"2026-09-17","Add new records to an Excel table in OneDrive or SharePoint without downloading the file: match columns to the table header, skip rows already present, send rows in batches, and retry safely.","md",[2543,2545,2547,2549],{"q":2462,"a":2544},"Yes. Send an empty value for the calculated column and Excel applies the column formula to each new row, just as it does when a person types below the table.",{"q":2468,"a":2546},"Yes. Set index to 0 in the request body. Rows below are shifted down, which is slower on large tables than appending at the end.",{"q":2474,"a":2548},"Rows can be addressed by index under the table's rows collection, updated through their range and deleted. Because indexes shift as rows change, locate rows by key immediately before changing them.",{"q":2480,"a":2550},"Yes. Rows added through Graph are ordinary table rows, so flows and Power BI datasets reading the table see them on their next run.","append rows to an excel online table with microsoft graph",null,{"breadcrumb":2554},[2555,2558,2561],{"name":2556,"item":2557},"Home","\u002F",{"name":2559,"item":2560},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph",{"title":2564,"description":2565},"Append Rows to an Excel Online Table with Microsoft Graph (Python)","Append pandas rows to an Excel table stored in SharePoint or OneDrive with the Graph rows\u002Fadd endpoint: align to headerRowRange, de-duplicate by key, batch the values, and handle throttling.","append-rows-to-an-excel-online-table-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph\u002Findex",[2569,2570],"excel-online","writing-excel","how-to","r5_FYqKx1OzH1gffqxxzUCNd-YQgU994k7YhKNkF7rY",[2574,2578],{"title":2575,"path":2576,"stem":2577,"children":-1},"Add, Rename and Delete Worksheets with Microsoft Graph","\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph\u002Findex",{"title":196,"path":2579,"stem":2580,"children":-1},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002Findex",[2582,2584],{"path":2583,"title":22},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph",{"path":2585,"title":2586},"\u002Fautomating-reporting-workflows","Automating Reporting Workflows with Python and Excel",[],[2589,2592,2595,2596,2597,2600],{"path":2590,"title":2591,"kind":2571},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api","Send an Excel Report with the Microsoft Graph API",{"path":2593,"title":2594,"kind":2571},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph","Add a Chart to an Excel Online Workbook with Microsoft Graph",{"path":2576,"title":2575,"kind":2571},{"path":2579,"title":196,"kind":2571},{"path":2598,"title":2599,"kind":2571},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph","Call Excel Worksheet Functions Through Microsoft Graph",{"path":2601,"title":2602,"kind":2571},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcopy-a-template-workbook-in-sharepoint-with-microsoft-graph","Copy a Template Workbook in SharePoint with Microsoft Graph",1790972118891]