[{"data":1,"prerenderedAt":2024},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python":2016},{"id":4,"title":5,"body":6,"dateModified":1993,"datePublished":1993,"description":1994,"extension":1995,"faq":1996,"meta":2007,"navigation":195,"path":2008,"seo":2009,"slug":2012,"stem":2013,"type":2014,"__hash__":2015},"docs\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python\u002Findex.md","Post an Excel Report to a Slack Channel with Python",{"type":7,"value":8,"toc":1977},"minimark",[9,19,107,112,144,163,167,418,433,437,440,660,667,671,674,995,1010,1085,1089,1092,1260,1267,1271,1274,1339,1346,1350,1353,1407,1410,1414,1417,1514,1521,1525,1528,1718,1721,1822,1826,1870,1874,1881,1885,1888,1892,1903,1912,1918,1928,1934,1938,1973],[10,11,12,13,18],"p",{},"Email is not where teams read reports any more. Posting the workbook into the channel where the conversation already happens — with a two-line summary of the numbers above it — is often the whole difference between a report that gets read and one that sits in an inbox. Slack's API does this in a few lines, and the operational details are the familiar ones: scopes, channel membership, secrets and retries. This guide uploads a workbook, adds a summary message, and makes the delivery reliable in a scheduled job. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",".",[20,21,29,30,29,34,29,38,29,45,29,52,29,61,29,67,29,72,29,77,29,85,29,91,29,97,29,102],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 226","img",[25,26],"sl-t","sl-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  ",[31,32,33],"title",{"id":25},"What arrives in the channel",[35,36,37],"desc",{"id":26},"The job posts a summary message carrying the headline numbers and attaches the workbook beneath it, so a reader gets the answer without opening the file.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","226","#ffffff",[46,47,51],"text",{"x":48,"y":49,"style":50},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","The summary is the message; the file is the evidence",[39,53],{"x":54,"y":55,"width":56,"height":57,"rx":58,"fill":59,"stroke":60},"80","46","600","66","10","#ebebfd","var(--line,#cdd5e6)",[46,62,66],{"x":63,"y":64,"style":65},"104","72","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","Weekly sales — week ending 2026-08-27",[46,68,71],{"x":63,"y":69,"style":70},"96","font-size:11.5px;fill:var(--text,#172033);text-anchor:start","Revenue 350,656 (+4.2% on last week) · North leads · 3 regions below target",[39,73],{"x":54,"y":74,"width":56,"height":75,"rx":58,"fill":76,"stroke":60},"122","60","#d9f4f1",[39,78],{"x":79,"y":80,"width":81,"height":82,"rx":83,"fill":84},"98","136","30","32","4","#0b6157",[46,86,90],{"x":87,"y":88,"style":89},"113","158","font-size:11px;font-weight:700;fill:#ffffff;text-anchor:middle","X",[46,92,96],{"x":93,"y":94,"style":95},"144","150","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","weekly_sales_2026-08-27.xlsx",[46,98,101],{"x":93,"y":99,"style":100},"170","font-size:11px;fill:var(--teal-ink,#0b6157);text-anchor:start","84 KB · shared by reporting-bot",[46,103,106],{"x":48,"y":104,"style":105},"208","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","Most readers never open the file — and that is the point",[108,109,111],"h2",{"id":110},"prerequisites","Prerequisites",[113,114,119],"pre",{"className":115,"code":116,"language":117,"meta":118,"style":118},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install slack-sdk pandas xlsxwriter\n","bash","",[120,121,122],"code",{"__ignoreMap":118},[123,124,127,131,135,138,141],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"sMTad","pip",[123,132,134],{"class":133},"srMev"," install",[123,136,137],{"class":133}," slack-sdk",[123,139,140],{"class":133}," pandas",[123,142,143],{"class":133}," xlsxwriter\n",[10,145,146,147,150,151,154,155,158,159,162],{},"Create a Slack app, give its bot token the ",[120,148,149],{},"files:write"," and ",[120,152,153],{},"chat:write"," scopes, install it to the workspace, and invite it to the target channel with ",[120,156,157],{},"\u002Finvite @yourbot",". Scopes and membership are separate — having the first without the second produces ",[120,160,161],{},"not_in_channel",", which is the most common first failure.",[108,164,166],{"id":165},"upload-the-workbook","Upload the workbook",[113,168,172],{"className":169,"code":170,"language":171,"meta":118,"style":118},"language-python shiki shiki-themes github-light github-dark-high-contrast","\"\"\"Post a report file to a channel.\"\"\"\nimport os\n\nfrom slack_sdk import WebClient\nfrom slack_sdk.errors import SlackApiError\n\nclient = WebClient(token=os.environ[\"SLACK_BOT_TOKEN\"])\n\ntry:\n    response = client.files_upload_v2(\n        channel=\"C0123456789\",                      # channel id, not name\n        file=\"weekly_sales.xlsx\",\n        filename=\"weekly_sales_2026-08-27.xlsx\",\n        title=\"Weekly sales — week ending 27 Aug 2026\",\n        initial_comment=\"Weekly sales summary attached.\",\n    )\n    print(\"uploaded:\", response[\"file\"][\"permalink\"])\nexcept SlackApiError as exc:\n    print(\"upload failed:\", exc.response[\"error\"])\n","python",[120,173,174,179,190,197,211,224,229,256,261,270,281,299,313,326,339,352,358,385,400],{"__ignoreMap":118},[123,175,176],{"class":125,"line":126},[123,177,178],{"class":133},"\"\"\"Post a report file to a channel.\"\"\"\n",[123,180,182,186],{"class":125,"line":181},2,[123,183,185],{"class":184},"s-kum","import",[123,187,189],{"class":188},"skGVy"," os\n",[123,191,193],{"class":125,"line":192},3,[123,194,196],{"emptyLinePlaceholder":195},true,"\n",[123,198,200,203,206,208],{"class":125,"line":199},4,[123,201,202],{"class":184},"from",[123,204,205],{"class":188}," slack_sdk ",[123,207,185],{"class":184},[123,209,210],{"class":188}," WebClient\n",[123,212,214,216,219,221],{"class":125,"line":213},5,[123,215,202],{"class":184},[123,217,218],{"class":188}," slack_sdk.errors ",[123,220,185],{"class":184},[123,222,223],{"class":188}," SlackApiError\n",[123,225,227],{"class":125,"line":226},6,[123,228,196],{"emptyLinePlaceholder":195},[123,230,232,235,238,241,245,247,250,253],{"class":125,"line":231},7,[123,233,234],{"class":188},"client ",[123,236,237],{"class":184},"=",[123,239,240],{"class":188}," WebClient(",[123,242,244],{"class":243},"sa561","token",[123,246,237],{"class":184},[123,248,249],{"class":188},"os.environ[",[123,251,252],{"class":133},"\"SLACK_BOT_TOKEN\"",[123,254,255],{"class":188},"])\n",[123,257,259],{"class":125,"line":258},8,[123,260,196],{"emptyLinePlaceholder":195},[123,262,264,267],{"class":125,"line":263},9,[123,265,266],{"class":184},"try",[123,268,269],{"class":188},":\n",[123,271,273,276,278],{"class":125,"line":272},10,[123,274,275],{"class":188},"    response ",[123,277,237],{"class":184},[123,279,280],{"class":188}," client.files_upload_v2(\n",[123,282,284,287,289,292,295],{"class":125,"line":283},11,[123,285,286],{"class":243},"        channel",[123,288,237],{"class":184},[123,290,291],{"class":133},"\"C0123456789\"",[123,293,294],{"class":188},",                      ",[123,296,298],{"class":297},"s-wDw","# channel id, not name\n",[123,300,302,305,307,310],{"class":125,"line":301},12,[123,303,304],{"class":243},"        file",[123,306,237],{"class":184},[123,308,309],{"class":133},"\"weekly_sales.xlsx\"",[123,311,312],{"class":188},",\n",[123,314,316,319,321,324],{"class":125,"line":315},13,[123,317,318],{"class":243},"        filename",[123,320,237],{"class":184},[123,322,323],{"class":133},"\"weekly_sales_2026-08-27.xlsx\"",[123,325,312],{"class":188},[123,327,329,332,334,337],{"class":125,"line":328},14,[123,330,331],{"class":243},"        title",[123,333,237],{"class":184},[123,335,336],{"class":133},"\"Weekly sales — week ending 27 Aug 2026\"",[123,338,312],{"class":188},[123,340,342,345,347,350],{"class":125,"line":341},15,[123,343,344],{"class":243},"        initial_comment",[123,346,237],{"class":184},[123,348,349],{"class":133},"\"Weekly sales summary attached.\"",[123,351,312],{"class":188},[123,353,355],{"class":125,"line":354},16,[123,356,357],{"class":188},"    )\n",[123,359,361,365,368,371,374,377,380,383],{"class":125,"line":360},17,[123,362,364],{"class":363},"sP0c6","    print",[123,366,367],{"class":188},"(",[123,369,370],{"class":133},"\"uploaded:\"",[123,372,373],{"class":188},", response[",[123,375,376],{"class":133},"\"file\"",[123,378,379],{"class":188},"][",[123,381,382],{"class":133},"\"permalink\"",[123,384,255],{"class":188},[123,386,388,391,394,397],{"class":125,"line":387},18,[123,389,390],{"class":184},"except",[123,392,393],{"class":188}," SlackApiError ",[123,395,396],{"class":184},"as",[123,398,399],{"class":188}," exc:\n",[123,401,403,405,407,410,413,416],{"class":125,"line":402},19,[123,404,364],{"class":363},[123,406,367],{"class":188},[123,408,409],{"class":133},"\"upload failed:\"",[123,411,412],{"class":188},", exc.response[",[123,414,415],{"class":133},"\"error\"",[123,417,255],{"class":188},[10,419,420,421,425,426,429,430,18],{},"Use the channel ",[422,423,424],"strong",{},"id"," (the ",[120,427,428],{},"C…"," string from the channel's details) rather than the name. Names change; ids do not, and a renamed channel silently breaks a job that referenced ",[120,431,432],{},"#sales-reports",[108,434,436],{"id":435},"upload-from-memory","Upload from memory",[10,438,439],{},"Since the workbook is usually built in the same process, skip the disk entirely:",[113,441,443],{"className":169,"code":442,"language":171,"meta":118,"style":118},"import io\n\nimport pandas as pd\nfrom slack_sdk import WebClient\n\ndf = pd.DataFrame({\"region\": [\"North\", \"South\"], \"revenue\": [128_400.5, 96_220.0]})\n\nbuffer = io.BytesIO()\nwith pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, index=False, sheet_name=\"Summary\")\nbuffer.seek(0)\n\nclient.files_upload_v2(\n    channel=os.environ[\"SLACK_CHANNEL_ID\"],\n    file=buffer.getvalue(),\n    filename=\"weekly_sales.xlsx\",\n    title=\"Weekly sales\",\n)\n",[120,444,445,452,456,468,478,482,526,530,540,564,590,599,603,608,623,633,644,656],{"__ignoreMap":118},[123,446,447,449],{"class":125,"line":126},[123,448,185],{"class":184},[123,450,451],{"class":188}," io\n",[123,453,454],{"class":125,"line":181},[123,455,196],{"emptyLinePlaceholder":195},[123,457,458,460,463,465],{"class":125,"line":192},[123,459,185],{"class":184},[123,461,462],{"class":188}," pandas ",[123,464,396],{"class":184},[123,466,467],{"class":188}," pd\n",[123,469,470,472,474,476],{"class":125,"line":199},[123,471,202],{"class":184},[123,473,205],{"class":188},[123,475,185],{"class":184},[123,477,210],{"class":188},[123,479,480],{"class":125,"line":213},[123,481,196],{"emptyLinePlaceholder":195},[123,483,484,487,489,492,495,498,501,504,507,510,513,515,518,520,523],{"class":125,"line":226},[123,485,486],{"class":188},"df ",[123,488,237],{"class":184},[123,490,491],{"class":188}," pd.DataFrame({",[123,493,494],{"class":133},"\"region\"",[123,496,497],{"class":188},": [",[123,499,500],{"class":133},"\"North\"",[123,502,503],{"class":188},", ",[123,505,506],{"class":133},"\"South\"",[123,508,509],{"class":188},"], ",[123,511,512],{"class":133},"\"revenue\"",[123,514,497],{"class":188},[123,516,517],{"class":363},"128_400.5",[123,519,503],{"class":188},[123,521,522],{"class":363},"96_220.0",[123,524,525],{"class":188},"]})\n",[123,527,528],{"class":125,"line":231},[123,529,196],{"emptyLinePlaceholder":195},[123,531,532,535,537],{"class":125,"line":258},[123,533,534],{"class":188},"buffer ",[123,536,237],{"class":184},[123,538,539],{"class":188}," io.BytesIO()\n",[123,541,542,545,548,551,553,556,559,561],{"class":125,"line":263},[123,543,544],{"class":184},"with",[123,546,547],{"class":188}," pd.ExcelWriter(buffer, ",[123,549,550],{"class":243},"engine",[123,552,237],{"class":184},[123,554,555],{"class":133},"\"xlsxwriter\"",[123,557,558],{"class":188},") ",[123,560,396],{"class":184},[123,562,563],{"class":188}," writer:\n",[123,565,566,569,572,574,577,579,582,584,587],{"class":125,"line":272},[123,567,568],{"class":188},"    df.to_excel(writer, ",[123,570,571],{"class":243},"index",[123,573,237],{"class":184},[123,575,576],{"class":363},"False",[123,578,503],{"class":188},[123,580,581],{"class":243},"sheet_name",[123,583,237],{"class":184},[123,585,586],{"class":133},"\"Summary\"",[123,588,589],{"class":188},")\n",[123,591,592,595,597],{"class":125,"line":283},[123,593,594],{"class":188},"buffer.seek(",[123,596,41],{"class":363},[123,598,589],{"class":188},[123,600,601],{"class":125,"line":301},[123,602,196],{"emptyLinePlaceholder":195},[123,604,605],{"class":125,"line":315},[123,606,607],{"class":188},"client.files_upload_v2(\n",[123,609,610,613,615,617,620],{"class":125,"line":328},[123,611,612],{"class":243},"    channel",[123,614,237],{"class":184},[123,616,249],{"class":188},[123,618,619],{"class":133},"\"SLACK_CHANNEL_ID\"",[123,621,622],{"class":188},"],\n",[123,624,625,628,630],{"class":125,"line":341},[123,626,627],{"class":243},"    file",[123,629,237],{"class":184},[123,631,632],{"class":188},"buffer.getvalue(),\n",[123,634,635,638,640,642],{"class":125,"line":354},[123,636,637],{"class":243},"    filename",[123,639,237],{"class":184},[123,641,309],{"class":133},[123,643,312],{"class":188},[123,645,646,649,651,654],{"class":125,"line":360},[123,647,648],{"class":243},"    title",[123,650,237],{"class":184},[123,652,653],{"class":133},"\"Weekly sales\"",[123,655,312],{"class":188},[123,657,658],{"class":125,"line":387},[123,659,589],{"class":188},[10,661,662,663,18],{},"The buffer rules are the same as anywhere else — close the writer before reading, and rewind — as covered in ",[14,664,666],{"href":665},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio\u002F","Build an Excel workbook in memory with BytesIO",[108,668,670],{"id":669},"put-the-numbers-in-the-message","Put the numbers in the message",[10,672,673],{},"An attachment nobody opens has delivered nothing. Compute the headline figures and post them as the message, with the file underneath as the detail:",[113,675,677],{"className":169,"code":676,"language":171,"meta":118,"style":118},"def summary_text(df: pd.DataFrame, previous_total: float) -> str:\n    total = float(df[\"revenue\"].sum())\n    change = (total - previous_total) \u002F previous_total if previous_total else 0.0\n    leader = df.loc[df[\"revenue\"].idxmax(), \"region\"]\n    below = int((df[\"revenue\"] \u003C df[\"target\"]).sum())\n    return (\n        f\"*Weekly sales — week ending {run_date:%d %b %Y}*\\n\"\n        f\"Revenue *{total:,.0f}* ({change:+.1%} on last week) · \"\n        f\"{leader} leads · {below} region(s) below target\"\n    )\n\nclient.files_upload_v2(\n    channel=channel_id,\n    file=buffer.getvalue(),\n    filename=f\"weekly_sales_{run_date:%Y-%m-%d}.xlsx\",\n    initial_comment=summary_text(df, previous_total),\n)\n",[120,678,679,702,720,753,773,803,811,854,887,914,918,922,926,935,943,981,991],{"__ignoreMap":118},[123,680,681,684,688,691,694,697,700],{"class":125,"line":126},[123,682,683],{"class":184},"def",[123,685,687],{"class":686},"s_Opv"," summary_text",[123,689,690],{"class":188},"(df: pd.DataFrame, previous_total: ",[123,692,693],{"class":363},"float",[123,695,696],{"class":188},") -> ",[123,698,699],{"class":363},"str",[123,701,269],{"class":188},[123,703,704,707,709,712,715,717],{"class":125,"line":181},[123,705,706],{"class":188},"    total ",[123,708,237],{"class":184},[123,710,711],{"class":363}," float",[123,713,714],{"class":188},"(df[",[123,716,512],{"class":133},[123,718,719],{"class":188},"].sum())\n",[123,721,722,725,727,730,733,736,739,742,745,747,750],{"class":125,"line":192},[123,723,724],{"class":188},"    change ",[123,726,237],{"class":184},[123,728,729],{"class":188}," (total ",[123,731,732],{"class":184},"-",[123,734,735],{"class":188}," previous_total) ",[123,737,738],{"class":184},"\u002F",[123,740,741],{"class":188}," previous_total ",[123,743,744],{"class":184},"if",[123,746,741],{"class":188},[123,748,749],{"class":184},"else",[123,751,752],{"class":363}," 0.0\n",[123,754,755,758,760,763,765,768,770],{"class":125,"line":199},[123,756,757],{"class":188},"    leader ",[123,759,237],{"class":184},[123,761,762],{"class":188}," df.loc[df[",[123,764,512],{"class":133},[123,766,767],{"class":188},"].idxmax(), ",[123,769,494],{"class":133},[123,771,772],{"class":188},"]\n",[123,774,775,778,780,783,786,788,791,794,797,800],{"class":125,"line":213},[123,776,777],{"class":188},"    below ",[123,779,237],{"class":184},[123,781,782],{"class":363}," int",[123,784,785],{"class":188},"((df[",[123,787,512],{"class":133},[123,789,790],{"class":188},"] ",[123,792,793],{"class":184},"\u003C",[123,795,796],{"class":188}," df[",[123,798,799],{"class":133},"\"target\"",[123,801,802],{"class":188},"]).sum())\n",[123,804,805,808],{"class":125,"line":226},[123,806,807],{"class":184},"    return",[123,809,810],{"class":188}," (\n",[123,812,813,816,819,823,826,829,832,834,837,839,842,845,848,851],{"class":125,"line":231},[123,814,815],{"class":184},"        f",[123,817,818],{"class":133},"\"*Weekly sales — week ending ",[123,820,822],{"class":821},"sSjpA","{",[123,824,825],{"class":188},"run_date:",[123,827,828],{"class":184},"%",[123,830,831],{"class":188},"d ",[123,833,828],{"class":184},[123,835,836],{"class":188},"b ",[123,838,828],{"class":184},[123,840,841],{"class":188},"Y",[123,843,844],{"class":821},"}",[123,846,847],{"class":133},"*",[123,849,850],{"class":821},"\\n",[123,852,853],{"class":133},"\"\n",[123,855,856,858,861,863,866,869,871,874,876,879,882,884],{"class":125,"line":258},[123,857,815],{"class":184},[123,859,860],{"class":133},"\"Revenue *",[123,862,822],{"class":821},[123,864,865],{"class":188},"total",[123,867,868],{"class":184},":,.0f",[123,870,844],{"class":821},[123,872,873],{"class":133},"* (",[123,875,822],{"class":821},[123,877,878],{"class":188},"change",[123,880,881],{"class":184},":+.1%",[123,883,844],{"class":821},[123,885,886],{"class":133}," on last week) · \"\n",[123,888,889,891,894,896,899,901,904,906,909,911],{"class":125,"line":263},[123,890,815],{"class":184},[123,892,893],{"class":133},"\"",[123,895,822],{"class":821},[123,897,898],{"class":188},"leader",[123,900,844],{"class":821},[123,902,903],{"class":133}," leads · ",[123,905,822],{"class":821},[123,907,908],{"class":188},"below",[123,910,844],{"class":821},[123,912,913],{"class":133}," region(s) below target\"\n",[123,915,916],{"class":125,"line":272},[123,917,357],{"class":188},[123,919,920],{"class":125,"line":283},[123,921,196],{"emptyLinePlaceholder":195},[123,923,924],{"class":125,"line":301},[123,925,607],{"class":188},[123,927,928,930,932],{"class":125,"line":315},[123,929,612],{"class":243},[123,931,237],{"class":184},[123,933,934],{"class":188},"channel_id,\n",[123,936,937,939,941],{"class":125,"line":328},[123,938,627],{"class":243},[123,940,237],{"class":184},[123,942,632],{"class":188},[123,944,945,947,949,952,955,957,959,961,963,966,969,971,974,976,979],{"class":125,"line":341},[123,946,637],{"class":243},[123,948,237],{"class":184},[123,950,951],{"class":184},"f",[123,953,954],{"class":133},"\"weekly_sales_",[123,956,822],{"class":821},[123,958,825],{"class":188},[123,960,828],{"class":184},[123,962,841],{"class":188},[123,964,965],{"class":184},"-%",[123,967,968],{"class":188},"m",[123,970,965],{"class":184},[123,972,973],{"class":188},"d",[123,975,844],{"class":821},[123,977,978],{"class":133},".xlsx\"",[123,980,312],{"class":188},[123,982,983,986,988],{"class":125,"line":354},[123,984,985],{"class":243},"    initial_comment",[123,987,237],{"class":184},[123,989,990],{"class":188},"summary_text(df, previous_total),\n",[123,992,993],{"class":125,"line":360},[123,994,589],{"class":188},[10,996,997,998,1001,1002,1005,1006,1009],{},"Slack's ",[120,999,1000],{},"mrkdwn"," uses single asterisks for bold and backticks for code, not Markdown's conventions. A number formatted as ",[120,1003,1004],{},"350,656"," reads far better in a channel than ",[120,1007,1008],{},"350656.4499999999",", so format deliberately.",[20,1011,29,1016,29,1019,29,1022,29,1025,29,1031,29,1036,29,1041,29,1047,29,1052,29,1054,29,1058,29,1062,29,1067,29,1070,29,1075,29,1077,29,1080,29,1082],{"viewBox":1012,"role":23,"ariaLabelledBy":1013,"xmlns":27,"style":28},"0 0 760 214",[1014,1015],"sl2-t","sl2-d",[31,1017,1018],{"id":1014},"Attachment-only delivery versus a summary plus attachment",[35,1020,1021],{"id":1015},"A bare file forces every reader to download and open it, while a message carrying the headline numbers answers the common question in the channel itself.",[39,1023],{"x":41,"y":41,"width":42,"height":1024,"fill":44},"214",[46,1026,1030],{"x":1027,"y":1028,"style":1029},"196","28","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","file only",[46,1032,1035],{"x":1033,"y":1028,"style":1034},"566","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","summary + file",[125,1037],{"x1":48,"y1":1038,"x2":48,"y2":1039,"stroke":60,"style":1040},"40","192","stroke-width:1px",[39,1042],{"x":81,"y":55,"width":1043,"height":1044,"rx":1045,"fill":1046,"stroke":60},"332","44","9","#fee8f2",[46,1048,1051],{"x":1027,"y":1049,"style":1050},"74","font-size:12px;fill:var(--accent-ink,#be185d);text-anchor:middle","\"report.xlsx\" — no context",[39,1053],{"x":81,"y":79,"width":1043,"height":1044,"rx":1045,"fill":1046,"stroke":60},[46,1055,1057],{"x":1027,"y":1056,"style":1050},"126","everyone downloads to find out",[39,1059],{"x":81,"y":94,"width":1043,"height":1060,"rx":1045,"fill":1061,"stroke":60},"42","#f0f2f5",[46,1063,1066],{"x":1027,"y":1064,"style":1065},"177","font-size:12px;fill:var(--text,#172033);text-anchor:middle","most people do not bother",[39,1068],{"x":1069,"y":55,"width":1043,"height":1044,"rx":1045,"fill":76,"stroke":60},"398",[46,1071,1074],{"x":1072,"y":1049,"style":1073},"564","font-size:12px;fill:var(--teal-ink,#0b6157);text-anchor:middle","headline numbers in the message",[39,1076],{"x":1069,"y":79,"width":1043,"height":1044,"rx":1045,"fill":76,"stroke":60},[46,1078,1079],{"x":1072,"y":1056,"style":1073},"the file for whoever needs detail",[39,1081],{"x":1069,"y":94,"width":1043,"height":1060,"rx":1045,"fill":1061,"stroke":60},[46,1083,1084],{"x":1072,"y":1064,"style":1065},"the discussion happens in the thread",[108,1086,1088],{"id":1087},"thread-the-detail-under-the-summary","Thread the detail under the summary",[10,1090,1091],{},"For a report with several files — one per region, say — post the summary first and attach the rest to its thread, so the channel shows one item instead of nine:",[113,1093,1095],{"className":169,"code":1094,"language":171,"meta":118,"style":118},"posted = client.chat_postMessage(channel=channel_id, text=summary_text(df, previous_total))\nthread_ts = posted[\"ts\"]\n\nfor region, region_df in df.groupby(\"region\"):\n    client.files_upload_v2(\n        channel=channel_id,\n        thread_ts=thread_ts,\n        file=build_workbook(region_df),\n        filename=f\"{region.lower()}_{run_date:%Y-%m-%d}.xlsx\",\n        title=f\"{region} detail\",\n    )\n",[120,1096,1097,1122,1137,1141,1160,1165,1173,1183,1192,1234,1256],{"__ignoreMap":118},[123,1098,1099,1102,1104,1107,1110,1112,1115,1117,1119],{"class":125,"line":126},[123,1100,1101],{"class":188},"posted ",[123,1103,237],{"class":184},[123,1105,1106],{"class":188}," client.chat_postMessage(",[123,1108,1109],{"class":243},"channel",[123,1111,237],{"class":184},[123,1113,1114],{"class":188},"channel_id, ",[123,1116,46],{"class":243},[123,1118,237],{"class":184},[123,1120,1121],{"class":188},"summary_text(df, previous_total))\n",[123,1123,1124,1127,1129,1132,1135],{"class":125,"line":181},[123,1125,1126],{"class":188},"thread_ts ",[123,1128,237],{"class":184},[123,1130,1131],{"class":188}," posted[",[123,1133,1134],{"class":133},"\"ts\"",[123,1136,772],{"class":188},[123,1138,1139],{"class":125,"line":192},[123,1140,196],{"emptyLinePlaceholder":195},[123,1142,1143,1146,1149,1152,1155,1157],{"class":125,"line":199},[123,1144,1145],{"class":184},"for",[123,1147,1148],{"class":188}," region, region_df ",[123,1150,1151],{"class":184},"in",[123,1153,1154],{"class":188}," df.groupby(",[123,1156,494],{"class":133},[123,1158,1159],{"class":188},"):\n",[123,1161,1162],{"class":125,"line":213},[123,1163,1164],{"class":188},"    client.files_upload_v2(\n",[123,1166,1167,1169,1171],{"class":125,"line":226},[123,1168,286],{"class":243},[123,1170,237],{"class":184},[123,1172,934],{"class":188},[123,1174,1175,1178,1180],{"class":125,"line":231},[123,1176,1177],{"class":243},"        thread_ts",[123,1179,237],{"class":184},[123,1181,1182],{"class":188},"thread_ts,\n",[123,1184,1185,1187,1189],{"class":125,"line":258},[123,1186,304],{"class":243},[123,1188,237],{"class":184},[123,1190,1191],{"class":188},"build_workbook(region_df),\n",[123,1193,1194,1196,1198,1200,1202,1204,1207,1209,1212,1214,1216,1218,1220,1222,1224,1226,1228,1230,1232],{"class":125,"line":263},[123,1195,318],{"class":243},[123,1197,237],{"class":184},[123,1199,951],{"class":184},[123,1201,893],{"class":133},[123,1203,822],{"class":821},[123,1205,1206],{"class":188},"region.lower()",[123,1208,844],{"class":821},[123,1210,1211],{"class":133},"_",[123,1213,822],{"class":821},[123,1215,825],{"class":188},[123,1217,828],{"class":184},[123,1219,841],{"class":188},[123,1221,965],{"class":184},[123,1223,968],{"class":188},[123,1225,965],{"class":184},[123,1227,973],{"class":188},[123,1229,844],{"class":821},[123,1231,978],{"class":133},[123,1233,312],{"class":188},[123,1235,1236,1238,1240,1242,1244,1246,1249,1251,1254],{"class":125,"line":272},[123,1237,331],{"class":243},[123,1239,237],{"class":184},[123,1241,951],{"class":184},[123,1243,893],{"class":133},[123,1245,822],{"class":821},[123,1247,1248],{"class":188},"region",[123,1250,844],{"class":821},[123,1252,1253],{"class":133}," detail\"",[123,1255,312],{"class":188},[123,1257,1258],{"class":125,"line":283},[123,1259,357],{"class":188},[10,1261,1262,1263,18],{},"Threading keeps a busy channel readable and gives the report a single place for questions. The per-group generation pattern is in ",[14,1264,1266],{"href":1265},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","Generate one Excel report per region in a loop",[108,1268,1270],{"id":1269},"handle-rate-limits-and-failures","Handle rate limits and failures",[10,1272,1273],{},"Slack rate-limits and occasionally returns transient errors, so a scheduled job needs a retry:",[113,1275,1277],{"className":169,"code":1276,"language":171,"meta":118,"style":118},"from slack_sdk import WebClient\nfrom slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler\n\nclient = WebClient(token=os.environ[\"SLACK_BOT_TOKEN\"])\nclient.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=3))\n",[120,1278,1279,1289,1301,1305,1323],{"__ignoreMap":118},[123,1280,1281,1283,1285,1287],{"class":125,"line":126},[123,1282,202],{"class":184},[123,1284,205],{"class":188},[123,1286,185],{"class":184},[123,1288,210],{"class":188},[123,1290,1291,1293,1296,1298],{"class":125,"line":181},[123,1292,202],{"class":184},[123,1294,1295],{"class":188}," slack_sdk.http_retry.builtin_handlers ",[123,1297,185],{"class":184},[123,1299,1300],{"class":188}," RateLimitErrorRetryHandler\n",[123,1302,1303],{"class":125,"line":192},[123,1304,196],{"emptyLinePlaceholder":195},[123,1306,1307,1309,1311,1313,1315,1317,1319,1321],{"class":125,"line":199},[123,1308,234],{"class":188},[123,1310,237],{"class":184},[123,1312,240],{"class":188},[123,1314,244],{"class":243},[123,1316,237],{"class":184},[123,1318,249],{"class":188},[123,1320,252],{"class":133},[123,1322,255],{"class":188},[123,1324,1325,1328,1331,1333,1336],{"class":125,"line":213},[123,1326,1327],{"class":188},"client.retry_handlers.append(RateLimitErrorRetryHandler(",[123,1329,1330],{"class":243},"max_retry_count",[123,1332,237],{"class":184},[123,1334,1335],{"class":363},"3",[123,1337,1338],{"class":188},"))\n",[10,1340,1341,1342,1345],{},"The SDK's handler reads the ",[120,1343,1344],{},"Retry-After"," header and waits the interval Slack asks for, which is better behaved than a fixed backoff. Wrap the whole delivery so a Slack outage does not lose a report that was successfully generated — write the file to storage first, then post; if the post fails, the report still exists and the retry is cheap.",[108,1347,1349],{"id":1348},"keep-the-token-safe","Keep the token safe",[10,1351,1352],{},"A bot token grants everything its scopes allow, to anyone holding it:",[113,1354,1356],{"className":169,"code":1355,"language":171,"meta":118,"style":118},"import os\n\ntoken = os.environ.get(\"SLACK_BOT_TOKEN\")\nif not token:\n    raise SystemExit(\"SLACK_BOT_TOKEN is not set\")\n",[120,1357,1358,1364,1368,1382,1392],{"__ignoreMap":118},[123,1359,1360,1362],{"class":125,"line":126},[123,1361,185],{"class":184},[123,1363,189],{"class":188},[123,1365,1366],{"class":125,"line":181},[123,1367,196],{"emptyLinePlaceholder":195},[123,1369,1370,1373,1375,1378,1380],{"class":125,"line":192},[123,1371,1372],{"class":188},"token ",[123,1374,237],{"class":184},[123,1376,1377],{"class":188}," os.environ.get(",[123,1379,252],{"class":133},[123,1381,589],{"class":188},[123,1383,1384,1386,1389],{"class":125,"line":199},[123,1385,744],{"class":184},[123,1387,1388],{"class":184}," not",[123,1390,1391],{"class":188}," token:\n",[123,1393,1394,1397,1400,1402,1405],{"class":125,"line":213},[123,1395,1396],{"class":184},"    raise",[123,1398,1399],{"class":363}," SystemExit",[123,1401,367],{"class":188},[123,1403,1404],{"class":133},"\"SLACK_BOT_TOKEN is not set\"",[123,1406,589],{"class":188},[10,1408,1409],{},"Read it from the environment, keep it out of logs — never interpolate it into a message or an error string — and rotate it on the same cadence as any other credential. If the job runs in CI, use the platform's secret store rather than a repository variable.",[108,1411,1413],{"id":1412},"post-a-link-instead-of-a-file-when-the-workbook-is-large","Post a link instead of a file when the workbook is large",[10,1415,1416],{},"Slack enforces a per-file size limit that depends on the plan, and a large workbook is unpleasant to download in a chat client anyway. Upload it to storage and post the link:",[113,1418,1420],{"className":169,"code":1419,"language":171,"meta":118,"style":118},"url = upload_to_s3(buffer.getvalue(), key=f\"reports\u002Fweekly_{run_date:%Y-%m-%d}.xlsx\")\nclient.chat_postMessage(\n    channel=channel_id,\n    text=f\"{summary_text(df, previous_total)}\\n\u003C{url}|Download the full workbook>\",\n)\n",[120,1421,1422,1464,1469,1477,1510],{"__ignoreMap":118},[123,1423,1424,1427,1429,1432,1435,1437,1439,1442,1444,1446,1448,1450,1452,1454,1456,1458,1460,1462],{"class":125,"line":126},[123,1425,1426],{"class":188},"url ",[123,1428,237],{"class":184},[123,1430,1431],{"class":188}," upload_to_s3(buffer.getvalue(), ",[123,1433,1434],{"class":243},"key",[123,1436,237],{"class":184},[123,1438,951],{"class":184},[123,1440,1441],{"class":133},"\"reports\u002Fweekly_",[123,1443,822],{"class":821},[123,1445,825],{"class":188},[123,1447,828],{"class":184},[123,1449,841],{"class":188},[123,1451,965],{"class":184},[123,1453,968],{"class":188},[123,1455,965],{"class":184},[123,1457,973],{"class":188},[123,1459,844],{"class":821},[123,1461,978],{"class":133},[123,1463,589],{"class":188},[123,1465,1466],{"class":125,"line":181},[123,1467,1468],{"class":188},"client.chat_postMessage(\n",[123,1470,1471,1473,1475],{"class":125,"line":192},[123,1472,612],{"class":243},[123,1474,237],{"class":184},[123,1476,934],{"class":188},[123,1478,1479,1482,1484,1486,1488,1490,1493,1496,1498,1500,1503,1505,1508],{"class":125,"line":199},[123,1480,1481],{"class":243},"    text",[123,1483,237],{"class":184},[123,1485,951],{"class":184},[123,1487,893],{"class":133},[123,1489,822],{"class":821},[123,1491,1492],{"class":188},"summary_text(df, previous_total)",[123,1494,1495],{"class":821},"}\\n",[123,1497,793],{"class":133},[123,1499,822],{"class":821},[123,1501,1502],{"class":188},"url",[123,1504,844],{"class":821},[123,1506,1507],{"class":133},"|Download the full workbook>\"",[123,1509,312],{"class":188},[123,1511,1512],{"class":125,"line":213},[123,1513,589],{"class":188},[10,1515,1516,1517,18],{},"Use a signed, time-limited URL so the link expires — the details are in ",[14,1518,1520],{"href":1519},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002F","Upload an Excel report to Amazon S3 with boto3",[108,1522,1524],{"id":1523},"put-the-delivery-behind-one-function","Put the delivery behind one function",[10,1526,1527],{},"Slack is one destination among several, and a reporting job is easier to change when the delivery step has a single shape regardless of where the file goes:",[113,1529,1531],{"className":169,"code":1530,"language":171,"meta":118,"style":118},"\"\"\"delivery.py — one interface, several destinations.\"\"\"\nimport os\nfrom dataclasses import dataclass\n\nfrom slack_sdk import WebClient\n\n@dataclass\nclass Report:\n    filename: str\n    payload: bytes\n    summary: str\n\ndef deliver_to_slack(report: Report, channel: str) -> str:\n    client = WebClient(token=os.environ[\"SLACK_BOT_TOKEN\"])\n    response = client.files_upload_v2(\n        channel=channel,\n        file=report.payload,\n        filename=report.filename,\n        initial_comment=report.summary,\n    )\n    return response[\"file\"][\"permalink\"]\n",[120,1532,1533,1538,1544,1556,1560,1570,1574,1579,1589,1597,1605,1612,1616,1634,1653,1661,1670,1679,1688,1697,1702],{"__ignoreMap":118},[123,1534,1535],{"class":125,"line":126},[123,1536,1537],{"class":133},"\"\"\"delivery.py — one interface, several destinations.\"\"\"\n",[123,1539,1540,1542],{"class":125,"line":181},[123,1541,185],{"class":184},[123,1543,189],{"class":188},[123,1545,1546,1548,1551,1553],{"class":125,"line":192},[123,1547,202],{"class":184},[123,1549,1550],{"class":188}," dataclasses ",[123,1552,185],{"class":184},[123,1554,1555],{"class":188}," dataclass\n",[123,1557,1558],{"class":125,"line":199},[123,1559,196],{"emptyLinePlaceholder":195},[123,1561,1562,1564,1566,1568],{"class":125,"line":213},[123,1563,202],{"class":184},[123,1565,205],{"class":188},[123,1567,185],{"class":184},[123,1569,210],{"class":188},[123,1571,1572],{"class":125,"line":226},[123,1573,196],{"emptyLinePlaceholder":195},[123,1575,1576],{"class":125,"line":231},[123,1577,1578],{"class":686},"@dataclass\n",[123,1580,1581,1584,1587],{"class":125,"line":258},[123,1582,1583],{"class":184},"class",[123,1585,1586],{"class":129}," Report",[123,1588,269],{"class":188},[123,1590,1591,1594],{"class":125,"line":263},[123,1592,1593],{"class":188},"    filename: ",[123,1595,1596],{"class":363},"str\n",[123,1598,1599,1602],{"class":125,"line":272},[123,1600,1601],{"class":188},"    payload: ",[123,1603,1604],{"class":363},"bytes\n",[123,1606,1607,1610],{"class":125,"line":283},[123,1608,1609],{"class":188},"    summary: ",[123,1611,1596],{"class":363},[123,1613,1614],{"class":125,"line":301},[123,1615,196],{"emptyLinePlaceholder":195},[123,1617,1618,1620,1623,1626,1628,1630,1632],{"class":125,"line":315},[123,1619,683],{"class":184},[123,1621,1622],{"class":686}," deliver_to_slack",[123,1624,1625],{"class":188},"(report: Report, channel: ",[123,1627,699],{"class":363},[123,1629,696],{"class":188},[123,1631,699],{"class":363},[123,1633,269],{"class":188},[123,1635,1636,1639,1641,1643,1645,1647,1649,1651],{"class":125,"line":328},[123,1637,1638],{"class":188},"    client ",[123,1640,237],{"class":184},[123,1642,240],{"class":188},[123,1644,244],{"class":243},[123,1646,237],{"class":184},[123,1648,249],{"class":188},[123,1650,252],{"class":133},[123,1652,255],{"class":188},[123,1654,1655,1657,1659],{"class":125,"line":341},[123,1656,275],{"class":188},[123,1658,237],{"class":184},[123,1660,280],{"class":188},[123,1662,1663,1665,1667],{"class":125,"line":354},[123,1664,286],{"class":243},[123,1666,237],{"class":184},[123,1668,1669],{"class":188},"channel,\n",[123,1671,1672,1674,1676],{"class":125,"line":360},[123,1673,304],{"class":243},[123,1675,237],{"class":184},[123,1677,1678],{"class":188},"report.payload,\n",[123,1680,1681,1683,1685],{"class":125,"line":387},[123,1682,318],{"class":243},[123,1684,237],{"class":184},[123,1686,1687],{"class":188},"report.filename,\n",[123,1689,1690,1692,1694],{"class":125,"line":402},[123,1691,344],{"class":243},[123,1693,237],{"class":184},[123,1695,1696],{"class":188},"report.summary,\n",[123,1698,1700],{"class":125,"line":1699},20,[123,1701,357],{"class":188},[123,1703,1705,1707,1710,1712,1714,1716],{"class":125,"line":1704},21,[123,1706,807],{"class":184},[123,1708,1709],{"class":188}," response[",[123,1711,376],{"class":133},[123,1713,379],{"class":188},[123,1715,382],{"class":133},[123,1717,772],{"class":188},[10,1719,1720],{},"With the report reduced to bytes plus a summary, adding an email or an S3 destination is another function of the same shape, and the job's main flow reads as generate, validate, deliver. That structure also makes the delivery step trivial to stub in a test, so a change to the report's contents can be tested without posting to a real channel.",[20,1722,29,1727,29,1730,29,1733,29,1736,29,1739,29,1745,29,1750,29,1753,29,1760,29,1765,29,1768,29,1771,29,1775,29,1778,29,1782,29,1788,29,1790,29,1794,29,1796,29,1800,29,1804,29,1809,29,1813,29,1818],{"viewBox":1723,"role":23,"ariaLabelledBy":1724,"xmlns":27,"style":28},"0 0 760 198",[1725,1726],"sl3-t","sl3-d",[31,1728,1729],{"id":1725},"One report object, several delivery functions",[35,1731,1732],{"id":1726},"The job produces a report of bytes plus a summary, and each destination — Slack, email, object storage — is a separate function taking the same object.",[39,1734],{"x":41,"y":41,"width":42,"height":1735,"fill":44},"198",[46,1737,1738],{"x":48,"y":49,"style":50},"Generate once, deliver anywhere",[39,1740],{"x":1741,"y":1742,"width":1743,"height":57,"rx":1744,"fill":59,"stroke":60},"34","70","184","12",[46,1746,1749],{"x":1056,"y":1747,"style":1748},"100","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Report",[46,1751,1752],{"x":1056,"y":74,"style":105},"bytes + summary",[125,1754],{"x1":1755,"y1":1756,"x2":1757,"y2":57,"stroke":1758,"style":1759},"218","88","262","var(--teal,#0f9488)","stroke-width:2px",[1761,1762],"polygon",{"points":1763,"fill":1764},"262,66 255,77 250,69","#0f766e",[125,1766],{"x1":1755,"y1":1767,"x2":1757,"y2":1767,"stroke":1758,"style":1759},"103",[1761,1769],{"points":1770,"fill":1764},"262,103 252,98 252,108",[125,1772],{"x1":1755,"y1":1773,"x2":1757,"y2":1774,"stroke":1758,"style":1759},"118","140",[1761,1776],{"points":1777,"fill":1764},"262,140 250,137 255,129",[39,1779],{"x":1780,"y":1060,"width":1781,"height":55,"rx":58,"fill":76,"stroke":60},"266","220",[46,1783,1787],{"x":1784,"y":1785,"style":1786},"376","71","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","deliver_to_slack()",[39,1789],{"x":1780,"y":69,"width":1781,"height":55,"rx":58,"fill":76,"stroke":60},[46,1791,1793],{"x":1784,"y":1792,"style":1786},"125","deliver_by_email()",[39,1795],{"x":1780,"y":94,"width":1781,"height":1038,"rx":58,"fill":76,"stroke":60},[46,1797,1799],{"x":1784,"y":1798,"style":1786},"176","deliver_to_s3()",[39,1801],{"x":1802,"y":1742,"width":1027,"height":57,"rx":1744,"fill":1803,"stroke":60},"530","#fdefd8",[46,1805,1808],{"x":1806,"y":1747,"style":1807},"628","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","stub in tests",[46,1810,1812],{"x":1806,"y":74,"style":1811},"font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","no real channel needed",[125,1814],{"x1":1815,"y1":1767,"x2":1816,"y2":1767,"stroke":1817,"style":1759},"486","526","var(--gold,#b4740a)",[1761,1819],{"points":1820,"fill":1821},"526,103 516,98 516,108","#7a4e06",[108,1823,1825],{"id":1824},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1827,1828,1829,1837,1843,1858,1864],"ul",{},[1830,1831,1832,1836],"li",{},[422,1833,1834,18],{},[120,1835,161],{}," Invite the bot; scopes are not membership.",[1830,1838,1839,1842],{},[422,1840,1841],{},"Channel names instead of ids."," A rename breaks the job silently.",[1830,1844,1845,1850,1851,1854,1855,18],{},[422,1846,1847,1848,18],{},"Markdown that is not ",[120,1849,1000],{}," Slack uses ",[120,1852,1853],{},"*bold*",", not ",[120,1856,1857],{},"**bold**",[1830,1859,1860,1863],{},[422,1861,1862],{},"Tokens in logs."," Never include the token in an error message or a debug print.",[1830,1865,1866,1869],{},[422,1867,1868],{},"Posting before the file exists."," Generate and store first, then announce — a message linking to a file that failed to upload is worse than no message.",[108,1871,1873],{"id":1872},"performance-and-scale-notes","Performance and scale notes",[10,1875,1876,1877,18],{},"The upload is I\u002FO-bound and small: a typical report workbook posts in well under a second. What needs care is fan-out — posting to twenty channels sequentially runs into rate limits, so batch with the SDK's retry handler in place and accept that the whole delivery takes a minute rather than firing everything at once. If several teams need the same report, prefer one post to a shared channel plus links over twenty separate uploads; it is faster, cheaper and leaves one place for the discussion. For alerting on a failed run rather than delivering a report, a lightweight message is enough — see ",[14,1878,1880],{"href":1879},"\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",[108,1882,1884],{"id":1883},"conclusion","Conclusion",[10,1886,1887],{},"Posting a report to Slack is one API call, but the version worth shipping does three things: it computes the headline numbers and puts them in the message, it attaches the workbook for whoever wants the detail, and it fails safely — storing the file before announcing it, retrying on rate limits, and keeping the token in the environment. Use channel ids, invite the bot, and switch to a signed link once the workbook outgrows a chat attachment.",[108,1889,1891],{"id":1890},"frequently-asked-questions","Frequently asked questions",[10,1893,1894,1897,1899,1900,1902],{},[422,1895,1896],{},"Which Slack scopes does the bot need?",[120,1898,149],{}," to upload, and ",[120,1901,153],{}," to post the accompanying message. Add the bot to the target channel as well — scopes alone do not grant access to a private channel.",[10,1904,1905,1908,1909,1911],{},[422,1906,1907],{},"Why does my upload return not_in_channel?","\nThe bot is not a member of the channel. Invite it with ",[120,1910,157],{},", or post to a channel it already belongs to. This is separate from the OAuth scopes.",[10,1913,1914,1917],{},[422,1915,1916],{},"Should I use a webhook or the API?","\nA webhook can post messages but cannot upload files. Use a bot token with the Web API for anything that attaches a workbook.",[10,1919,1920,1923,1924,1927],{},[422,1921,1922],{},"Can I upload from memory rather than a file?","\nYes. The upload call accepts a bytes object or a file-like object, so a workbook built in ",[120,1925,1926],{},"BytesIO"," never has to touch disk.",[10,1929,1930,1933],{},[422,1931,1932],{},"What happens if the report is large?","\nSlack enforces a per-file size limit that varies by plan. For a large workbook, upload it to object storage and post a link instead of the file itself.",[108,1935,1937],{"id":1936},"related","Related",[1827,1939,1940,1949,1954,1961,1966],{},[1830,1941,1942,1945,1946,1948],{},[422,1943,1944],{},"Up:"," ",[14,1947,17],{"href":16}," — the other destinations a finished workbook reaches.",[1830,1950,1951,1953],{},[14,1952,1520],{"href":1519}," — where a large workbook should live before you link to it.",[1830,1955,1956,1960],{},[14,1957,1959],{"href":1958},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002F","Email an Excel report with an HTML summary body"," — the same summary-plus-attachment idea over email.",[1830,1962,1963,1965],{},[14,1964,666],{"href":665}," — producing the bytes this upload sends.",[1830,1967,1968,1972],{},[14,1969,1971],{"href":1970},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel report before sending it"," — the check that belongs immediately before the post.",[1974,1975,1976],"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 .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}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":118,"searchDepth":181,"depth":181,"links":1978},[1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992],{"id":110,"depth":181,"text":111},{"id":165,"depth":181,"text":166},{"id":435,"depth":181,"text":436},{"id":669,"depth":181,"text":670},{"id":1087,"depth":181,"text":1088},{"id":1269,"depth":181,"text":1270},{"id":1348,"depth":181,"text":1349},{"id":1412,"depth":181,"text":1413},{"id":1523,"depth":181,"text":1524},{"id":1824,"depth":181,"text":1825},{"id":1872,"depth":181,"text":1873},{"id":1883,"depth":181,"text":1884},{"id":1890,"depth":181,"text":1891},{"id":1936,"depth":181,"text":1937},"2026-08-27","Deliver a workbook to Slack: bot token and scopes, the file upload API, a summary message with the numbers, threading, retries and keeping the token out of the code.","md",[1997,1999,2001,2003,2005],{"q":1896,"a":1998},"files:write to upload, and chat:write to post the accompanying message. Add the bot to the target channel as well — scopes alone do not grant access to a private channel.",{"q":1907,"a":2000},"The bot is not a member of the channel. Invite it with \u002Finvite @yourbot, or post to a channel it already belongs to. This is separate from the OAuth scopes.",{"q":1916,"a":2002},"A webhook can post messages but cannot upload files. Use a bot token with the Web API for anything that attaches a workbook.",{"q":1922,"a":2004},"Yes. The upload call accepts a bytes object or a file-like object, so a workbook built in BytesIO never has to touch disk.",{"q":1932,"a":2006},"Slack enforces a per-file size limit that varies by plan. For a large workbook, upload it to object storage and post a link instead of the file itself.",{},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python",{"title":2010,"description":2011},"Post an Excel Report to Slack in Python","Upload an xlsx to Slack from Python with the SDK: bot scopes, files_upload_v2, an in-message summary, threaded replies, rate-limit retries and secure token handling.","post-an-excel-report-to-a-slack-channel-with-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python\u002Findex","how-to","GSvqeb66FDkei232X0INZx2M_Dg2Vb6MSR8h-YiI-zg",[2017,2020],{"title":17,"path":2018,"stem":2019,"children":-1},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Findex",{"title":2021,"path":2022,"stem":2023,"children":-1},"Post an Excel Report to Microsoft Teams with Python","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-microsoft-teams-with-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-microsoft-teams-with-python\u002Findex",1788710158998]