[{"data":1,"prerenderedAt":1526},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers":1492,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers":1501,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers":1506,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers":1507},{"id":4,"title":5,"body":6,"dateModified":1459,"datePublished":1459,"description":1460,"extension":1461,"faq":1462,"keyword":1471,"kind":1472,"meta":1473,"navigation":211,"path":1482,"seo":1483,"slug":1486,"stem":1487,"topics":1488,"type":1490,"__hash__":1491},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers\u002Findex.md","Schedule an Excel Report with systemd Timers",{"type":7,"value":8,"toc":1446},"minimark",[9,28,169,174,177,407,410,414,426,537,556,566,570,577,648,657,784,788,794,835,853,963,966,970,973,1061,1076,1091,1095,1101,1160,1175,1179,1182,1239,1263,1284,1288,1346,1350,1377,1381,1387,1393,1403,1409,1413,1442],[10,11,12,13,18,19,23,24,27],"p",{},"cron has run report jobs for decades, and it still works. On a modern Linux server, systemd timers do the same job with fewer surprises: the job's output lands in the journal with timestamps, its exit status is recorded, missed runs after downtime can be caught up, and schedules such as \"the first Monday of the month\" are expressed directly. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with cron",", moves a weekday Excel report onto a service and timer. The unit files were checked with ",[20,21,22],"code",{},"systemd-analyze verify",", each schedule with ",[20,25,26],{},"systemd-analyze calendar",", and the logging behaviour with test runs.",[29,30,38,39,38,43,38,47,38,55,38,65,38,76,38,83,38,90,38,96,38,104,38,108,38,112,38,116,38,120,38,127,38,132,38,136,38,140,38,145,38,148,38,153,38,156,38,161,38,164],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"4 0 752 233","img",[34,35],"sd-vs-cron-t","sd-vs-cron-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  ",[40,41,42],"title",{"id":34},"The same weekday report under cron and under systemd",[44,45,46],"desc",{"id":35},"A cron line is short but runs with a minimal environment, mails or discards output, and silently skips runs while the machine is off. A systemd timer and service take two small files, but set the working directory and environment explicitly, send output to the journal, record the exit status, can catch up missed runs and can trigger an alert unit on failure.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"4","0","752","233","#ffffff",[48,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"20.0","16.0","351.0","201.0","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[66,67,69],"g",{"transform":68},"translate(182.5,29.0) scale(1.083)",[70,71],"path",{"d":72,"fill":73,"stroke":74,"style":75},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","none","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[77,78,82],"text",{"x":79,"y":80,"style":81},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","cron",[84,85],"line",{"x1":86,"y1":87,"x2":88,"y2":87,"stroke":63,"style":89},"36.0","91.0","355.0","stroke-width:1px",[77,91,95],{"x":92,"y":93,"style":94},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","one line",[66,97,99],{"transform":98},"translate(38.5,99.5) scale(0.625)",[70,100],{"d":101,"fill":73,"stroke":102,"style":103},"M5 12.5l4.5 4.5L19 7","var(--teal,#0f9488)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[77,105,107],{"x":92,"y":106,"style":94},"137.0","− minimal, surprising environment",[77,109,111],{"x":92,"y":110,"style":94},"163.0","− output mailed or lost",[77,113,115],{"x":92,"y":114,"style":94},"189.0","− runs missed while down are skipped",[48,117],{"x":118,"y":58,"width":59,"height":60,"rx":61,"fill":119,"stroke":102,"style":64},"389.0","#d9f4f1",[66,121,123],{"transform":122},"translate(551.5,29.0) scale(1.083)",[70,124],{"d":125,"fill":73,"stroke":126,"style":75},"M4 5h16v14H4z M7 9l3 3-3 3 M12 15h5","var(--teal-ink,#0b6157)",[77,128,131],{"x":129,"y":80,"style":130},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","systemd timer",[84,133],{"x1":134,"y1":87,"x2":135,"y2":87,"stroke":102,"style":89},"405.0","724.0",[77,137,139],{"x":138,"y":93,"style":94},"429.0","explicit directory and environment",[66,141,143],{"transform":142},"translate(407.5,99.5) scale(0.625)",[70,144],{"d":101,"fill":73,"stroke":102,"style":103},[77,146,147],{"x":138,"y":106,"style":94},"output and exit status in the journal",[66,149,151],{"transform":150},"translate(407.5,125.5) scale(0.625)",[70,152],{"d":101,"fill":73,"stroke":102,"style":103},[77,154,155],{"x":138,"y":110,"style":94},"Persistent=true catches up",[66,157,159],{"transform":158},"translate(407.5,151.5) scale(0.625)",[70,160],{"d":101,"fill":73,"stroke":102,"style":103},[77,162,163],{"x":138,"y":114,"style":94},"OnFailure= alert hook",[66,165,167],{"transform":166},"translate(407.5,177.5) scale(0.625)",[70,168],{"d":101,"fill":73,"stroke":102,"style":103},[170,171,173],"h2",{"id":172},"prerequisites","Prerequisites",[10,175,176],{},"A Linux machine with systemd, the report in a project folder with its own virtual environment, and the report script exiting non-zero on failure. That last point matters more than any unit setting: systemd judges success by the exit status. A minimal entry point that follows the rule:",[178,179,184],"pre",{"className":180,"code":181,"language":182,"meta":183,"style":183},"language-python shiki shiki-themes github-light github-dark-high-contrast","import logging\nimport sys\n\nlog = logging.getLogger(\"sales-report\")\n\ndef build_report():\n    log.info(\"building weekly sales report\")\n    # … read inputs, write reports\u002Fsales.xlsx …\n\ndef main():\n    logging.basicConfig(level=logging.INFO, format=\"%(levelname)s %(message)s\")\n    try:\n        build_report()\n    except Exception:\n        log.exception(\"report failed\")\n        return 1\n    return 0\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n","python","",[20,185,186,198,206,213,232,237,250,261,268,273,283,324,333,339,350,361,370,379,384,401],{"__ignoreMap":183},[187,188,190,194],"span",{"class":84,"line":189},1,[187,191,193],{"class":192},"s-kum","import",[187,195,197],{"class":196},"skGVy"," logging\n",[187,199,201,203],{"class":84,"line":200},2,[187,202,193],{"class":192},[187,204,205],{"class":196}," sys\n",[187,207,209],{"class":84,"line":208},3,[187,210,212],{"emptyLinePlaceholder":211},true,"\n",[187,214,216,219,222,225,229],{"class":84,"line":215},4,[187,217,218],{"class":196},"log ",[187,220,221],{"class":192},"=",[187,223,224],{"class":196}," logging.getLogger(",[187,226,228],{"class":227},"srMev","\"sales-report\"",[187,230,231],{"class":196},")\n",[187,233,235],{"class":84,"line":234},5,[187,236,212],{"emptyLinePlaceholder":211},[187,238,240,243,247],{"class":84,"line":239},6,[187,241,242],{"class":192},"def",[187,244,246],{"class":245},"s_Opv"," build_report",[187,248,249],{"class":196},"():\n",[187,251,253,256,259],{"class":84,"line":252},7,[187,254,255],{"class":196},"    log.info(",[187,257,258],{"class":227},"\"building weekly sales report\"",[187,260,231],{"class":196},[187,262,264],{"class":84,"line":263},8,[187,265,267],{"class":266},"s-wDw","    # … read inputs, write reports\u002Fsales.xlsx …\n",[187,269,271],{"class":84,"line":270},9,[187,272,212],{"emptyLinePlaceholder":211},[187,274,276,278,281],{"class":84,"line":275},10,[187,277,242],{"class":192},[187,279,280],{"class":245}," main",[187,282,249],{"class":196},[187,284,286,289,293,295,298,302,305,308,310,313,317,320,322],{"class":84,"line":285},11,[187,287,288],{"class":196},"    logging.basicConfig(",[187,290,292],{"class":291},"sa561","level",[187,294,221],{"class":192},[187,296,297],{"class":196},"logging.",[187,299,301],{"class":300},"sP0c6","INFO",[187,303,304],{"class":196},", ",[187,306,307],{"class":291},"format",[187,309,221],{"class":192},[187,311,312],{"class":227},"\"",[187,314,316],{"class":315},"sSjpA","%(levelname)s",[187,318,319],{"class":315}," %(message)s",[187,321,312],{"class":227},[187,323,231],{"class":196},[187,325,327,330],{"class":84,"line":326},12,[187,328,329],{"class":192},"    try",[187,331,332],{"class":196},":\n",[187,334,336],{"class":84,"line":335},13,[187,337,338],{"class":196},"        build_report()\n",[187,340,342,345,348],{"class":84,"line":341},14,[187,343,344],{"class":192},"    except",[187,346,347],{"class":300}," Exception",[187,349,332],{"class":196},[187,351,353,356,359],{"class":84,"line":352},15,[187,354,355],{"class":196},"        log.exception(",[187,357,358],{"class":227},"\"report failed\"",[187,360,231],{"class":196},[187,362,364,367],{"class":84,"line":363},16,[187,365,366],{"class":192},"        return",[187,368,369],{"class":300}," 1\n",[187,371,373,376],{"class":84,"line":372},17,[187,374,375],{"class":192},"    return",[187,377,378],{"class":300}," 0\n",[187,380,382],{"class":84,"line":381},18,[187,383,212],{"emptyLinePlaceholder":211},[187,385,387,390,393,396,399],{"class":84,"line":386},19,[187,388,389],{"class":192},"if",[187,391,392],{"class":300}," __name__",[187,394,395],{"class":192}," ==",[187,397,398],{"class":227}," \"__main__\"",[187,400,332],{"class":196},[187,402,404],{"class":84,"line":403},20,[187,405,406],{"class":196},"    sys.exit(main())\n",[10,408,409],{},"Log to standard error without timestamps: the journal adds its own.",[170,411,413],{"id":412},"step-1-write-the-service","Step 1 — Write the service",[10,415,416,417,421,422,425],{},"The service describes ",[418,419,420],"em",{},"how"," to run the job. ",[20,423,424],{},"Type=oneshot"," means \"run to completion\"; the working directory and environment are explicit, so the job behaves the same from a timer as from your shell:",[178,427,431],{"className":428,"code":429,"language":430,"meta":183,"style":183},"language-ini shiki shiki-themes github-light github-dark-high-contrast","# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fsales-report.service\n[Unit]\nDescription=Weekly sales Excel report\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nUser=reports\nWorkingDirectory=\u002Fsrv\u002Fsales-report\nEnvironmentFile=\u002Fsrv\u002Fsales-report\u002Freport.env\nEnvironment=PYTHONUNBUFFERED=1\nExecStart=\u002Fsrv\u002Fsales-report\u002F.venv\u002Fbin\u002Fpython -m sales_report\nTimeoutStartSec=30min\n","ini",[20,432,433,438,444,452,460,467,471,476,484,492,500,508,521,529],{"__ignoreMap":183},[187,434,435],{"class":84,"line":189},[187,436,437],{"class":266},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fsales-report.service\n",[187,439,440],{"class":84,"line":200},[187,441,443],{"class":442},"sMTad","[Unit]\n",[187,445,446,449],{"class":84,"line":208},[187,447,448],{"class":192},"Description",[187,450,451],{"class":196},"=Weekly sales Excel report\n",[187,453,454,457],{"class":84,"line":215},[187,455,456],{"class":192},"After",[187,458,459],{"class":196},"=network-online.target\n",[187,461,462,465],{"class":84,"line":234},[187,463,464],{"class":192},"Wants",[187,466,459],{"class":196},[187,468,469],{"class":84,"line":239},[187,470,212],{"emptyLinePlaceholder":211},[187,472,473],{"class":84,"line":252},[187,474,475],{"class":442},"[Service]\n",[187,477,478,481],{"class":84,"line":263},[187,479,480],{"class":192},"Type",[187,482,483],{"class":196},"=oneshot\n",[187,485,486,489],{"class":84,"line":270},[187,487,488],{"class":192},"User",[187,490,491],{"class":196},"=reports\n",[187,493,494,497],{"class":84,"line":275},[187,495,496],{"class":192},"WorkingDirectory",[187,498,499],{"class":196},"=\u002Fsrv\u002Fsales-report\n",[187,501,502,505],{"class":84,"line":285},[187,503,504],{"class":192},"EnvironmentFile",[187,506,507],{"class":196},"=\u002Fsrv\u002Fsales-report\u002Freport.env\n",[187,509,510,513,515,518],{"class":84,"line":326},[187,511,512],{"class":192},"Environment",[187,514,221],{"class":196},[187,516,517],{"class":192},"PYTHONUNBUFFERED",[187,519,520],{"class":196},"=1\n",[187,522,523,526],{"class":84,"line":335},[187,524,525],{"class":192},"ExecStart",[187,527,528],{"class":196},"=\u002Fsrv\u002Fsales-report\u002F.venv\u002Fbin\u002Fpython -m sales_report\n",[187,530,531,534],{"class":84,"line":341},[187,532,533],{"class":192},"TimeoutStartSec",[187,535,536],{"class":196},"=30min\n",[10,538,539,540,542,543,545,546,548,549,552,553,555],{},"Point ",[20,541,525],{}," at the virtual environment's own ",[20,544,182],{}," — no activation step is needed. ",[20,547,504],{}," holds settings and secrets as ",[20,550,551],{},"NAME=value"," lines; make it readable only by the service user. ",[20,554,533],{}," stops a hung run, such as a network share that never answers, instead of letting it block the next day's run.",[10,557,558,561,562,565],{},[20,559,560],{},"PYTHONUNBUFFERED=1"," fixes a confusing log: when standard output is not a terminal, Python buffers it until exit. In a test run, a warning printed to standard error appeared in the journal ",[418,563,564],{},"before"," the \"building report\" line the script had printed first.",[170,567,569],{"id":568},"step-2-write-the-timer","Step 2 — Write the timer",[10,571,572,573,576],{},"The timer describes ",[418,574,575],{},"when",". It has the same name as the service, so it starts it automatically:",[178,578,580],{"className":428,"code":579,"language":430,"meta":183,"style":183},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fsales-report.timer\n[Unit]\nDescription=Run the sales report on weekday mornings\n\n[Timer]\nOnCalendar=Mon..Fri 07:30\nPersistent=true\nRandomizedDelaySec=5min\n\n[Install]\nWantedBy=timers.target\n",[20,581,582,587,591,598,602,607,615,623,631,635,640],{"__ignoreMap":183},[187,583,584],{"class":84,"line":189},[187,585,586],{"class":266},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fsales-report.timer\n",[187,588,589],{"class":84,"line":200},[187,590,443],{"class":442},[187,592,593,595],{"class":84,"line":208},[187,594,448],{"class":192},[187,596,597],{"class":196},"=Run the sales report on weekday mornings\n",[187,599,600],{"class":84,"line":215},[187,601,212],{"emptyLinePlaceholder":211},[187,603,604],{"class":84,"line":234},[187,605,606],{"class":442},"[Timer]\n",[187,608,609,612],{"class":84,"line":239},[187,610,611],{"class":192},"OnCalendar",[187,613,614],{"class":196},"=Mon..Fri 07:30\n",[187,616,617,620],{"class":84,"line":252},[187,618,619],{"class":192},"Persistent",[187,621,622],{"class":196},"=true\n",[187,624,625,628],{"class":84,"line":263},[187,626,627],{"class":192},"RandomizedDelaySec",[187,629,630],{"class":196},"=5min\n",[187,632,633],{"class":84,"line":270},[187,634,212],{"emptyLinePlaceholder":211},[187,636,637],{"class":84,"line":275},[187,638,639],{"class":442},"[Install]\n",[187,641,642,645],{"class":84,"line":285},[187,643,644],{"class":192},"WantedBy",[187,646,647],{"class":196},"=timers.target\n",[10,649,650,653,654,656],{},[20,651,652],{},"Persistent=true"," makes systemd remember the last run: if the server was down at 07:30, the report runs once shortly after boot instead of being skipped until tomorrow. ",[20,655,627],{}," spreads jobs that share a schedule, so twenty reports scheduled at 07:30 do not all hit the database in the same second.",[29,658,38,663,38,666,38,669,38,672,38,679,38,686,38,694,38,700,38,705,38,709,38,713,38,719,38,725,38,731,38,735,38,737,38,739,38,742,38,748,38,753,38,756,38,758,38,760,38,765,38,772,38,777,38,780,38,782],{"viewBox":659,"role":32,"ariaLabelledBy":660,"xmlns":36,"style":37},"4 0 752 279",[661,662],"sd-persist-t","sd-persist-d",[40,664,665],{"id":661},"A missed run caught up by Persistent=true",[44,667,668],{"id":662},"The report is scheduled for 07:30 on weekdays. On Tuesday the server is down for maintenance from 06:00 to 09:10. Without Persistent the 07:30 run is skipped and the next report arrives on Wednesday. With Persistent=true systemd sees that the scheduled time passed and starts the report right after boot at 09:10.",[48,670],{"x":50,"y":51,"width":52,"height":671,"fill":54},"279",[84,673],{"x1":674,"y1":675,"x2":676,"y2":675,"stroke":677,"style":678},"94.0","139.5","666.0","var(--muted,#5b6780)","stroke-width:2.5px",[48,680],{"x":57,"y":58,"width":681,"height":682,"rx":683,"fill":684,"stroke":685,"style":64},"172.0","91.5","12","#fce9e9","var(--danger,#dc2626)",[66,687,689],{"transform":688},"translate(94.0,29.0) scale(1.000)",[70,690],{"d":691,"fill":73,"stroke":692,"style":693},"M6 6l12 12 M18 6L6 18","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[77,695,699],{"x":696,"y":697,"style":698},"106.0","74.5","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","06:00",[77,701,704],{"x":696,"y":702,"style":703},"93.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","server down",[84,706],{"x1":696,"y1":707,"x2":696,"y2":675,"stroke":677,"style":708},"107.5","stroke-width:1.5px",[710,711],"circle",{"cx":696,"cy":675,"r":712,"fill":685,"stroke":54,"style":64},"7.0",[48,714],{"x":715,"y":716,"width":681,"height":682,"rx":683,"fill":717,"stroke":718,"style":64},"202.7","171.5","#e7ebef","var(--line,#cdd5e6)",[66,720,722],{"transform":721},"translate(276.7,184.5) scale(1.000)",[70,723],{"d":72,"fill":73,"stroke":724,"style":693},"var(--text,#172033)",[77,726,730],{"x":727,"y":728,"style":729},"288.7","230.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","07:30",[77,732,734],{"x":727,"y":733,"style":703},"248.5","scheduled run missed",[84,736],{"x1":727,"y1":675,"x2":727,"y2":716,"stroke":677,"style":708},[710,738],{"cx":727,"cy":675,"r":712,"fill":677,"stroke":54,"style":64},[48,740],{"x":741,"y":58,"width":681,"height":682,"rx":683,"fill":119,"stroke":102,"style":64},"385.3",[66,743,745],{"transform":744},"translate(459.3,29.0) scale(1.000)",[70,746],{"d":747,"fill":73,"stroke":126,"style":693},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[77,749,752],{"x":750,"y":697,"style":751},"471.3","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","09:10",[77,754,755],{"x":750,"y":702,"style":703},"boot → report runs",[84,757],{"x1":750,"y1":707,"x2":750,"y2":675,"stroke":677,"style":708},[710,759],{"cx":750,"cy":675,"r":712,"fill":102,"stroke":54,"style":64},[48,761],{"x":762,"y":716,"width":681,"height":682,"rx":683,"fill":763,"stroke":764,"style":64},"568.0","#f0f4ff","var(--brand,#5b5cf0)",[66,766,768],{"transform":767},"translate(642.0,184.5) scale(1.000)",[70,769],{"d":770,"fill":73,"stroke":771,"style":693},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--brand-strong,#4338ca)",[77,773,776],{"x":774,"y":728,"style":775},"654.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Wed 07:30",[77,778,779],{"x":774,"y":733,"style":703},"normal schedule",[84,781],{"x1":774,"y1":675,"x2":774,"y2":716,"stroke":677,"style":708},[710,783],{"cx":774,"cy":675,"r":712,"fill":764,"stroke":54,"style":64},[170,785,787],{"id":786},"step-3-check-the-schedule-before-you-trust-it","Step 3 — Check the schedule before you trust it",[10,789,790,791,793],{},"Calendar expressions are easy to get subtly wrong. ",[20,792,26],{}," prints the next run times, so check every expression before deploying it:",[178,795,799],{"className":796,"code":797,"language":798,"meta":183,"style":183},"language-bash shiki shiki-themes github-light github-dark-high-contrast","systemd-analyze calendar --iterations=3 \"Mon..Fri 07:30\"\n#   Normalized form: Mon..Fri *-*-* 07:30:00\n#       Next elapse: Thu 2026-09-24 07:30:00 CEST\n#      Iteration #2: Fri 2026-09-25 07:30:00 CEST\n#      Iteration #3: Mon 2026-09-28 07:30:00 CEST\n","bash",[20,800,801,815,820,825,830],{"__ignoreMap":183},[187,802,803,806,809,812],{"class":84,"line":189},[187,804,805],{"class":442},"systemd-analyze",[187,807,808],{"class":227}," calendar",[187,810,811],{"class":300}," --iterations=3",[187,813,814],{"class":227}," \"Mon..Fri 07:30\"\n",[187,816,817],{"class":84,"line":200},[187,818,819],{"class":266},"#   Normalized form: Mon..Fri *-*-* 07:30:00\n",[187,821,822],{"class":84,"line":208},[187,823,824],{"class":266},"#       Next elapse: Thu 2026-09-24 07:30:00 CEST\n",[187,826,827],{"class":84,"line":215},[187,828,829],{"class":266},"#      Iteration #2: Fri 2026-09-25 07:30:00 CEST\n",[187,831,832],{"class":84,"line":234},[187,833,834],{"class":266},"#      Iteration #3: Mon 2026-09-28 07:30:00 CEST\n",[10,836,837,838,841,842,845,846,849,850,852],{},"Reporting calendars often need schedules that cron cannot express. In cron, a line with both a day of the month and a weekday runs when ",[418,839,840],{},"either"," matches, so ",[20,843,844],{},"0 8 1-7 * 1"," runs on every one of the first seven days ",[418,847,848],{},"and"," on every Monday. systemd combines them with ",[418,851,848],{},":",[29,854,38,859,38,862,38,865,38,868,38,872,38,877,38,882,38,885,38,889,38,893,38,898,38,900,38,905,38,907,38,910,38,913,38,917,38,919,38,922,38,924,38,927,38,930,38,934,38,936,38,940,38,942,38,945,38,948,38,952,38,954,38,958,38,960],{"viewBox":855,"role":32,"ariaLabelledBy":856,"xmlns":36,"style":37},"4 0 752 208",[857,858],"sd-calendar-t","sd-calendar-d",[40,860,861],{"id":857},"OnCalendar expressions for reporting schedules",[44,863,864],{"id":858},"Mon..Fri 07:30 runs on weekday mornings. Mon *-*-1..7 08:00 runs on the first Monday of each month, which cron cannot express in one line. *-*~01 18:00 runs on the last day of each month, whatever its length. Mon..Fri 07:30 Europe\u002FLondon runs at 07:30 London time regardless of the server's time zone. Each was checked with systemd-analyze calendar.",[48,866],{"x":50,"y":51,"width":52,"height":867,"fill":54},"208",[48,869],{"x":57,"y":58,"width":870,"height":871,"rx":51,"fill":54,"stroke":718,"style":89},"150.2","40.0",[48,873],{"x":874,"y":58,"width":875,"height":871,"rx":51,"fill":763,"stroke":764,"style":876},"170.2","284.9","stroke-width:1.2px",[77,878,611],{"x":879,"y":880,"style":881},"312.7","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[48,883],{"x":884,"y":58,"width":875,"height":871,"rx":51,"fill":763,"stroke":764,"style":876},"455.1",[77,886,888],{"x":887,"y":880,"style":881},"597.6","Next runs checked",[48,890],{"x":57,"y":891,"width":870,"height":892,"rx":51,"fill":717,"stroke":718,"style":89},"56.0","34.0",[77,894,897],{"x":895,"y":80,"style":896},"32.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","weekday mornings",[48,899],{"x":874,"y":891,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,901,904],{"x":879,"y":902,"style":903},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Mon..Fri 07:30",[48,906],{"x":884,"y":891,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,908,909],{"x":887,"y":902,"style":903},"Thu 24, Fri 25, Mon 28 Sep",[48,911],{"x":57,"y":912,"width":870,"height":892,"rx":51,"fill":717,"stroke":718,"style":89},"90.0",[77,914,916],{"x":895,"y":915,"style":896},"112.0","first Monday",[48,918],{"x":874,"y":912,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,920,921],{"x":879,"y":93,"style":903},"Mon *-*-1..7 08:00",[48,923],{"x":884,"y":912,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,925,926],{"x":887,"y":93,"style":903},"5 Oct, 2 Nov, 7 Dec",[48,928],{"x":57,"y":929,"width":870,"height":892,"rx":51,"fill":717,"stroke":718,"style":89},"124.0",[77,931,933],{"x":895,"y":932,"style":896},"146.0","last day of month",[48,935],{"x":874,"y":929,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,937,939],{"x":879,"y":938,"style":903},"145.0","*-*~01 18:00",[48,941],{"x":884,"y":929,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,943,944],{"x":887,"y":938,"style":903},"30 Sep, 31 Oct, 30 Nov",[48,946],{"x":57,"y":947,"width":870,"height":892,"rx":51,"fill":717,"stroke":718,"style":89},"158.0",[77,949,951],{"x":895,"y":950,"style":896},"180.0","fixed time zone",[48,953],{"x":874,"y":947,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,955,957],{"x":879,"y":956,"style":903},"179.0","Mon..Fri 07:30 Europe\u002FLondon",[48,959],{"x":884,"y":947,"width":875,"height":892,"rx":51,"fill":54,"stroke":718,"style":89},[77,961,962],{"x":887,"y":956,"style":903},"07:30 in London",[10,964,965],{},"The time zone suffix matters when the server runs in UTC but readers expect the report at 07:30 local time, including across daylight saving changes.",[170,967,969],{"id":968},"step-4-enable-test-and-inspect","Step 4 — Enable, test and inspect",[10,971,972],{},"Load the units, run the service once by hand to prove it works, then enable the timer:",[178,974,976],{"className":796,"code":975,"language":798,"meta":183,"style":183},"sudo systemctl daemon-reload\nsudo systemctl start sales-report.service        # one run now, waits for completion\nsudo systemctl enable --now sales-report.timer\nsystemctl list-timers sales-report.timer         # next and last run\njournalctl -u sales-report.service --since today\nsystemctl status sales-report.service            # last result and exit code\n",[20,977,978,989,1004,1019,1033,1049],{"__ignoreMap":183},[187,979,980,983,986],{"class":84,"line":189},[187,981,982],{"class":442},"sudo",[187,984,985],{"class":227}," systemctl",[187,987,988],{"class":227}," daemon-reload\n",[187,990,991,993,995,998,1001],{"class":84,"line":200},[187,992,982],{"class":442},[187,994,985],{"class":227},[187,996,997],{"class":227}," start",[187,999,1000],{"class":227}," sales-report.service",[187,1002,1003],{"class":266},"        # one run now, waits for completion\n",[187,1005,1006,1008,1010,1013,1016],{"class":84,"line":208},[187,1007,982],{"class":442},[187,1009,985],{"class":227},[187,1011,1012],{"class":227}," enable",[187,1014,1015],{"class":300}," --now",[187,1017,1018],{"class":227}," sales-report.timer\n",[187,1020,1021,1024,1027,1030],{"class":84,"line":215},[187,1022,1023],{"class":442},"systemctl",[187,1025,1026],{"class":227}," list-timers",[187,1028,1029],{"class":227}," sales-report.timer",[187,1031,1032],{"class":266},"         # next and last run\n",[187,1034,1035,1038,1041,1043,1046],{"class":84,"line":234},[187,1036,1037],{"class":442},"journalctl",[187,1039,1040],{"class":300}," -u",[187,1042,1000],{"class":227},[187,1044,1045],{"class":300}," --since",[187,1047,1048],{"class":227}," today\n",[187,1050,1051,1053,1056,1058],{"class":84,"line":239},[187,1052,1023],{"class":442},[187,1054,1055],{"class":227}," status",[187,1057,1000],{"class":227},[187,1059,1060],{"class":266},"            # last result and exit code\n",[10,1062,1063,1064,1067,1068,1071,1072,1075],{},"A failed run shows in ",[20,1065,1066],{},"systemctl status"," as ",[20,1069,1070],{},"Failed with result 'exit-code'"," together with the status number, and ",[20,1073,1074],{},"systemctl --failed"," lists every failed unit on the machine — a single place to look after a long weekend.",[10,1077,1078,1079,1082,1083,1086,1087,1090],{},"On a machine where you have no root access, the same files go in ",[20,1080,1081],{},"~\u002F.config\u002Fsystemd\u002Fuser\u002F"," and every command takes ",[20,1084,1085],{},"--user",". User timers only run while you are logged in unless an administrator enables lingering for your account with ",[20,1088,1089],{},"loginctl enable-linger \u003Cuser>",".",[170,1092,1094],{"id":1093},"step-5-alert-on-failure","Step 5 — Alert on failure",[10,1096,1097,1100],{},[20,1098,1099],{},"OnFailure="," starts another unit whenever the service fails. A small templated alert unit receives the failed unit's name:",[178,1102,1104],{"className":428,"code":1103,"language":430,"meta":183,"style":183},"# in sales-report.service, [Unit] section\nOnFailure=report-alert@%n.service\n\n# \u002Fetc\u002Fsystemd\u002Fsystem\u002Freport-alert@.service\n[Unit]\nDescription=Alert about failed %i\n\n[Service]\nType=oneshot\nExecStart=\u002Fsrv\u002Fsales-report\u002F.venv\u002Fbin\u002Fpython -m report_alert %i\n",[20,1105,1106,1111,1119,1123,1128,1132,1139,1143,1147,1153],{"__ignoreMap":183},[187,1107,1108],{"class":84,"line":189},[187,1109,1110],{"class":266},"# in sales-report.service, [Unit] section\n",[187,1112,1113,1116],{"class":84,"line":200},[187,1114,1115],{"class":192},"OnFailure",[187,1117,1118],{"class":196},"=report-alert@%n.service\n",[187,1120,1121],{"class":84,"line":208},[187,1122,212],{"emptyLinePlaceholder":211},[187,1124,1125],{"class":84,"line":215},[187,1126,1127],{"class":266},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Freport-alert@.service\n",[187,1129,1130],{"class":84,"line":234},[187,1131,443],{"class":442},[187,1133,1134,1136],{"class":84,"line":239},[187,1135,448],{"class":192},[187,1137,1138],{"class":196},"=Alert about failed %i\n",[187,1140,1141],{"class":84,"line":252},[187,1142,212],{"emptyLinePlaceholder":211},[187,1144,1145],{"class":84,"line":263},[187,1146,475],{"class":442},[187,1148,1149,1151],{"class":84,"line":270},[187,1150,480],{"class":192},[187,1152,483],{"class":196},[187,1154,1155,1157],{"class":84,"line":275},[187,1156,525],{"class":192},[187,1158,1159],{"class":196},"=\u002Fsrv\u002Fsales-report\u002F.venv\u002Fbin\u002Fpython -m report_alert %i\n",[10,1161,1162,1165,1166,1169,1170,1174],{},[20,1163,1164],{},"report_alert"," can read the last lines with ",[20,1167,1168],{},"journalctl -u \u003Cunit> -n 50 --no-pager"," and send them by email or chat, as in ",[14,1171,1173],{"href":1172},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fsend-an-alert-when-an-excel-report-job-fails\u002F","Send an Alert When an Excel Report Job Fails",". Because the hook lives in systemd, it also fires when the job is killed for running out of memory or hits its timeout — cases where the script's own exception handler never runs.",[170,1176,1178],{"id":1177},"step-6-put-limits-around-the-job","Step 6 — Put limits around the job",[10,1180,1181],{},"A report that reads a much larger export than usual can exhaust the server's memory and take other services down with it. A service can carry its own limits, and a few sandboxing settings keep a buggy or compromised script from writing anywhere but its output folder:",[178,1183,1185],{"className":428,"code":1184,"language":430,"meta":183,"style":183},"# in sales-report.service, [Service] section\nMemoryMax=2G\nCPUQuota=100%\nProtectSystem=strict\nReadWritePaths=\u002Fsrv\u002Fsales-report\u002Freports \u002Fsrv\u002Fsales-report\u002Fstate\nPrivateTmp=yes\nNoNewPrivileges=yes\n",[20,1186,1187,1192,1200,1208,1216,1224,1232],{"__ignoreMap":183},[187,1188,1189],{"class":84,"line":189},[187,1190,1191],{"class":266},"# in sales-report.service, [Service] section\n",[187,1193,1194,1197],{"class":84,"line":200},[187,1195,1196],{"class":192},"MemoryMax",[187,1198,1199],{"class":196},"=2G\n",[187,1201,1202,1205],{"class":84,"line":208},[187,1203,1204],{"class":192},"CPUQuota",[187,1206,1207],{"class":196},"=100%\n",[187,1209,1210,1213],{"class":84,"line":215},[187,1211,1212],{"class":192},"ProtectSystem",[187,1214,1215],{"class":196},"=strict\n",[187,1217,1218,1221],{"class":84,"line":234},[187,1219,1220],{"class":192},"ReadWritePaths",[187,1222,1223],{"class":196},"=\u002Fsrv\u002Fsales-report\u002Freports \u002Fsrv\u002Fsales-report\u002Fstate\n",[187,1225,1226,1229],{"class":84,"line":239},[187,1227,1228],{"class":192},"PrivateTmp",[187,1230,1231],{"class":196},"=yes\n",[187,1233,1234,1237],{"class":84,"line":252},[187,1235,1236],{"class":192},"NoNewPrivileges",[187,1238,1231],{"class":196},[10,1240,1241,1243,1244,1247,1248,1251,1252,1254,1255,1257,1258,1262],{},[20,1242,1196],{}," is a hard ceiling for the whole unit. In a test run with a 150 MB limit, a script allocating 400 MB was stopped at once, and the journal recorded ",[20,1245,1246],{},"The kernel OOM killer killed some processes in this unit"," followed by ",[20,1249,1250],{},"Failed with result 'oom-kill'"," — a clear, specific failure that fires ",[20,1253,1099],{}," like any other, instead of a server grinding into swap. Size the limit from a normal run's peak, which ",[20,1256,1066],{}," and the journal report as \"Memory peak\" after each run, and leave generous headroom for month-end files. ",[14,1259,1261],{"href":1260},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-memory-when-reading-excel-with-pandas\u002F","Reduce Memory When Reading Excel with pandas"," helps when the peak is too high to begin with.",[10,1264,1265,1268,1269,1271,1272,1275,1276,1279,1280,1283],{},[20,1266,1267],{},"ProtectSystem=strict"," makes the whole file system read-only for the job except the paths listed in ",[20,1270,1220],{},", so the report can write its output and state and nothing else. ",[20,1273,1274],{},"PrivateTmp=yes"," gives it a private ",[20,1277,1278],{},"\u002Ftmp",", which also means leftover temporary files disappear with the run. ",[20,1281,1282],{},"systemd-analyze security sales-report.service"," scores the unit and lists the settings that would tighten it further.",[170,1285,1287],{"id":1286},"common-pitfalls","Common pitfalls",[1289,1290,1291,1310,1319,1336],"ul",{},[1292,1293,1294,1298,1299,1301,1302,1305,1306,1309],"li",{},[1295,1296,1297],"strong",{},"Relative paths."," Without ",[20,1300,496],{},", a service starts in ",[20,1303,1304],{},"\u002F",", and ",[20,1307,1308],{},"open(\"config.toml\")"," fails. Set it, or build paths from the script's location.",[1292,1311,1312,1315,1316,1090],{},[1295,1313,1314],{},"Editing units without reloading."," Changes take effect only after ",[20,1317,1318],{},"systemctl daemon-reload",[1292,1320,1321,1324,1325,1328,1329,1332,1333,1090],{},[1295,1322,1323],{},"Enabling the service instead of the timer."," ",[20,1326,1327],{},"enable sales-report.service"," does nothing useful for a oneshot job without an ",[20,1330,1331],{},"[Install]"," section. Enable the ",[20,1334,1335],{},".timer",[1292,1337,1338,1341,1342,1090],{},[1295,1339,1340],{},"Excel-dependent scripts."," A Linux timer cannot drive desktop Excel. Reports that need Excel belong on Windows, covered in ",[14,1343,1345],{"href":1344},"\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",[170,1347,1349],{"id":1348},"conclusion","Conclusion",[10,1351,1352,1353,1355,1356,1358,1359,1361,1362,1364,1365,1367,1368,1370,1371,1373,1374,1376],{},"Put the job in a ",[20,1354,424],{}," service with an explicit working directory, environment file, ",[20,1357,560],{}," and a start timeout, and the schedule in a matching timer with ",[20,1360,652],{},". Check every ",[20,1363,611],{}," expression with ",[20,1366,26],{},", test the service by starting it once, read results with ",[20,1369,1037],{}," and ",[20,1372,1066],{},", and add an ",[20,1375,1099],{}," unit so failures reach a person.",[170,1378,1380],{"id":1379},"frequently-asked-questions","Frequently asked questions",[10,1382,1383,1386],{},[1295,1384,1385],{},"How do I schedule a Python script with systemd?","\nCreate a .service unit with Type=oneshot whose ExecStart runs the virtual environment's python on your script, and a .timer unit with the same name and an OnCalendar schedule. Enable and start the timer with systemctl enable --now name.timer.",[10,1388,1389,1392],{},[1295,1390,1391],{},"How do I check an OnCalendar expression before using it?","\nRun systemd-analyze calendar --iterations=5 \"Mon..Fri 07:30\". It prints the normalised expression and the next run times, so a schedule mistake shows up before the first missed report.",[10,1394,1395,1398,1399,1402],{},[1295,1396,1397],{},"Does a systemd timer run a job that was missed while the server was off?","\nOnly with Persistent=true in the ",[187,1400,1401],{},"Timer"," section. systemd then records the last run and, after boot, starts the service once if a scheduled time passed while the machine was down.",[10,1404,1405,1408],{},[1295,1406,1407],{},"Where does the output of a systemd timer job go?","\nTo the journal. Read it with journalctl -u name.service (add --user for user units). Set PYTHONUNBUFFERED=1 in the service, or Python's standard output is written at exit, after messages on standard error.",[170,1410,1412],{"id":1411},"related","Related",[1289,1414,1415,1421,1428,1435],{},[1292,1416,1417,1418,1420],{},"Up one level: ",[14,1419,17],{"href":16}," — running reports on a schedule.",[1292,1422,1423,1427],{},[14,1424,1426],{"href":1425},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-python-excel-report-in-docker\u002F","Run a Python Excel Report in Docker"," — packaging the job so any scheduler can run it.",[1292,1429,1430,1434],{},[14,1431,1433],{"href":1432},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002F","Schedule Recurring Excel Reports with APScheduler"," — scheduling from inside a long-running Python process.",[1292,1436,1437,1441],{},[14,1438,1440],{"href":1439},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," — when the journal is not an option.",[1443,1444,1445],"style",{},"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 .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html .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 .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}",{"title":183,"searchDepth":200,"depth":200,"links":1447},[1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458],{"id":172,"depth":200,"text":173},{"id":412,"depth":200,"text":413},{"id":568,"depth":200,"text":569},{"id":786,"depth":200,"text":787},{"id":968,"depth":200,"text":969},{"id":1093,"depth":200,"text":1094},{"id":1177,"depth":200,"text":1178},{"id":1286,"depth":200,"text":1287},{"id":1348,"depth":200,"text":1349},{"id":1379,"depth":200,"text":1380},{"id":1411,"depth":200,"text":1412},"2026-09-23","Run a Python Excel report on a Linux server with a systemd service and timer instead of cron — a oneshot service with its own working directory and environment file, calendar expressions checked before deployment, catch-up runs after downtime, logs in the journal, failure status you can query and an OnFailure alert hook.","md",[1463,1465,1467,1469],{"q":1385,"a":1464},"Create a .service unit with Type=oneshot whose ExecStart runs the virtual environment's python on your script, and a .timer unit with the same name and an OnCalendar schedule. Enable and start the timer with systemctl enable --now name.timer.",{"q":1391,"a":1466},"Run systemd-analyze calendar --iterations=5 \"Mon..Fri 07:30\". It prints the normalised expression and the next run times, so a schedule mistake shows up before the first missed report.",{"q":1397,"a":1468},"Only with Persistent=true in the [Timer] section. systemd then records the last run and, after boot, starts the service once if a scheduled time passed while the machine was down.",{"q":1407,"a":1470},"To the journal. Read it with journalctl -u name.service (add --user for user units). Set PYTHONUNBUFFERED=1 in the service, or Python's standard output is written at exit, after messages on standard error.","schedule an excel report with systemd timers",null,{"breadcrumb":1474},[1475,1477,1480],{"name":1476,"item":1304},"Home",{"name":1478,"item":1479},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":1481,"item":16},"Scheduling Python Excel Scripts with Cron","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers",{"title":1484,"description":1485},"Schedule a Python Excel Report with systemd Timers (Linux)","Replace a cron line with a systemd .service and .timer for a Python Excel report — Type=oneshot, WorkingDirectory, EnvironmentFile, PYTHONUNBUFFERED, OnCalendar tested with systemd-analyze, Persistent=true, RandomizedDelaySec, first-Monday and last-day schedules, journalctl, linger for user timers and OnFailure.","schedule-an-excel-report-with-systemd-timers","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers\u002Findex",[1489],"scheduling","how-to","Ii-ofnIHpH7lrk-Cg_d3LyeHyf0gEVpJXg9qv2ZB_xQ",[1493,1497],{"title":1494,"path":1495,"stem":1496,"children":-1},"Schedule a Python Excel Script on macOS with launchd","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd\u002Findex",{"title":1498,"path":1499,"stem":1500,"children":-1},"Schedule Excel Reports with Celery Beat","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat\u002Findex",[1502,1504],{"path":1503,"title":1478},"\u002Fautomating-reporting-workflows",{"path":1505,"title":1481},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[1508,1511,1514,1517,1520,1523],{"path":1509,"title":1510,"kind":1490},"\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":1512,"title":1513,"kind":1490},"\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":1515,"title":1516,"kind":1490},"\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":1518,"title":1519,"kind":1490},"\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":1521,"title":1522,"kind":1490},"\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":1524,"title":1525,"kind":1490},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",1790972140085]