[{"data":1,"prerenderedAt":1639},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler":1631},{"id":4,"title":5,"body":6,"dateModified":1608,"datePublished":1608,"description":1609,"extension":1610,"faq":1611,"meta":1622,"navigation":375,"path":1623,"seo":1624,"slug":1627,"stem":1628,"type":1629,"__hash__":1630},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002Findex.md","Schedule Recurring Excel Reports with APScheduler",{"type":7,"value":8,"toc":1596},"minimark",[9,29,210,215,254,257,261,264,295,302,306,320,1008,1011,1023,1030,1034,1043,1136,1144,1148,1151,1187,1194,1198,1205,1212,1314,1336,1345,1349,1456,1472,1476,1485,1499,1515,1528,1548,1552,1568,1572,1592],[10,11,12,13,17,18,22,23,28],"p",{},"OS schedulers like cron and Windows Task Scheduler trigger a ",[14,15,16],"em",{},"fresh process"," at a fixed time. ",[19,20,21],"strong",{},"APScheduler"," takes the opposite approach: it lives inside a long-running Python process and fires job functions on a schedule you define in code. That makes it cross-platform, timezone-aware, and the natural fit when scheduling is part of an application rather than an OS-level concern. This page schedules a recurring Excel report with APScheduler — a weekday-morning cron trigger and an interval trigger — with the safeguards that keep it reliable. It's the in-process alternative within ",[24,25,27],"a",{"href":26},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",".",[30,31,39,40,39,44,39,48,39,55,39,59,39,67,39,75,39,80,39,83,39,87,39,90,39,94,39,98,39,100,39,102,39,107,39,110,39,113,39,120,39,124,39,126,39,129,39,131,39,134,39,139,39,145,39,151,39,156,39,163,39,168,39,171,39,175,39,181,39,184,39,187,39,190,39,197,39,203,39,207],"svg",{"viewBox":32,"role":33,"ariaLabelledBy":34,"xmlns":37,"style":38},"0 0 760 250","img",[35,36],"aps-t","aps-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  ",[41,42,43],"title",{"id":35},"APScheduler fires jobs from inside one long-running process",[45,46,47],"desc",{"id":36},"Unlike OS cron, which starts a fresh process at each tick, APScheduler lives inside a single long-running Python process where a CronTrigger or IntervalTrigger fires the same report job repeatedly.",[49,50,54],"text",{"x":51,"y":52,"style":53},"200","30","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","OS cron",[49,56,21],{"x":57,"y":52,"style":58},"560","font-size:13px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle",[60,61],"line",{"x1":62,"y1":63,"x2":62,"y2":64,"stroke":65,"style":66},"380","44","232","var(--line,#cdd5e6)","stroke-width:1px",[68,69],"rect",{"x":70,"y":71,"width":72,"height":70,"rx":73,"fill":74,"stroke":65},"40","50","80","9","var(--brand-soft,rgba(91,92,240,0.12))",[49,76,79],{"x":72,"y":77,"style":78},"75","font-size:12px;fill:var(--muted,#5b6780);text-anchor:middle","tick 1",[68,81],{"x":70,"y":82,"width":72,"height":70,"rx":73,"fill":74,"stroke":65},"100",[49,84,86],{"x":72,"y":85,"style":78},"125","tick 2",[68,88],{"x":70,"y":89,"width":72,"height":70,"rx":73,"fill":74,"stroke":65},"150",[49,91,93],{"x":72,"y":92,"style":78},"175","tick 3",[68,95],{"x":96,"y":71,"width":96,"height":70,"rx":73,"fill":97,"stroke":65},"170","var(--surface-muted,#eef2ff)",[68,99],{"x":96,"y":82,"width":96,"height":70,"rx":73,"fill":97,"stroke":65},[68,101],{"x":96,"y":89,"width":96,"height":70,"rx":73,"fill":97,"stroke":65},[49,103,16],{"x":104,"y":105,"style":106},"255","74","font-size:12px;fill:var(--text,#172033);text-anchor:middle",[49,108,16],{"x":104,"y":109,"style":106},"124",[49,111,16],{"x":104,"y":112,"style":106},"174",[60,114],{"x1":115,"y1":116,"x2":117,"y2":116,"stroke":118,"style":119},"120","70","166","var(--brand,#5b5cf0)","stroke-width:2px",[121,122],"polygon",{"points":123,"fill":118},"166,70 156,65 156,75",[60,125],{"x1":115,"y1":115,"x2":117,"y2":115,"stroke":118,"style":119},[121,127],{"points":128,"fill":118},"166,120 156,115 156,125",[60,130],{"x1":115,"y1":96,"x2":117,"y2":96,"stroke":118,"style":119},[121,132],{"points":133,"fill":118},"166,170 156,165 156,175",[49,135,138],{"x":51,"y":136,"style":137},"218","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","no resident process to babysit",[68,140],{"x":141,"y":71,"width":142,"height":142,"rx":143,"fill":118,"stroke":144},"410","160","14","var(--brand-strong,#4338ca)",[49,146,150],{"x":147,"y":148,"style":149},"490","78","font-size:13.5px;font-weight:700;fill:#ffffff;text-anchor:middle","one process",[49,152,155],{"x":147,"y":153,"style":154},"98","font-size:11.5px;fill:rgba(255,255,255,0.88);text-anchor:middle","BlockingScheduler",[68,157],{"x":158,"y":159,"width":115,"height":160,"rx":161,"fill":162},"430","112","34","8","rgba(255,255,255,0.16)",[49,164,167],{"x":147,"y":165,"style":166},"134","font-size:11.5px;fill:#ffffff;text-anchor:middle","CronTrigger",[68,169],{"x":158,"y":170,"width":115,"height":160,"rx":161,"fill":162},"154",[49,172,174],{"x":147,"y":173,"style":166},"176","IntervalTrigger",[60,176],{"x1":177,"y1":178,"x2":179,"y2":178,"stroke":180,"style":119},"570","129","636","var(--teal,#0f9488)",[121,182],{"points":183,"fill":180},"636,129 626,124 626,134",[60,185],{"x1":177,"y1":186,"x2":179,"y2":186,"stroke":180,"style":119},"171",[121,188],{"points":189,"fill":180},"636,171 626,166 626,176",[68,191],{"x":192,"y":193,"width":82,"height":194,"rx":195,"fill":196,"stroke":180,"style":119},"640","108","84","12","rgba(15,148,136,0.12)",[49,198,202],{"x":199,"y":200,"style":201},"690","146","font-size:13px;font-weight:700;fill:var(--teal,#0f9488);text-anchor:middle","report",[49,204,206],{"x":199,"y":205,"style":201},"164","job",[49,208,209],{"x":57,"y":64,"style":137},"same job fired repeatedly, in code",[211,212,214],"h2",{"id":213},"when-to-choose-apscheduler-over-cron-or-task-scheduler","When to choose APScheduler over cron or Task Scheduler",[216,217,218,231],"table",{},[219,220,221],"thead",{},[222,223,224,228],"tr",{},[225,226,227],"th",{},"Choose",[225,229,230],{},"When",[232,233,234,244],"tbody",{},[222,235,236,241],{},[237,238,239],"td",{},[19,240,21],{},[237,242,243],{},"You want one cross-platform schedule in code, timezone-aware triggers, dynamic add\u002Fremove of jobs, or scheduling embedded in an app\u002Fservice you already run",[222,245,246,251],{},[237,247,248],{},[19,249,250],{},"cron \u002F Task Scheduler",[237,252,253],{},"You want the OS to own the schedule, no resident process to babysit, and each run isolated in its own process",[10,255,256],{},"The key trade-off: APScheduler only runs while its process is alive. Cron and Task Scheduler fire even after a reboot with no resident process. If you choose APScheduler, you'll need a supervisor (systemd, a Windows service, or a container restart policy) to keep the process up — covered below.",[211,258,260],{"id":259},"prerequisites","Prerequisites",[10,262,263],{},"Install the scheduler and the report libraries:",[265,266,271],"pre",{"className":267,"code":268,"language":269,"meta":270,"style":270},"language-bash shiki shiki-themes github-light github-dark","pip install apscheduler pandas openpyxl\n","bash","",[272,273,274],"code",{"__ignoreMap":270},[275,276,278,282,286,289,292],"span",{"class":60,"line":277},1,[275,279,281],{"class":280},"sScJk","pip",[275,283,285],{"class":284},"sZZnC"," install",[275,287,288],{"class":284}," apscheduler",[275,290,291],{"class":284}," pandas",[275,293,294],{"class":284}," openpyxl\n",[10,296,297,298,301],{},"This page targets APScheduler 3.x, the current stable line. The job function below is plain Python — anything that builds and writes an ",[272,299,300],{},".xlsx"," works.",[211,303,305],{"id":304},"a-blockingscheduler-with-a-cron-trigger","A BlockingScheduler with a cron trigger",[10,307,308,309,311,312,315,316,319],{},"A ",[272,310,155],{}," runs the scheduler in the foreground and blocks the calling thread — ideal when the script's only purpose is to schedule reports. The job builds sample data and writes ",[272,313,314],{},"daily_summary.xlsx",". Save this as ",[272,317,318],{},"report_scheduler.py",":",[265,321,325],{"className":322,"code":323,"language":324,"meta":270,"style":270},"language-python shiki shiki-themes github-light github-dark","\"\"\"Recurring Excel reports via APScheduler. Runs as a long-lived process.\"\"\"\nimport logging\nfrom datetime import datetime\nfrom pathlib import Path\n\nimport pandas as pd\nfrom apscheduler.schedulers.blocking import BlockingScheduler\nfrom apscheduler.triggers.cron import CronTrigger\n\nlogging.basicConfig(\n    level=logging.INFO,\n    format=\"%(asctime)s | %(levelname)s | %(message)s\",\n)\n\nOUTPUT_DIR = Path(\"reports\")\nOUTPUT_DIR.mkdir(exist_ok=True)\n\ndef generate_report():\n    \"\"\"Build data and write the Excel file. One scheduled run = one call.\"\"\"\n    logging.info(\"Generating report...\")\n    df = pd.DataFrame({\n        \"region\": [\"North\", \"South\", \"North\", \"East\", \"South\"],\n        \"revenue\": [1200.0, 980.5, 1450.0, 610.25, 980.5],\n    })\n    summary = df.groupby(\"region\", as_index=False)[\"revenue\"].sum()\n\n    stamp = datetime.now().strftime(\"%Y%m%d_%H%M\")\n    out = OUTPUT_DIR \u002F f\"daily_summary_{stamp}.xlsx\"\n    summary.to_excel(out, index=False, engine=\"openpyxl\")\n    logging.info(\"Wrote %s\", out)\n\nscheduler = BlockingScheduler(timezone=\"America\u002FNew_York\")\n\n# Every weekday at 06:00 in the scheduler's timezone.\nscheduler.add_job(\n    generate_report,\n    CronTrigger(day_of_week=\"mon-fri\", hour=6, minute=0),\n    id=\"weekday_morning_report\",\n    max_instances=1,        # never overlap a run with itself\n    coalesce=True,          # collapse multiple missed runs into one\n    misfire_grace_time=300, # still run if up to 5 min late\n)\n\nif __name__ == \"__main__\":\n    logging.info(\"Scheduler starting. Ctrl+C to stop.\")\n    try:\n        scheduler.start()   # blocks here forever\n    except (KeyboardInterrupt, SystemExit):\n        logging.info(\"Scheduler stopped.\")\n","python",[272,326,327,332,343,357,370,377,391,404,417,422,428,448,477,483,488,505,523,528,540,546,557,568,602,634,640,673,678,700,732,757,773,778,799,804,811,817,823,860,873,890,906,922,927,932,950,960,968,977,997],{"__ignoreMap":270},[275,328,329],{"class":60,"line":277},[275,330,331],{"class":284},"\"\"\"Recurring Excel reports via APScheduler. Runs as a long-lived process.\"\"\"\n",[275,333,335,339],{"class":60,"line":334},2,[275,336,338],{"class":337},"szBVR","import",[275,340,342],{"class":341},"sVt8B"," logging\n",[275,344,346,349,352,354],{"class":60,"line":345},3,[275,347,348],{"class":337},"from",[275,350,351],{"class":341}," datetime ",[275,353,338],{"class":337},[275,355,356],{"class":341}," datetime\n",[275,358,360,362,365,367],{"class":60,"line":359},4,[275,361,348],{"class":337},[275,363,364],{"class":341}," pathlib ",[275,366,338],{"class":337},[275,368,369],{"class":341}," Path\n",[275,371,373],{"class":60,"line":372},5,[275,374,376],{"emptyLinePlaceholder":375},true,"\n",[275,378,380,382,385,388],{"class":60,"line":379},6,[275,381,338],{"class":337},[275,383,384],{"class":341}," pandas ",[275,386,387],{"class":337},"as",[275,389,390],{"class":341}," pd\n",[275,392,394,396,399,401],{"class":60,"line":393},7,[275,395,348],{"class":337},[275,397,398],{"class":341}," apscheduler.schedulers.blocking ",[275,400,338],{"class":337},[275,402,403],{"class":341}," BlockingScheduler\n",[275,405,407,409,412,414],{"class":60,"line":406},8,[275,408,348],{"class":337},[275,410,411],{"class":341}," apscheduler.triggers.cron ",[275,413,338],{"class":337},[275,415,416],{"class":341}," CronTrigger\n",[275,418,420],{"class":60,"line":419},9,[275,421,376],{"emptyLinePlaceholder":375},[275,423,425],{"class":60,"line":424},10,[275,426,427],{"class":341},"logging.basicConfig(\n",[275,429,431,435,438,441,445],{"class":60,"line":430},11,[275,432,434],{"class":433},"s4XuR","    level",[275,436,437],{"class":337},"=",[275,439,440],{"class":341},"logging.",[275,442,444],{"class":443},"sj4cs","INFO",[275,446,447],{"class":341},",\n",[275,449,451,454,456,459,462,465,468,470,473,475],{"class":60,"line":450},12,[275,452,453],{"class":433},"    format",[275,455,437],{"class":337},[275,457,458],{"class":284},"\"",[275,460,461],{"class":443},"%(asctime)s",[275,463,464],{"class":284}," | ",[275,466,467],{"class":443},"%(levelname)s",[275,469,464],{"class":284},[275,471,472],{"class":443},"%(message)s",[275,474,458],{"class":284},[275,476,447],{"class":341},[275,478,480],{"class":60,"line":479},13,[275,481,482],{"class":341},")\n",[275,484,486],{"class":60,"line":485},14,[275,487,376],{"emptyLinePlaceholder":375},[275,489,491,494,497,500,503],{"class":60,"line":490},15,[275,492,493],{"class":443},"OUTPUT_DIR",[275,495,496],{"class":337}," =",[275,498,499],{"class":341}," Path(",[275,501,502],{"class":284},"\"reports\"",[275,504,482],{"class":341},[275,506,508,510,513,516,518,521],{"class":60,"line":507},16,[275,509,493],{"class":443},[275,511,512],{"class":341},".mkdir(",[275,514,515],{"class":433},"exist_ok",[275,517,437],{"class":337},[275,519,520],{"class":443},"True",[275,522,482],{"class":341},[275,524,526],{"class":60,"line":525},17,[275,527,376],{"emptyLinePlaceholder":375},[275,529,531,534,537],{"class":60,"line":530},18,[275,532,533],{"class":337},"def",[275,535,536],{"class":280}," generate_report",[275,538,539],{"class":341},"():\n",[275,541,543],{"class":60,"line":542},19,[275,544,545],{"class":284},"    \"\"\"Build data and write the Excel file. One scheduled run = one call.\"\"\"\n",[275,547,549,552,555],{"class":60,"line":548},20,[275,550,551],{"class":341},"    logging.info(",[275,553,554],{"class":284},"\"Generating report...\"",[275,556,482],{"class":341},[275,558,560,563,565],{"class":60,"line":559},21,[275,561,562],{"class":341},"    df ",[275,564,437],{"class":337},[275,566,567],{"class":341}," pd.DataFrame({\n",[275,569,571,574,577,580,583,586,588,590,592,595,597,599],{"class":60,"line":570},22,[275,572,573],{"class":284},"        \"region\"",[275,575,576],{"class":341},": [",[275,578,579],{"class":284},"\"North\"",[275,581,582],{"class":341},", ",[275,584,585],{"class":284},"\"South\"",[275,587,582],{"class":341},[275,589,579],{"class":284},[275,591,582],{"class":341},[275,593,594],{"class":284},"\"East\"",[275,596,582],{"class":341},[275,598,585],{"class":284},[275,600,601],{"class":341},"],\n",[275,603,605,608,610,613,615,618,620,623,625,628,630,632],{"class":60,"line":604},23,[275,606,607],{"class":284},"        \"revenue\"",[275,609,576],{"class":341},[275,611,612],{"class":443},"1200.0",[275,614,582],{"class":341},[275,616,617],{"class":443},"980.5",[275,619,582],{"class":341},[275,621,622],{"class":443},"1450.0",[275,624,582],{"class":341},[275,626,627],{"class":443},"610.25",[275,629,582],{"class":341},[275,631,617],{"class":443},[275,633,601],{"class":341},[275,635,637],{"class":60,"line":636},24,[275,638,639],{"class":341},"    })\n",[275,641,643,646,648,651,654,656,659,661,664,667,670],{"class":60,"line":642},25,[275,644,645],{"class":341},"    summary ",[275,647,437],{"class":337},[275,649,650],{"class":341}," df.groupby(",[275,652,653],{"class":284},"\"region\"",[275,655,582],{"class":341},[275,657,658],{"class":433},"as_index",[275,660,437],{"class":337},[275,662,663],{"class":443},"False",[275,665,666],{"class":341},")[",[275,668,669],{"class":284},"\"revenue\"",[275,671,672],{"class":341},"].sum()\n",[275,674,676],{"class":60,"line":675},26,[275,677,376],{"emptyLinePlaceholder":375},[275,679,681,684,686,689,692,695,698],{"class":60,"line":680},27,[275,682,683],{"class":341},"    stamp ",[275,685,437],{"class":337},[275,687,688],{"class":341}," datetime.now().strftime(",[275,690,691],{"class":284},"\"%Y%m",[275,693,694],{"class":443},"%d",[275,696,697],{"class":284},"_%H%M\"",[275,699,482],{"class":341},[275,701,703,706,708,711,714,717,720,723,726,729],{"class":60,"line":702},28,[275,704,705],{"class":341},"    out ",[275,707,437],{"class":337},[275,709,710],{"class":443}," OUTPUT_DIR",[275,712,713],{"class":337}," \u002F",[275,715,716],{"class":337}," f",[275,718,719],{"class":284},"\"daily_summary_",[275,721,722],{"class":443},"{",[275,724,725],{"class":341},"stamp",[275,727,728],{"class":443},"}",[275,730,731],{"class":284},".xlsx\"\n",[275,733,735,738,741,743,745,747,750,752,755],{"class":60,"line":734},29,[275,736,737],{"class":341},"    summary.to_excel(out, ",[275,739,740],{"class":433},"index",[275,742,437],{"class":337},[275,744,663],{"class":443},[275,746,582],{"class":341},[275,748,749],{"class":433},"engine",[275,751,437],{"class":337},[275,753,754],{"class":284},"\"openpyxl\"",[275,756,482],{"class":341},[275,758,760,762,765,768,770],{"class":60,"line":759},30,[275,761,551],{"class":341},[275,763,764],{"class":284},"\"Wrote ",[275,766,767],{"class":443},"%s",[275,769,458],{"class":284},[275,771,772],{"class":341},", out)\n",[275,774,776],{"class":60,"line":775},31,[275,777,376],{"emptyLinePlaceholder":375},[275,779,781,784,786,789,792,794,797],{"class":60,"line":780},32,[275,782,783],{"class":341},"scheduler ",[275,785,437],{"class":337},[275,787,788],{"class":341}," BlockingScheduler(",[275,790,791],{"class":433},"timezone",[275,793,437],{"class":337},[275,795,796],{"class":284},"\"America\u002FNew_York\"",[275,798,482],{"class":341},[275,800,802],{"class":60,"line":801},33,[275,803,376],{"emptyLinePlaceholder":375},[275,805,807],{"class":60,"line":806},34,[275,808,810],{"class":809},"sJ8bj","# Every weekday at 06:00 in the scheduler's timezone.\n",[275,812,814],{"class":60,"line":813},35,[275,815,816],{"class":341},"scheduler.add_job(\n",[275,818,820],{"class":60,"line":819},36,[275,821,822],{"class":341},"    generate_report,\n",[275,824,826,829,832,834,837,839,842,844,847,849,852,854,857],{"class":60,"line":825},37,[275,827,828],{"class":341},"    CronTrigger(",[275,830,831],{"class":433},"day_of_week",[275,833,437],{"class":337},[275,835,836],{"class":284},"\"mon-fri\"",[275,838,582],{"class":341},[275,840,841],{"class":433},"hour",[275,843,437],{"class":337},[275,845,846],{"class":443},"6",[275,848,582],{"class":341},[275,850,851],{"class":433},"minute",[275,853,437],{"class":337},[275,855,856],{"class":443},"0",[275,858,859],{"class":341},"),\n",[275,861,863,866,868,871],{"class":60,"line":862},38,[275,864,865],{"class":433},"    id",[275,867,437],{"class":337},[275,869,870],{"class":284},"\"weekday_morning_report\"",[275,872,447],{"class":341},[275,874,876,879,881,884,887],{"class":60,"line":875},39,[275,877,878],{"class":433},"    max_instances",[275,880,437],{"class":337},[275,882,883],{"class":443},"1",[275,885,886],{"class":341},",        ",[275,888,889],{"class":809},"# never overlap a run with itself\n",[275,891,893,896,898,900,903],{"class":60,"line":892},40,[275,894,895],{"class":433},"    coalesce",[275,897,437],{"class":337},[275,899,520],{"class":443},[275,901,902],{"class":341},",          ",[275,904,905],{"class":809},"# collapse multiple missed runs into one\n",[275,907,909,912,914,917,919],{"class":60,"line":908},41,[275,910,911],{"class":433},"    misfire_grace_time",[275,913,437],{"class":337},[275,915,916],{"class":443},"300",[275,918,582],{"class":341},[275,920,921],{"class":809},"# still run if up to 5 min late\n",[275,923,925],{"class":60,"line":924},42,[275,926,482],{"class":341},[275,928,930],{"class":60,"line":929},43,[275,931,376],{"emptyLinePlaceholder":375},[275,933,935,938,941,944,947],{"class":60,"line":934},44,[275,936,937],{"class":337},"if",[275,939,940],{"class":443}," __name__",[275,942,943],{"class":337}," ==",[275,945,946],{"class":284}," \"__main__\"",[275,948,949],{"class":341},":\n",[275,951,953,955,958],{"class":60,"line":952},45,[275,954,551],{"class":341},[275,956,957],{"class":284},"\"Scheduler starting. Ctrl+C to stop.\"",[275,959,482],{"class":341},[275,961,963,966],{"class":60,"line":962},46,[275,964,965],{"class":337},"    try",[275,967,949],{"class":341},[275,969,971,974],{"class":60,"line":970},47,[275,972,973],{"class":341},"        scheduler.start()   ",[275,975,976],{"class":809},"# blocks here forever\n",[275,978,980,983,986,989,991,994],{"class":60,"line":979},48,[275,981,982],{"class":337},"    except",[275,984,985],{"class":341}," (",[275,987,988],{"class":443},"KeyboardInterrupt",[275,990,582],{"class":341},[275,992,993],{"class":443},"SystemExit",[275,995,996],{"class":341},"):\n",[275,998,1000,1003,1006],{"class":60,"line":999},49,[275,1001,1002],{"class":341},"        logging.info(",[275,1004,1005],{"class":284},"\"Scheduler stopped.\"",[275,1007,482],{"class":341},[10,1009,1010],{},"Run it and leave it running:",[265,1012,1014],{"className":267,"code":1013,"language":269,"meta":270,"style":270},"python report_scheduler.py\n",[272,1015,1016],{"__ignoreMap":270},[275,1017,1018,1020],{"class":60,"line":277},[275,1019,324],{"class":280},[275,1021,1022],{"class":284}," report_scheduler.py\n",[10,1024,1025,1026,1029],{},"The process now stays alive and fires ",[272,1027,1028],{},"generate_report"," every weekday at 06:00. Stop it with Ctrl+C.",[211,1031,1033],{"id":1032},"adding-an-interval-trigger","Adding an interval trigger",[10,1035,1036,1037,1039,1040,319],{},"Use an ",[272,1038,174],{}," for \"every N minutes\u002Fhours\" instead of a wall-clock time. Add a second job before ",[272,1041,1042],{},"scheduler.start()",[265,1044,1046],{"className":322,"code":1045,"language":324,"meta":270,"style":270},"from apscheduler.triggers.interval import IntervalTrigger\n\nscheduler.add_job(\n    generate_report,\n    IntervalTrigger(hours=4),   # every 4 hours from process start\n    id=\"four_hourly_report\",\n    max_instances=1,\n    coalesce=True,\n    misfire_grace_time=300,\n)\n",[272,1047,1048,1060,1064,1068,1072,1091,1102,1112,1122,1132],{"__ignoreMap":270},[275,1049,1050,1052,1055,1057],{"class":60,"line":277},[275,1051,348],{"class":337},[275,1053,1054],{"class":341}," apscheduler.triggers.interval ",[275,1056,338],{"class":337},[275,1058,1059],{"class":341}," IntervalTrigger\n",[275,1061,1062],{"class":60,"line":334},[275,1063,376],{"emptyLinePlaceholder":375},[275,1065,1066],{"class":60,"line":345},[275,1067,816],{"class":341},[275,1069,1070],{"class":60,"line":359},[275,1071,822],{"class":341},[275,1073,1074,1077,1080,1082,1085,1088],{"class":60,"line":372},[275,1075,1076],{"class":341},"    IntervalTrigger(",[275,1078,1079],{"class":433},"hours",[275,1081,437],{"class":337},[275,1083,1084],{"class":443},"4",[275,1086,1087],{"class":341},"),   ",[275,1089,1090],{"class":809},"# every 4 hours from process start\n",[275,1092,1093,1095,1097,1100],{"class":60,"line":379},[275,1094,865],{"class":433},[275,1096,437],{"class":337},[275,1098,1099],{"class":284},"\"four_hourly_report\"",[275,1101,447],{"class":341},[275,1103,1104,1106,1108,1110],{"class":60,"line":393},[275,1105,878],{"class":433},[275,1107,437],{"class":337},[275,1109,883],{"class":443},[275,1111,447],{"class":341},[275,1113,1114,1116,1118,1120],{"class":60,"line":406},[275,1115,895],{"class":433},[275,1117,437],{"class":337},[275,1119,520],{"class":443},[275,1121,447],{"class":341},[275,1123,1124,1126,1128,1130],{"class":60,"line":419},[275,1125,911],{"class":433},[275,1127,437],{"class":337},[275,1129,916],{"class":443},[275,1131,447],{"class":341},[275,1133,1134],{"class":60,"line":424},[275,1135,482],{"class":341},[10,1137,1138,1140,1141,1143],{},[272,1139,167],{}," anchors to the clock (06:00 sharp); ",[272,1142,174],{}," counts from when the scheduler started. Pick cron for fixed report times, interval for steady cadence regardless of time of day.",[211,1145,1147],{"id":1146},"handling-missed-and-overlapping-runs","Handling missed and overlapping runs",[10,1149,1150],{},"These three options are what make an in-process scheduler trustworthy:",[1152,1153,1154,1167,1179],"ul",{},[1155,1156,1157,1162,1163,1166],"li",{},[19,1158,1159],{},[272,1160,1161],{},"misfire_grace_time"," — if the process was busy or briefly down when a run was due, APScheduler will still fire it if it's no more than this many seconds late. Without it (and with the default of ",[272,1164,1165],{},"None"," being interpreted as \"run if at all possible\"), set it explicitly so behavior is predictable.",[1155,1168,1169,1174,1175,1178],{},[19,1170,1171],{},[272,1172,1173],{},"coalesce=True"," — if several runs were missed (the process was down for hours), run the job ",[19,1176,1177],{},"once"," when it resumes instead of firing every missed occurrence in a burst.",[1155,1180,1181,1186],{},[19,1182,1183],{},[272,1184,1185],{},"max_instances=1"," — prevents a slow run from overlapping the next scheduled run of the same job. The second invocation is skipped (and logged) rather than running concurrently against the same output.",[10,1188,1189,1190,1193],{},"Together they mirror the protections you'd otherwise build with ",[272,1191,1192],{},"flock"," and careful timing under cron, but they're configured per job in code.",[211,1195,1197],{"id":1196},"keeping-the-process-alive","Keeping the process alive",[10,1199,1200,1201,1204],{},"The single biggest difference from cron: ",[19,1202,1203],{},"if the process dies, no reports run."," A crash, a deploy, or a reboot stops the schedule until something restarts the process. Put it under a supervisor.",[10,1206,1207,1208,1211],{},"systemd unit (",[272,1209,1210],{},"\u002Fetc\u002Fsystemd\u002Fsystem\u002Fexcel-reports.service","):",[265,1213,1217],{"className":1214,"code":1215,"language":1216,"meta":270,"style":270},"language-ini shiki shiki-themes github-light github-dark","[Unit]\nDescription=APScheduler Excel reports\nAfter=network.target\n\n[Service]\nType=simple\nWorkingDirectory=\u002Fopt\u002Freporting\nExecStart=\u002Fopt\u002Freporting\u002Fvenv\u002Fbin\u002Fpython \u002Fopt\u002Freporting\u002Freport_scheduler.py\nRestart=always\nRestartSec=10\nUser=svc_reports\n\n[Install]\nWantedBy=multi-user.target\n","ini",[272,1218,1219,1224,1232,1240,1244,1249,1257,1265,1273,1281,1289,1297,1301,1306],{"__ignoreMap":270},[275,1220,1221],{"class":60,"line":277},[275,1222,1223],{"class":280},"[Unit]\n",[275,1225,1226,1229],{"class":60,"line":334},[275,1227,1228],{"class":337},"Description",[275,1230,1231],{"class":341},"=APScheduler Excel reports\n",[275,1233,1234,1237],{"class":60,"line":345},[275,1235,1236],{"class":337},"After",[275,1238,1239],{"class":341},"=network.target\n",[275,1241,1242],{"class":60,"line":359},[275,1243,376],{"emptyLinePlaceholder":375},[275,1245,1246],{"class":60,"line":372},[275,1247,1248],{"class":280},"[Service]\n",[275,1250,1251,1254],{"class":60,"line":379},[275,1252,1253],{"class":337},"Type",[275,1255,1256],{"class":341},"=simple\n",[275,1258,1259,1262],{"class":60,"line":393},[275,1260,1261],{"class":337},"WorkingDirectory",[275,1263,1264],{"class":341},"=\u002Fopt\u002Freporting\n",[275,1266,1267,1270],{"class":60,"line":406},[275,1268,1269],{"class":337},"ExecStart",[275,1271,1272],{"class":341},"=\u002Fopt\u002Freporting\u002Fvenv\u002Fbin\u002Fpython \u002Fopt\u002Freporting\u002Freport_scheduler.py\n",[275,1274,1275,1278],{"class":60,"line":419},[275,1276,1277],{"class":337},"Restart",[275,1279,1280],{"class":341},"=always\n",[275,1282,1283,1286],{"class":60,"line":424},[275,1284,1285],{"class":337},"RestartSec",[275,1287,1288],{"class":341},"=10\n",[275,1290,1291,1294],{"class":60,"line":430},[275,1292,1293],{"class":337},"User",[275,1295,1296],{"class":341},"=svc_reports\n",[275,1298,1299],{"class":60,"line":450},[275,1300,376],{"emptyLinePlaceholder":375},[275,1302,1303],{"class":60,"line":479},[275,1304,1305],{"class":280},"[Install]\n",[275,1307,1308,1311],{"class":60,"line":485},[275,1309,1310],{"class":337},"WantedBy",[275,1312,1313],{"class":341},"=multi-user.target\n",[265,1315,1317],{"className":267,"code":1316,"language":269,"meta":270,"style":270},"sudo systemctl enable --now excel-reports.service\n",[272,1318,1319],{"__ignoreMap":270},[275,1320,1321,1324,1327,1330,1333],{"class":60,"line":277},[275,1322,1323],{"class":280},"sudo",[275,1325,1326],{"class":284}," systemctl",[275,1328,1329],{"class":284}," enable",[275,1331,1332],{"class":443}," --now",[275,1334,1335],{"class":284}," excel-reports.service\n",[10,1337,1338,1341,1342,1344],{},[272,1339,1340],{},"Restart=always"," brings the process back after a crash or reboot, and ",[272,1343,1173],{}," ensures the catch-up after a restart is a single run, not a flood. On Windows, run the same script as a service via NSSM or a scheduled task set to run on startup; on a container platform, use a restart policy.",[211,1346,1348],{"id":1347},"common-pitfalls","Common pitfalls",[216,1350,1351,1364],{},[219,1352,1353],{},[222,1354,1355,1358,1361],{},[225,1356,1357],{},"Symptom",[225,1359,1360],{},"Cause",[225,1362,1363],{},"Fix",[232,1365,1366,1379,1394,1408,1420,1438],{},[222,1367,1368,1371,1374],{},[237,1369,1370],{},"Schedule simply stops",[237,1372,1373],{},"The Python process died and nothing restarted it",[237,1375,1376,1377],{},"Run under systemd\u002FNSSM with ",[272,1378,1340],{},[222,1380,1381,1384,1387],{},[237,1382,1383],{},"Jobs fire at the wrong hour",[237,1385,1386],{},"No timezone set; APScheduler used the host's",[237,1388,1389,1390,1393],{},"Pass ",[272,1391,1392],{},"timezone=\"America\u002FNew_York\""," to the scheduler",[222,1395,1396,1399,1402],{},[237,1397,1398],{},"Two copies of the report run at once",[237,1400,1401],{},"Long run overlapped the next trigger",[237,1403,1404,1405,1407],{},"Set ",[272,1406,1185],{}," on the job",[222,1409,1410,1413,1416],{},[237,1411,1412],{},"A burst of runs after downtime",[237,1414,1415],{},"Every missed run fired on resume",[237,1417,1404,1418],{},[272,1419,1173],{},[222,1421,1422,1425,1432],{},[237,1423,1424],{},"Script exits immediately, never schedules",[237,1426,1427,1428,1431],{},"Used ",[272,1429,1430],{},"BackgroundScheduler"," in a plain script",[237,1433,1434,1435,1437],{},"Use ",[272,1436,155],{},", or keep the main thread alive",[222,1439,1440,1443,1446],{},[237,1441,1442],{},"Job added twice on reload",[237,1444,1445],{},"Module imported\u002Freloaded twice in dev servers",[237,1447,1448,1449,1452,1453],{},"Give each job a stable ",[272,1450,1451],{},"id"," and add with ",[272,1454,1455],{},"replace_existing=True",[10,1457,1458,1459,1461,1462,1465,1466,1468,1469,1471],{},"A note on Blocking vs Background: ",[272,1460,155],{}," is for standalone scripts — ",[272,1463,1464],{},"start()"," blocks and runs the loop. ",[272,1467,1430],{}," runs in a separate thread and returns immediately, so it suits embedding in a web app — but in a standalone script the program would exit right after ",[272,1470,1464],{}," and nothing would fire.",[211,1473,1475],{"id":1474},"frequently-asked-questions","Frequently asked questions",[10,1477,1478,1481,1482,1484],{},[19,1479,1480],{},"How is this different from just using cron?","\nCron launches a new process per run and survives reboots without a resident process. APScheduler runs jobs inside one long-lived Python process — better for in-app scheduling, dynamic jobs, and timezone handling, but it needs a supervisor to stay alive. See ",[24,1483,27],{"href":26}," for the cron approach.",[10,1486,1487,1490,1491,1494,1495,1498],{},[19,1488,1489],{},"Why are my jobs running in UTC?","\nAPScheduler defaults to the host timezone, which is often UTC on servers. Pass ",[272,1492,1493],{},"timezone="," to the scheduler (or per trigger) with an IANA name like ",[272,1496,1497],{},"\"Europe\u002FLondon\""," so triggers fire at the local time you intend.",[10,1500,1501,1504,1505,1508,1509,1511,1512,1514],{},[19,1502,1503],{},"My jobs get added twice when I reload — why?","\nAuto-reloaders and re-imports can run your ",[272,1506,1507],{},"add_job"," code more than once. Use a stable ",[272,1510,1451],{}," per job and ",[272,1513,1455],{},", so a re-add updates the existing job instead of creating a duplicate.",[10,1516,1517,1520,1521,1524,1525,1527],{},[19,1518,1519],{},"Can I persist jobs across restarts?","\nYes — configure a ",[272,1522,1523],{},"jobstore"," (e.g. SQLAlchemy or Redis) so pending jobs survive a process restart. For a fixed in-code schedule like this one, the default in-memory store plus ",[272,1526,1173],{}," is usually enough.",[10,1529,1530,1533,1534,1536,1537,1539,1540,1543,1544,1547],{},[19,1531,1532],{},"Should the job run heavy work directly in the scheduler thread?","\nWith ",[272,1535,155],{}," a long job blocks the next trigger; that's why ",[272,1538,1185],{}," matters. For CPU-heavy reports, configure a ",[272,1541,1542],{},"ThreadPoolExecutor"," or ",[272,1545,1546],{},"ProcessPoolExecutor"," in the scheduler so runs don't starve each other.",[211,1549,1551],{"id":1550},"conclusion","Conclusion",[10,1553,1554,1555,1543,1557,1559,1560,582,1562,1564,1565,1567],{},"APScheduler moves scheduling into your Python process: define a ",[272,1556,167],{},[272,1558,174],{},", build the report in the job function, and harden it with ",[272,1561,1161],{},[272,1563,1173],{},", and ",[272,1566,1185],{},". Its one liability is that nothing runs if the process dies — so wrap it in systemd or an equivalent supervisor with automatic restart. Choose it when you want cross-platform, timezone-aware, in-app scheduling; choose cron or Task Scheduler when you'd rather the OS own the schedule.",[211,1569,1571],{"id":1570},"where-to-go-next","Where to go next",[10,1573,1574,1575,1577,1578,1582,1583,1587,1588,28],{},"This is the in-process alternative within ",[24,1576,27],{"href":26},". For the OS-scheduler route on Windows, see the sibling guide ",[24,1579,1581],{"href":1580},"\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",". To enrich the report the scheduler produces, see ",[24,1584,1586],{"href":1585},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002F","Building Multi-Sheet Excel Dashboards",", and to ship it on each run, ",[24,1589,1591],{"href":1590},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib",[1593,1594,1595],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":270,"searchDepth":334,"depth":334,"links":1597},[1598,1599,1600,1601,1602,1603,1604,1605,1606,1607],{"id":213,"depth":334,"text":214},{"id":259,"depth":334,"text":260},{"id":304,"depth":334,"text":305},{"id":1032,"depth":334,"text":1033},{"id":1146,"depth":334,"text":1147},{"id":1196,"depth":334,"text":1197},{"id":1347,"depth":334,"text":1348},{"id":1474,"depth":334,"text":1475},{"id":1550,"depth":334,"text":1551},{"id":1570,"depth":334,"text":1571},"2026-06-18","Schedule recurring Excel reports inside Python with APScheduler: a BlockingScheduler, cron and interval triggers, misfire handling, overlap prevention, and timezones.","md",[1612,1614,1616,1618,1620],{"q":1480,"a":1613},"Cron launches a new process per run and survives reboots without a resident process. APScheduler runs jobs inside one long-lived Python process — better for in-app scheduling, dynamic jobs, and timezone handling, but it needs a supervisor to stay alive. See Scheduling Python Excel Scripts with Cron for the cron approach.",{"q":1489,"a":1615},"APScheduler defaults to the host timezone, which is often UTC on servers. Pass timezone= to the scheduler (or per trigger) with an IANA name like \"Europe\u002FLondon\" so triggers fire at the local time you intend.",{"q":1503,"a":1617},"Auto-reloaders and re-imports can run your add_job code more than once. Use a stable id per job and replace_existing=True, so a re-add updates the existing job instead of creating a duplicate.",{"q":1519,"a":1619},"Yes — configure a jobstore (e.g. SQLAlchemy or Redis) so pending jobs survive a process restart. For a fixed in-code schedule like this one, the default in-memory store plus coalesce=True is usually enough.",{"q":1532,"a":1621},"With BlockingScheduler a long job blocks the next trigger; that's why max_instances=1 matters. For CPU-heavy reports, configure a ThreadPoolExecutor or ProcessPoolExecutor in the scheduler so runs don't starve each other.",{},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler",{"title":1625,"description":1626},"Schedule Excel Reports with APScheduler","Run recurring Python Excel reports with APScheduler: BlockingScheduler, CronTrigger and IntervalTrigger, coalesce and misfire_grace_time, max_instances, timezone setup.","schedule-recurring-excel-reports-with-apscheduler","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002Findex","long_tail","XbrBlnet8zvv0V_nSrrYgLyA2zXEjPotx45uJ_hT5s0",[1632,1635],{"title":1581,"path":1633,"stem":1634,"children":-1},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler\u002Findex",{"title":1636,"path":1637,"stem":1638,"children":-1},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python","formatting-and-charting-excel-reports-with-python\u002Findex",1781795518492]