[{"data":1,"prerenderedAt":2062},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd":2030,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd":2037,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd":2042,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd":2043},{"id":4,"title":5,"body":6,"dateModified":1997,"datePublished":1997,"description":1998,"extension":1999,"faq":2000,"keyword":2009,"kind":2010,"meta":2011,"navigation":213,"path":2020,"seo":2021,"slug":2024,"stem":2025,"topics":2026,"type":2028,"__hash__":2029},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd\u002Findex.md","Schedule a Python Excel Script on macOS with launchd",{"type":7,"value":8,"toc":1984},"minimark",[9,37,42,52,153,157,172,582,608,612,638,857,868,1114,1118,1121,1377,1388,1392,1403,1530,1538,1652,1656,1670,1788,1808,1812,1827,1859,1863,1900,1904,1919,1923,1929,1935,1941,1947,1951,1980],[10,11,12,13,17,18,21,22,26,27,32,33,36],"p",{},"Plenty of reporting automation runs on an analyst's or a small team's Mac: the script that builds the Monday numbers from an export in ",[14,15,16],"code",{},"~\u002FReports",", the one that refreshes a workbook every morning before the stand-up. macOS still ships ",[14,19,20],{},"cron",", but Apple's supported scheduler is ",[23,24,25],"em",{},"launchd",", and it behaves better on a laptop: a job scheduled while the Mac was asleep runs when it wakes instead of being skipped. A launchd job is a small XML property list describing what to run and when. This page, part of ",[28,29,31],"a",{"href":30},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",", generates that file from Python — so the schedule is code, not hand-edited XML — checks it, and shows the ",[14,34,35],{},"launchctl"," commands that load, test and remove it.",[38,39,41],"h2",{"id":40},"prerequisites","Prerequisites",[10,43,44,45,48,49,51],{},"The report is a Python script in a project folder with its own virtual environment — ",[14,46,47],{},"~\u002Freports\u002Fweekly_sales\u002F.venv"," — so the scheduled job uses exactly the packages the script was tested with. Generating and checking the property list needs only the standard library, which is how the examples below ran; the ",[14,50,35],{}," commands in Steps 5 and 6 are macOS-only and are shown for completeness.",[53,54,62,63,62,67,62,71,62,79,62,89,62,100,62,107,62,112,62,117,62,124,62,129,62,133,62,138,62,145,62,149],"svg",{"viewBox":55,"role":56,"ariaLabelledBy":57,"xmlns":60,"style":61},"54 0 652 331","img",[58,59],"ld-where-t","ld-where-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:652px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[64,65,66],"title",{"id":58},"Where launchd jobs live",[68,69,70],"desc",{"id":59},"Three layers. A user LaunchAgent in the home folder runs as that user while they are logged in, which suits personal reports. A LaunchDaemon in \u002FLibrary runs as root at boot without anyone logged in. Both are property list files that launchctl loads.",[72,73],"rect",{"x":74,"y":75,"width":76,"height":77,"fill":78},"54","0","652","331","#ffffff",[72,80],{"x":81,"y":82,"width":83,"height":84,"rx":85,"fill":86,"stroke":87,"style":88},"70.0","16.0","620.0","93.0","12","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[90,91,93],"g",{"transform":92},"translate(368.0,29.0) scale(1.000)",[94,95],"path",{"d":96,"fill":97,"stroke":98,"style":99},"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21c0-4 3.6-7 8-7s8 3 8 7","none","var(--teal-ink,#0b6157)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[101,102,106],"text",{"x":103,"y":104,"style":105},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","~\u002FLibrary\u002FLaunchAgents",[101,108,111],{"x":103,"y":109,"style":110},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","runs as you, while you are logged in — reports on your Mac",[72,113],{"x":81,"y":114,"width":83,"height":84,"rx":85,"fill":115,"stroke":116,"style":88},"119.0","#f0f4ff","var(--brand,#5b5cf0)",[90,118,120],{"transform":119},"translate(368.0,132.0) scale(1.000)",[94,121],{"d":122,"fill":97,"stroke":123,"style":99},"M4 4h16v6H4z M4 14h16v6H4z M8 7h.01 M8 17h.01","var(--brand-strong,#4338ca)",[101,125,128],{"x":103,"y":126,"style":127},"178.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","\u002FLibrary\u002FLaunchDaemons",[101,130,132],{"x":103,"y":131,"style":110},"198.0","runs as root at boot, nobody logged in — a shared machine",[72,134],{"x":81,"y":135,"width":83,"height":84,"rx":85,"fill":136,"stroke":137,"style":88},"222.0","#e7ebef","var(--line,#cdd5e6)",[90,139,141],{"transform":140},"translate(368.0,235.0) scale(1.000)",[94,142],{"d":143,"fill":97,"stroke":144,"style":99},"M4 5h16v14H4z M7 9l3 3-3 3 M12 15h5","var(--text,#172033)",[101,146,35],{"x":103,"y":147,"style":148},"281.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[101,150,152],{"x":103,"y":151,"style":110},"301.0","bootstrap, kickstart, print, bootout",[38,154,156],{"id":155},"step-1-describe-the-job-in-python","Step 1 — Describe the job in Python",[10,158,159,160,163,164,167,168,171],{},"A launch agent is a dictionary: a unique ",[14,161,162],{},"Label",", the command as ",[14,165,166],{},"ProgramArguments",", the schedule, and where to write output. Build it as a Python dictionary and write it with ",[14,169,170],{},"plistlib",", which produces valid XML with the right types:",[173,174,179],"pre",{"className":175,"code":176,"language":177,"meta":178,"style":178},"language-python shiki shiki-themes github-light github-dark-high-contrast","import plistlib\nfrom pathlib import Path\n\nHOME = Path(\"\u002FUsers\u002Fanalyst\")\nPROJECT = HOME \u002F \"reports\" \u002F \"weekly_sales\"\nLABEL = \"com.acme.reports.weekly-sales\"\n\njob = {\n    \"Label\": LABEL,\n    \"ProgramArguments\": [str(PROJECT \u002F \".venv\" \u002F \"bin\" \u002F \"python\"), str(PROJECT \u002F \"build_report.py\")],\n    \"WorkingDirectory\": str(PROJECT),\n    \"StartCalendarInterval\": [{\"Weekday\": 1, \"Hour\": 7, \"Minute\": 0}],\n    \"StandardOutPath\": str(PROJECT \u002F \"logs\" \u002F \"weekly_sales.out.log\"),\n    \"StandardErrorPath\": str(PROJECT \u002F \"logs\" \u002F \"weekly_sales.err.log\"),\n    \"EnvironmentVariables\": {\"REPORT_ENV\": \"production\", \"PYTHONUNBUFFERED\": \"1\"},\n    \"ProcessType\": \"Background\",\n}\n\nplist_path = Path(f\"{LABEL}.plist\")\nwith plist_path.open(\"wb\") as f:\n    plistlib.dump(job, f)\nprint(plist_path.read_text()[:420])\n","python","",[14,180,181,194,208,215,235,257,268,273,285,299,348,365,405,431,456,486,499,505,510,540,561,567],{"__ignoreMap":178},[182,183,186,190],"span",{"class":184,"line":185},"line",1,[182,187,189],{"class":188},"s-kum","import",[182,191,193],{"class":192},"skGVy"," plistlib\n",[182,195,197,200,203,205],{"class":184,"line":196},2,[182,198,199],{"class":188},"from",[182,201,202],{"class":192}," pathlib ",[182,204,189],{"class":188},[182,206,207],{"class":192}," Path\n",[182,209,211],{"class":184,"line":210},3,[182,212,214],{"emptyLinePlaceholder":213},true,"\n",[182,216,218,222,225,228,232],{"class":184,"line":217},4,[182,219,221],{"class":220},"sP0c6","HOME",[182,223,224],{"class":188}," =",[182,226,227],{"class":192}," Path(",[182,229,231],{"class":230},"srMev","\"\u002FUsers\u002Fanalyst\"",[182,233,234],{"class":192},")\n",[182,236,238,241,243,246,249,252,254],{"class":184,"line":237},5,[182,239,240],{"class":220},"PROJECT",[182,242,224],{"class":188},[182,244,245],{"class":220}," HOME",[182,247,248],{"class":188}," \u002F",[182,250,251],{"class":230}," \"reports\"",[182,253,248],{"class":188},[182,255,256],{"class":230}," \"weekly_sales\"\n",[182,258,260,263,265],{"class":184,"line":259},6,[182,261,262],{"class":220},"LABEL",[182,264,224],{"class":188},[182,266,267],{"class":230}," \"com.acme.reports.weekly-sales\"\n",[182,269,271],{"class":184,"line":270},7,[182,272,214],{"emptyLinePlaceholder":213},[182,274,276,279,282],{"class":184,"line":275},8,[182,277,278],{"class":192},"job ",[182,280,281],{"class":188},"=",[182,283,284],{"class":192}," {\n",[182,286,288,291,294,296],{"class":184,"line":287},9,[182,289,290],{"class":230},"    \"Label\"",[182,292,293],{"class":192},": ",[182,295,262],{"class":220},[182,297,298],{"class":192},",\n",[182,300,302,305,308,311,314,316,318,321,323,326,328,331,334,336,338,340,342,345],{"class":184,"line":301},10,[182,303,304],{"class":230},"    \"ProgramArguments\"",[182,306,307],{"class":192},": [",[182,309,310],{"class":220},"str",[182,312,313],{"class":192},"(",[182,315,240],{"class":220},[182,317,248],{"class":188},[182,319,320],{"class":230}," \".venv\"",[182,322,248],{"class":188},[182,324,325],{"class":230}," \"bin\"",[182,327,248],{"class":188},[182,329,330],{"class":230}," \"python\"",[182,332,333],{"class":192},"), ",[182,335,310],{"class":220},[182,337,313],{"class":192},[182,339,240],{"class":220},[182,341,248],{"class":188},[182,343,344],{"class":230}," \"build_report.py\"",[182,346,347],{"class":192},")],\n",[182,349,351,354,356,358,360,362],{"class":184,"line":350},11,[182,352,353],{"class":230},"    \"WorkingDirectory\"",[182,355,293],{"class":192},[182,357,310],{"class":220},[182,359,313],{"class":192},[182,361,240],{"class":220},[182,363,364],{"class":192},"),\n",[182,366,368,371,374,377,379,382,385,388,390,393,395,398,400,402],{"class":184,"line":367},12,[182,369,370],{"class":230},"    \"StartCalendarInterval\"",[182,372,373],{"class":192},": [{",[182,375,376],{"class":230},"\"Weekday\"",[182,378,293],{"class":192},[182,380,381],{"class":220},"1",[182,383,384],{"class":192},", ",[182,386,387],{"class":230},"\"Hour\"",[182,389,293],{"class":192},[182,391,392],{"class":220},"7",[182,394,384],{"class":192},[182,396,397],{"class":230},"\"Minute\"",[182,399,293],{"class":192},[182,401,75],{"class":220},[182,403,404],{"class":192},"}],\n",[182,406,408,411,413,415,417,419,421,424,426,429],{"class":184,"line":407},13,[182,409,410],{"class":230},"    \"StandardOutPath\"",[182,412,293],{"class":192},[182,414,310],{"class":220},[182,416,313],{"class":192},[182,418,240],{"class":220},[182,420,248],{"class":188},[182,422,423],{"class":230}," \"logs\"",[182,425,248],{"class":188},[182,427,428],{"class":230}," \"weekly_sales.out.log\"",[182,430,364],{"class":192},[182,432,434,437,439,441,443,445,447,449,451,454],{"class":184,"line":433},14,[182,435,436],{"class":230},"    \"StandardErrorPath\"",[182,438,293],{"class":192},[182,440,310],{"class":220},[182,442,313],{"class":192},[182,444,240],{"class":220},[182,446,248],{"class":188},[182,448,423],{"class":230},[182,450,248],{"class":188},[182,452,453],{"class":230}," \"weekly_sales.err.log\"",[182,455,364],{"class":192},[182,457,459,462,465,468,470,473,475,478,480,483],{"class":184,"line":458},15,[182,460,461],{"class":230},"    \"EnvironmentVariables\"",[182,463,464],{"class":192},": {",[182,466,467],{"class":230},"\"REPORT_ENV\"",[182,469,293],{"class":192},[182,471,472],{"class":230},"\"production\"",[182,474,384],{"class":192},[182,476,477],{"class":230},"\"PYTHONUNBUFFERED\"",[182,479,293],{"class":192},[182,481,482],{"class":230},"\"1\"",[182,484,485],{"class":192},"},\n",[182,487,489,492,494,497],{"class":184,"line":488},16,[182,490,491],{"class":230},"    \"ProcessType\"",[182,493,293],{"class":192},[182,495,496],{"class":230},"\"Background\"",[182,498,298],{"class":192},[182,500,502],{"class":184,"line":501},17,[182,503,504],{"class":192},"}\n",[182,506,508],{"class":184,"line":507},18,[182,509,214],{"emptyLinePlaceholder":213},[182,511,513,516,518,520,523,526,530,532,535,538],{"class":184,"line":512},19,[182,514,515],{"class":192},"plist_path ",[182,517,281],{"class":188},[182,519,227],{"class":192},[182,521,522],{"class":188},"f",[182,524,525],{"class":230},"\"",[182,527,529],{"class":528},"sSjpA","{",[182,531,262],{"class":220},[182,533,534],{"class":528},"}",[182,536,537],{"class":230},".plist\"",[182,539,234],{"class":192},[182,541,543,546,549,552,555,558],{"class":184,"line":542},20,[182,544,545],{"class":188},"with",[182,547,548],{"class":192}," plist_path.open(",[182,550,551],{"class":230},"\"wb\"",[182,553,554],{"class":192},") ",[182,556,557],{"class":188},"as",[182,559,560],{"class":192}," f:\n",[182,562,564],{"class":184,"line":563},21,[182,565,566],{"class":192},"    plistlib.dump(job, f)\n",[182,568,570,573,576,579],{"class":184,"line":569},22,[182,571,572],{"class":220},"print",[182,574,575],{"class":192},"(plist_path.read_text()[:",[182,577,578],{"class":220},"420",[182,580,581],{"class":192},"])\n",[10,583,584,585,588,589,592,593,596,597,600,601,604,605,607],{},"The interpreter is given by absolute path: launchd does not read shell profiles, so ",[14,586,587],{},"python3"," on its own would mean the system Python with none of the report's packages. Pointing at the virtualenv's ",[14,590,591],{},"bin\u002Fpython"," runs the script with its own dependencies without any activation. ",[14,594,595],{},"WorkingDirectory"," makes relative paths in the script — ",[14,598,599],{},"exports\u002Flatest.xlsx"," — resolve inside the project. ",[14,602,603],{},"PYTHONUNBUFFERED"," flushes ",[14,606,572],{}," output straight to the log file. The label is conventionally reverse-DNS and must be unique on the Mac; the file is named after it.",[38,609,611],{"id":610},"step-2-choose-the-schedule","Step 2 — Choose the schedule",[10,613,614,617,618,384,621,384,624,384,627,630,631,634,635,637],{},[14,615,616],{},"StartCalendarInterval"," takes a dictionary of ",[14,619,620],{},"Minute",[14,622,623],{},"Hour",[14,625,626],{},"Day",[14,628,629],{},"Weekday"," and ",[14,632,633],{},"Month","; missing keys mean \"every\". ",[14,636,629],{}," counts from 0 or 7 for Sunday, so 1 is Monday. A list of dictionaries gives several times — which is also how to express \"weekdays\", because there are no ranges:",[173,639,641],{"className":175,"code":640,"language":177,"meta":178,"style":178},"def calendar_intervals(hour, minute, weekdays=None):\n    if weekdays is None:\n        return [{\"Hour\": hour, \"Minute\": minute}]\n    return [{\"Weekday\": d, \"Hour\": hour, \"Minute\": minute} for d in weekdays]\n\nevery_morning = calendar_intervals(6, 30)\nweekday_mornings = calendar_intervals(6, 30, weekdays=range(1, 6))\nmonday_and_thursday = calendar_intervals(7, 0, weekdays=[1, 4])\nprint(every_morning)\nprint(weekday_mornings)\nprint(len(monday_and_thursday), \"entries:\", monday_and_thursday)\n",[14,642,643,663,680,698,731,735,755,792,825,832,839],{"__ignoreMap":178},[182,644,645,648,652,655,657,660],{"class":184,"line":185},[182,646,647],{"class":188},"def",[182,649,651],{"class":650},"s_Opv"," calendar_intervals",[182,653,654],{"class":192},"(hour, minute, weekdays",[182,656,281],{"class":188},[182,658,659],{"class":220},"None",[182,661,662],{"class":192},"):\n",[182,664,665,668,671,674,677],{"class":184,"line":196},[182,666,667],{"class":188},"    if",[182,669,670],{"class":192}," weekdays ",[182,672,673],{"class":188},"is",[182,675,676],{"class":220}," None",[182,678,679],{"class":192},":\n",[182,681,682,685,688,690,693,695],{"class":184,"line":210},[182,683,684],{"class":188},"        return",[182,686,687],{"class":192}," [{",[182,689,387],{"class":230},[182,691,692],{"class":192},": hour, ",[182,694,397],{"class":230},[182,696,697],{"class":192},": minute}]\n",[182,699,700,703,705,707,710,712,714,716,719,722,725,728],{"class":184,"line":217},[182,701,702],{"class":188},"    return",[182,704,687],{"class":192},[182,706,376],{"class":230},[182,708,709],{"class":192},": d, ",[182,711,387],{"class":230},[182,713,692],{"class":192},[182,715,397],{"class":230},[182,717,718],{"class":192},": minute} ",[182,720,721],{"class":188},"for",[182,723,724],{"class":192}," d ",[182,726,727],{"class":188},"in",[182,729,730],{"class":192}," weekdays]\n",[182,732,733],{"class":184,"line":237},[182,734,214],{"emptyLinePlaceholder":213},[182,736,737,740,742,745,748,750,753],{"class":184,"line":259},[182,738,739],{"class":192},"every_morning ",[182,741,281],{"class":188},[182,743,744],{"class":192}," calendar_intervals(",[182,746,747],{"class":220},"6",[182,749,384],{"class":192},[182,751,752],{"class":220},"30",[182,754,234],{"class":192},[182,756,757,760,762,764,766,768,770,772,776,778,781,783,785,787,789],{"class":184,"line":270},[182,758,759],{"class":192},"weekday_mornings ",[182,761,281],{"class":188},[182,763,744],{"class":192},[182,765,747],{"class":220},[182,767,384],{"class":192},[182,769,752],{"class":220},[182,771,384],{"class":192},[182,773,775],{"class":774},"sa561","weekdays",[182,777,281],{"class":188},[182,779,780],{"class":220},"range",[182,782,313],{"class":192},[182,784,381],{"class":220},[182,786,384],{"class":192},[182,788,747],{"class":220},[182,790,791],{"class":192},"))\n",[182,793,794,797,799,801,803,805,807,809,811,813,816,818,820,823],{"class":184,"line":275},[182,795,796],{"class":192},"monday_and_thursday ",[182,798,281],{"class":188},[182,800,744],{"class":192},[182,802,392],{"class":220},[182,804,384],{"class":192},[182,806,75],{"class":220},[182,808,384],{"class":192},[182,810,775],{"class":774},[182,812,281],{"class":188},[182,814,815],{"class":192},"[",[182,817,381],{"class":220},[182,819,384],{"class":192},[182,821,822],{"class":220},"4",[182,824,581],{"class":192},[182,826,827,829],{"class":184,"line":287},[182,828,572],{"class":220},[182,830,831],{"class":192},"(every_morning)\n",[182,833,834,836],{"class":184,"line":301},[182,835,572],{"class":220},[182,837,838],{"class":192},"(weekday_mornings)\n",[182,840,841,843,845,848,851,854],{"class":184,"line":350},[182,842,572],{"class":220},[182,844,313],{"class":192},[182,846,847],{"class":220},"len",[182,849,850],{"class":192},"(monday_and_thursday), ",[182,852,853],{"class":230},"\"entries:\"",[182,855,856],{"class":192},", monday_and_thursday)\n",[10,858,859,860,863,864,867],{},"A cron line such as ",[14,861,862],{},"30 6 * * 1-5"," becomes five dictionaries, one per weekday. Times are in the Mac's local time zone and follow daylight-saving changes. For \"every 15 minutes\" use ",[14,865,866],{},"StartInterval"," with a number of seconds instead — it counts from when the job was loaded rather than from the clock.",[53,869,62,875,62,878,62,881,62,886,62,893,62,897,62,903,62,907,62,911,62,915,62,919,62,923,62,927,62,929,62,933,62,935,62,938,62,940,62,943,62,946,62,950,62,952,62,955,62,957,62,959,62,961,62,964,62,967,62,971,62,974,62,978,62,981,62,985,62,988,62,990,62,993,62,996,62,999,62,1001,62,1003,62,1005,62,1007,62,1009,62,1011,62,1013,62,1015,62,1017,62,1019,62,1021,62,1023,62,1026,62,1030,62,1032,62,1034,62,1036,62,1038,62,1040,62,1042,62,1045,62,1048,62,1050,62,1053,62,1055,62,1057,62,1059,62,1061,62,1064,62,1068,62,1070,62,1072,62,1074,62,1076,62,1078,62,1080,62,1086,62,1090,62,1095,62,1100,62,1108],{"viewBox":870,"role":56,"ariaLabelledBy":871,"xmlns":60,"style":874},"40 0 679 220",[872,873],"ld-cron-t","ld-cron-d","width:100%;max-width:679px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[64,876,877],{"id":872},"A cron line and its launchd equivalent",[68,879,880],{"id":873},"On the left the cron expression 30 6 * * 1-5 split into its fields. On the right the StartCalendarInterval list with one entry per weekday, Monday to Friday, each at hour 6 minute 30.",[72,882],{"x":883,"y":75,"width":884,"height":885,"fill":78},"40","679","220",[72,887],{"x":888,"y":889,"width":890,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"56.9","40.0","32.0","24.0","stroke-width:1px",[72,894],{"x":895,"y":889,"width":896,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"88.9","63.2",[101,898,902],{"x":899,"y":900,"style":901},"120.5","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[72,904],{"x":905,"y":889,"width":906,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"152.1","50.2",[101,908,910],{"x":909,"y":900,"style":901},"177.1","B",[72,912],{"x":913,"y":889,"width":914,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"202.2","75.2",[101,916,918],{"x":917,"y":900,"style":901},"239.8","C",[72,920],{"x":888,"y":921,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},"64.0","28.0",[101,924,381],{"x":925,"y":926,"style":901},"72.9","83.0",[72,928],{"x":895,"y":921,"width":896,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,930,932],{"x":899,"y":926,"style":931},"font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","minute",[72,934],{"x":905,"y":921,"width":906,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,936,937],{"x":909,"y":926,"style":931},"hour",[72,939],{"x":913,"y":921,"width":914,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,941,942],{"x":917,"y":926,"style":931},"weekday",[72,944],{"x":888,"y":945,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},"92.0",[101,947,949],{"x":925,"y":948,"style":901},"111.0","2",[72,951],{"x":895,"y":945,"width":896,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,953,752],{"x":899,"y":948,"style":954},"font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[72,956],{"x":905,"y":945,"width":906,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,958,747],{"x":909,"y":948,"style":954},[72,960],{"x":913,"y":945,"width":914,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,962,963],{"x":917,"y":948,"style":954},"1-5",[72,965],{"x":966,"y":889,"width":890,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"478.7",[72,968],{"x":969,"y":889,"width":970,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"510.7","77.4",[101,972,902],{"x":973,"y":900,"style":901},"549.4",[72,975],{"x":976,"y":889,"width":977,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"588.1","51.6",[101,979,910],{"x":980,"y":900,"style":901},"613.9",[72,982],{"x":983,"y":889,"width":984,"height":891,"rx":75,"fill":136,"stroke":137,"style":892},"639.7","63.4",[101,986,918],{"x":987,"y":900,"style":901},"671.4",[72,989],{"x":966,"y":921,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},[101,991,381],{"x":992,"y":926,"style":901},"494.7",[72,994],{"x":969,"y":921,"width":970,"height":922,"rx":75,"fill":86,"stroke":87,"style":995},"stroke-width:1.5px",[101,997,629],{"x":973,"y":926,"style":998},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[72,1000],{"x":976,"y":921,"width":977,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1002,623],{"x":980,"y":926,"style":931},[72,1004],{"x":983,"y":921,"width":984,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1006,620],{"x":987,"y":926,"style":931},[72,1008],{"x":966,"y":945,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},[101,1010,949],{"x":992,"y":948,"style":901},[72,1012],{"x":969,"y":945,"width":970,"height":922,"rx":75,"fill":86,"stroke":87,"style":995},[101,1014,381],{"x":973,"y":948,"style":998},[72,1016],{"x":976,"y":945,"width":977,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1018,747],{"x":980,"y":948,"style":954},[72,1020],{"x":983,"y":945,"width":984,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1022,752],{"x":987,"y":948,"style":954},[72,1024],{"x":966,"y":1025,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},"120.0",[101,1027,1029],{"x":992,"y":1028,"style":901},"139.0","3",[72,1031],{"x":969,"y":1025,"width":970,"height":922,"rx":75,"fill":86,"stroke":87,"style":995},[101,1033,949],{"x":973,"y":1028,"style":998},[72,1035],{"x":976,"y":1025,"width":977,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1037,747],{"x":980,"y":1028,"style":954},[72,1039],{"x":983,"y":1025,"width":984,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1041,752],{"x":987,"y":1028,"style":954},[72,1043],{"x":966,"y":1044,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},"148.0",[101,1046,822],{"x":992,"y":1047,"style":901},"167.0",[72,1049],{"x":969,"y":1044,"width":970,"height":922,"rx":75,"fill":86,"stroke":87,"style":995},[101,1051,1052],{"x":973,"y":1047,"style":998},"…",[72,1054],{"x":976,"y":1044,"width":977,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1056,1052],{"x":980,"y":1047,"style":954},[72,1058],{"x":983,"y":1044,"width":984,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1060,1052],{"x":987,"y":1047,"style":954},[72,1062],{"x":966,"y":1063,"width":890,"height":922,"rx":75,"fill":136,"stroke":137,"style":892},"176.0",[101,1065,1067],{"x":992,"y":1066,"style":901},"195.0","5",[72,1069],{"x":969,"y":1063,"width":970,"height":922,"rx":75,"fill":86,"stroke":87,"style":995},[101,1071,1067],{"x":973,"y":1066,"style":998},[72,1073],{"x":976,"y":1063,"width":977,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1075,747],{"x":980,"y":1066,"style":954},[72,1077],{"x":983,"y":1063,"width":984,"height":922,"rx":75,"fill":78,"stroke":137,"style":892},[101,1079,752],{"x":987,"y":1066,"style":954},[101,1081,1085],{"x":1082,"y":1083,"style":1084},"167.2","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[101,1087,1089],{"x":1088,"y":1083,"style":1084},"590.9","after",[184,1091],{"x1":1092,"y1":1093,"x2":1094,"y2":1093,"stroke":116,"style":88},"287.5","80.0","464.7",[1096,1097],"polygon",{"points":1098,"fill":1099},"468.7,80.0 459.7,85.0 459.7,75.0","#5b5cf0",[72,1101],{"x":1102,"y":1103,"width":1104,"height":1105,"rx":1106,"fill":78,"stroke":137,"style":1107},"313.6","47.0","129.0","22.0","11","stroke-width:1.2px",[101,1109,1113],{"x":1110,"y":1111,"style":1112},"378.1","62.0","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","calendar_intervals",[38,1115,1117],{"id":1116},"step-3-check-the-file-before-loading-it","Step 3 — Check the file before loading it",[10,1119,1120],{},"A launchd file with a wrong key or type is ignored or rejected when loaded, often with a terse message. Read the file back and check the parts that matter:",[173,1122,1124],{"className":175,"code":1123,"language":177,"meta":178,"style":178},"loaded = plistlib.loads(plist_path.read_bytes())\nproblems = []\nif not Path(loaded[\"ProgramArguments\"][0]).is_absolute():\n    problems.append(\"interpreter path must be absolute\")\nfor entry in loaded.get(\"StartCalendarInterval\", []):\n    unknown = set(entry) - {\"Minute\", \"Hour\", \"Day\", \"Weekday\", \"Month\"}\n    if unknown:\n        problems.append(f\"unknown schedule keys {unknown}\")\n    if not 0 \u003C= entry.get(\"Weekday\", 0) \u003C= 7:\n        problems.append(f\"bad weekday {entry['Weekday']}\")\nif Path(loaded[\"StandardOutPath\"]).parent.name != \"logs\":\n    problems.append(\"log files should go to the project's logs folder\")\nprint(loaded[\"Label\"], \"->\", problems or \"looks valid\")\n",[14,1125,1126,1136,1146,1168,1178,1196,1237,1244,1265,1296,1322,1341,1350],{"__ignoreMap":178},[182,1127,1128,1131,1133],{"class":184,"line":185},[182,1129,1130],{"class":192},"loaded ",[182,1132,281],{"class":188},[182,1134,1135],{"class":192}," plistlib.loads(plist_path.read_bytes())\n",[182,1137,1138,1141,1143],{"class":184,"line":196},[182,1139,1140],{"class":192},"problems ",[182,1142,281],{"class":188},[182,1144,1145],{"class":192}," []\n",[182,1147,1148,1151,1154,1157,1160,1163,1165],{"class":184,"line":210},[182,1149,1150],{"class":188},"if",[182,1152,1153],{"class":188}," not",[182,1155,1156],{"class":192}," Path(loaded[",[182,1158,1159],{"class":230},"\"ProgramArguments\"",[182,1161,1162],{"class":192},"][",[182,1164,75],{"class":220},[182,1166,1167],{"class":192},"]).is_absolute():\n",[182,1169,1170,1173,1176],{"class":184,"line":217},[182,1171,1172],{"class":192},"    problems.append(",[182,1174,1175],{"class":230},"\"interpreter path must be absolute\"",[182,1177,234],{"class":192},[182,1179,1180,1182,1185,1187,1190,1193],{"class":184,"line":237},[182,1181,721],{"class":188},[182,1183,1184],{"class":192}," entry ",[182,1186,727],{"class":188},[182,1188,1189],{"class":192}," loaded.get(",[182,1191,1192],{"class":230},"\"StartCalendarInterval\"",[182,1194,1195],{"class":192},", []):\n",[182,1197,1198,1201,1203,1206,1209,1212,1215,1217,1219,1221,1223,1226,1228,1230,1232,1235],{"class":184,"line":259},[182,1199,1200],{"class":192},"    unknown ",[182,1202,281],{"class":188},[182,1204,1205],{"class":220}," set",[182,1207,1208],{"class":192},"(entry) ",[182,1210,1211],{"class":188},"-",[182,1213,1214],{"class":192}," {",[182,1216,397],{"class":230},[182,1218,384],{"class":192},[182,1220,387],{"class":230},[182,1222,384],{"class":192},[182,1224,1225],{"class":230},"\"Day\"",[182,1227,384],{"class":192},[182,1229,376],{"class":230},[182,1231,384],{"class":192},[182,1233,1234],{"class":230},"\"Month\"",[182,1236,504],{"class":192},[182,1238,1239,1241],{"class":184,"line":270},[182,1240,667],{"class":188},[182,1242,1243],{"class":192}," unknown:\n",[182,1245,1246,1249,1251,1254,1256,1259,1261,1263],{"class":184,"line":275},[182,1247,1248],{"class":192},"        problems.append(",[182,1250,522],{"class":188},[182,1252,1253],{"class":230},"\"unknown schedule keys ",[182,1255,529],{"class":528},[182,1257,1258],{"class":192},"unknown",[182,1260,534],{"class":528},[182,1262,525],{"class":230},[182,1264,234],{"class":192},[182,1266,1267,1269,1271,1274,1277,1280,1282,1284,1286,1288,1291,1294],{"class":184,"line":287},[182,1268,667],{"class":188},[182,1270,1153],{"class":188},[182,1272,1273],{"class":220}," 0",[182,1275,1276],{"class":188}," \u003C=",[182,1278,1279],{"class":192}," entry.get(",[182,1281,376],{"class":230},[182,1283,384],{"class":192},[182,1285,75],{"class":220},[182,1287,554],{"class":192},[182,1289,1290],{"class":188},"\u003C=",[182,1292,1293],{"class":220}," 7",[182,1295,679],{"class":192},[182,1297,1298,1300,1302,1305,1307,1310,1313,1316,1318,1320],{"class":184,"line":301},[182,1299,1248],{"class":192},[182,1301,522],{"class":188},[182,1303,1304],{"class":230},"\"bad weekday ",[182,1306,529],{"class":528},[182,1308,1309],{"class":192},"entry[",[182,1311,1312],{"class":230},"'Weekday'",[182,1314,1315],{"class":192},"]",[182,1317,534],{"class":528},[182,1319,525],{"class":230},[182,1321,234],{"class":192},[182,1323,1324,1326,1328,1331,1334,1337,1339],{"class":184,"line":350},[182,1325,1150],{"class":188},[182,1327,1156],{"class":192},[182,1329,1330],{"class":230},"\"StandardOutPath\"",[182,1332,1333],{"class":192},"]).parent.name ",[182,1335,1336],{"class":188},"!=",[182,1338,423],{"class":230},[182,1340,679],{"class":192},[182,1342,1343,1345,1348],{"class":184,"line":367},[182,1344,1172],{"class":192},[182,1346,1347],{"class":230},"\"log files should go to the project's logs folder\"",[182,1349,234],{"class":192},[182,1351,1352,1354,1357,1360,1363,1366,1369,1372,1375],{"class":184,"line":407},[182,1353,572],{"class":220},[182,1355,1356],{"class":192},"(loaded[",[182,1358,1359],{"class":230},"\"Label\"",[182,1361,1362],{"class":192},"], ",[182,1364,1365],{"class":230},"\"->\"",[182,1367,1368],{"class":192},", problems ",[182,1370,1371],{"class":188},"or",[182,1373,1374],{"class":230}," \"looks valid\"",[182,1376,234],{"class":192},[10,1378,1379,1380,1383,1384,1387],{},"On a Mac, ",[14,1381,1382],{},"plutil -lint"," checks the XML syntax as well. The log folder must exist before the job runs — launchd creates the log files but not missing directories — so the setup script should create ",[14,1385,1386],{},"logs\u002F"," too.",[38,1389,1391],{"id":1390},"step-4-what-happens-when-the-mac-is-asleep-or-off","Step 4 — What happens when the Mac is asleep or off",[10,1393,1394,1395,1398,1399,1402],{},"This is the main reason to use launchd rather than cron on a laptop. If the Mac is ",[23,1396,1397],{},"asleep"," at 07:00 on Monday, launchd runs the job when it wakes; if several scheduled times were missed during one sleep, they are combined into a single run. If the Mac was ",[23,1400,1401],{},"shut down",", the missed run does not happen. The script therefore should not assume it runs at exactly 07:00 — derive the report period from the data or from the calendar, not from the time the script happened to start:",[173,1404,1406],{"className":175,"code":1405,"language":177,"meta":178,"style":178},"from datetime import date, timedelta\n\ndef report_week(today: date) -> date:\n    return today - timedelta(days=today.weekday())      # the Monday of this week\n\nfor run_day in (date(2026, 9, 28), date(2026, 9, 29), date(2026, 10, 1)):\n    print(run_day, \"->\", report_week(run_day))\n",[14,1407,1408,1420,1424,1434,1458,1462,1517],{"__ignoreMap":178},[182,1409,1410,1412,1415,1417],{"class":184,"line":185},[182,1411,199],{"class":188},[182,1413,1414],{"class":192}," datetime ",[182,1416,189],{"class":188},[182,1418,1419],{"class":192}," date, timedelta\n",[182,1421,1422],{"class":184,"line":196},[182,1423,214],{"emptyLinePlaceholder":213},[182,1425,1426,1428,1431],{"class":184,"line":210},[182,1427,647],{"class":188},[182,1429,1430],{"class":650}," report_week",[182,1432,1433],{"class":192},"(today: date) -> date:\n",[182,1435,1436,1438,1441,1443,1446,1449,1451,1454],{"class":184,"line":217},[182,1437,702],{"class":188},[182,1439,1440],{"class":192}," today ",[182,1442,1211],{"class":188},[182,1444,1445],{"class":192}," timedelta(",[182,1447,1448],{"class":774},"days",[182,1450,281],{"class":188},[182,1452,1453],{"class":192},"today.weekday())      ",[182,1455,1457],{"class":1456},"s-wDw","# the Monday of this week\n",[182,1459,1460],{"class":184,"line":237},[182,1461,214],{"emptyLinePlaceholder":213},[182,1463,1464,1466,1469,1471,1474,1477,1479,1482,1484,1487,1490,1492,1494,1496,1498,1501,1503,1505,1507,1510,1512,1514],{"class":184,"line":259},[182,1465,721],{"class":188},[182,1467,1468],{"class":192}," run_day ",[182,1470,727],{"class":188},[182,1472,1473],{"class":192}," (date(",[182,1475,1476],{"class":220},"2026",[182,1478,384],{"class":192},[182,1480,1481],{"class":220},"9",[182,1483,384],{"class":192},[182,1485,1486],{"class":220},"28",[182,1488,1489],{"class":192},"), date(",[182,1491,1476],{"class":220},[182,1493,384],{"class":192},[182,1495,1481],{"class":220},[182,1497,384],{"class":192},[182,1499,1500],{"class":220},"29",[182,1502,1489],{"class":192},[182,1504,1476],{"class":220},[182,1506,384],{"class":192},[182,1508,1509],{"class":220},"10",[182,1511,384],{"class":192},[182,1513,381],{"class":220},[182,1515,1516],{"class":192},")):\n",[182,1518,1519,1522,1525,1527],{"class":184,"line":270},[182,1520,1521],{"class":220},"    print",[182,1523,1524],{"class":192},"(run_day, ",[182,1526,1365],{"class":230},[182,1528,1529],{"class":192},", report_week(run_day))\n",[10,1531,1532,1533,1537],{},"Whether the job runs on Monday at 07:00 or on Tuesday when the lid is opened, it builds the report for the week starting Monday 28 September. A late run of the right report is useful; an on-time run of the wrong one is not. ",[28,1534,1536],{"href":1535},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates\u002F","Backfill Excel Reports for Past Dates"," covers catching up after longer gaps.",[53,1539,62,1545,62,1548,62,1551,62,1555,62,1562,62,1567,62,1573,62,1579,62,1582,62,1585,62,1589,62,1593,62,1598,62,1604,62,1608,62,1610,62,1612,62,1615,62,1621,62,1625,62,1628,62,1630,62,1632,62,1635,62,1641,62,1645,62,1648,62,1650],{"viewBox":1540,"role":56,"ariaLabelledBy":1541,"xmlns":60,"style":1544},"4 0 752 279",[1542,1543],"ld-sleep-t","ld-sleep-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[64,1546,1547],{"id":1542},"A missed Monday on a laptop",[68,1549,1550],{"id":1543},"A timeline. The job is scheduled for Monday 07:00 while the laptop is asleep; nothing runs then; when the lid opens at 08:40 launchd runs the job once, and the script builds the report for the week of 28 September.",[72,1552],{"x":822,"y":75,"width":1553,"height":1554,"fill":78},"752","279",[184,1556],{"x1":1557,"y1":1558,"x2":1559,"y2":1558,"stroke":1560,"style":1561},"94.0","139.5","666.0","var(--muted,#5b6780)","stroke-width:2.5px",[72,1563],{"x":1564,"y":82,"width":1565,"height":1566,"rx":85,"fill":136,"stroke":137,"style":88},"20.0","172.0","91.5",[90,1568,1570],{"transform":1569},"translate(94.0,29.0) scale(1.000)",[94,1571],{"d":1572,"fill":97,"stroke":144,"style":99},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2",[101,1574,1578],{"x":1575,"y":1576,"style":1577},"106.0","74.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Mon 07:00",[101,1580,1581],{"x":1575,"y":84,"style":954},"scheduled — Mac asleep",[184,1583],{"x1":1575,"y1":1584,"x2":1575,"y2":1558,"stroke":1560,"style":995},"107.5",[1586,1587],"circle",{"cx":1575,"cy":1558,"r":1588,"fill":1560,"stroke":78,"style":88},"7.0",[72,1590],{"x":1591,"y":1592,"width":1565,"height":1566,"rx":85,"fill":115,"stroke":116,"style":88},"202.7","171.5",[90,1594,1596],{"transform":1595},"translate(276.7,184.5) scale(1.000)",[94,1597],{"d":96,"fill":97,"stroke":123,"style":99},[101,1599,1603],{"x":1600,"y":1601,"style":1602},"288.7","230.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Mon 08:40",[101,1605,1607],{"x":1600,"y":1606,"style":954},"248.5","lid opened",[184,1609],{"x1":1600,"y1":1558,"x2":1600,"y2":1592,"stroke":1560,"style":995},[1586,1611],{"cx":1600,"cy":1558,"r":1588,"fill":116,"stroke":78,"style":88},[72,1613],{"x":1614,"y":82,"width":1565,"height":1566,"rx":85,"fill":86,"stroke":87,"style":88},"385.3",[90,1616,1618],{"transform":1617},"translate(459.3,29.0) scale(1.000)",[94,1619],{"d":1620,"fill":97,"stroke":98,"style":99},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[101,1622,1603],{"x":1623,"y":1576,"style":1624},"471.3","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[101,1626,1627],{"x":1623,"y":84,"style":954},"launchd runs the job once",[184,1629],{"x1":1623,"y1":1584,"x2":1623,"y2":1558,"stroke":1560,"style":995},[1586,1631],{"cx":1623,"cy":1558,"r":1588,"fill":87,"stroke":78,"style":88},[72,1633],{"x":1634,"y":1592,"width":1565,"height":1566,"rx":85,"fill":86,"stroke":87,"style":88},"568.0",[90,1636,1638],{"transform":1637},"translate(642.0,184.5) scale(1.000)",[94,1639],{"d":1640,"fill":97,"stroke":98,"style":99},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6",[101,1642,1644],{"x":1643,"y":1601,"style":1624},"654.0","Report",[101,1646,1647],{"x":1643,"y":1606,"style":954},"week of 28 September",[184,1649],{"x1":1643,"y1":1558,"x2":1643,"y2":1592,"stroke":1560,"style":995},[1586,1651],{"cx":1643,"cy":1558,"r":1588,"fill":87,"stroke":78,"style":88},[38,1653,1655],{"id":1654},"step-5-load-test-and-inspect-the-job","Step 5 — Load, test and inspect the job",[10,1657,1658,1659,1661,1662,1665,1666,1669],{},"Copy the file to ",[14,1660,106],{}," and load it into your login session with ",[14,1663,1664],{},"launchctl bootstrap",". ",[14,1667,1668],{},"kickstart"," runs it immediately, which is the way to test it without waiting for Monday:",[173,1671,1675],{"className":1672,"code":1673,"language":1674,"meta":178,"style":178},"language-bash shiki shiki-themes github-light github-dark-high-contrast","mkdir -p ~\u002Freports\u002Fweekly_sales\u002Flogs\ncp com.acme.reports.weekly-sales.plist ~\u002FLibrary\u002FLaunchAgents\u002F\nlaunchctl bootstrap gui\u002F$(id -u) ~\u002FLibrary\u002FLaunchAgents\u002Fcom.acme.reports.weekly-sales.plist\nlaunchctl kickstart -k gui\u002F$(id -u)\u002Fcom.acme.reports.weekly-sales\nlaunchctl print gui\u002F$(id -u)\u002Fcom.acme.reports.weekly-sales | grep -E \"state|last exit code\"\ntail ~\u002Freports\u002Fweekly_sales\u002Flogs\u002Fweekly_sales.err.log\n","bash",[14,1676,1677,1689,1700,1724,1748,1780],{"__ignoreMap":178},[182,1678,1679,1683,1686],{"class":184,"line":185},[182,1680,1682],{"class":1681},"sMTad","mkdir",[182,1684,1685],{"class":220}," -p",[182,1687,1688],{"class":230}," ~\u002Freports\u002Fweekly_sales\u002Flogs\n",[182,1690,1691,1694,1697],{"class":184,"line":196},[182,1692,1693],{"class":1681},"cp",[182,1695,1696],{"class":230}," com.acme.reports.weekly-sales.plist",[182,1698,1699],{"class":230}," ~\u002FLibrary\u002FLaunchAgents\u002F\n",[182,1701,1702,1704,1707,1710,1713,1716,1719,1721],{"class":184,"line":210},[182,1703,35],{"class":1681},[182,1705,1706],{"class":230}," bootstrap",[182,1708,1709],{"class":230}," gui\u002F",[182,1711,1712],{"class":192},"$(",[182,1714,1715],{"class":1681},"id",[182,1717,1718],{"class":220}," -u",[182,1720,554],{"class":192},[182,1722,1723],{"class":230},"~\u002FLibrary\u002FLaunchAgents\u002Fcom.acme.reports.weekly-sales.plist\n",[182,1725,1726,1728,1731,1734,1736,1738,1740,1742,1745],{"class":184,"line":217},[182,1727,35],{"class":1681},[182,1729,1730],{"class":230}," kickstart",[182,1732,1733],{"class":220}," -k",[182,1735,1709],{"class":230},[182,1737,1712],{"class":192},[182,1739,1715],{"class":1681},[182,1741,1718],{"class":220},[182,1743,1744],{"class":192},")",[182,1746,1747],{"class":230},"\u002Fcom.acme.reports.weekly-sales\n",[182,1749,1750,1752,1755,1757,1759,1761,1763,1765,1768,1771,1774,1777],{"class":184,"line":237},[182,1751,35],{"class":1681},[182,1753,1754],{"class":230}," print",[182,1756,1709],{"class":230},[182,1758,1712],{"class":192},[182,1760,1715],{"class":1681},[182,1762,1718],{"class":220},[182,1764,1744],{"class":192},[182,1766,1767],{"class":230},"\u002Fcom.acme.reports.weekly-sales",[182,1769,1770],{"class":188}," |",[182,1772,1773],{"class":1681}," grep",[182,1775,1776],{"class":220}," -E",[182,1778,1779],{"class":230}," \"state|last exit code\"\n",[182,1781,1782,1785],{"class":184,"line":259},[182,1783,1784],{"class":1681},"tail",[182,1786,1787],{"class":230}," ~\u002Freports\u002Fweekly_sales\u002Flogs\u002Fweekly_sales.err.log\n",[10,1789,1790,1793,1794,630,1797,1800,1801,630,1804,1807],{},[14,1791,1792],{},"launchctl print"," shows whether the job is loaded and the exit code of its last run; a non-zero code sends you to the error log. The older ",[14,1795,1796],{},"launchctl load",[14,1798,1799],{},"unload"," commands still work but are deprecated in favour of ",[14,1802,1803],{},"bootstrap",[14,1805,1806],{},"bootout",".",[38,1809,1811],{"id":1810},"step-6-privacy-permissions-and-removal","Step 6 — Privacy permissions and removal",[10,1813,1814,1815,1818,1819,1822,1823,1826],{},"Recent macOS versions protect folders such as Documents, Desktop and Downloads. A script that reads an export from ",[14,1816,1817],{},"~\u002FDownloads"," works when run from Terminal — which has been granted access — and fails under launchd with \"Operation not permitted\", because the job's interpreter has not. Either keep the report's files outside the protected folders, in a project directory like ",[14,1820,1821],{},"~\u002Freports",", or grant the virtualenv's Python ",[23,1824,1825],{},"Full Disk Access"," in System Settings → Privacy & Security. The first is simpler and safer. To remove the job:",[173,1828,1830],{"className":1672,"code":1829,"language":1674,"meta":178,"style":178},"launchctl bootout gui\u002F$(id -u)\u002Fcom.acme.reports.weekly-sales\nrm ~\u002FLibrary\u002FLaunchAgents\u002Fcom.acme.reports.weekly-sales.plist\n",[14,1831,1832,1851],{"__ignoreMap":178},[182,1833,1834,1836,1839,1841,1843,1845,1847,1849],{"class":184,"line":185},[182,1835,35],{"class":1681},[182,1837,1838],{"class":230}," bootout",[182,1840,1709],{"class":230},[182,1842,1712],{"class":192},[182,1844,1715],{"class":1681},[182,1846,1718],{"class":220},[182,1848,1744],{"class":192},[182,1850,1747],{"class":230},[182,1852,1853,1856],{"class":184,"line":196},[182,1854,1855],{"class":1681},"rm",[182,1857,1858],{"class":230}," ~\u002FLibrary\u002FLaunchAgents\u002Fcom.acme.reports.weekly-sales.plist\n",[38,1860,1862],{"id":1861},"common-pitfalls","Common pitfalls",[1864,1865,1866,1876,1882,1888,1894],"ul",{},[1867,1868,1869,1875],"li",{},[1870,1871,1872,1874],"strong",{},[14,1873,587],{}," without a path."," launchd has a minimal environment; use the virtualenv's interpreter by absolute path.",[1867,1877,1878,1881],{},[1870,1879,1880],{},"Missing log directory."," launchd will not create it; the job fails before the script starts.",[1867,1883,1884,1887],{},[1870,1885,1886],{},"Files in Documents, Desktop or Downloads."," Privacy protection blocks the job; move them or grant Full Disk Access.",[1867,1889,1890,1893],{},[1870,1891,1892],{},"Expecting runs while shut down."," Sleep is handled, power-off is not; derive the report period from the calendar.",[1867,1895,1896,1899],{},[1870,1897,1898],{},"A LaunchAgent on a shared server."," Agents run only while the user is logged in; use a LaunchDaemon or a proper server scheduler.",[38,1901,1903],{"id":1902},"conclusion","Conclusion",[10,1905,1906,1907,1909,1910,1912,1913,1915,1916,1918],{},"Describe the job as a dictionary and write it with ",[14,1908,170],{},": a unique label, the virtualenv's Python and the script by absolute path, a working directory, ",[14,1911,616],{}," entries for the schedule and log file paths. Check the file before loading it, bootstrap it into your session with ",[14,1914,35],{},", test it with ",[14,1917,1668],{},", keep the report's files out of privacy-protected folders, and write the script so a late run after sleep still produces the right period's report.",[38,1920,1922],{"id":1921},"frequently-asked-questions","Frequently asked questions",[10,1924,1925,1928],{},[1870,1926,1927],{},"Should I use cron or launchd on a Mac?","\nlaunchd. It is Apple's supported scheduler, and it runs a job missed while the Mac was asleep as soon as it wakes, whereas cron skips it.",[10,1930,1931,1934],{},[1870,1932,1933],{},"How do I run a Python script every Monday at 7am on macOS?","\nCreate a LaunchAgent plist with ProgramArguments pointing at your Python interpreter and script, and StartCalendarInterval set to Weekday 1, Hour 7, Minute 0. Load it with launchctl bootstrap gui\u002F$(id -u) and the plist path.",[10,1936,1937,1940],{},[1870,1938,1939],{},"Why does my launchd job fail with \"Operation not permitted\"?","\nThe script is reading or writing a privacy-protected folder such as Documents, Desktop or Downloads, and the Python interpreter the job runs has not been granted access. Move the files elsewhere or give that interpreter Full Disk Access.",[10,1942,1943,1946],{},[1870,1944,1945],{},"How do I test a launchd job without waiting for its schedule?","\nRun launchctl kickstart -k gui\u002F$(id -u)\u002Fyour.label, then check launchctl print for the last exit code and read the log files named in the plist.",[38,1948,1950],{"id":1949},"related","Related",[1864,1952,1953,1959,1966,1973],{},[1867,1954,1955,1956,1958],{},"Up one level: ",[28,1957,31],{"href":30}," — every way to run reports on a schedule.",[1867,1960,1961,1965],{},[28,1962,1964],{"href":1963},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler\u002F","Run a Python Excel Script on Windows Task Scheduler"," — the Windows equivalent.",[1867,1967,1968,1972],{},[28,1969,1971],{"href":1970},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers\u002F","Schedule an Excel Report with systemd Timers"," — the Linux equivalent.",[1867,1974,1975,1979],{},[28,1976,1978],{"href":1977},"\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"," — what to write to those log files.",[1981,1982,1983],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}",{"title":178,"searchDepth":196,"depth":196,"links":1985},[1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996],{"id":40,"depth":196,"text":41},{"id":155,"depth":196,"text":156},{"id":610,"depth":196,"text":611},{"id":1116,"depth":196,"text":1117},{"id":1390,"depth":196,"text":1391},{"id":1654,"depth":196,"text":1655},{"id":1810,"depth":196,"text":1811},{"id":1861,"depth":196,"text":1862},{"id":1902,"depth":196,"text":1903},{"id":1921,"depth":196,"text":1922},{"id":1949,"depth":196,"text":1950},"2026-09-29","Run a Python report on a Mac on a schedule the way macOS intends: a launch agent property list generated and checked with plistlib, StartCalendarInterval for weekly and weekday schedules, the virtualenv's Python by absolute path, logs to files, what happens when the Mac was asleep, privacy permissions for Documents and Desktop, and loading, testing and removing the job with launchctl.","md",[2001,2003,2005,2007],{"q":1927,"a":2002},"launchd. It is Apple's supported scheduler, and it runs a job missed while the Mac was asleep as soon as it wakes, whereas cron skips it.",{"q":1933,"a":2004},"Create a LaunchAgent plist with ProgramArguments pointing at your Python interpreter and script, and StartCalendarInterval set to Weekday 1, Hour 7, Minute 0. Load it with launchctl bootstrap gui\u002F$(id -u) and the plist path.",{"q":1939,"a":2006},"The script is reading or writing a privacy-protected folder such as Documents, Desktop or Downloads, and the Python interpreter the job runs has not been granted access. Move the files elsewhere or give that interpreter Full Disk Access.",{"q":1945,"a":2008},"Run launchctl kickstart -k gui\u002F$(id -u)\u002Fyour.label, then check launchctl print for the last exit code and read the log files named in the plist.","schedule a python excel script on macos with launchd",null,{"breadcrumb":2012},[2013,2016,2019],{"name":2014,"item":2015},"Home","\u002F",{"name":2017,"item":2018},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":31,"item":30},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-a-python-excel-script-on-macos-with-launchd",{"title":2022,"description":2023},"Schedule a Python Script on macOS with launchd (Not cron)","Schedule a Python Excel report on a Mac with a launchd LaunchAgent: generate the plist with plistlib, StartCalendarInterval weekday and hour keys, absolute interpreter paths, StandardOutPath logs, runs after sleep, Full Disk Access, and launchctl bootstrap, kickstart and bootout.","schedule-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",[2027],"scheduling","how-to","7Vjuo4ZOVsgASh5X_ydsvENG6GRZINKQpgUu-PUnYAY",[2031,2034],{"title":1964,"path":2032,"stem":2033,"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":1971,"path":2035,"stem":2036,"children":-1},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers\u002Findex",[2038,2040],{"path":2039,"title":2017},"\u002Fautomating-reporting-workflows",{"path":2041,"title":31},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[2044,2047,2050,2053,2056,2059],{"path":2045,"title":2046,"kind":2028},"\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":2048,"title":2049,"kind":2028},"\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":2051,"title":2052,"kind":2028},"\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":2054,"title":2055,"kind":2028},"\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":2057,"title":2058,"kind":2028},"\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":2060,"title":2061,"kind":2028},"\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",1790972138941]