[{"data":1,"prerenderedAt":1040},["ShallowReactive",2],{"blog-en-morning-briefing-no-inference":3,"header-blog-translations-/en/blog/morning-briefing-no-inference":1037},{"id":4,"title":5,"author":6,"body":7,"date":1019,"description":1020,"draft":1021,"extension":1022,"image":1023,"meta":1024,"navigation":1025,"path":1026,"seo":1027,"stem":1028,"tags":1029,"translationKey":1035,"__hash__":1036},"blog_en/blog/en/morning-briefing-no-inference.md","Not Every Morning Briefing Needs Inference","Patrick Hofmann",{"type":8,"value":9,"toc":1010},"minimark",[10,14,17,22,29,48,51,58,62,65,68,71,79,83,89,130,133,529,539,553,825,833,839,843,846,849,860,864,871,881,884,887,893,897,900,912,931,934,950,953,968,972,981,984,987,1006],[11,12,13],"p",{},"My first signal of the day arrives at 7:00 in the morning as a Telegram message. In it: what's on the calendar today, which tasks are due today, how full the inbox is, plus a 7-day lookahead. Four sections, always the same fields, always the same shape. I read it on my phone before I'm at the computer, and then roughly know what the day looks like.",[11,15,16],{},"The setup has worked for a few weeks. Until recently an LLM was part of the pipeline. Now it isn't. Here's why.",[18,19,21],"h2",{"id":20},"how-it-was-before","How it was before",[11,23,24,25],{},"The briefing script called a headless Claude with a prompt that sounded roughly like: ",[26,27,28],"em",{},"\"Collect the appointments for the next hours from both Outlook accounts. Get the open tasks. Count the unread mails. Write a compact German briefing with emoji accents from that.\"",[11,30,31,32,36,37,36,40,43,44,47],{},"Claude called the necessary CLIs via tool-use — ",[33,34,35],"code",{},"o365-cli calendar today",", ",[33,38,39],{},"ape-tasks list",[33,41,42],{},"o365-cli mail list --unread",". Collected the JSON outputs. Formatted the briefing. My Bash wrapper took the finished briefing and sent it via ",[33,45,46],{},"curl"," to the Telegram bot API.",[11,49,50],{},"That worked. Really. Every morning a reasonably summarized briefing came in. Patrick-style formatted, with emojis in the right places, with weekday abbreviations, with a 1–2-sentence focus at the end that prioritized the most important thing of the day.",[11,52,53,54,57],{},"The focus sentence, by the way, was the only part an LLM was needed for at all. The rest could have been done by a Bash script too — with ",[33,55,56],{},"jq"," over the JSON outputs, deterministic, without an API call.",[18,59,61],{"id":60},"why-i-took-it-out","Why I took it out",[11,63,64],{},"I built it in because it was convenient: one prompt, a single call, the model's tool-use does the rest. On substance, though, it was clear to me from the start that this doesn't fit. The AI spent about a minute on intelligent tool-call processing, which my current script does deterministically in two seconds — and that only because the Microsoft Graph API takes a moment to answer. Factor 30. Plus every run costs a few cents of inference. Plus on every run there's a tiny chance the model parses something wrong — a date, a time, an account name.",[11,66,67],{},"For a briefing built on structured data in known formats, all of that is unnecessary. The calendar events come as JSON from the Microsoft Graph API. The tasks come as JSON from the ape-tasks API. The mail counts are a simple length operation on a JSON array. There's nothing to interpret, nothing to balance, nothing to infer.",[11,69,70],{},"In the script header I noted it like this:",[72,73,74],"blockquote",{},[11,75,76],{},[26,77,78],{},"\"The previous Claude-headless version made cost & latency that wasn't earning its keep on this kind of structured data.\"",[18,80,82],{"id":81},"how-it-looks-now","How it looks now",[11,84,85,86,88],{},"The current script is pure shell with ",[33,87,56],{}," for the JSON processing. Four functions, one per section:",[90,91,92,102,110,122],"ul",{},[93,94,95,101],"li",{},[96,97,98],"strong",{},[33,99,100],{},"section_today"," — appointments today, both Outlook accounts",[93,103,104,109],{},[96,105,106],{},[33,107,108],{},"section_tasks"," — open/in-progress tasks, with overdue marker and reminder counter",[93,111,112,117,118,121],{},[96,113,114],{},[33,115,116],{},"section_mails"," — unread counts per account, simple ",[33,119,120],{},"length"," operation on JSON array",[93,123,124,129],{},[96,125,126],{},[33,127,128],{},"section_next7"," — appointments from tomorrow for the next 7 days, both accounts",[11,131,132],{},"One section as an example, today's appointments:",[134,135,140],"pre",{"className":136,"code":137,"language":138,"meta":139,"style":139},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","section_today() {\n  local out=\"\" events lines\n  for acct in \"${ACCOUNTS[@]}\"; do\n    events=$(o365-cli calendar today --account \"$acct\" --json 2>/dev/null)\n    if [ -z \"$events\" ] || [ \"$events\" = \"null\" ]; then continue; fi\n    lines=$(printf '%s' \"$events\" | jq -r '\n      .[]\n      | if .is_all_day\n        then \"- ganztags \" + .subject\n        else \"- \"\n             + (.start | fromdateiso8601 | localtime | strftime(\"%H:%M\"))\n             + \"-\"\n             + (.end   | fromdateiso8601 | localtime | strftime(\"%H:%M\"))\n             + \" \" + .subject\n        end')\n    if [ -n \"$lines\" ]; then\n      out+=\"$lines\"$'\\n'\n    fi\n  done\n  if [ -z \"$out\" ]; then\n    printf '🗓 Keine Termine heute\\n'\n  else\n    printf '🗓 Termine heute\\n%s' \"$out\"\n  fi\n}\n","bash","",[33,141,142,157,177,205,249,306,344,350,356,362,368,374,380,386,392,402,424,446,452,458,479,492,498,517,523],{"__ignoreMap":139},[143,144,147,150,154],"span",{"class":145,"line":146},"line",1,[143,148,100],{"class":149},"s2Zo4",[143,151,153],{"class":152},"sMK4o","()",[143,155,156],{"class":152}," {\n",[143,158,160,164,168,171,174],{"class":145,"line":159},2,[143,161,163],{"class":162},"spNyl","  local",[143,165,167],{"class":166},"sTEyZ"," out",[143,169,170],{"class":152},"=",[143,172,173],{"class":152},"\"\"",[143,175,176],{"class":166}," events lines\n",[143,178,180,184,187,190,193,196,199,202],{"class":145,"line":179},3,[143,181,183],{"class":182},"s7zQu","  for",[143,185,186],{"class":166}," acct ",[143,188,189],{"class":182},"in",[143,191,192],{"class":152}," \"${",[143,194,195],{"class":166},"ACCOUNTS",[143,197,198],{"class":152},"[@]}\"",[143,200,201],{"class":152},";",[143,203,204],{"class":182}," do\n",[143,206,208,211,214,218,222,225,228,231,234,237,240,243,246],{"class":145,"line":207},4,[143,209,210],{"class":166},"    events",[143,212,213],{"class":152},"=$(",[143,215,217],{"class":216},"sBMFI","o365-cli",[143,219,221],{"class":220},"sfazB"," calendar",[143,223,224],{"class":220}," today",[143,226,227],{"class":220}," --account",[143,229,230],{"class":152}," \"",[143,232,233],{"class":166},"$acct",[143,235,236],{"class":152},"\"",[143,238,239],{"class":220}," --json",[143,241,242],{"class":152}," 2>",[143,244,245],{"class":220},"/dev/null",[143,247,248],{"class":152},")\n",[143,250,252,255,258,261,263,266,268,271,274,276,278,280,282,285,287,290,292,295,298,301,303],{"class":145,"line":251},5,[143,253,254],{"class":182},"    if",[143,256,257],{"class":152}," [",[143,259,260],{"class":152}," -z",[143,262,230],{"class":152},[143,264,265],{"class":166},"$events",[143,267,236],{"class":152},[143,269,270],{"class":152}," ]",[143,272,273],{"class":152}," ||",[143,275,257],{"class":152},[143,277,230],{"class":152},[143,279,265],{"class":166},[143,281,236],{"class":152},[143,283,284],{"class":152}," =",[143,286,230],{"class":152},[143,288,289],{"class":220},"null",[143,291,236],{"class":152},[143,293,294],{"class":152}," ];",[143,296,297],{"class":182}," then",[143,299,300],{"class":182}," continue",[143,302,201],{"class":152},[143,304,305],{"class":182}," fi\n",[143,307,309,312,314,317,320,323,326,328,330,332,335,338,341],{"class":145,"line":308},6,[143,310,311],{"class":166},"    lines",[143,313,213],{"class":152},[143,315,316],{"class":149},"printf",[143,318,319],{"class":152}," '",[143,321,322],{"class":220},"%s",[143,324,325],{"class":152},"'",[143,327,230],{"class":152},[143,329,265],{"class":166},[143,331,236],{"class":152},[143,333,334],{"class":152}," |",[143,336,337],{"class":216}," jq",[143,339,340],{"class":220}," -r",[143,342,343],{"class":152}," '\n",[143,345,347],{"class":145,"line":346},7,[143,348,349],{"class":220},"      .[]\n",[143,351,353],{"class":145,"line":352},8,[143,354,355],{"class":220},"      | if .is_all_day\n",[143,357,359],{"class":145,"line":358},9,[143,360,361],{"class":220},"        then \"- ganztags \" + .subject\n",[143,363,365],{"class":145,"line":364},10,[143,366,367],{"class":220},"        else \"- \"\n",[143,369,371],{"class":145,"line":370},11,[143,372,373],{"class":220},"             + (.start | fromdateiso8601 | localtime | strftime(\"%H:%M\"))\n",[143,375,377],{"class":145,"line":376},12,[143,378,379],{"class":220},"             + \"-\"\n",[143,381,383],{"class":145,"line":382},13,[143,384,385],{"class":220},"             + (.end   | fromdateiso8601 | localtime | strftime(\"%H:%M\"))\n",[143,387,389],{"class":145,"line":388},14,[143,390,391],{"class":220},"             + \" \" + .subject\n",[143,393,395,398,400],{"class":145,"line":394},15,[143,396,397],{"class":220},"        end",[143,399,325],{"class":152},[143,401,248],{"class":152},[143,403,405,407,409,412,414,417,419,421],{"class":145,"line":404},16,[143,406,254],{"class":182},[143,408,257],{"class":152},[143,410,411],{"class":152}," -n",[143,413,230],{"class":152},[143,415,416],{"class":166},"$lines",[143,418,236],{"class":152},[143,420,294],{"class":152},[143,422,423],{"class":182}," then\n",[143,425,427,430,433,435,437,440,443],{"class":145,"line":426},17,[143,428,429],{"class":166},"      out",[143,431,432],{"class":152},"+=",[143,434,236],{"class":152},[143,436,416],{"class":166},[143,438,439],{"class":152},"\"$'",[143,441,442],{"class":166},"\\n",[143,444,445],{"class":152},"'\n",[143,447,449],{"class":145,"line":448},18,[143,450,451],{"class":182},"    fi\n",[143,453,455],{"class":145,"line":454},19,[143,456,457],{"class":182},"  done\n",[143,459,461,464,466,468,470,473,475,477],{"class":145,"line":460},20,[143,462,463],{"class":182},"  if",[143,465,257],{"class":152},[143,467,260],{"class":152},[143,469,230],{"class":152},[143,471,472],{"class":166},"$out",[143,474,236],{"class":152},[143,476,294],{"class":152},[143,478,423],{"class":182},[143,480,482,485,487,490],{"class":145,"line":481},21,[143,483,484],{"class":149},"    printf",[143,486,319],{"class":152},[143,488,489],{"class":220},"🗓 Keine Termine heute\\n",[143,491,445],{"class":152},[143,493,495],{"class":145,"line":494},22,[143,496,497],{"class":182},"  else\n",[143,499,501,503,505,508,510,512,514],{"class":145,"line":500},23,[143,502,484],{"class":149},[143,504,319],{"class":152},[143,506,507],{"class":220},"🗓 Termine heute\\n%s",[143,509,325],{"class":152},[143,511,230],{"class":152},[143,513,472],{"class":166},[143,515,516],{"class":152},"\"\n",[143,518,520],{"class":145,"line":519},24,[143,521,522],{"class":182},"  fi\n",[143,524,526],{"class":145,"line":525},25,[143,527,528],{"class":152},"}\n",[11,530,531,532,534,535,538],{},"Classic ",[33,533,56],{}," pipeline that turns a calendar JSON into a Markdown-like list. All-day events get their own format, normal events get ",[33,536,537],{},"HH:MM-HH:MM Title",". When both accounts deliver no appointments, there's a default string.",[11,540,541,542,544,545,548,549,552],{},"The ",[33,543,108],{}," function is a bit more sophisticated, because it's supposed to distinguish between ",[26,546,547],{},"due today"," and ",[26,550,551],{},"overdue",", plus dig the reminder counter out of the task object:",[134,554,556],{"className":136,"code":555,"language":138,"meta":139,"style":139},"section_tasks() {\n  local tasks lines\n  tasks=$(ape-tasks list --status open,doing --json 2>/dev/null)\n  if [ -z \"$tasks\" ] || [ \"$tasks\" = \"null\" ]; then return; fi\n  lines=$(printf '%s' \"$tasks\" | jq -r \\\n    --argjson today_end   \"$TODAY_END\" \\\n    --argjson today_start \"$TODAY_START\" '\n    .[]\n    | select(\n        (.remind_at != null and .remind_at \u003C= $today_end)\n        or (.due_at != null and .due_at \u003C= $today_end)\n      )\n    | \"- \" + .title\n      + (if (.remind_at != null and .remind_at \u003C $today_start)\n         then (if (.reminder_count // 0) > 0\n               then \" (überfällig, \" + ((.reminder_count) | tostring) + \"x erinnert)\"\n               else \" (überfällig)\"\n               end)\n         else \"\"\n         end)\n  ')\n  if [ -n \"$lines\" ]; then\n    printf '\\n✅ Heute fällig\\n%s\\n' \"$lines\"\n  fi\n}\n",[33,557,558,566,573,600,646,676,694,710,715,720,725,730,735,740,745,750,755,760,765,770,775,782,800,817,821],{"__ignoreMap":139},[143,559,560,562,564],{"class":145,"line":146},[143,561,108],{"class":149},[143,563,153],{"class":152},[143,565,156],{"class":152},[143,567,568,570],{"class":145,"line":159},[143,569,163],{"class":162},[143,571,572],{"class":166}," tasks lines\n",[143,574,575,578,580,583,586,589,592,594,596,598],{"class":145,"line":179},[143,576,577],{"class":166},"  tasks",[143,579,213],{"class":152},[143,581,582],{"class":216},"ape-tasks",[143,584,585],{"class":220}," list",[143,587,588],{"class":220}," --status",[143,590,591],{"class":220}," open,doing",[143,593,239],{"class":220},[143,595,242],{"class":152},[143,597,245],{"class":220},[143,599,248],{"class":152},[143,601,602,604,606,608,610,613,615,617,619,621,623,625,627,629,631,633,635,637,639,642,644],{"class":145,"line":207},[143,603,463],{"class":182},[143,605,257],{"class":152},[143,607,260],{"class":152},[143,609,230],{"class":152},[143,611,612],{"class":166},"$tasks",[143,614,236],{"class":152},[143,616,270],{"class":152},[143,618,273],{"class":152},[143,620,257],{"class":152},[143,622,230],{"class":152},[143,624,612],{"class":166},[143,626,236],{"class":152},[143,628,284],{"class":152},[143,630,230],{"class":152},[143,632,289],{"class":220},[143,634,236],{"class":152},[143,636,294],{"class":152},[143,638,297],{"class":182},[143,640,641],{"class":182}," return",[143,643,201],{"class":152},[143,645,305],{"class":182},[143,647,648,651,653,655,657,659,661,663,665,667,669,671,673],{"class":145,"line":251},[143,649,650],{"class":166},"  lines",[143,652,213],{"class":152},[143,654,316],{"class":149},[143,656,319],{"class":152},[143,658,322],{"class":220},[143,660,325],{"class":152},[143,662,230],{"class":152},[143,664,612],{"class":166},[143,666,236],{"class":152},[143,668,334],{"class":152},[143,670,337],{"class":216},[143,672,340],{"class":220},[143,674,675],{"class":166}," \\\n",[143,677,678,681,684,687,690,692],{"class":145,"line":308},[143,679,680],{"class":220},"    --argjson",[143,682,683],{"class":220}," today_end",[143,685,686],{"class":152},"   \"",[143,688,689],{"class":166},"$TODAY_END",[143,691,236],{"class":152},[143,693,675],{"class":166},[143,695,696,698,701,703,706,708],{"class":145,"line":346},[143,697,680],{"class":220},[143,699,700],{"class":220}," today_start",[143,702,230],{"class":152},[143,704,705],{"class":166},"$TODAY_START",[143,707,236],{"class":152},[143,709,343],{"class":152},[143,711,712],{"class":145,"line":352},[143,713,714],{"class":220},"    .[]\n",[143,716,717],{"class":145,"line":358},[143,718,719],{"class":220},"    | select(\n",[143,721,722],{"class":145,"line":364},[143,723,724],{"class":220},"        (.remind_at != null and .remind_at \u003C= $today_end)\n",[143,726,727],{"class":145,"line":370},[143,728,729],{"class":220},"        or (.due_at != null and .due_at \u003C= $today_end)\n",[143,731,732],{"class":145,"line":376},[143,733,734],{"class":220},"      )\n",[143,736,737],{"class":145,"line":382},[143,738,739],{"class":220},"    | \"- \" + .title\n",[143,741,742],{"class":145,"line":388},[143,743,744],{"class":220},"      + (if (.remind_at != null and .remind_at \u003C $today_start)\n",[143,746,747],{"class":145,"line":394},[143,748,749],{"class":220},"         then (if (.reminder_count // 0) > 0\n",[143,751,752],{"class":145,"line":404},[143,753,754],{"class":220},"               then \" (überfällig, \" + ((.reminder_count) | tostring) + \"x erinnert)\"\n",[143,756,757],{"class":145,"line":426},[143,758,759],{"class":220},"               else \" (überfällig)\"\n",[143,761,762],{"class":145,"line":448},[143,763,764],{"class":220},"               end)\n",[143,766,767],{"class":145,"line":454},[143,768,769],{"class":220},"         else \"\"\n",[143,771,772],{"class":145,"line":460},[143,773,774],{"class":220},"         end)\n",[143,776,777,780],{"class":145,"line":481},[143,778,779],{"class":152},"  '",[143,781,248],{"class":152},[143,783,784,786,788,790,792,794,796,798],{"class":145,"line":494},[143,785,463],{"class":182},[143,787,257],{"class":152},[143,789,411],{"class":152},[143,791,230],{"class":152},[143,793,416],{"class":166},[143,795,236],{"class":152},[143,797,294],{"class":152},[143,799,423],{"class":182},[143,801,802,804,806,809,811,813,815],{"class":145,"line":500},[143,803,484],{"class":149},[143,805,319],{"class":152},[143,807,808],{"class":220},"\\n✅ Heute fällig\\n%s\\n",[143,810,325],{"class":152},[143,812,230],{"class":152},[143,814,416],{"class":166},[143,816,516],{"class":152},[143,818,819],{"class":145,"line":519},[143,820,522],{"class":182},[143,822,823],{"class":145,"line":525},[143,824,528],{"class":152},[11,826,827,829,830,832],{},[33,828,56],{}," with two external arguments (day boundary in Unix time), select filter over the tasks, conditional formatting. One line per due task, with a hint whether it's overdue and how often a reminder already went out. That's a concrete example of how far you get with ",[33,831,56],{}," alone: filter, conditions, formatting — all deterministic, all offline.",[11,834,835,836,838],{},"The rest of the script is of similar nature. The four functions are called one after another, the output lands in a Bash variable, a single ",[33,837,46],{}," sends the whole thing to the Telegram bot API.",[18,840,842],{"id":841},"what-fell-away","What fell away",[11,844,845],{},"The focus sentence. The one part inference would actually have been suited for. I deleted that too.",[11,847,848],{},"The reason: with three appointments a day and a handful of due tasks, the most important thing of the day is mostly obvious. If I have an architecture workshop at 11:00, then that's the most important thing of the day — regardless of what a model recommends. If nothing is clearly important, then that too is information I can put together myself.",[11,850,851,852,855,856,859],{},"Inference is good when ",[26,853,854],{},"unclear"," data should become a ",[26,857,858],{},"clear"," recommendation. For a briefing that has the same shape daily and shows me four concrete sections, the clarity is already there. An additional recommendation sentence only lengthens the briefing.",[18,861,863],{"id":862},"use-ai-where-it-helps","Use-AI-where-it-helps",[11,865,866,867,870],{},"This isn't an anti-AI position. It's a position on ",[26,868,869],{},"where"," in a pipeline AI belongs.",[11,872,873,874,876,877,880],{},"Of course I wrote the Bash code with AI — the ",[33,875,56],{}," filter for the tasks, the date conversion with ",[33,878,879],{},"fromdateiso8601",", the script scaffold. If it works, the only thing that really matters is that it doesn't shoot a security hole into the system for me. The rest matters to me only out of interest.",[11,882,883],{},"What really makes a difference, though: solving the deterministic with inference is slow and expensive — even if that's not so obvious with subscription models. Everywhere I don't want to engage much with the how and what, I can quickly drop AI in and pray it works. And often it does. But it doesn't take super-intelligence to see that it's more efficient to build a solution deterministically than to work out the solution path anew on every run.",[11,885,886],{},"AI is perfectly suited to working out the solution path. That's also where it belongs. The repetition is taken over by deterministic code.",[11,888,889,890,892],{},"CLIs and ",[33,891,56],{}," are simply good at manipulating structured data. Cron too. If I replace that layer with an LLM, I gain nothing and lose time, money, determinism.",[18,894,896],{"id":895},"the-office-365-part","The Office 365 part",[11,898,899],{},"For the whole briefing construct to work, I need CLI access to my Outlook calendar and my mailbox. Microsoft Graph API is the official interface for that — open, well documented. But the way there is an app-registration dance: open the Azure portal, register an app, copy the client ID, allow the public-client flow, add permissions, possibly obtain admin consent. At many companies that means an IT ticket and a few weeks of waiting.",[11,901,902,903,911],{},"I built a CLI that bypasses this dance: ",[904,905,909],"a",{"href":906,"rel":907},"https://github.com/patrick-hofmann/o365-cli",[908],"nofollow",[33,910,217],{},". It uses OAuth2 Device Authorization Flow with a multi-tenant public client app. Every Office 365 user can log in themselves — no admin consent, no own app registration, no weeks of waiting.",[11,913,914,915,918,919,36,922,36,924,36,927,930],{},"You log in via ",[33,916,917],{},"apes login"," (or follow Microsoft OAuth directly), get a refresh token back, and can then run ",[33,920,921],{},"o365-cli mail list",[33,923,35],{},[33,925,926],{},"o365-cli mail query --kql \"...\"",[33,928,929],{},"o365-cli mail create-reply"," and more. Multi-account support is built in, because most devs in enterprise contexts have more than one identity anyway.",[11,932,933],{},"The tool is MIT-licensed, on GitHub, brew-installable:",[134,935,937],{"className":136,"code":936,"language":138,"meta":139,"style":139},"brew install patrick-hofmann/tap/o365-cli\n",[33,938,939],{"__ignoreMap":139},[143,940,941,944,947],{"class":145,"line":146},[143,942,943],{"class":216},"brew",[143,945,946],{"class":220}," install",[143,948,949],{"class":220}," patrick-hofmann/tap/o365-cli\n",[11,951,952],{},"Or directly from source:",[134,954,956],{"className":136,"code":955,"language":138,"meta":139,"style":139},"go install github.com/patrick-hofmann/o365-cli/cmd/o365-cli@latest\n",[33,957,958],{"__ignoreMap":139},[143,959,960,963,965],{"class":145,"line":146},[143,961,962],{"class":216},"go",[143,964,946],{"class":220},[143,966,967],{"class":220}," github.com/patrick-hofmann/o365-cli/cmd/o365-cli@latest\n",[18,969,971],{"id":970},"closing","Closing",[11,973,974,975,977,978,980],{},"The briefing script today is 130 lines of Bash with a handful of ",[33,976,56],{}," pipelines. It calls two CLIs, formats their output, sends the result via ",[33,979,46],{}," to a Telegram bot. No OpenAI call. No token consumption. No hallucination possibility for a date.",[11,982,983],{},"Sometimes the right answer is to take away a tool you built in because it's trending. Not every morning briefing needs inference. Sometimes a cron, a CLI, a Telegram bot is enough.",[985,986],"hr",{},[11,988,989],{},[26,990,991,992,997,998,1005],{},"Tools in the setup: ",[904,993,995],{"href":906,"rel":994},[908],[33,996,217],{}," (MIT-licensed), ",[904,999,1002],{"href":1000,"rel":1001},"https://github.com/openape-ai/tasks",[908],[33,1003,1004],{},"@openape/ape-tasks"," (MIT-licensed). The briefing script itself lives in my private dotfiles collection — if you want a template, write me.",[1007,1008,1009],"style",{},"html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}",{"title":139,"searchDepth":159,"depth":159,"links":1011},[1012,1013,1014,1015,1016,1017,1018],{"id":20,"depth":159,"text":21},{"id":60,"depth":159,"text":61},{"id":81,"depth":159,"text":82},{"id":841,"depth":159,"text":842},{"id":862,"depth":159,"text":863},{"id":895,"depth":159,"text":896},{"id":970,"depth":159,"text":971},"2026-04-30","My briefing ran through a headless Claude for months. Via tool-use it pulled Calendar, Tasks and mails, formatted the result, sent it to a Telegram bot. It worked. It was conveniently wired up that way. On substance it was clear to me from the start: for structured data in known formats, inference is overhead — latency and cost without a corresponding gain in the output. Today the setup is pure Bash with jq.",false,"md",null,{},true,"/blog/en/morning-briefing-no-inference",{"title":5,"description":1020},"blog/en/morning-briefing-no-inference",[1030,1031,1032,1033,1034],"Automation","Productivity","AI","Building in Public","Tooling","morning-briefing-no-inference","u0xIX8zIqTb_zsmtU9IBHxApHujzEoWapKhw2rHJUwY",{"en":1038,"de":1039},"/en/blog/morning-briefing-no-inference","/de/blog/nicht-jedes-morning-briefing-braucht-inferenz",1779001886870]