[{"data":1,"prerenderedAt":1634},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs":1602,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs":1610,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs":1615,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs":1616},{"id":4,"title":5,"body":6,"dateModified":1569,"datePublished":1569,"description":1570,"extension":1571,"faq":1572,"keyword":1581,"kind":1582,"meta":1583,"navigation":323,"path":1591,"seo":1592,"slug":1595,"stem":1596,"topics":1597,"type":1600,"__hash__":1601},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs\u002Findex.md","Run an Excel Report on Google Cloud Run Jobs",{"type":7,"value":8,"toc":1556},"minimark",[9,24,29,68,76,257,261,280,735,745,749,756,763,805,863,869,873,880,980,983,1065,1069,1072,1200,1223,1227,1230,1307,1327,1429,1433,1436,1440,1475,1479,1491,1495,1501,1507,1513,1519,1523,1552],[10,11,12,13,17,18,23],"p",{},"Cloud Run ",[14,15,16],"em",{},"jobs"," are Google Cloud's way to run a container to completion: start it, let it do its work, and stop — exactly the shape of a report script. Unlike Cloud Run services, a job listens on no port, can run for hours, and can fan out into parallel tasks. Cloud Scheduler triggers it on a cron schedule. Everything the report needs — pandas, openpyxl, even LibreOffice for PDF export — goes into the image, so there are no runtime limits on dependencies of the kind functions impose. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",", builds the container, runs it locally with Docker in exactly the way Cloud Run will, and then deploys and schedules it.",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl        # for editing the script locally\n# Docker for the local test; the gcloud CLI to deploy\n","bash","",[37,38,39,62],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl",[40,59,61],{"class":60},"s-wDw","        # for editing the script locally\n",[40,63,65],{"class":42,"line":64},2,[40,66,67],{"class":60},"# Docker for the local test; the gcloud CLI to deploy\n",[10,69,70,71,75],{},"The report writes one workbook per sales region. On Cloud Run the workbooks would be uploaded to Cloud Storage; to keep the local test self-contained, the script writes them to a folder inside the container and the test lists them. ",[19,72,74],{"href":73},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-google-cloud-storage\u002F","Upload an Excel Report to Google Cloud Storage"," covers the upload step.",[77,78,86,87,86,91,86,95,86,103,86,113,86,124,86,131,86,136,86,142,86,147,86,151,86,158,86,163,86,166,86,170,86,173,86,176,86,182,86,187,86,190,86,193,86,197,86,201,86,204,86,209,86,216,86,222,86,226,86,230,86,234,86,237,86,242,86,249,86,254],"svg",{"viewBox":79,"role":80,"ariaLabelledBy":81,"xmlns":84,"style":85},"4 0 752 160","img",[82,83],"crj-flow-t","crj-flow-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  ",[88,89,90],"title",{"id":82},"From a script to a scheduled job",[92,93,94],"desc",{"id":83},"The report script is packaged in a container image, pushed to Artifact Registry, deployed as a Cloud Run job with retries and a timeout, and started on a cron schedule by Cloud Scheduler; each execution runs one or more tasks.",[96,97],"rect",{"x":98,"y":99,"width":100,"height":101,"fill":102},"4","0","752","160","#ffffff",[96,104],{"x":105,"y":106,"width":107,"height":108,"rx":109,"fill":110,"stroke":111,"style":112},"20.0","16.0","115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[114,115,117],"g",{"transform":116},"translate(65.6,46.5) scale(1.000)",[118,119],"path",{"d":120,"fill":121,"stroke":122,"style":123},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[125,126,130],"text",{"x":127,"y":128,"style":129},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Script",[125,132,135],{"x":127,"y":133,"style":134},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","exits 0 or 1",[42,137],{"x1":138,"y1":139,"x2":140,"y2":139,"stroke":141,"style":112},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[143,144],"polygon",{"points":145,"fill":146},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[96,148],{"x":149,"y":106,"width":107,"height":108,"rx":109,"fill":150,"stroke":141,"style":112},"171.2","#f0f4ff",[114,152,154],{"transform":153},"translate(216.8,46.5) scale(1.000)",[118,155],{"d":156,"fill":121,"stroke":157,"style":123},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","var(--brand-strong,#4338ca)",[125,159,162],{"x":160,"y":128,"style":161},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Image",[125,164,165],{"x":160,"y":133,"style":134},"Dockerfile",[42,167],{"x1":168,"y1":139,"x2":169,"y2":139,"stroke":141,"style":112},"291.4","313.4",[143,171],{"points":172,"fill":146},"317.4,80.0 308.4,85.0 308.4,75.0",[96,174],{"x":175,"y":106,"width":107,"height":108,"rx":109,"fill":150,"stroke":141,"style":112},"322.4",[114,177,179],{"transform":178},"translate(368.0,29.0) scale(1.000)",[118,180],{"d":181,"fill":121,"stroke":157,"style":123},"M4 4h16v6H4z M4 14h16v6H4z M8 7h.01 M8 17h.01",[125,183,186],{"x":184,"y":185,"style":161},"380.0","75.5","Cloud Run",[125,188,189],{"x":184,"y":128,"style":161},"job",[125,191,192],{"x":184,"y":133,"style":134},"tasks, retries,",[125,194,196],{"x":184,"y":195,"style":134},"130.0","timeout",[42,198],{"x1":199,"y1":139,"x2":200,"y2":139,"stroke":141,"style":112},"442.6","464.6",[143,202],{"points":203,"fill":146},"468.6,80.0 459.6,85.0 459.6,75.0",[96,205],{"x":206,"y":106,"width":107,"height":108,"rx":109,"fill":207,"stroke":208,"style":112},"473.6","#d9f4f1","var(--teal,#0f9488)",[114,210,212],{"transform":211},"translate(519.2,37.8) scale(1.000)",[118,213],{"d":214,"fill":121,"stroke":215,"style":123},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--teal-ink,#0b6157)",[125,217,221],{"x":218,"y":219,"style":220},"531.2","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Cloud",[125,223,225],{"x":218,"y":224,"style":220},"101.8","Scheduler",[125,227,229],{"x":218,"y":228,"style":134},"121.2","cron trigger",[42,231],{"x1":232,"y1":139,"x2":233,"y2":139,"stroke":141,"style":112},"593.8","615.8",[143,235],{"points":236,"fill":146},"619.8,80.0 610.8,85.0 610.8,75.0",[96,238],{"x":239,"y":106,"width":107,"height":108,"rx":109,"fill":240,"stroke":241,"style":112},"624.8","#fdefd8","var(--gold,#b4740a)",[114,243,245],{"transform":244},"translate(670.4,46.5) scale(1.000)",[118,246],{"d":247,"fill":121,"stroke":248,"style":123},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--gold-ink,#7a4e06)",[125,250,253],{"x":251,"y":128,"style":252},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Execution",[125,255,256],{"x":251,"y":133,"style":134},"parallel tasks",[25,258,260],{"id":259},"step-1-a-script-that-knows-which-part-is-its-job","Step 1 — A script that knows which part is its job",[10,262,263,264,267,268,271,272,275,276,279],{},"A job can run several ",[14,265,266],{},"tasks"," in parallel. Each task receives its position in ",[37,269,270],{},"CLOUD_RUN_TASK_INDEX"," and the total in ",[37,273,274],{},"CLOUD_RUN_TASK_COUNT",", so the script can take its share of the work — here, every ",[14,277,278],{},"count","-th region starting at its index. With one task, it does everything:",[30,281,285],{"className":282,"code":283,"language":284,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\nimport sys\nfrom pathlib import Path\n\nimport pandas as pd\n\nREGIONS = [\"North\", \"South\", \"East\", \"West\"]\n\ndef main() -> int:\n    index = int(os.environ.get(\"CLOUD_RUN_TASK_INDEX\", 0))\n    count = int(os.environ.get(\"CLOUD_RUN_TASK_COUNT\", 1))\n    attempt = int(os.environ.get(\"CLOUD_RUN_TASK_ATTEMPT\", 0))\n    mine = REGIONS[index::count]\n    out = Path(os.environ.get(\"OUTPUT_DIR\", \"\u002Ftmp\u002Freports\"))\n    out.mkdir(parents=True, exist_ok=True)\n    for region in mine:\n        df = pd.DataFrame({\"Region\": [region], \"Sales\": [100_000 + 1_000 * REGIONS.index(region)]})\n        df.to_excel(out \u002F f\"weekly_sales_{region}.xlsx\", index=False)\n    print(f\"task {index + 1}\u002F{count} (attempt {attempt}) wrote {mine}\", flush=True)\n    return 0\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n","python",[37,286,287,297,304,318,325,339,344,379,384,403,428,450,471,485,506,533,548,588,628,698,707,712,729],{"__ignoreMap":35},[40,288,289,293],{"class":42,"line":43},[40,290,292],{"class":291},"s-kum","import",[40,294,296],{"class":295},"skGVy"," os\n",[40,298,299,301],{"class":42,"line":64},[40,300,292],{"class":291},[40,302,303],{"class":295}," sys\n",[40,305,307,310,313,315],{"class":42,"line":306},3,[40,308,309],{"class":291},"from",[40,311,312],{"class":295}," pathlib ",[40,314,292],{"class":291},[40,316,317],{"class":295}," Path\n",[40,319,321],{"class":42,"line":320},4,[40,322,324],{"emptyLinePlaceholder":323},true,"\n",[40,326,328,330,333,336],{"class":42,"line":327},5,[40,329,292],{"class":291},[40,331,332],{"class":295}," pandas ",[40,334,335],{"class":291},"as",[40,337,338],{"class":295}," pd\n",[40,340,342],{"class":42,"line":341},6,[40,343,324],{"emptyLinePlaceholder":323},[40,345,347,351,354,357,360,363,366,368,371,373,376],{"class":42,"line":346},7,[40,348,350],{"class":349},"sP0c6","REGIONS",[40,352,353],{"class":291}," =",[40,355,356],{"class":295}," [",[40,358,359],{"class":50},"\"North\"",[40,361,362],{"class":295},", ",[40,364,365],{"class":50},"\"South\"",[40,367,362],{"class":295},[40,369,370],{"class":50},"\"East\"",[40,372,362],{"class":295},[40,374,375],{"class":50},"\"West\"",[40,377,378],{"class":295},"]\n",[40,380,382],{"class":42,"line":381},8,[40,383,324],{"emptyLinePlaceholder":323},[40,385,387,390,394,397,400],{"class":42,"line":386},9,[40,388,389],{"class":291},"def",[40,391,393],{"class":392},"s_Opv"," main",[40,395,396],{"class":295},"() -> ",[40,398,399],{"class":349},"int",[40,401,402],{"class":295},":\n",[40,404,406,409,412,415,418,421,423,425],{"class":42,"line":405},10,[40,407,408],{"class":295},"    index ",[40,410,411],{"class":291},"=",[40,413,414],{"class":349}," int",[40,416,417],{"class":295},"(os.environ.get(",[40,419,420],{"class":50},"\"CLOUD_RUN_TASK_INDEX\"",[40,422,362],{"class":295},[40,424,99],{"class":349},[40,426,427],{"class":295},"))\n",[40,429,431,434,436,438,440,443,445,448],{"class":42,"line":430},11,[40,432,433],{"class":295},"    count ",[40,435,411],{"class":291},[40,437,414],{"class":349},[40,439,417],{"class":295},[40,441,442],{"class":50},"\"CLOUD_RUN_TASK_COUNT\"",[40,444,362],{"class":295},[40,446,447],{"class":349},"1",[40,449,427],{"class":295},[40,451,453,456,458,460,462,465,467,469],{"class":42,"line":452},12,[40,454,455],{"class":295},"    attempt ",[40,457,411],{"class":291},[40,459,414],{"class":349},[40,461,417],{"class":295},[40,463,464],{"class":50},"\"CLOUD_RUN_TASK_ATTEMPT\"",[40,466,362],{"class":295},[40,468,99],{"class":349},[40,470,427],{"class":295},[40,472,474,477,479,482],{"class":42,"line":473},13,[40,475,476],{"class":295},"    mine ",[40,478,411],{"class":291},[40,480,481],{"class":349}," REGIONS",[40,483,484],{"class":295},"[index::count]\n",[40,486,488,491,493,496,499,501,504],{"class":42,"line":487},14,[40,489,490],{"class":295},"    out ",[40,492,411],{"class":291},[40,494,495],{"class":295}," Path(os.environ.get(",[40,497,498],{"class":50},"\"OUTPUT_DIR\"",[40,500,362],{"class":295},[40,502,503],{"class":50},"\"\u002Ftmp\u002Freports\"",[40,505,427],{"class":295},[40,507,509,512,516,518,521,523,526,528,530],{"class":42,"line":508},15,[40,510,511],{"class":295},"    out.mkdir(",[40,513,515],{"class":514},"sa561","parents",[40,517,411],{"class":291},[40,519,520],{"class":349},"True",[40,522,362],{"class":295},[40,524,525],{"class":514},"exist_ok",[40,527,411],{"class":291},[40,529,520],{"class":349},[40,531,532],{"class":295},")\n",[40,534,536,539,542,545],{"class":42,"line":535},16,[40,537,538],{"class":291},"    for",[40,540,541],{"class":295}," region ",[40,543,544],{"class":291},"in",[40,546,547],{"class":295}," mine:\n",[40,549,551,554,556,559,562,565,568,571,574,577,580,583,585],{"class":42,"line":550},17,[40,552,553],{"class":295},"        df ",[40,555,411],{"class":291},[40,557,558],{"class":295}," pd.DataFrame({",[40,560,561],{"class":50},"\"Region\"",[40,563,564],{"class":295},": [region], ",[40,566,567],{"class":50},"\"Sales\"",[40,569,570],{"class":295},": [",[40,572,573],{"class":349},"100_000",[40,575,576],{"class":291}," +",[40,578,579],{"class":349}," 1_000",[40,581,582],{"class":291}," *",[40,584,481],{"class":349},[40,586,587],{"class":295},".index(region)]})\n",[40,589,591,594,597,600,603,607,610,613,616,618,621,623,626],{"class":42,"line":590},18,[40,592,593],{"class":295},"        df.to_excel(out ",[40,595,596],{"class":291},"\u002F",[40,598,599],{"class":291}," f",[40,601,602],{"class":50},"\"weekly_sales_",[40,604,606],{"class":605},"sSjpA","{",[40,608,609],{"class":295},"region",[40,611,612],{"class":605},"}",[40,614,615],{"class":50},".xlsx\"",[40,617,362],{"class":295},[40,619,620],{"class":514},"index",[40,622,411],{"class":291},[40,624,625],{"class":349},"False",[40,627,532],{"class":295},[40,629,631,634,637,640,643,645,648,651,654,656,658,660,662,664,667,669,672,674,677,679,682,684,687,689,692,694,696],{"class":42,"line":630},19,[40,632,633],{"class":349},"    print",[40,635,636],{"class":295},"(",[40,638,639],{"class":291},"f",[40,641,642],{"class":50},"\"task ",[40,644,606],{"class":605},[40,646,647],{"class":295},"index ",[40,649,650],{"class":291},"+",[40,652,653],{"class":349}," 1",[40,655,612],{"class":605},[40,657,596],{"class":50},[40,659,606],{"class":605},[40,661,278],{"class":295},[40,663,612],{"class":605},[40,665,666],{"class":50}," (attempt ",[40,668,606],{"class":605},[40,670,671],{"class":295},"attempt",[40,673,612],{"class":605},[40,675,676],{"class":50},") wrote ",[40,678,606],{"class":605},[40,680,681],{"class":295},"mine",[40,683,612],{"class":605},[40,685,686],{"class":50},"\"",[40,688,362],{"class":295},[40,690,691],{"class":514},"flush",[40,693,411],{"class":291},[40,695,520],{"class":349},[40,697,532],{"class":295},[40,699,701,704],{"class":42,"line":700},20,[40,702,703],{"class":291},"    return",[40,705,706],{"class":349}," 0\n",[40,708,710],{"class":42,"line":709},21,[40,711,324],{"emptyLinePlaceholder":323},[40,713,715,718,721,724,727],{"class":42,"line":714},22,[40,716,717],{"class":291},"if",[40,719,720],{"class":349}," __name__",[40,722,723],{"class":291}," ==",[40,725,726],{"class":50}," \"__main__\"",[40,728,402],{"class":295},[40,730,732],{"class":42,"line":731},23,[40,733,734],{"class":295},"    sys.exit(main())\n",[10,736,737,740,741,744],{},[37,738,739],{},"REGIONS[index::count]"," is a simple, stable way to split work: with two tasks, task 0 takes North and East and task 1 takes South and West, and no region is done twice or missed. ",[37,742,743],{},"CLOUD_RUN_TASK_ATTEMPT"," counts retries of this task, useful in logs. The exit status matters more than it looks: Cloud Run decides whether a task succeeded from the process exit code, so a script that catches every exception and exits 0 turns failures into green runs. Let errors propagate, or return 1.",[25,746,748],{"id":747},"step-2-package-it","Step 2 — Package it",[10,750,751,752,755],{},"A slim Python base image, the dependencies pinned in ",[37,753,754],{},"requirements.txt",", and the script. Running as a non-root user is good practice and costs nothing:",[30,757,761],{"className":758,"code":760,"language":125,"meta":35},[759],"language-text","pandas==3.0.6\nopenpyxl==3.1.5\n",[37,762,760],{"__ignoreMap":35},[30,764,768],{"className":765,"code":766,"language":767,"meta":35,"style":35},"language-dockerfile shiki shiki-themes github-light github-dark-high-contrast","FROM python:3.12-slim\nWORKDIR \u002Fapp\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY main.py .\nUSER nobody\nCMD [\"python\", \"main.py\"]\n","dockerfile",[37,769,770,775,780,785,790,795,800],{"__ignoreMap":35},[40,771,772],{"class":42,"line":43},[40,773,774],{},"FROM python:3.12-slim\n",[40,776,777],{"class":42,"line":64},[40,778,779],{},"WORKDIR \u002Fapp\n",[40,781,782],{"class":42,"line":306},[40,783,784],{},"COPY requirements.txt .\n",[40,786,787],{"class":42,"line":320},[40,788,789],{},"RUN pip install --no-cache-dir -r requirements.txt\n",[40,791,792],{"class":42,"line":327},[40,793,794],{},"COPY main.py .\n",[40,796,797],{"class":42,"line":341},[40,798,799],{},"USER nobody\n",[40,801,802],{"class":42,"line":346},[40,803,804],{},"CMD [\"python\", \"main.py\"]\n",[30,806,808],{"className":32,"code":807,"language":34,"meta":35,"style":35},"docker build -q -t weekly-sales-job . > \u002Fdev\u002Fnull && echo \"image built\"\ndocker image inspect weekly-sales-job --format '{{.Config.User}} {{.Config.Cmd}}'\n",[37,809,810,845],{"__ignoreMap":35},[40,811,812,815,818,821,824,827,830,833,836,839,842],{"class":42,"line":43},[40,813,814],{"class":46},"docker",[40,816,817],{"class":50}," build",[40,819,820],{"class":349}," -q",[40,822,823],{"class":349}," -t",[40,825,826],{"class":50}," weekly-sales-job",[40,828,829],{"class":50}," .",[40,831,832],{"class":291}," >",[40,834,835],{"class":50}," \u002Fdev\u002Fnull",[40,837,838],{"class":295}," && ",[40,840,841],{"class":349},"echo",[40,843,844],{"class":50}," \"image built\"\n",[40,846,847,849,852,855,857,860],{"class":42,"line":64},[40,848,814],{"class":46},[40,850,851],{"class":50}," image",[40,853,854],{"class":50}," inspect",[40,856,826],{"class":50},[40,858,859],{"class":349}," --format",[40,861,862],{"class":50}," '{{.Config.User}} {{.Config.Cmd}}'\n",[10,864,865,866,868],{},"Copying ",[37,867,754],{}," and installing before copying the script means Docker reuses the dependency layer when only the script changes, so rebuilds take seconds. Pinning exact versions keeps next month's build from quietly picking up a pandas release that changes an output.",[25,870,872],{"id":871},"step-3-test-the-job-locally-the-way-cloud-run-runs-it","Step 3 — Test the job locally, the way Cloud Run runs it",[10,874,875,876,879],{},"Cloud Run runs the image's command with the task environment variables set. ",[37,877,878],{},"docker run"," can do the same, which tests the container — not just the script — before anything is deployed. Run it once as a single task, then as the two tasks of a parallel execution:",[30,881,883],{"className":32,"code":882,"language":34,"meta":35,"style":35},"docker run --rm weekly-sales-job sh -c 'python main.py && ls \u002Ftmp\u002Freports'\necho \"exit status: $?\"\nfor i in 0 1; do\n  docker run --rm -e CLOUD_RUN_TASK_INDEX=$i -e CLOUD_RUN_TASK_COUNT=2 weekly-sales-job sh -c 'python main.py && ls \u002Ftmp\u002Freports'\ndone\n",[37,884,885,906,919,940,975],{"__ignoreMap":35},[40,886,887,889,892,895,897,900,903],{"class":42,"line":43},[40,888,814],{"class":46},[40,890,891],{"class":50}," run",[40,893,894],{"class":349}," --rm",[40,896,826],{"class":50},[40,898,899],{"class":50}," sh",[40,901,902],{"class":349}," -c",[40,904,905],{"class":50}," 'python main.py && ls \u002Ftmp\u002Freports'\n",[40,907,908,910,913,916],{"class":42,"line":64},[40,909,841],{"class":349},[40,911,912],{"class":50}," \"exit status: ",[40,914,915],{"class":349},"$?",[40,917,918],{"class":50},"\"\n",[40,920,921,924,927,929,932,934,937],{"class":42,"line":306},[40,922,923],{"class":291},"for",[40,925,926],{"class":295}," i ",[40,928,544],{"class":291},[40,930,931],{"class":50}," 0",[40,933,653],{"class":50},[40,935,936],{"class":295},"; ",[40,938,939],{"class":291},"do\n",[40,941,942,945,947,949,952,955,958,961,964,967,969,971,973],{"class":42,"line":320},[40,943,944],{"class":46},"  docker",[40,946,891],{"class":50},[40,948,894],{"class":349},[40,950,951],{"class":349}," -e",[40,953,954],{"class":50}," CLOUD_RUN_TASK_INDEX=",[40,956,957],{"class":295},"$i ",[40,959,960],{"class":349},"-e",[40,962,963],{"class":50}," CLOUD_RUN_TASK_COUNT=",[40,965,966],{"class":349},"2",[40,968,826],{"class":50},[40,970,899],{"class":50},[40,972,902],{"class":349},[40,974,905],{"class":50},[40,976,977],{"class":42,"line":327},[40,978,979],{"class":291},"done\n",[10,981,982],{},"As one task, the container writes all four workbooks and exits with status 0. As two tasks, the regions split cleanly — North and East in the first, South and West in the second — and each task runs in its own container with its own file system, which is why the output must go to shared storage such as Cloud Storage rather than a local folder. A failing script would show a non-zero status here, exactly as Cloud Run would record it.",[77,984,86,989,86,992,86,995,86,998,86,1002,86,1007,86,1010,86,1014,86,1019,86,1025,86,1031,86,1036,86,1039,86,1042,86,1045,86,1050,86,1055,86,1059,86,1062],{"viewBox":985,"role":80,"ariaLabelledBy":986,"xmlns":84,"style":85},"4 0 752 228",[987,988],"crj-tasks-t","crj-tasks-d",[88,990,991],{"id":987},"One execution, two parallel tasks",[92,993,994],{"id":988},"A job execution with a task count of two fans out to two containers. Task 0 builds the North and East workbooks and task 1 builds South and West, each uploading to Cloud Storage.",[96,996],{"x":98,"y":99,"width":100,"height":997,"fill":102},"228",[96,999],{"x":105,"y":1000,"width":1001,"height":128,"rx":109,"fill":110,"stroke":111,"style":112},"67.5","220.0",[114,1003,1005],{"transform":1004},"translate(118.0,80.5) scale(1.000)",[118,1006],{"d":181,"fill":121,"stroke":122,"style":123},[125,1008,253],{"x":195,"y":1009,"style":129},"127.0",[125,1011,1013],{"x":195,"y":1012,"style":134},"146.5","--tasks 2",[96,1015],{"x":1016,"y":106,"width":1017,"height":1018,"rx":109,"fill":150,"stroke":141,"style":112},"490.0","250.0","92.0",[114,1020,1022],{"transform":1021},"translate(603.0,29.0) scale(1.000)",[118,1023],{"d":1024,"fill":121,"stroke":157,"style":123},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6",[125,1026,1030],{"x":1027,"y":1028,"style":1029},"615.0","75.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Task 0 — North, East",[125,1032,1035],{"x":1027,"y":1033,"style":1034},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","CLOUD_RUN_TASK_INDEX=0",[118,1037],{"d":1038,"fill":121,"stroke":141,"style":112},"M244.0,114.0 C364.0,114.0 364.0,62.0 480.0,62.0",[143,1040],{"points":1041,"fill":146},"484.0,62.0 475.0,67.0 475.0,57.0",[96,1043],{"x":1016,"y":1044,"width":1017,"height":1018,"rx":109,"fill":207,"stroke":208,"style":112},"120.0",[114,1046,1048],{"transform":1047},"translate(603.0,133.0) scale(1.000)",[118,1049],{"d":1024,"fill":121,"stroke":215,"style":123},[125,1051,1054],{"x":1027,"y":1052,"style":1053},"179.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Task 1 — South, West",[125,1056,1058],{"x":1027,"y":1057,"style":1034},"198.0","CLOUD_RUN_TASK_INDEX=1",[118,1060],{"d":1061,"fill":121,"stroke":141,"style":112},"M244.0,114.0 C364.0,114.0 364.0,166.0 480.0,166.0",[143,1063],{"points":1064,"fill":146},"484.0,166.0 475.0,171.0 475.0,161.0",[25,1066,1068],{"id":1067},"step-4-deploy-the-job","Step 4 — Deploy the job",[10,1070,1071],{},"Build the image in Cloud Build, then create the job with its retry and timeout settings and a dedicated service account:",[30,1073,1075],{"className":32,"code":1074,"language":34,"meta":35,"style":35},"gcloud builds submit --tag europe-west2-docker.pkg.dev\u002Facme-analytics\u002Freports\u002Fweekly-sales-job\ngcloud run jobs create weekly-sales-job \\\n  --image europe-west2-docker.pkg.dev\u002Facme-analytics\u002Freports\u002Fweekly-sales-job \\\n  --region europe-west2 \\\n  --tasks 2 --parallelism 2 \\\n  --max-retries 2 --task-timeout 20m \\\n  --memory 1Gi \\\n  --service-account weekly-reports@acme-analytics.iam.gserviceaccount.com\ngcloud run jobs execute weekly-sales-job --region europe-west2 --wait\n",[37,1076,1077,1094,1111,1121,1131,1146,1161,1171,1179],{"__ignoreMap":35},[40,1078,1079,1082,1085,1088,1091],{"class":42,"line":43},[40,1080,1081],{"class":46},"gcloud",[40,1083,1084],{"class":50}," builds",[40,1086,1087],{"class":50}," submit",[40,1089,1090],{"class":349}," --tag",[40,1092,1093],{"class":50}," europe-west2-docker.pkg.dev\u002Facme-analytics\u002Freports\u002Fweekly-sales-job\n",[40,1095,1096,1098,1100,1103,1106,1108],{"class":42,"line":64},[40,1097,1081],{"class":46},[40,1099,891],{"class":50},[40,1101,1102],{"class":50}," jobs",[40,1104,1105],{"class":50}," create",[40,1107,826],{"class":50},[40,1109,1110],{"class":605}," \\\n",[40,1112,1113,1116,1119],{"class":42,"line":306},[40,1114,1115],{"class":349},"  --image",[40,1117,1118],{"class":50}," europe-west2-docker.pkg.dev\u002Facme-analytics\u002Freports\u002Fweekly-sales-job",[40,1120,1110],{"class":605},[40,1122,1123,1126,1129],{"class":42,"line":320},[40,1124,1125],{"class":349},"  --region",[40,1127,1128],{"class":50}," europe-west2",[40,1130,1110],{"class":605},[40,1132,1133,1136,1139,1142,1144],{"class":42,"line":327},[40,1134,1135],{"class":349},"  --tasks",[40,1137,1138],{"class":349}," 2",[40,1140,1141],{"class":349}," --parallelism",[40,1143,1138],{"class":349},[40,1145,1110],{"class":605},[40,1147,1148,1151,1153,1156,1159],{"class":42,"line":341},[40,1149,1150],{"class":349},"  --max-retries",[40,1152,1138],{"class":349},[40,1154,1155],{"class":349}," --task-timeout",[40,1157,1158],{"class":50}," 20m",[40,1160,1110],{"class":605},[40,1162,1163,1166,1169],{"class":42,"line":346},[40,1164,1165],{"class":349},"  --memory",[40,1167,1168],{"class":50}," 1Gi",[40,1170,1110],{"class":605},[40,1172,1173,1176],{"class":42,"line":381},[40,1174,1175],{"class":349},"  --service-account",[40,1177,1178],{"class":50}," weekly-reports@acme-analytics.iam.gserviceaccount.com\n",[40,1180,1181,1183,1185,1187,1190,1192,1195,1197],{"class":42,"line":386},[40,1182,1081],{"class":46},[40,1184,891],{"class":50},[40,1186,1102],{"class":50},[40,1188,1189],{"class":50}," execute",[40,1191,826],{"class":50},[40,1193,1194],{"class":349}," --region",[40,1196,1128],{"class":50},[40,1198,1199],{"class":349}," --wait\n",[10,1201,1202,1205,1206,1209,1210,1213,1214,1218,1219,1222],{},[37,1203,1204],{},"--max-retries"," retries a failed ",[14,1207,1208],{},"task"," — not the whole job — so a transient database error in one region reruns only that region. ",[37,1211,1212],{},"--task-timeout"," stops a stuck task; set it to a few times the normal run time rather than the maximum. The service account carries the job's permissions — read the source, write the report bucket — so no key files are needed inside the image; see ",[19,1215,1217],{"href":1216},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002F","Store Credentials for Excel Report Scripts Safely",". ",[37,1220,1221],{},"execute --wait"," runs it once and waits, which is the deployment smoke test.",[25,1224,1226],{"id":1225},"step-5-schedule-it","Step 5 — Schedule it",[10,1228,1229],{},"Cloud Scheduler starts the job by calling the Cloud Run Admin API. Create a scheduler job with a cron expression, a time zone and a service account allowed to run the job:",[30,1231,1233],{"className":32,"code":1232,"language":34,"meta":35,"style":35},"gcloud scheduler jobs create http weekly-sales-monday \\\n  --location europe-west2 \\\n  --schedule \"0 7 * * 1\" --time-zone \"Europe\u002FLondon\" \\\n  --uri \"https:\u002F\u002Frun.googleapis.com\u002Fv2\u002Fprojects\u002Facme-analytics\u002Flocations\u002Feurope-west2\u002Fjobs\u002Fweekly-sales-job:run\" \\\n  --http-method POST \\\n  --oauth-service-account-email scheduler-invoker@acme-analytics.iam.gserviceaccount.com\n",[37,1234,1235,1254,1263,1279,1289,1299],{"__ignoreMap":35},[40,1236,1237,1239,1242,1244,1246,1249,1252],{"class":42,"line":43},[40,1238,1081],{"class":46},[40,1240,1241],{"class":50}," scheduler",[40,1243,1102],{"class":50},[40,1245,1105],{"class":50},[40,1247,1248],{"class":50}," http",[40,1250,1251],{"class":50}," weekly-sales-monday",[40,1253,1110],{"class":605},[40,1255,1256,1259,1261],{"class":42,"line":64},[40,1257,1258],{"class":349},"  --location",[40,1260,1128],{"class":50},[40,1262,1110],{"class":605},[40,1264,1265,1268,1271,1274,1277],{"class":42,"line":306},[40,1266,1267],{"class":349},"  --schedule",[40,1269,1270],{"class":50}," \"0 7 * * 1\"",[40,1272,1273],{"class":349}," --time-zone",[40,1275,1276],{"class":50}," \"Europe\u002FLondon\"",[40,1278,1110],{"class":605},[40,1280,1281,1284,1287],{"class":42,"line":320},[40,1282,1283],{"class":349},"  --uri",[40,1285,1286],{"class":50}," \"https:\u002F\u002Frun.googleapis.com\u002Fv2\u002Fprojects\u002Facme-analytics\u002Flocations\u002Feurope-west2\u002Fjobs\u002Fweekly-sales-job:run\"",[40,1288,1110],{"class":605},[40,1290,1291,1294,1297],{"class":42,"line":327},[40,1292,1293],{"class":349},"  --http-method",[40,1295,1296],{"class":50}," POST",[40,1298,1110],{"class":605},[40,1300,1301,1304],{"class":42,"line":341},[40,1302,1303],{"class":349},"  --oauth-service-account-email",[40,1305,1306],{"class":50}," scheduler-invoker@acme-analytics.iam.gserviceaccount.com\n",[10,1308,1309,1310,1313,1314,1317,1318,1321,1322,1326],{},"Unlike Azure's six-field format, Cloud Scheduler uses standard five-field cron, and ",[37,1311,1312],{},"--time-zone"," makes 07:00 mean 07:00 London time all year, through the clock changes. The console's ",[14,1315,1316],{},"Triggers"," tab on the job page creates the same scheduler job with a form. Cloud Logging collects each task's output — the ",[37,1319,1320],{},"print"," line from Step 1 — and a log-based alert on failed executions closes the loop: ",[19,1323,1325],{"href":1324},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run\u002F","Detect a Scheduled Report That Did Not Run"," covers the other failure, a run that never started.",[77,1328,86,1333,86,1336,86,1339,86,1342,86,1346,86,1351,86,1354,86,1357,86,1360,86,1364,86,1369,86,1371,86,1373,86,1378,86,1381,86,1388,86,1394,86,1399,86,1402,86,1407,86,1412,86,1417,86,1420,86,1425],{"viewBox":1329,"role":80,"ariaLabelledBy":1330,"xmlns":84,"style":85},"4 0 752 244",[1331,1332],"crj-seq-t","crj-seq-d",[88,1334,1335],{"id":1331},"Monday 07:00, London time",[92,1337,1338],{"id":1332},"A sequence diagram. Cloud Scheduler calls the Cloud Run Admin API to run the job; Cloud Run starts two task containers, which build and upload their workbooks and exit with status zero; the execution is marked succeeded.",[96,1340],{"x":98,"y":99,"width":100,"height":1341,"fill":102},"244",[96,1343],{"x":105,"y":106,"width":1344,"height":1345,"rx":109,"fill":150,"stroke":141,"style":112},"180.0","48.0",[125,1347,1350],{"x":1348,"y":1349,"style":1029},"110.0","44.5","Cloud Scheduler",[96,1352],{"x":1353,"y":106,"width":1344,"height":1345,"rx":109,"fill":110,"stroke":111,"style":112},"290.0",[125,1355,186],{"x":184,"y":1349,"style":1356},"font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[96,1358],{"x":1359,"y":106,"width":1344,"height":1345,"rx":109,"fill":110,"stroke":111,"style":112},"560.0",[125,1361,1363],{"x":1362,"y":1349,"style":1356},"650.0","task containers",[42,1365],{"x1":1348,"y1":1366,"x2":1348,"y2":1367,"stroke":111,"style":1368},"64.0","228.0","stroke-width:1.5px;stroke-dasharray:5 4",[42,1370],{"x1":184,"y1":1366,"x2":184,"y2":1367,"stroke":111,"style":1368},[42,1372],{"x1":1362,"y1":1366,"x2":1362,"y2":1367,"stroke":111,"style":1368},[42,1374],{"x1":1375,"y1":1376,"x2":1377,"y2":1376,"stroke":141,"style":112},"115.0","104.0","371.0",[143,1379],{"points":1380,"fill":146},"375.0,104.0 366.0,109.0 366.0,99.0",[96,1382],{"x":1383,"y":1028,"width":1384,"height":1385,"rx":1386,"fill":102,"stroke":111,"style":1387},"125.3","239.4","21.0","10","stroke-width:1.2px",[125,1389,1393],{"x":1390,"y":1391,"style":1392},"245.0","89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. POST …\u002Fjobs\u002Fweekly-sales-job:run",[42,1395],{"x1":1396,"y1":1397,"x2":1398,"y2":1397,"stroke":141,"style":112},"385.0","152.0","641.0",[143,1400],{"points":1401,"fill":146},"645.0,152.0 636.0,157.0 636.0,147.0",[96,1403],{"x":1404,"y":1405,"width":1406,"height":1385,"rx":1386,"fill":102,"stroke":111,"style":1387},"408.4","123.0","213.2",[125,1408,1411],{"x":1409,"y":1410,"style":1392},"515.0","137.5","2. start 2 tasks with index 0 and 1",[42,1413],{"x1":1414,"y1":1415,"x2":1416,"y2":1415,"stroke":141,"style":112},"645.0","200.0","389.0",[143,1418],{"points":1419,"fill":146},"385.0,200.0 394.0,195.0 394.0,205.0",[96,1421],{"x":1422,"y":1423,"width":1424,"height":1385,"rx":1386,"fill":102,"stroke":111,"style":1387},"410.2","171.0","209.6",[125,1426,1428],{"x":1409,"y":1427,"style":1392},"185.5","3. exit 0 → execution succeeded",[25,1430,1432],{"id":1431},"performance-notes","Performance notes",[10,1434,1435],{},"A job is billed for the CPU and memory its tasks use while they run, so a weekly report that takes two minutes costs very little; parallel tasks finish sooner but do not cost less in total. Start-up adds a few seconds per task to pull the image and start Python — keep images slim and put heavy imports only where they are needed. Splitting work into tasks pays off when each unit takes minutes, as a large regional extract does; for four small workbooks, one task is simpler.",[25,1437,1439],{"id":1438},"common-pitfalls","Common pitfalls",[1441,1442,1443,1451,1457,1463,1469],"ul",{},[1444,1445,1446,1450],"li",{},[1447,1448,1449],"strong",{},"Swallowing exceptions."," Cloud Run judges success by exit code; a script that exits 0 after an error reports success.",[1444,1452,1453,1456],{},[1447,1454,1455],{},"Writing output to the container's disk."," It disappears when the task ends; upload to Cloud Storage.",[1444,1458,1459,1462],{},[1447,1460,1461],{},"Uneven work splits."," Splitting by index works when units are similar in size; for very uneven work, give big units their own task.",[1444,1464,1465,1468],{},[1447,1466,1467],{},"Retries that duplicate output."," A retried task runs again from the start; make uploads idempotent, for example by overwriting the same object name.",[1444,1470,1471,1474],{},[1447,1472,1473],{},"Timeouts set to the maximum."," A stuck task then burns hours; set a timeout a few times the normal duration.",[25,1476,1478],{"id":1477},"conclusion","Conclusion",[10,1480,1481,1482,1484,1485,1487,1488,1490],{},"Write the report as a script that exits non-zero on failure and takes its share of the work from ",[37,1483,270],{}," and ",[37,1486,274],{},", package it in a slim image with pinned dependencies, and test the container locally with ",[37,1489,878],{}," and the same environment variables. Deploy it as a Cloud Run job with task retries, a sensible timeout and its own service account, and start it with a Cloud Scheduler job on a five-field cron schedule in your time zone.",[25,1492,1494],{"id":1493},"frequently-asked-questions","Frequently asked questions",[10,1496,1497,1500],{},[1447,1498,1499],{},"What is the difference between a Cloud Run service and a Cloud Run job?","\nA service runs a web server that answers requests; a job runs a container to completion and stops. Reports, exports and batch scripts are jobs.",[10,1502,1503,1506],{},[1447,1504,1505],{},"How do I schedule a Cloud Run job?","\nCreate a Cloud Scheduler job that sends a POST to the job's run endpoint in the Cloud Run Admin API, with a cron schedule, a time zone and a service account that is allowed to run the job. The job page's Triggers tab creates it for you.",[10,1508,1509,1512],{},[1447,1510,1511],{},"How do I run parts of a report in parallel on Cloud Run?","\nSet --tasks and --parallelism on the job and have the script read CLOUD_RUN_TASK_INDEX and CLOUD_RUN_TASK_COUNT to pick its share of the work, for example every count-th region starting at its index.",[10,1514,1515,1518],{},[1447,1516,1517],{},"How long can a Cloud Run job run?","\nEach task has a timeout you set with --task-timeout, which can be much longer than function limits — hours rather than minutes — so long reports that do not fit in a function fit comfortably in a job.",[25,1520,1522],{"id":1521},"related","Related",[1441,1524,1525,1531,1538,1545],{},[1444,1526,1527,1528,1530],{},"Up one level: ",[19,1529,22],{"href":21}," — every way to run reports on a schedule.",[1444,1532,1533,1537],{},[19,1534,1536],{"href":1535},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-python-excel-report-in-docker\u002F","Run a Python Excel Report in Docker"," — the container basics this page builds on.",[1444,1539,1540,1544],{},[19,1541,1543],{"href":1542},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob\u002F","Run an Excel Report as a Kubernetes CronJob"," — the same container on Kubernetes.",[1444,1546,1547,1551],{},[19,1548,1550],{"href":1549},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-in-azure-functions\u002F","Run an Excel Report in Azure Functions"," — the serverless option for short reports.",[1553,1554,1555],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":64,"depth":64,"links":1557},[1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568],{"id":27,"depth":64,"text":28},{"id":259,"depth":64,"text":260},{"id":747,"depth":64,"text":748},{"id":871,"depth":64,"text":872},{"id":1067,"depth":64,"text":1068},{"id":1225,"depth":64,"text":1226},{"id":1431,"depth":64,"text":1432},{"id":1438,"depth":64,"text":1439},{"id":1477,"depth":64,"text":1478},{"id":1493,"depth":64,"text":1494},{"id":1521,"depth":64,"text":1522},"2026-09-29","Package a Python Excel report as a container and run it on a schedule with Cloud Run jobs and Cloud Scheduler: a slim image, a script that exits with a meaningful status, parallel tasks that split work by CLOUD_RUN_TASK_INDEX, retries and timeouts, a service account instead of keys, and a local test with Docker before anything is deployed.","md",[1573,1575,1577,1579],{"q":1499,"a":1574},"A service runs a web server that answers requests; a job runs a container to completion and stops. Reports, exports and batch scripts are jobs.",{"q":1505,"a":1576},"Create a Cloud Scheduler job that sends a POST to the job's run endpoint in the Cloud Run Admin API, with a cron schedule, a time zone and a service account that is allowed to run the job. The job page's Triggers tab creates it for you.",{"q":1511,"a":1578},"Set --tasks and --parallelism on the job and have the script read CLOUD_RUN_TASK_INDEX and CLOUD_RUN_TASK_COUNT to pick its share of the work, for example every count-th region starting at its index.",{"q":1517,"a":1580},"Each task has a timeout you set with --task-timeout, which can be much longer than function limits — hours rather than minutes — so long reports that do not fit in a function fit comfortably in a job.","run an excel report on google cloud run jobs",null,{"breadcrumb":1584},[1585,1587,1590],{"name":1586,"item":596},"Home",{"name":1588,"item":1589},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs",{"title":1593,"description":1594},"Schedule a Python Excel Report with Cloud Run Jobs","Run an Excel report as a Google Cloud Run job: Dockerfile, exit codes, CLOUD_RUN_TASK_INDEX and CLOUD_RUN_TASK_COUNT for parallel regions, --max-retries and --task-timeout, Cloud Scheduler triggers, service accounts and local testing with docker run.","run-an-excel-report-on-google-cloud-run-jobs","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs\u002Findex",[1598,1599],"scheduling","publishing","how-to","Gp_35O3VNQ0Y-C41td6DcrxTEtyhkR1BJzq9-TOowS0",[1603,1606],{"title":1550,"path":1604,"stem":1605,"children":-1},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-in-azure-functions","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-in-azure-functions\u002Findex",{"title":1607,"path":1608,"stem":1609,"children":-1},"Run a Python Excel Script on Windows Task Scheduler","\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",[1611,1613],{"path":1612,"title":1588},"\u002Fautomating-reporting-workflows",{"path":1614,"title":22},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[1617,1620,1623,1626,1629,1632],{"path":1618,"title":1619,"kind":1600},"\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":1621,"title":1622,"kind":1600},"\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":1624,"title":1625,"kind":1600},"\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":1627,"title":1628,"kind":1600},"\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":1630,"title":1631,"kind":1600},"\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":1633,"title":1325,"kind":1600},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run",1790972138879]