[{"data":1,"prerenderedAt":1040},["ShallowReactive",2],{"blog-de-nicht-jedes-morning-briefing-braucht-inferenz":3,"header-blog-translations-/de/blog/nicht-jedes-morning-briefing-braucht-inferenz":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_de/blog/de/nicht-jedes-morning-briefing-braucht-inferenz.md","Nicht jedes Morning-Briefing braucht Inferenz","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",{},"Mein erstes Signal vom Tag kommt morgens um 7:00 als Telegram-Nachricht. Drin steht: was heute an Terminen ansteht, welche Tasks heute fällig sind, wie voll die Inbox ist, plus ein 7-Tage-Vorausblick. Vier Sektionen, immer dieselben Felder, immer dieselbe Form. Ich lese es am Handy, bevor ich am Rechner bin, und weiß dann ungefähr, wie der Tag aussieht.",[11,15,16],{},"Das Setup hat seit ein paar Wochen funktioniert. Bis vor kurzem war ein LLM Teil der Pipeline. Jetzt nicht mehr. Hier ist warum.",[18,19,21],"h2",{"id":20},"wie-es-vorher-war","Wie es vorher war",[11,23,24,25],{},"Das Briefing-Skript rief einen headless Claude mit einem Prompt auf, der ungefähr so klang: ",[26,27,28],"em",{},"\"Sammle die Termine der nächsten Stunden aus beiden Outlook-Accounts. Hol die offenen Tasks. Zähl die ungelesenen Mails. Schreibe daraus ein kompaktes deutsches Briefing mit Emoji-Akzenten.\"",[11,30,31,32,36,37,36,40,43,44,47],{},"Claude rief per Tool-Use die nötigen CLIs auf — ",[33,34,35],"code",{},"o365-cli calendar today",", ",[33,38,39],{},"ape-tasks list",[33,41,42],{},"o365-cli mail list --unread",". Sammelte die JSON-Outputs ein. Formatierte das Briefing. Mein Bash-Wrapper nahm das fertige Briefing und schickte es per ",[33,45,46],{},"curl"," an die Telegram-Bot-API.",[11,49,50],{},"Das hat funktioniert. Wirklich. Jeden Morgen kam ein vernünftig zusammengefasstes Briefing. Patrick-stylisch formatiert, mit Emojis an den richtigen Stellen, mit Wochentag-Kürzeln, mit einem 1–2-Sätze-Fokus am Ende, der die wichtigste Sache des Tages priorisierte.",[11,52,53,54,57],{},"Der Fokus-Satz war übrigens der einzige Teil, für den ein LLM überhaupt nötig war. Den Rest hätte auch ein Bash-Skript machen können — mit ",[33,55,56],{},"jq"," über die JSON-Outputs, deterministisch, ohne API-Call.",[18,59,61],{"id":60},"warum-ich-es-rausgenommen-habe","Warum ich es rausgenommen habe",[11,63,64],{},"Eingebaut habe ich es, weil es bequem war: ein Prompt, ein einziger Aufruf, das Tool-Use des Modells erledigt den Rest. Inhaltlich war mir aber von Anfang an klar, dass das nicht passt. Die KI hat ungefähr eine Minute mit intelligenter Tool-Call-Verarbeitung verbracht, was mein heutiges Skript deterministisch in zwei Sekunden erledigt — und auch das nur, weil die Microsoft Graph API ein bisschen zum Antworten braucht. Faktor 30. Plus jeder Run kostet ein paar Cent an Inferenz. Plus bei jedem Run gibt es eine winzige Chance, dass das Modell etwas falsch parst — ein Datum, eine Uhrzeit, einen Account-Namen.",[11,66,67],{},"Bei einem Briefing, das auf strukturierten Daten in bekannten Formaten aufsetzt, ist das alles unnötig. Die Calendar-Events kommen als JSON aus der Microsoft Graph API. Die Tasks kommen als JSON aus der ape-tasks-API. Die Mail-Counts sind eine simple Längen-Operation auf einem JSON-Array. Es gibt nichts zu interpretieren, nichts auszubalancieren, nichts zu inferieren.",[11,69,70],{},"Im Skript-Header habe ich es so notiert:",[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},"wie-es-jetzt-aussieht","Wie es jetzt aussieht",[11,84,85,86,88],{},"Das aktuelle Skript ist Pure Shell mit ",[33,87,56],{}," für die JSON-Verarbeitung. Vier Funktionen, eine pro Sektion:",[90,91,92,102,110,122],"ul",{},[93,94,95,101],"li",{},[96,97,98],"strong",{},[33,99,100],{},"section_today"," — Termine heute, beide Outlook-Accounts",[93,103,104,109],{},[96,105,106],{},[33,107,108],{},"section_tasks"," — offene/in-Arbeit Tasks, mit Overdue-Marker und Reminder-Zähler",[93,111,112,117,118,121],{},[96,113,114],{},[33,115,116],{},"section_mails"," — Unread-Counts pro Account, simple ",[33,119,120],{},"length","-Operation auf JSON-Array",[93,123,124,129],{},[96,125,126],{},[33,127,128],{},"section_next7"," — Termine ab morgen für die nächsten 7 Tage, beide Accounts",[11,131,132],{},"Eine Sektion exemplarisch, die heutigen Termine:",[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],{},"Klassische ",[33,533,56],{},"-Pipeline, die aus einem Calendar-JSON eine Markdown-ähnliche Liste macht. Ganztags-Events bekommen ihr eigenes Format, normale Events kriegen ",[33,536,537],{},"HH:MM-HH:MM Titel",". Wenn beide Accounts keine Termine liefern, gibt es einen Default-String.",[11,540,541,542,544,545,548,549,552],{},"Die ",[33,543,108],{},"-Funktion ist ein bisschen sophisticated, weil sie zwischen ",[26,546,547],{},"fällig heute"," und ",[26,550,551],{},"überfällig"," unterscheiden soll, plus den Reminder-Zähler aus dem Task-Objekt heraussuchen muss:",[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],{}," mit zwei externen Argumenten (Tagesgrenze in Unix-Zeit), select-Filter über die Tasks, conditional Formatierung. Eine Zeile pro fälliger Task, mit Hinweis ob sie überfällig ist und wie oft schon ein Reminder rausging. Das ist ein konkretes Beispiel dafür, wie weit man mit ",[33,831,56],{}," allein kommt: Filter, Bedingungen, Formatierung — alles deterministisch, alles offline.",[11,834,835,836,838],{},"Der Rest des Skripts ist ähnlicher Natur. Die vier Funktionen werden hintereinander aufgerufen, der Output landet in einer Bash-Variable, ein einzelnes ",[33,837,46],{}," schickt das Ganze an die Telegram-Bot-API.",[18,840,842],{"id":841},"was-weggefallen-ist","Was weggefallen ist",[11,844,845],{},"Der Fokus-Satz. Der eine Teil, für den Inferenz tatsächlich passend gewesen wäre. Den habe ich auch gelöscht.",[11,847,848],{},"Der Grund: bei drei Terminen am Tag und einer Handvoll fälligen Tasks ist die wichtigste Sache des Tages meistens offensichtlich. Wenn ich um 11:00 einen Architektur-Workshop habe, dann ist das die wichtigste Sache des Tages — egal was ein Modell mir empfiehlt. Wenn nichts klar wichtig ist, dann ist auch das eine Information, die ich selbst zusammenführen kann.",[11,850,851,852,855,856,859],{},"Inferenz ist gut, wenn aus ",[26,853,854],{},"unklaren"," Daten eine ",[26,857,858],{},"klare"," Empfehlung werden soll. Bei einem Briefing, das täglich dieselbe Form hat und mir vier konkrete Sektionen zeigt, ist die Klarheit schon da. Ein zusätzlicher Empfehlungs-Satz verlängert nur das Briefing.",[18,861,863],{"id":862},"use-ai-where-it-helps","Use-AI-where-it-helps",[11,865,866,867,870],{},"Das ist keine Anti-AI-Position. Es ist eine Position dazu, ",[26,868,869],{},"wo"," in einer Pipeline AI gehört.",[11,872,873,874,876,877,880],{},"Natürlich habe ich den Bash-Code mit AI geschrieben — den ",[33,875,56],{},"-Filter für die Tasks, die Datumsumrechnung mit ",[33,878,879],{},"fromdateiso8601",", das Skript-Gerüst. Wenn er funktioniert, ist das Einzige was wirklich wichtig ist, dass er mir keine Sicherheitslücke ins System schießt. Der Rest ist mir nur aus Interesse wichtig.",[11,882,883],{},"Was aber wirklich einen Unterschied macht: Deterministisches mit Inferenz zu lösen ist langsam und teuer — auch wenn das bei den Abo-Modellen nicht so offensichtlich ist. Überall wo ich mich mit dem Wie und Was wenig beschäftigen möchte, kann ich schnell mal KI reinsetzen und beten dass es funktioniert. Und häufig tut es das auch. Aber es braucht keine Super-Intelligence, um zu sehen, dass es effizienter ist eine Lösung deterministisch zu bauen, als den Lösungsweg mit jedem Run aufs Neue zu erarbeiten.",[11,885,886],{},"Die KI ist perfekt dazu geeignet, den Lösungsweg zu erarbeiten. Dort gehört sie auch hin. Die Wiederholung übernimmt der deterministische Code.",[11,888,889,890,892],{},"CLIs und ",[33,891,56],{}," sind einfach gut darin, strukturierte Daten zu manipulieren. Cron auch. Wenn ich diese Schicht durch ein LLM ersetze, gewinne ich nichts und verliere Zeit, Geld, Determinismus.",[18,894,896],{"id":895},"der-office-365-teil","Der Office-365-Teil",[11,898,899],{},"Damit das ganze Briefing-Konstrukt funktioniert, brauche ich CLI-Zugang zu meinem Outlook-Calendar und meiner Mailbox. Microsoft Graph API ist die offizielle Schnittstelle dafür — offen, gut dokumentiert. Aber der Weg dorthin ist ein App-Registrierungs-Tanz: Azure-Portal aufmachen, App registrieren, Client-ID kopieren, Public-Client-Flow erlauben, Permissions hinzufügen, eventuell Admin-Consent einholen. Bei vielen Firmen heißt das ein IT-Ticket und einige Wochen warten.",[11,901,902,903,911],{},"Ich habe ein CLI gebaut, das diesen Tanz umgeht: ",[904,905,909],"a",{"href":906,"rel":907},"https://github.com/patrick-hofmann/o365-cli",[908],"nofollow",[33,910,217],{},". Es nutzt OAuth2 Device Authorization Flow mit einer Multi-Tenant Public Client App. Jeder Office-365-User kann sich selbst einloggen — kein Admin-Consent, keine eigene App-Registrierung, keine Wochenwartezeit.",[11,913,914,915,918,919,36,922,36,924,36,927,930],{},"Du kannst dir per ",[33,916,917],{},"apes login"," anmelden (oder direkt Microsoft-OAuth folgen), bekommst einen Refresh-Token zurück, und kannst danach ",[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"," und mehr ausführen. Multi-Account-Support ist eingebaut, weil die meisten Devs in Enterprise-Kontexten ohnehin mehr als eine Identity haben.",[11,932,933],{},"Das Tool ist MIT-lizenziert, auf GitHub, brew-installierbar:",[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],{},"Oder direkt aus den Sourcen:",[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},"schluss","Schluss",[11,973,974,975,977,978,980],{},"Das Briefing-Skript ist heute 130 Zeilen Bash mit einer Handvoll ",[33,976,56],{},"-Pipelines. Es ruft zwei CLIs auf, formatiert deren Output, schickt das Ergebnis per ",[33,979,46],{}," an einen Telegram-Bot. Kein OpenAI-Aufruf. Kein Token-Verbrauch. Keine Halluzinations-Möglichkeit für ein Datum.",[11,982,983],{},"Manchmal ist die richtige Antwort, ein Tool wegzunehmen, das man eingebaut hat, weil es trendet. Nicht jedes Morning-Briefing braucht Inferenz. Manchmal reicht ein Cron, eine CLI, ein Telegram-Bot.",[985,986],"hr",{},[11,988,989],{},[26,990,991,992,997,998,1005],{},"Tools im Setup: ",[904,993,995],{"href":906,"rel":994},[908],[33,996,217],{}," (MIT-lizenziert), ",[904,999,1002],{"href":1000,"rel":1001},"https://github.com/openape-ai/tasks",[908],[33,1003,1004],{},"@openape/ape-tasks"," (MIT-lizenziert). Das Briefing-Skript selbst lebt in meiner privaten dotfiles-Sammlung — wer ein Template will, schreibt mich an.",[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","Mein Briefing lief monatelang durch einen headless Claude. Per Tool-Use rief er Calendar, Tasks und Mails ab, formatierte das Ergebnis, schickte es an einen Telegram-Bot. Hat funktioniert. War bequem so eingebaut. Inhaltlich war mir von Anfang an klar: für strukturierte Daten in bekannten Formaten ist Inferenz Overhead — Latenz und Kosten ohne entsprechenden Mehrwert im Output. Heute ist das Setup pure Bash mit jq.",false,"md",null,{},true,"/blog/de/nicht-jedes-morning-briefing-braucht-inferenz",{"title":5,"description":1020},"blog/de/nicht-jedes-morning-briefing-braucht-inferenz",[1030,1031,1032,1033,1034],"Automation","Productivity","AI","Building in Public","Tooling","morning-briefing-no-inference","vie-TvguFo43ugVpTv1MHvzfgN5LojVoDLcG0oCRmWQ",{"de":1038,"en":1039},"/de/blog/nicht-jedes-morning-briefing-braucht-inferenz","/en/blog/morning-briefing-no-inference",1779001885835]