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