[{"data":1,"prerenderedAt":1744},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports":1711,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports":1720,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports":1725,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports":1726},{"id":4,"title":5,"body":6,"dateModified":1678,"datePublished":1678,"description":1679,"extension":1680,"faq":1681,"keyword":1690,"kind":1691,"meta":1692,"navigation":748,"path":1700,"seo":1701,"slug":1704,"stem":1705,"topics":1706,"type":1709,"__hash__":1710},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports\u002Findex.md","Choose a Scheduler for Python Excel Reports",{"type":7,"value":8,"toc":1666},"minimark",[9,19,24,27,56,264,268,275,285,291,297,303,307,616,627,631,668,676,680,683,1319,1419,1427,1551,1555,1558,1562,1594,1598,1601,1605,1611,1617,1623,1629,1633,1662],[10,11,12,13,18],"p",{},"The script works; now it has to run every Monday at 07:00 without anyone pressing a button. There are a dozen ways to do that, from a one-line crontab entry to a workflow platform, and the right one depends less on the report than on where it has to run, what happens when a run is missed, and who needs to know when it fails. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",", compares the options on those questions, recommends defaults for common situations, and shows how to write the report so that moving it from one scheduler to another later is a configuration change, not a rewrite.",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[10,25,26],{},"The comparison needs no installation. The example entry point at the end uses only pandas and the standard library:",[28,29,34],"pre",{"className":30,"code":31,"language":32,"meta":33,"style":33},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[35,36,37],"code",{"__ignoreMap":33},[38,39,42,46,50,53],"span",{"class":40,"line":41},"line",1,[38,43,45],{"class":44},"sMTad","pip",[38,47,49],{"class":48},"srMev"," install",[38,51,52],{"class":48}," pandas",[38,54,55],{"class":48}," openpyxl\n",[57,58,66,67,66,71,66,75,66,83,66,93,66,104,66,110,66,114,66,118,66,121,66,126,66,135,66,140,66,147,66,154,66,159,66,164,66,167,66,170,66,174,66,177,66,180,66,186,66,189,66,192,66,195,66,198,66,202,66,205,66,210,66,217,66,222,66,226,66,230,66,232,66,235,66,239,66,242,66,247,66,254,66,258,66,261],"svg",{"viewBox":59,"role":60,"ariaLabelledBy":61,"xmlns":64,"style":65},"4 0 752 290","img",[62,63],"sch-where-t","sch-where-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[68,69,70],"title",{"id":62},"Start with where the report must run",[72,73,74],"desc",{"id":63},"A decision diagram. A report on one person's computer uses the operating system's scheduler. A report on a company server uses cron or systemd timers. A report inside an existing application uses its own scheduler such as Celery beat. A report in the cloud uses the provider's scheduled job service. Many interdependent pipelines call for an orchestrator.",[76,77],"rect",{"x":78,"y":79,"width":80,"height":81,"fill":82},"4","0","752","290","#ffffff",[76,84],{"x":85,"y":86,"width":87,"height":88,"rx":89,"fill":90,"stroke":91,"style":92},"227.5","16.0","304.9","75.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[94,95,97],"g",{"transform":96},"translate(368.0,29.0) scale(1.000)",[98,99],"path",{"d":100,"fill":101,"stroke":102,"style":103},"M4 4h16v6H4z M4 14h16v6H4z M8 7h.01 M8 17h.01","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[105,106,109],"text",{"x":107,"y":88,"style":108},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Where does the report have to run?",[40,111],{"x1":107,"y1":112,"x2":107,"y2":113,"stroke":91,"style":92},"91.5","111.5",[40,115],{"x1":116,"y1":113,"x2":117,"y2":113,"stroke":91,"style":92},"104.0","656.0",[40,119],{"x1":116,"y1":113,"x2":116,"y2":120,"stroke":91,"style":92},"157.5",[122,123],"polygon",{"points":124,"fill":125},"104.0,161.5 99.0,152.5 109.0,152.5","#5b5cf0",[76,127],{"x":128,"y":129,"width":130,"height":131,"rx":132,"fill":82,"stroke":133,"style":134},"38.4","121.5","131.2","21.0","10","var(--line,#cdd5e6)","stroke-width:1.2px",[105,136,139],{"x":116,"y":137,"style":138},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","a laptop or desktop",[76,141],{"x":142,"y":143,"width":144,"height":145,"rx":89,"fill":146,"stroke":133,"style":92},"20.0","163.5","168.0","110.5","#e7ebef",[94,148,150],{"transform":149},"translate(92.0,185.2) scale(1.000)",[98,151],{"d":152,"fill":101,"stroke":153,"style":103},"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21c0-4 3.6-7 8-7s8 3 8 7","var(--text,#172033)",[105,155,158],{"x":116,"y":156,"style":157},"231.8","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","OS scheduler",[105,160,163],{"x":116,"y":161,"style":162},"251.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","launchd, Task Scheduler",[40,165],{"x1":166,"y1":113,"x2":166,"y2":120,"stroke":91,"style":92},"288.0",[122,168],{"points":169,"fill":125},"288.0,161.5 283.0,152.5 293.0,152.5",[76,171],{"x":172,"y":129,"width":173,"height":131,"rx":132,"fill":82,"stroke":133,"style":134},"237.4","101.1",[105,175,176],{"x":166,"y":137,"style":138},"a Linux server",[76,178],{"x":179,"y":143,"width":144,"height":145,"rx":89,"fill":90,"stroke":91,"style":92},"204.0",[94,181,183],{"transform":182},"translate(276.0,185.2) scale(1.000)",[98,184],{"d":185,"fill":101,"stroke":102,"style":103},"M4 5h16v14H4z M7 9l3 3-3 3 M12 15h5",[105,187,188],{"x":166,"y":156,"style":108},"systemd timer",[105,190,191],{"x":166,"y":161,"style":162},"or cron",[40,193],{"x1":194,"y1":113,"x2":194,"y2":120,"stroke":91,"style":92},"472.0",[122,196],{"points":197,"fill":125},"472.0,161.5 467.0,152.5 477.0,152.5",[76,199],{"x":200,"y":129,"width":201,"height":131,"rx":132,"fill":82,"stroke":133,"style":134},"423.9","96.1",[105,203,204],{"x":194,"y":137,"style":138},"inside an app",[76,206],{"x":207,"y":143,"width":144,"height":145,"rx":89,"fill":208,"stroke":209,"style":92},"388.0","#d9f4f1","var(--teal,#0f9488)",[94,211,213],{"transform":212},"translate(460.0,176.5) scale(1.000)",[98,214],{"d":215,"fill":101,"stroke":216,"style":103},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--teal-ink,#0b6157)",[105,218,221],{"x":194,"y":219,"style":220},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","App scheduler",[105,223,225],{"x":194,"y":224,"style":162},"242.5","Celery beat,",[105,227,229],{"x":194,"y":228,"style":162},"260.0","APScheduler",[40,231],{"x1":117,"y1":113,"x2":117,"y2":120,"stroke":91,"style":92},[122,233],{"points":234,"fill":125},"656.0,161.5 651.0,152.5 661.0,152.5",[76,236],{"x":237,"y":129,"width":238,"height":131,"rx":132,"fill":82,"stroke":133,"style":134},"612.7","86.6",[105,240,241],{"x":117,"y":137,"style":138},"in the cloud",[76,243],{"x":244,"y":143,"width":144,"height":145,"rx":89,"fill":245,"stroke":246,"style":92},"572.0","#fdefd8","var(--gold,#b4740a)",[94,248,250],{"transform":249},"translate(644.0,176.5) scale(1.000)",[98,251],{"d":252,"fill":101,"stroke":253,"style":103},"M7 18h10a4 4 0 0 0 .6-8A6 6 0 0 0 6.1 11.4 3.4 3.4 0 0 0 7 18z","var(--gold-ink,#7a4e06)",[105,255,257],{"x":117,"y":219,"style":256},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Cloud job",[105,259,260],{"x":117,"y":224,"style":162},"Cloud Run, Lambda,",[105,262,263],{"x":117,"y":228,"style":162},"Functions",[20,265,267],{"id":266},"the-questions-that-decide-it","The questions that decide it",[10,269,270,274],{},[271,272,273],"strong",{},"Where must it run?"," A report that reads files from someone's laptop or drives Excel through COM on a Windows desktop needs that machine's own scheduler. One that reads a database and writes to a share can run anywhere, including a cloud service with no machine to maintain.",[10,276,277,280,281,284],{},[271,278,279],{},"What happens when a run is missed?"," Machines sleep, servers reboot, clouds have incidents. Cron simply skips a run whose time passed while the machine was off. launchd, systemd timers with ",[35,282,283],{},"Persistent=true",", and Windows Task Scheduler with \"run as soon as possible after a missed start\" catch up once. Orchestrators record the missed run and can backfill it.",[10,286,287,290],{},[271,288,289],{},"What happens when a run fails?"," A retry with backoff turns most failures — a database restart, a network blip — into non-events. Cron and Task Scheduler do not retry by themselves; Celery, Prefect, Airflow, Cloud Run jobs, Kubernetes Jobs and Azure's retry policies do.",[10,292,293,296],{},[271,294,295],{},"Who finds out, and where is the history?"," A failed report nobody hears about is worse than no report. Orchestrators and cloud services keep a history of every run with logs; cron keeps what the script writes to a log file.",[10,298,299,302],{},[271,300,301],{},"How much is there to run and maintain?"," A crontab line costs nothing. An Airflow installation is a platform with a database, a scheduler and workers. The overhead is worth it when there are many pipelines with dependencies between them — rarely for three weekly reports.",[20,304,306],{"id":305},"how-the-options-compare","How the options compare",[57,308,66,313,66,316,66,319,66,322,66,327,66,331,66,337,66,340,66,344,66,347,66,351,66,354,66,358,66,362,66,368,66,370,66,378,66,380,66,385,66,387,66,391,66,393,66,396,66,399,66,402,66,404,66,408,66,410,66,413,66,415,66,418,66,420,66,423,66,426,66,430,66,432,66,439,66,441,66,448,66,450,66,454,66,456,66,458,66,461,66,464,66,466,66,470,66,472,66,477,66,479,66,484,66,486,66,489,66,492,66,496,66,498,66,503,66,505,66,510,66,512,66,516,66,518,66,522,66,526,66,529,66,533,66,535,66,540,66,542,66,547,66,549,66,553,66,555,66,557,66,559,66,563,66,565,66,569,66,571,66,576,66,578,66,581,66,583,66,586,66,589,66,593,66,595,66,599,66,601,66,606,66,608,66,611,66,613],{"viewBox":309,"role":60,"ariaLabelledBy":310,"xmlns":64,"style":65},"4 0 752 344",[311,312],"sch-matrix-t","sch-matrix-d",[68,314,315],{"id":311},"Schedulers compared on the questions that matter for reports",[72,317,318],{"id":312},"A comparison table of scheduling options against catch-up after missed runs, automatic retries, run history, and operational effort. OS schedulers are low effort with little history; app schedulers and orchestrators retry and record runs; cloud jobs retry and record with no servers to run.",[76,320],{"x":78,"y":79,"width":80,"height":321,"fill":82},"344",[76,323],{"x":142,"y":86,"width":324,"height":325,"rx":79,"fill":82,"stroke":133,"style":326},"191.1","40.0","stroke-width:1px",[76,328],{"x":329,"y":86,"width":330,"height":325,"rx":79,"fill":90,"stroke":91,"style":134},"211.1","132.2",[105,332,336],{"x":333,"y":334,"style":335},"277.2","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Catches up",[76,338],{"x":339,"y":86,"width":330,"height":325,"rx":79,"fill":90,"stroke":91,"style":134},"343.3",[105,341,343],{"x":342,"y":334,"style":335},"409.4","Retries",[76,345],{"x":346,"y":86,"width":330,"height":325,"rx":79,"fill":90,"stroke":91,"style":134},"475.6",[105,348,350],{"x":349,"y":334,"style":335},"541.7","Run history",[76,352],{"x":353,"y":86,"width":330,"height":325,"rx":79,"fill":90,"stroke":91,"style":134},"607.8",[105,355,357],{"x":356,"y":334,"style":335},"673.9","Effort",[76,359],{"x":142,"y":360,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"56.0","34.0",[105,363,367],{"x":364,"y":365,"style":366},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","cron",[76,369],{"x":329,"y":360,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,371,373],{"transform":372},"translate(269.2,65.0) scale(0.667)",[98,374],{"d":375,"fill":101,"stroke":376,"style":377},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[76,379],{"x":339,"y":360,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,381,383],{"transform":382},"translate(401.4,65.0) scale(0.667)",[98,384],{"d":375,"fill":101,"stroke":376,"style":377},[76,386],{"x":346,"y":360,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,388,390],{"x":349,"y":389,"style":162},"77.0","log files",[76,392],{"x":353,"y":360,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,394,395],{"x":356,"y":389,"style":162},"minimal",[76,397],{"x":142,"y":398,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"90.0",[105,400,188],{"x":364,"y":401,"style":366},"112.0",[76,403],{"x":329,"y":398,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,405,407],{"x":333,"y":406,"style":162},"111.0","Persistent=",[76,409],{"x":339,"y":398,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,411,412],{"x":342,"y":406,"style":162},"Restart=",[76,414],{"x":346,"y":398,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,416,417],{"x":349,"y":406,"style":162},"journal",[76,419],{"x":353,"y":398,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,421,422],{"x":356,"y":406,"style":162},"low",[76,424],{"x":142,"y":425,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"124.0",[105,427,429],{"x":364,"y":428,"style":366},"146.0","Task Scheduler \u002F launchd",[76,431],{"x":329,"y":425,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,433,435],{"transform":434},"translate(268.2,132.0) scale(0.750)",[98,436],{"d":437,"fill":101,"stroke":209,"style":438},"M5 12.5l4.5 4.5L19 7","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[76,440],{"x":339,"y":425,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,442,444],{"transform":443},"translate(400.9,132.5) scale(0.708)",[98,445],{"d":446,"fill":101,"stroke":246,"style":447},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","stroke-width:2.54px;stroke-linecap:round;stroke-linejoin:round",[76,449],{"x":346,"y":425,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,451,453],{"x":349,"y":452,"style":162},"145.0","basic",[76,455],{"x":353,"y":425,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,457,422],{"x":356,"y":452,"style":162},[76,459],{"x":142,"y":460,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"158.0",[105,462,229],{"x":364,"y":463,"style":366},"180.0",[76,465],{"x":329,"y":460,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,467,469],{"x":333,"y":468,"style":162},"179.0","misfire grace",[76,471],{"x":339,"y":460,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,473,475],{"transform":474},"translate(401.4,167.0) scale(0.667)",[98,476],{"d":375,"fill":101,"stroke":376,"style":377},[76,478],{"x":346,"y":460,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,480,482],{"transform":481},"translate(533.7,167.0) scale(0.667)",[98,483],{"d":375,"fill":101,"stroke":376,"style":377},[76,485],{"x":353,"y":460,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,487,488],{"x":356,"y":468,"style":162},"low, in-process",[76,490],{"x":142,"y":491,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"192.0",[105,493,495],{"x":364,"y":494,"style":366},"214.0","Celery beat",[76,497],{"x":329,"y":491,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,499,501],{"transform":500},"translate(269.2,201.0) scale(0.667)",[98,502],{"d":375,"fill":101,"stroke":376,"style":377},[76,504],{"x":339,"y":491,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,506,508],{"transform":507},"translate(400.4,200.0) scale(0.750)",[98,509],{"d":437,"fill":101,"stroke":209,"style":438},[76,511],{"x":346,"y":491,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,513,515],{"x":349,"y":514,"style":162},"213.0","result backend",[76,517],{"x":353,"y":491,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,519,521],{"x":356,"y":520,"style":162},"207.0","medium, needs",[105,523,525],{"x":356,"y":524,"style":162},"220.0","broker",[76,527],{"x":142,"y":528,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"226.0",[105,530,532],{"x":364,"y":531,"style":366},"248.0","GitHub Actions",[76,534],{"x":329,"y":528,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,536,538],{"transform":537},"translate(269.2,235.0) scale(0.667)",[98,539],{"d":375,"fill":101,"stroke":376,"style":377},[76,541],{"x":339,"y":528,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,543,545],{"transform":544},"translate(401.4,235.0) scale(0.667)",[98,546],{"d":375,"fill":101,"stroke":376,"style":377},[76,548],{"x":346,"y":528,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,550,552],{"x":349,"y":551,"style":162},"247.0","run logs",[76,554],{"x":353,"y":528,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,556,422],{"x":356,"y":551,"style":162},[76,558],{"x":142,"y":228,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},[105,560,562],{"x":364,"y":561,"style":366},"282.0","Prefect \u002F Airflow",[76,564],{"x":329,"y":228,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,566,568],{"x":333,"y":567,"style":162},"281.0","backfill",[76,570],{"x":339,"y":228,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,572,574],{"transform":573},"translate(400.4,268.0) scale(0.750)",[98,575],{"d":437,"fill":101,"stroke":209,"style":438},[76,577],{"x":346,"y":228,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,579,580],{"x":349,"y":567,"style":162},"full UI",[76,582],{"x":353,"y":228,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,584,585],{"x":356,"y":567,"style":162},"medium to high",[76,587],{"x":142,"y":588,"width":324,"height":361,"rx":79,"fill":146,"stroke":133,"style":326},"294.0",[105,590,592],{"x":364,"y":591,"style":366},"316.0","Cloud jobs \u002F K8s CronJob",[76,594],{"x":329,"y":588,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,596,598],{"x":333,"y":597,"style":162},"315.0","deadline window",[76,600],{"x":339,"y":588,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[94,602,604],{"transform":603},"translate(400.4,302.0) scale(0.750)",[98,605],{"d":437,"fill":101,"stroke":209,"style":438},[76,607],{"x":346,"y":588,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,609,610],{"x":349,"y":597,"style":162},"logs and console",[76,612],{"x":353,"y":588,"width":330,"height":361,"rx":79,"fill":82,"stroke":133,"style":326},[105,614,615],{"x":356,"y":597,"style":162},"low to medium",[10,617,618,619,621,622,626],{},"A few notes behind the table. systemd timers are the best plain option on a Linux server: ",[35,620,283],{}," runs a missed job at boot, and the journal keeps output with timestamps — see ",[14,623,625],{"href":624},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers\u002F","Schedule an Excel Report with systemd Timers",". APScheduler runs inside a long-lived Python process and is convenient for a small service, but its schedule dies with the process. GitHub Actions' scheduled workflows are free for light use and handy for reports that pull from APIs, but they are documented as best-effort: runs can start late under load and scheduled workflows in inactive repositories are disabled. Cloud jobs — Cloud Run jobs, Azure Functions timers, Lambda with EventBridge, Kubernetes CronJobs — combine retries and history with no machine to patch, at the cost of packaging the report as a container or function.",[20,628,630],{"id":629},"recommended-defaults","Recommended defaults",[632,633,634,641,650,656,662],"ul",{},[635,636,637,640],"li",{},[271,638,639],{},"One analyst, reports from local files:"," the operating system's scheduler — launchd on macOS, Task Scheduler on Windows. Keep the files in a project folder and log to a file.",[635,642,643,646,647,649],{},[271,644,645],{},"A handful of reports on a Linux server:"," systemd timers with ",[35,648,283],{},", plus a heartbeat check that alerts when a report did not arrive.",[635,651,652,655],{},[271,653,654],{},"Reports inside a web application that already uses Celery:"," Celery beat, with exactly one beat process.",[635,657,658,661],{},[271,659,660],{},"A team on a cloud provider:"," that provider's scheduled container or function service; Cloud Run jobs and Kubernetes CronJobs handle long reports, functions short ones.",[635,663,664,667],{},[271,665,666],{},"Many pipelines with dependencies, backfills and SLAs:"," an orchestrator. Prefect if the team wants flows that are plain Python; Airflow if the organisation already runs it.",[10,669,670,671,675],{},"Whatever the choice, add monitoring that does not depend on the scheduler: ",[14,672,674],{"href":673},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run\u002F","Detect a Scheduled Report That Did Not Run"," covers the dead-man's-switch pattern that catches a scheduler which silently stopped.",[20,677,679],{"id":678},"keep-the-choice-reversible-one-entry-point","Keep the choice reversible: one entry point",[10,681,682],{},"Schedulers differ; what they run should not. A report written as a command-line entry point that takes its date as an argument, is safe to run twice, logs to standard output and exits non-zero on failure runs unchanged under every scheduler above:",[28,684,688],{"className":685,"code":686,"language":687,"meta":33,"style":33},"language-python shiki shiki-themes github-light github-dark-high-contrast","import argparse\nimport logging\nimport sys\nfrom datetime import date, timedelta\nfrom pathlib import Path\n\nimport pandas as pd\n\nlog = logging.getLogger(\"weekly_report\")\n\ndef monday_of(d: date) -> date:\n    return d - timedelta(days=d.weekday())\n\ndef run(report_date: date, out_dir: Path) -> Path:\n    out = out_dir \u002F f\"weekly_sales_{report_date:%Y-%m-%d}.xlsx\"\n    if out.exists():\n        log.info(\"already built: %s\", out)\n        return out\n    df = pd.DataFrame({\"Region\": [\"North\", \"South\"], \"Sales\": [482100, 391250]})\n    out_dir.mkdir(parents=True, exist_ok=True)\n    tmp = out.with_suffix(\".tmp.xlsx\")\n    df.to_excel(tmp, index=False)\n    tmp.replace(out)\n    log.info(\"built %s\", out)\n    return out\n\ndef main(argv=None) -> int:\n    p = argparse.ArgumentParser()\n    p.add_argument(\"--date\", type=date.fromisoformat, default=None,\n                   help=\"any day in the report week; defaults to this week\")\n    p.add_argument(\"--out\", type=Path, default=Path(\"reports\"))\n    args = p.parse_args(argv)\n    logging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\", stream=sys.stdout)\n    try:\n        run(monday_of(args.date or date.today()), args.out)\n        return 0\n    except Exception:\n        log.exception(\"report failed\")\n        return 1\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n","python",[35,689,690,700,708,716,730,743,750,764,769,787,792,805,829,834,845,895,904,922,931,977,1002,1018,1034,1040,1055,1062,1067,1092,1103,1132,1145,1175,1186,1233,1241,1253,1261,1272,1283,1291,1296,1313],{"__ignoreMap":33},[38,691,692,696],{"class":40,"line":41},[38,693,695],{"class":694},"s-kum","import",[38,697,699],{"class":698},"skGVy"," argparse\n",[38,701,703,705],{"class":40,"line":702},2,[38,704,695],{"class":694},[38,706,707],{"class":698}," logging\n",[38,709,711,713],{"class":40,"line":710},3,[38,712,695],{"class":694},[38,714,715],{"class":698}," sys\n",[38,717,719,722,725,727],{"class":40,"line":718},4,[38,720,721],{"class":694},"from",[38,723,724],{"class":698}," datetime ",[38,726,695],{"class":694},[38,728,729],{"class":698}," date, timedelta\n",[38,731,733,735,738,740],{"class":40,"line":732},5,[38,734,721],{"class":694},[38,736,737],{"class":698}," pathlib ",[38,739,695],{"class":694},[38,741,742],{"class":698}," Path\n",[38,744,746],{"class":40,"line":745},6,[38,747,749],{"emptyLinePlaceholder":748},true,"\n",[38,751,753,755,758,761],{"class":40,"line":752},7,[38,754,695],{"class":694},[38,756,757],{"class":698}," pandas ",[38,759,760],{"class":694},"as",[38,762,763],{"class":698}," pd\n",[38,765,767],{"class":40,"line":766},8,[38,768,749],{"emptyLinePlaceholder":748},[38,770,772,775,778,781,784],{"class":40,"line":771},9,[38,773,774],{"class":698},"log ",[38,776,777],{"class":694},"=",[38,779,780],{"class":698}," logging.getLogger(",[38,782,783],{"class":48},"\"weekly_report\"",[38,785,786],{"class":698},")\n",[38,788,790],{"class":40,"line":789},10,[38,791,749],{"emptyLinePlaceholder":748},[38,793,795,798,802],{"class":40,"line":794},11,[38,796,797],{"class":694},"def",[38,799,801],{"class":800},"s_Opv"," monday_of",[38,803,804],{"class":698},"(d: date) -> date:\n",[38,806,808,811,814,817,820,824,826],{"class":40,"line":807},12,[38,809,810],{"class":694},"    return",[38,812,813],{"class":698}," d ",[38,815,816],{"class":694},"-",[38,818,819],{"class":698}," timedelta(",[38,821,823],{"class":822},"sa561","days",[38,825,777],{"class":694},[38,827,828],{"class":698},"d.weekday())\n",[38,830,832],{"class":40,"line":831},13,[38,833,749],{"emptyLinePlaceholder":748},[38,835,837,839,842],{"class":40,"line":836},14,[38,838,797],{"class":694},[38,840,841],{"class":800}," run",[38,843,844],{"class":698},"(report_date: date, out_dir: Path) -> Path:\n",[38,846,848,851,853,856,859,862,865,869,872,875,878,881,884,886,889,892],{"class":40,"line":847},15,[38,849,850],{"class":698},"    out ",[38,852,777],{"class":694},[38,854,855],{"class":698}," out_dir ",[38,857,858],{"class":694},"\u002F",[38,860,861],{"class":694}," f",[38,863,864],{"class":48},"\"weekly_sales_",[38,866,868],{"class":867},"sSjpA","{",[38,870,871],{"class":698},"report_date:",[38,873,874],{"class":694},"%",[38,876,877],{"class":698},"Y",[38,879,880],{"class":694},"-%",[38,882,883],{"class":698},"m",[38,885,880],{"class":694},[38,887,888],{"class":698},"d",[38,890,891],{"class":867},"}",[38,893,894],{"class":48},".xlsx\"\n",[38,896,898,901],{"class":40,"line":897},16,[38,899,900],{"class":694},"    if",[38,902,903],{"class":698}," out.exists():\n",[38,905,907,910,913,916,919],{"class":40,"line":906},17,[38,908,909],{"class":698},"        log.info(",[38,911,912],{"class":48},"\"already built: ",[38,914,915],{"class":867},"%s",[38,917,918],{"class":48},"\"",[38,920,921],{"class":698},", out)\n",[38,923,925,928],{"class":40,"line":924},18,[38,926,927],{"class":694},"        return",[38,929,930],{"class":698}," out\n",[38,932,934,937,939,942,945,948,951,954,957,960,963,965,969,971,974],{"class":40,"line":933},19,[38,935,936],{"class":698},"    df ",[38,938,777],{"class":694},[38,940,941],{"class":698}," pd.DataFrame({",[38,943,944],{"class":48},"\"Region\"",[38,946,947],{"class":698},": [",[38,949,950],{"class":48},"\"North\"",[38,952,953],{"class":698},", ",[38,955,956],{"class":48},"\"South\"",[38,958,959],{"class":698},"], ",[38,961,962],{"class":48},"\"Sales\"",[38,964,947],{"class":698},[38,966,968],{"class":967},"sP0c6","482100",[38,970,953],{"class":698},[38,972,973],{"class":967},"391250",[38,975,976],{"class":698},"]})\n",[38,978,980,983,986,988,991,993,996,998,1000],{"class":40,"line":979},20,[38,981,982],{"class":698},"    out_dir.mkdir(",[38,984,985],{"class":822},"parents",[38,987,777],{"class":694},[38,989,990],{"class":967},"True",[38,992,953],{"class":698},[38,994,995],{"class":822},"exist_ok",[38,997,777],{"class":694},[38,999,990],{"class":967},[38,1001,786],{"class":698},[38,1003,1005,1008,1010,1013,1016],{"class":40,"line":1004},21,[38,1006,1007],{"class":698},"    tmp ",[38,1009,777],{"class":694},[38,1011,1012],{"class":698}," out.with_suffix(",[38,1014,1015],{"class":48},"\".tmp.xlsx\"",[38,1017,786],{"class":698},[38,1019,1021,1024,1027,1029,1032],{"class":40,"line":1020},22,[38,1022,1023],{"class":698},"    df.to_excel(tmp, ",[38,1025,1026],{"class":822},"index",[38,1028,777],{"class":694},[38,1030,1031],{"class":967},"False",[38,1033,786],{"class":698},[38,1035,1037],{"class":40,"line":1036},23,[38,1038,1039],{"class":698},"    tmp.replace(out)\n",[38,1041,1043,1046,1049,1051,1053],{"class":40,"line":1042},24,[38,1044,1045],{"class":698},"    log.info(",[38,1047,1048],{"class":48},"\"built ",[38,1050,915],{"class":867},[38,1052,918],{"class":48},[38,1054,921],{"class":698},[38,1056,1058,1060],{"class":40,"line":1057},25,[38,1059,810],{"class":694},[38,1061,930],{"class":698},[38,1063,1065],{"class":40,"line":1064},26,[38,1066,749],{"emptyLinePlaceholder":748},[38,1068,1070,1072,1075,1078,1080,1083,1086,1089],{"class":40,"line":1069},27,[38,1071,797],{"class":694},[38,1073,1074],{"class":800}," main",[38,1076,1077],{"class":698},"(argv",[38,1079,777],{"class":694},[38,1081,1082],{"class":967},"None",[38,1084,1085],{"class":698},") -> ",[38,1087,1088],{"class":967},"int",[38,1090,1091],{"class":698},":\n",[38,1093,1095,1098,1100],{"class":40,"line":1094},28,[38,1096,1097],{"class":698},"    p ",[38,1099,777],{"class":694},[38,1101,1102],{"class":698}," argparse.ArgumentParser()\n",[38,1104,1106,1109,1112,1114,1117,1119,1122,1125,1127,1129],{"class":40,"line":1105},29,[38,1107,1108],{"class":698},"    p.add_argument(",[38,1110,1111],{"class":48},"\"--date\"",[38,1113,953],{"class":698},[38,1115,1116],{"class":822},"type",[38,1118,777],{"class":694},[38,1120,1121],{"class":698},"date.fromisoformat, ",[38,1123,1124],{"class":822},"default",[38,1126,777],{"class":694},[38,1128,1082],{"class":967},[38,1130,1131],{"class":698},",\n",[38,1133,1135,1138,1140,1143],{"class":40,"line":1134},30,[38,1136,1137],{"class":822},"                   help",[38,1139,777],{"class":694},[38,1141,1142],{"class":48},"\"any day in the report week; defaults to this week\"",[38,1144,786],{"class":698},[38,1146,1148,1150,1153,1155,1157,1159,1162,1164,1166,1169,1172],{"class":40,"line":1147},31,[38,1149,1108],{"class":698},[38,1151,1152],{"class":48},"\"--out\"",[38,1154,953],{"class":698},[38,1156,1116],{"class":822},[38,1158,777],{"class":694},[38,1160,1161],{"class":698},"Path, ",[38,1163,1124],{"class":822},[38,1165,777],{"class":694},[38,1167,1168],{"class":698},"Path(",[38,1170,1171],{"class":48},"\"reports\"",[38,1173,1174],{"class":698},"))\n",[38,1176,1178,1181,1183],{"class":40,"line":1177},32,[38,1179,1180],{"class":698},"    args ",[38,1182,777],{"class":694},[38,1184,1185],{"class":698}," p.parse_args(argv)\n",[38,1187,1189,1192,1195,1197,1200,1203,1205,1208,1210,1212,1215,1218,1221,1223,1225,1228,1230],{"class":40,"line":1188},33,[38,1190,1191],{"class":698},"    logging.basicConfig(",[38,1193,1194],{"class":822},"level",[38,1196,777],{"class":694},[38,1198,1199],{"class":698},"logging.",[38,1201,1202],{"class":967},"INFO",[38,1204,953],{"class":698},[38,1206,1207],{"class":822},"format",[38,1209,777],{"class":694},[38,1211,918],{"class":48},[38,1213,1214],{"class":867},"%(asctime)s",[38,1216,1217],{"class":867}," %(levelname)s",[38,1219,1220],{"class":867}," %(message)s",[38,1222,918],{"class":48},[38,1224,953],{"class":698},[38,1226,1227],{"class":822},"stream",[38,1229,777],{"class":694},[38,1231,1232],{"class":698},"sys.stdout)\n",[38,1234,1236,1239],{"class":40,"line":1235},34,[38,1237,1238],{"class":694},"    try",[38,1240,1091],{"class":698},[38,1242,1244,1247,1250],{"class":40,"line":1243},35,[38,1245,1246],{"class":698},"        run(monday_of(args.date ",[38,1248,1249],{"class":694},"or",[38,1251,1252],{"class":698}," date.today()), args.out)\n",[38,1254,1256,1258],{"class":40,"line":1255},36,[38,1257,927],{"class":694},[38,1259,1260],{"class":967}," 0\n",[38,1262,1264,1267,1270],{"class":40,"line":1263},37,[38,1265,1266],{"class":694},"    except",[38,1268,1269],{"class":967}," Exception",[38,1271,1091],{"class":698},[38,1273,1275,1278,1281],{"class":40,"line":1274},38,[38,1276,1277],{"class":698},"        log.exception(",[38,1279,1280],{"class":48},"\"report failed\"",[38,1282,786],{"class":698},[38,1284,1286,1288],{"class":40,"line":1285},39,[38,1287,927],{"class":694},[38,1289,1290],{"class":967}," 1\n",[38,1292,1294],{"class":40,"line":1293},40,[38,1295,749],{"emptyLinePlaceholder":748},[38,1297,1299,1302,1305,1308,1311],{"class":40,"line":1298},41,[38,1300,1301],{"class":694},"if",[38,1303,1304],{"class":967}," __name__",[38,1306,1307],{"class":694}," ==",[38,1309,1310],{"class":48}," \"__main__\"",[38,1312,1091],{"class":698},[38,1314,1316],{"class":40,"line":1315},42,[38,1317,1318],{"class":698},"    sys.exit(main())\n",[28,1320,1322],{"className":30,"code":1321,"language":32,"meta":33,"style":33},"python weekly_report.py --date 2026-09-30; echo \"exit $?\"\npython weekly_report.py --date 2026-09-28; echo \"exit $?\"\npython weekly_report.py --date not-a-date 2> err.txt; echo \"exit $?\"; tail -1 err.txt\nls reports\n",[35,1323,1324,1352,1373,1411],{"__ignoreMap":33},[38,1325,1326,1328,1331,1334,1337,1340,1343,1346,1349],{"class":40,"line":41},[38,1327,687],{"class":44},[38,1329,1330],{"class":48}," weekly_report.py",[38,1332,1333],{"class":967}," --date",[38,1335,1336],{"class":48}," 2026-09-30",[38,1338,1339],{"class":698},"; ",[38,1341,1342],{"class":967},"echo",[38,1344,1345],{"class":48}," \"exit ",[38,1347,1348],{"class":967},"$?",[38,1350,1351],{"class":48},"\"\n",[38,1353,1354,1356,1358,1360,1363,1365,1367,1369,1371],{"class":40,"line":702},[38,1355,687],{"class":44},[38,1357,1330],{"class":48},[38,1359,1333],{"class":967},[38,1361,1362],{"class":48}," 2026-09-28",[38,1364,1339],{"class":698},[38,1366,1342],{"class":967},[38,1368,1345],{"class":48},[38,1370,1348],{"class":967},[38,1372,1351],{"class":48},[38,1374,1375,1377,1379,1381,1384,1387,1390,1392,1394,1396,1398,1400,1402,1405,1408],{"class":40,"line":710},[38,1376,687],{"class":44},[38,1378,1330],{"class":48},[38,1380,1333],{"class":967},[38,1382,1383],{"class":48}," not-a-date",[38,1385,1386],{"class":694}," 2>",[38,1388,1389],{"class":48}," err.txt",[38,1391,1339],{"class":698},[38,1393,1342],{"class":967},[38,1395,1345],{"class":48},[38,1397,1348],{"class":967},[38,1399,918],{"class":48},[38,1401,1339],{"class":698},[38,1403,1404],{"class":44},"tail",[38,1406,1407],{"class":967}," -1",[38,1409,1410],{"class":48}," err.txt\n",[38,1412,1413,1416],{"class":40,"line":718},[38,1414,1415],{"class":44},"ls",[38,1417,1418],{"class":48}," reports\n",[10,1420,1421,1422,1426],{},"The first run builds the report for the week of 28 September — any day in the week gives the same Monday. The second, a rerun such as a retry would cause, finds the file and does nothing, which is what makes retries and duplicate triggers harmless. A bad argument exits with status 2 from argparse, and a failure inside the report exits 1 after logging the traceback; every scheduler on this page treats a non-zero exit as a failed run. Writing to a temporary file and renaming it means readers never see a half-written workbook. ",[14,1423,1425],{"href":1424},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002F","Build a Command-Line Tool for Excel Reports with argparse"," develops the pattern further.",[57,1428,66,1433,66,1436,66,1439,66,1442,66,1447,66,1453,66,1458,66,1462,66,1466,66,1471,66,1476,66,1481,66,1484,66,1487,66,1490,66,1495,66,1498,66,1501,66,1504,66,1506,66,1509,66,1514,66,1519,66,1523,66,1526,66,1528,66,1531,66,1537,66,1542,66,1546,66,1549],{"viewBox":1429,"role":60,"ariaLabelledBy":1430,"xmlns":64,"style":65},"4 0 752 436",[1431,1432],"sch-entry-t","sch-entry-d",[68,1434,1435],{"id":1431},"One entry point, any scheduler",[72,1437,1438],{"id":1432},"A single command-line entry point with a date argument, idempotent output and meaningful exit codes fans in from many schedulers — a crontab line, a systemd service, a launchd agent, a Celery task, a container job and a Prefect flow all run the same command.",[76,1440],{"x":78,"y":79,"width":80,"height":1441,"fill":82},"436",[76,1443],{"x":1444,"y":1445,"width":524,"height":1446,"rx":89,"fill":90,"stroke":91,"style":92},"520.0","171.5","93.0",[94,1448,1450],{"transform":1449},"translate(618.0,184.5) scale(1.000)",[98,1451],{"d":1452,"fill":101,"stroke":102,"style":103},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01",[105,1454,1457],{"x":1455,"y":1456,"style":108},"630.0","231.0","weekly_report.py --date …",[105,1459,1461],{"x":1455,"y":1460,"style":162},"250.5","idempotent, exit codes, logs",[76,1463],{"x":142,"y":86,"width":1464,"height":1465,"rx":89,"fill":146,"stroke":133,"style":92},"250.0","92.0",[94,1467,1469],{"transform":1468},"translate(133.0,29.0) scale(1.000)",[98,1470],{"d":185,"fill":101,"stroke":153,"style":103},[105,1472,1475],{"x":452,"y":1473,"style":1474},"75.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","crontab \u002F systemd",[105,1477,1480],{"x":452,"y":1478,"style":1479},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","runs the command",[98,1482],{"d":1483,"fill":101,"stroke":91,"style":92},"M274.0,62.0 C394.0,62.0 394.0,218.0 510.0,218.0",[122,1485],{"points":1486,"fill":125},"514.0,218.0 505.0,223.0 505.0,213.0",[76,1488],{"x":142,"y":1489,"width":1464,"height":1465,"rx":89,"fill":146,"stroke":133,"style":92},"120.0",[94,1491,1493],{"transform":1492},"translate(133.0,133.0) scale(1.000)",[98,1494],{"d":152,"fill":101,"stroke":153,"style":103},[105,1496,1497],{"x":452,"y":468,"style":1474},"launchd \u002F Task Scheduler",[105,1499,1480],{"x":452,"y":1500,"style":1479},"198.0",[98,1502],{"d":1503,"fill":101,"stroke":91,"style":92},"M274.0,166.0 C394.0,166.0 394.0,218.0 510.0,218.0",[122,1505],{"points":1486,"fill":125},[76,1507],{"x":142,"y":1508,"width":1464,"height":1465,"rx":89,"fill":208,"stroke":209,"style":92},"224.0",[94,1510,1512],{"transform":1511},"translate(133.0,237.0) scale(1.000)",[98,1513],{"d":215,"fill":101,"stroke":216,"style":103},[105,1515,1518],{"x":452,"y":1516,"style":1517},"283.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Celery task \u002F Prefect flow",[105,1520,1522],{"x":452,"y":1521,"style":1479},"302.0","calls main([...])",[98,1524],{"d":1525,"fill":101,"stroke":91,"style":92},"M274.0,270.0 C394.0,270.0 394.0,218.0 510.0,218.0",[122,1527],{"points":1486,"fill":125},[76,1529],{"x":142,"y":1530,"width":1464,"height":1465,"rx":89,"fill":245,"stroke":246,"style":92},"328.0",[94,1532,1534],{"transform":1533},"translate(133.0,341.0) scale(1.000)",[98,1535],{"d":1536,"fill":101,"stroke":253,"style":103},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6",[105,1538,1541],{"x":452,"y":1539,"style":1540},"387.0","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Container job",[105,1543,1545],{"x":452,"y":1544,"style":1479},"406.0","CMD [python, weekly_report.py]",[98,1547],{"d":1548,"fill":101,"stroke":91,"style":92},"M274.0,374.0 C394.0,374.0 394.0,218.0 510.0,218.0",[122,1550],{"points":1486,"fill":125},[20,1552,1554],{"id":1553},"performance-notes","Performance notes",[10,1556,1557],{},"Scheduling overhead is negligible next to the report itself for every option here, with two exceptions worth knowing. Cloud functions and containers pay a cold-start cost of a few seconds per run — irrelevant weekly, noticeable for a report that runs every minute. Orchestrators add per-task scheduling latency, which matters only when a pipeline is split into hundreds of tiny tasks; keep a report as a handful of meaningful steps. Beyond that, the choice is about operations, not speed.",[20,1559,1561],{"id":1560},"common-pitfalls","Common pitfalls",[632,1563,1564,1570,1576,1582,1588],{},[635,1565,1566,1569],{},[271,1567,1568],{},"Choosing by familiarity alone."," A crontab on a laptop that sleeps misses runs; match the scheduler to where the report runs.",[635,1571,1572,1575],{},[271,1573,1574],{},"No retries anywhere."," If the scheduler does not retry, the script or a wrapper must.",[635,1577,1578,1581],{},[271,1579,1580],{},"No alert on missing runs."," Failures are logged; runs that never started are not. Monitor the output.",[635,1583,1584,1587],{},[271,1585,1586],{},"Dates from the clock."," A late or repeated run must produce the right period; take the date as an argument.",[635,1589,1590,1593],{},[271,1591,1592],{},"An orchestrator for three reports."," The platform overhead outweighs the benefit until there are many pipelines.",[20,1595,1597],{"id":1596},"conclusion","Conclusion",[10,1599,1600],{},"Pick the scheduler from where the report must run and what must happen when a run is missed or fails: the operating system's scheduler for a single machine, systemd timers on a Linux server, the application's scheduler inside an app, the cloud provider's job service in the cloud, and an orchestrator only when pipelines multiply. Write the report as an idempotent command-line entry point with a date argument and meaningful exit codes, and changing scheduler later becomes a matter of configuration.",[20,1602,1604],{"id":1603},"frequently-asked-questions","Frequently asked questions",[10,1606,1607,1610],{},[271,1608,1609],{},"What is the simplest way to schedule a Python Excel report?","\nOn a Linux server, a systemd timer or a cron entry that runs the script. On a Mac, a launchd agent; on Windows, Task Scheduler. Add logging to a file and an alert for runs that do not arrive.",[10,1612,1613,1616],{},[271,1614,1615],{},"Should I use Airflow for scheduling reports?","\nOnly if you already run Airflow or have many pipelines with dependencies, backfills and SLAs. For a few reports, the platform overhead is larger than the benefit; Prefect or a cloud job service are lighter alternatives.",[10,1618,1619,1622],{},[271,1620,1621],{},"Which schedulers retry failed runs automatically?","\nCelery with autoretry, Prefect and Airflow tasks, Cloud Run jobs, Kubernetes Jobs, and Azure Functions with a retry policy. cron and Task Scheduler do not retry on their own.",[10,1624,1625,1628],{},[271,1626,1627],{},"How do I make a report safe to run twice?","\nName the output after the reporting period, skip the work if that output already exists, and write to a temporary file that is renamed when complete. A second run then does nothing.",[20,1630,1632],{"id":1631},"related","Related",[632,1634,1635,1641,1648,1655],{},[635,1636,1637,1638,1640],{},"Up one level: ",[14,1639,17],{"href":16}," — every way to run reports on a schedule.",[635,1642,1643,1647],{},[14,1644,1646],{"href":1645},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Forchestrate-excel-reports-with-prefect\u002F","Orchestrate Excel Reports with Prefect"," — the orchestrator option in practice.",[635,1649,1650,1654],{},[14,1651,1653],{"href":1652},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler\u002F","Run a Python Excel Script on Windows Task Scheduler"," — the Windows desktop option.",[635,1656,1657,1661],{},[14,1658,1660],{"href":1659},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpython-vs-power-automate-for-report-workflows\u002F","Python vs Power Automate for Report Workflows"," — when a low-code flow is the better scheduler.",[1663,1664,1665],"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 .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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}",{"title":33,"searchDepth":702,"depth":702,"links":1667},[1668,1669,1670,1671,1672,1673,1674,1675,1676,1677],{"id":22,"depth":702,"text":23},{"id":266,"depth":702,"text":267},{"id":305,"depth":702,"text":306},{"id":629,"depth":702,"text":630},{"id":678,"depth":702,"text":679},{"id":1553,"depth":702,"text":1554},{"id":1560,"depth":702,"text":1561},{"id":1596,"depth":702,"text":1597},{"id":1603,"depth":702,"text":1604},{"id":1631,"depth":702,"text":1632},"2026-09-29","Cron, systemd timers, Task Scheduler, launchd, APScheduler, Celery beat, GitHub Actions, Airflow, Prefect, Lambda, Azure Functions, Cloud Run jobs and Kubernetes CronJobs compared for running Excel reports: where each runs, what it does about missed runs, retries, history and alerts, and a scheduler-agnostic script design that keeps the choice reversible.","md",[1682,1684,1686,1688],{"q":1609,"a":1683},"On a Linux server, a systemd timer or a cron entry that runs the script. On a Mac, a launchd agent; on Windows, Task Scheduler. Add logging to a file and an alert for runs that do not arrive.",{"q":1615,"a":1685},"Only if you already run Airflow or have many pipelines with dependencies, backfills and SLAs. For a few reports, the platform overhead is larger than the benefit; Prefect or a cloud job service are lighter alternatives.",{"q":1621,"a":1687},"Celery with autoretry, Prefect and Airflow tasks, Cloud Run jobs, Kubernetes Jobs, and Azure Functions with a retry policy. cron and Task Scheduler do not retry on their own.",{"q":1627,"a":1689},"Name the output after the reporting period, skip the work if that output already exists, and write to a temporary file that is renamed when complete. A second run then does nothing.","choose a scheduler for python excel reports",null,{"breadcrumb":1693},[1694,1696,1699],{"name":1695,"item":858},"Home",{"name":1697,"item":1698},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports",{"title":1702,"description":1703},"Which Scheduler for Python Excel Reports? Cron to Cloud","Compare schedulers for Python Excel reports — cron, systemd, Task Scheduler, launchd, APScheduler, Celery beat, GitHub Actions, Airflow, Prefect, AWS Lambda, Azure Functions, Cloud Run jobs, Kubernetes — on missed runs, retries, history, alerting and effort, with a portable entry point.","choose-a-scheduler-for-python-excel-reports","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports\u002Findex",[1707,1708],"scheduling","choosing-libraries","how-to","0L96hE7AEigKMs-Id_kSVD6h4CZMsjEgyz0paEWshR0",[1712,1716],{"title":1713,"path":1714,"stem":1715,"children":-1},"Backfill Excel Reports for Past Dates with Python","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates\u002Findex",{"title":1717,"path":1718,"stem":1719,"children":-1},"Orchestrate Excel Reports with Apache Airflow","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Forchestrate-excel-reports-with-apache-airflow","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Forchestrate-excel-reports-with-apache-airflow\u002Findex",[1721,1723],{"path":1722,"title":1697},"\u002Fautomating-reporting-workflows",{"path":1724,"title":17},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[1727,1730,1733,1736,1739,1742],{"path":1728,"title":1729,"kind":1709},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server","Download New Excel Files from an SFTP Server",{"path":1731,"title":1732,"kind":1709},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fread-excel-attachments-into-a-database-nightly","Read Excel Attachments into a Database Nightly",{"path":1734,"title":1735,"kind":1709},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule","Refresh an Excel Report from a Database on a Schedule",{"path":1737,"title":1738,"kind":1709},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frun-a-report-when-an-excel-file-changes-in-onedrive","Run a Report When an Excel File Changes in OneDrive",{"path":1740,"title":1741,"kind":1709},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-report-emails-at-each-recipients-local-time","Send Report Emails at Each Recipient's Local Time",{"path":1743,"title":674,"kind":1709},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run",1790972138631]