<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Guido Tapia</title>
    <description>The latest articles on DEV Community by Guido Tapia (@gatapia).</description>
    <link>https://dev.to/gatapia</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063183%2Fa21b9f42-c4f1-4abf-8442-c06a35468320.jpg</url>
      <title>DEV Community: Guido Tapia</title>
      <link>https://dev.to/gatapia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gatapia"/>
    <language>en</language>
    <item>
      <title>Listening at scale: monitoring industry forums and communities with AI</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Wed, 16 Sep 2026 01:03:28 +0000</pubDate>
      <link>https://dev.to/gatapia/listening-at-scale-monitoring-industry-forums-and-communities-with-ai-3b8b</link>
      <guid>https://dev.to/gatapia/listening-at-scale-monitoring-industry-forums-and-communities-with-ai-3b8b</guid>
      <description>&lt;p&gt;Most listening set-ups I see at Australian mid-market companies are a saved search and a daily email digest. That catches press mentions and misses almost everything that happens in the communities where your buyers ask each other for advice. Practitioners comparing monitoring tools in September 2026 put the problem plainly: on Reddit, &lt;a href="https://www.reddit.com/r/jenova_ai/comments/1w51yaq/what_is_the_best_ai_brand_monitoring_tool_in_2026/" rel="noopener noreferrer"&gt;thread titles are often vague and the brand mention is three comments deep&lt;/a&gt;. A keyword alert on your company name never fires on those threads, and the question you could have answered gets a dozen replies and drops off the front page before anyone at your end notices.&lt;/p&gt;

&lt;p&gt;Language models are good at the part that was previously impossible to staff: reading everything and deciding what matters. They are bad at the part people assume you would automate, which is writing the reply. This post, part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-marketing-the-series/" rel="noopener noreferrer"&gt;Practical AI in Marketing&lt;/a&gt; series, covers how we build the first half and why we deliberately stop before the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the pipeline
&lt;/h2&gt;

&lt;p&gt;The systems we build for this have five parts, and only one of them involves a model doing anything clever.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collection. Pull whole threads rather than matching posts, because the classifier needs the parent question and the existing answers to judge whether you have anything to add. Expect to cover several channels. The same practitioner thread lists &lt;a href="https://www.reddit.com/r/jenova_ai/comments/1w51yaq/what_is_the_best_ai_brand_monitoring_tool_in_2026/" rel="noopener noreferrer"&gt;X, LinkedIn, Reddit, Facebook, YouTube and Hacker News&lt;/a&gt; as normal coverage, with specialist tooling for Reddit on its own.&lt;/li&gt;
&lt;li&gt;Cheap pre-filter. A small model or an embedding similarity check against your topic list throws away 90 per cent of the volume before you pay for anything larger.&lt;/li&gt;
&lt;li&gt;Classification. A larger model reads the surviving threads and returns structured fields: what is being asked, whether the asker has a problem you have solved before, how competent the existing answers are, and a confidence score.&lt;/li&gt;
&lt;li&gt;Policy engine. Plain rules, held in config, that decide whether a classified thread is even eligible for a reply. This is where community etiquette lives.&lt;/li&gt;
&lt;li&gt;Human queue. A short daily list for a named person, with the thread, the classifier's reasoning, and a draft set of talking points rather than a draft post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The policy engine is the part that determines whether this reads as a helpful specialist or as spam, so it is worth being specific about what goes in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encode each community's rules, not a house style
&lt;/h2&gt;

&lt;p&gt;Communities now publish their own written positions on AI participation and commercial posting, and those documents are your specification. r/gamedev published a formal &lt;a href="https://www.reddit.com/r/gamedev/comments/1vrnqyt/rgamedev_policy_on_ai_use/" rel="noopener noreferrer"&gt;Policy on AI Use&lt;/a&gt; on 18 August 2026. The right behaviour for your system is to treat each community's stated policy as the binding constraint, with no organisation-wide default that overrides it.&lt;/p&gt;

&lt;p&gt;Tolerance varies by audience in ways that are predictable enough to encode. Commenters in that same thread expected that &lt;a href="https://www.reddit.com/r/gamedev/comments/1vrnqyt/rgamedev_policy_on_ai_use/" rel="noopener noreferrer"&gt;hobby devs and non-programmers would be outraged while professional game devs and software engineers would find it pretty normal&lt;/a&gt;. A threshold that works in a professional subreddit will get you removed from an enthusiast one.&lt;/p&gt;

&lt;p&gt;Some venues are simply off the list. The llama.cpp pull request adding Qwen3.8-Flash-Next opens with a pinned notice that &lt;a href="https://github.com/ggml-org/llama.cpp/pull/27742" rel="noopener noreferrer"&gt;off-topic comments and AI slop or spam in the thread will be deleted&lt;/a&gt;. Engineering forums moderate aggressively now, and a posting bot aimed at one earns a ban that takes months to undo.&lt;/p&gt;

&lt;p&gt;In practice the config looks like this, one entry per community:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;community&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r/gamedev"&lt;/span&gt;
  &lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;reply_allowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;promotional_reply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;      &lt;span class="c1"&gt;# per published AI use policy, 2026-08-18&lt;/span&gt;
  &lt;span class="na"&gt;disclosure_required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;min_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;
  &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marco"&lt;/span&gt;
  &lt;span class="na"&gt;policy_reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-09-01"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;community&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github/llama.cpp"&lt;/span&gt;
  &lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;reply_allowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;          &lt;span class="c1"&gt;# pinned no-spam notice on active PRs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;policy_reviewed&lt;/code&gt; date matters. Communities change their rules, and a rule set nobody has looked at since last financial year will eventually have you posting against a policy that has moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure is the rule that protects you
&lt;/h2&gt;

&lt;p&gt;The loudest criticism in the r/gamedev policy thread was not aimed at AI. It was aimed at the original poster, who &lt;a href="https://www.reddit.com/r/gamedev/comments/1vrnqyt/rgamedev_policy_on_ai_use/" rel="noopener noreferrer"&gt;worked at a company developing a plugin for Unity AI integration and chose not to disclose it&lt;/a&gt;. Concealed commercial motive does the reputational damage, and no amount of careful tone repairs it once someone checks your post history.&lt;/p&gt;

&lt;p&gt;The working norm is visible even among vendors recommending their own products. One founder in the monitoring tools thread prefixed his recommendation with &lt;a href="https://www.reddit.com/r/jenova_ai/comments/1w51yaq/what_is_the_best_ai_brand_monitoring_tool_in_2026/" rel="noopener noreferrer"&gt;Disclaimer: that's me! :-) Not shilling&lt;/a&gt;. Hard-code that. Every reply your team posts carries an affiliation statement in the first line, with no exceptions for threads where it feels awkward. It also keeps you clear of the Australian Consumer Law's misleading conduct provisions, which apply to a staff member posting as an apparently disinterested punter just as they apply to an ad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human writes the post
&lt;/h2&gt;

&lt;p&gt;The argument for AI-drafted replies is time. The argument against is that the register gives it away. Members of a language learning community describe LLM output as &lt;a href="https://www.reddit.com/r/languagelearning/comments/1wf4fz6/please_beware_of_ai_when_learning_languages_its/" rel="noopener noreferrer"&gt;unbearably sycophantic and corporate in its speech patterns&lt;/a&gt;, and that is exactly the voice that gets a reply downvoted and your account tagged as a marketer.&lt;/p&gt;

&lt;p&gt;The upside of doing it properly is real. When the designer of the board game Cola Wars responded personally to criticism of AI art in his product, commenters on r/boardgames called it &lt;a href="https://www.reddit.com/r/boardgames/comments/1w696oe/cola_wars_and_ai_a_response_from_the_designer/" rel="noopener noreferrer"&gt;an incredibly well made response, and it's what should be the standard in situations like this&lt;/a&gt;, including people who had no interest in the game beforehand. A named person answering directly is the whole product here. The model's job is to make sure that person spends their twenty minutes a day on the six threads worth answering instead of scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs and the parts that break
&lt;/h2&gt;

&lt;p&gt;Model inference is the cheap line item once the pre-filter is doing its job. The expensive lines are collection and maintenance.&lt;/p&gt;

&lt;p&gt;Scrapers break. The OpenAmer project's Workflow Immune System exists specifically because automations fail &lt;a href="https://github.com/openamer/openamer/issues/18" rel="noopener noreferrer"&gt;when sites redesign&lt;/a&gt;, and it re-finds UI elements and patches its own workflows to keep running. Self-healing helps, although it does not remove the need for someone to own the pipeline. Budget a few hours a month for broken collectors, changed API terms and rate limits, and assume at least one platform will close off access during the first year.&lt;/p&gt;

&lt;p&gt;Where the data cannot leave your control, self-hosting the collection layer is a live option. OpenAmer is open source and &lt;a href="https://github.com/openamer/openamer/issues/18" rel="noopener noreferrer"&gt;fully self-hosted&lt;/a&gt;, which matters for health-sector and government clients who cannot ship community and customer signals into third-party SaaS analytics under their privacy obligations.&lt;/p&gt;

&lt;p&gt;The other honest limitation is precision. Classifiers over-select early on, and the first month of queues will contain threads where you have nothing useful to say. Have your reviewer mark those, feed the marked examples back into the prompt or a small evaluation set, and expect two or three rounds before the daily list is short enough that someone reads all of it.&lt;/p&gt;

&lt;p&gt;Start with two or three communities you already care about, run the queue for a month with nobody posting at all, and see whether the threads it surfaces are ones you wish you had answered. If they are, you have a case for the posting half. If they are not, you have saved yourself a bot that annoys people.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/listening-at-scale-monitoring-industry-forums-and-communities-with-ai/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
      <category>sociallistening</category>
      <category>communitymarketing</category>
    </item>
    <item>
      <title>Answering from your own documents: a policies and procedures assistant for any business</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Mon, 14 Sep 2026 01:18:30 +0000</pubDate>
      <link>https://dev.to/gatapia/answering-from-your-own-documents-a-policies-and-procedures-assistant-for-any-business-57f7</link>
      <guid>https://dev.to/gatapia/answering-from-your-own-documents-a-policies-and-procedures-assistant-for-any-business-57f7</guid>
      <description>&lt;p&gt;Most of the questions that clog an HR inbox or an operations channel have already been answered in writing. The leave policy says what happens when a public holiday falls during annual leave. The installation manual says which bracket suits a tiled roof. The problem is that the answer sits on page 34 of a PDF someone uploaded to SharePoint in 2019, so people ask a colleague instead, and the colleague guesses.&lt;/p&gt;

&lt;p&gt;This is the least glamorous use of a language model and the one with the clearest payback. It is also the pattern we are asked about most often, which is why it appears early in &lt;a href="https://picnet.com.au/blog/practical-ai-in-business-operations-the-series/" rel="noopener noreferrer"&gt;Practical AI in Business Operations&lt;/a&gt;. The approach below is what we build for clients outside health as well as in it: HR policies, operations manuals, product documentation, supplier agreements, safety procedures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the system actually does
&lt;/h2&gt;

&lt;p&gt;A user asks a question in plain English. The system finds the passages in your documents most likely to contain the answer, hands those passages to a language model, and asks it to answer using only what it was given. The answer comes back with links to the source documents and the sections used. If the passages do not contain the answer, the system says so.&lt;/p&gt;

&lt;p&gt;That last behaviour is the one that decides whether staff trust the thing. A general model will happily produce a confident paragraph about your redundancy process based on what redundancy processes usually look like. That answer is worse than no answer, because it sounds like it came from your policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture sketch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion.&lt;/strong&gt; Pull documents from wherever they live (SharePoint, Confluence, a file share, a DMS) on a schedule. Convert to text, keeping headings and page numbers so a citation can point at a section rather than a 90 page file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata.&lt;/strong&gt; Every chunk carries the document title, owner, effective date, the version it supersedes, and which entity or state it applies to. This is where most of the value hides. Without it you cannot filter a Queensland question away from a Victorian policy, and you cannot tell the user the answer is current as at a date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking.&lt;/strong&gt; Split on document structure, not on a fixed character count. A clause with its heading and its parent heading retrieves far better than 800 characters that start mid sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index.&lt;/strong&gt; Embeddings plus keyword search, with results merged. Pure vector search misses exact terms like award classification codes or part numbers. Keyword search alone misses "can I take leave at half pay". You want both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer.&lt;/strong&gt; The model receives the question, the top passages, and a strict instruction set. It returns structured output, not free text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging.&lt;/strong&gt; Every question, the passages retrieved, the answer, and any user feedback. This is your evaluation data and your audit trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We usually put the index in Postgres with pgvector rather than adding a new vendor. For corpora in the tens of thousands of chunks, which covers most SMEs, it is fast enough and it keeps your document content inside infrastructure you already govern under the Privacy Act.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt contract
&lt;/h2&gt;

&lt;p&gt;The instruction to the model is short and it does not negotiate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer using only the numbered passages below.
Every sentence in the answer must cite at least one passage id.
If the passages do not contain enough to answer, return not_in_corpus.
Do not use general knowledge about how policies usually work.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the output is a schema, so the application can check it before a human ever sees it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Annual leave accrues progressively..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"citations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"passage_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leave Policy v4.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.1 Annual leave"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effective"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://.../leave-policy-v4-2.pdf#page=12"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify before rendering. If &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;answered&lt;/code&gt; and &lt;code&gt;citations&lt;/code&gt; is empty, drop the answer and show the retrieved passages instead. If a cited passage id was not in the set you supplied, treat it as a failure. These checks cost nothing and they catch the most embarrassing errors.&lt;/p&gt;

&lt;p&gt;Make the citation a link to the source document at the right page, not a file name. Staff click through more often than you would expect, and the ones who click are the ones who will tell you when a policy is out of date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation before anyone else sees it
&lt;/h2&gt;

&lt;p&gt;Do not roll this out on a demo. Build a question set first.&lt;/p&gt;

&lt;p&gt;Take 100 to 200 real questions from your HR inbox, service desk tickets or the operations channel. For each one, have the person who currently answers it write down the correct answer and the document and section it comes from. That is a day of someone's time and it is the highest value day in the project.&lt;/p&gt;

&lt;p&gt;Then run the set and grade three things: whether the answer is correct, whether the citation points at the right section, and whether the system refused when it should have answered. Add a second set of 30 or so questions you know are &lt;em&gt;not&lt;/em&gt; covered by the corpus, including a few that sound like they should be. Count how often the system answers those anyway. That false answer rate is the number to watch, and it is the one that moves when someone changes a prompt, swaps a model version or adds a new document set.&lt;/p&gt;

&lt;p&gt;Re-run both sets on every change. It takes minutes once it is scripted, and it turns "the new model seems better" into something you can check.&lt;/p&gt;

&lt;p&gt;If your documents are clinically adjacent, keep the assistant on administrative ground: rosters, credentialling, procurement, incident reporting workflow. Anything that could shape care needs a named clinician signing off the answer before it reaches anyone, designed in as a workflow step with an approval record, not added later as a disclaimer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to run
&lt;/h2&gt;

&lt;p&gt;Three lines, in descending order of size.&lt;/p&gt;

&lt;p&gt;The first is corpus preparation. Someone has to decide which documents are authoritative, retire the superseded ones, and attach the metadata. In most engagements this is the bulk of the effort and it is internal staff time, not licence fees. It is also work that pays off whether or not you build the assistant.&lt;/p&gt;

&lt;p&gt;The second is ongoing ownership. A document set that nobody maintains degrades, and a confidently cited answer from a withdrawn policy is a real problem. Budget a few hours a month for someone to review flagged answers and update the corpus.&lt;/p&gt;

&lt;p&gt;The third is inference, and it is usually the smallest. Work it out yourself rather than trusting anyone's estimate: multiply the tokens you send per question (retrieved passages plus the question plus the answer, typically a few thousand) by your expected monthly question volume, then check your provider's current published price. A department sized deployment of a few thousand questions a month generally lands in the range of a single software subscription. Embedding the corpus is a one off charge plus a trickle for new documents. If you self host the index, add nothing for the database because it is already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a wiki search is honestly good enough
&lt;/h2&gt;

&lt;p&gt;Sometimes it is. If your corpus is a well structured intranet with good headings, your staff know the vocabulary, and volume is low, decent search plus a table of contents will solve the problem for the cost of nobody's afternoon. The same goes for a corpus small enough that a person can hold it in their head.&lt;/p&gt;

&lt;p&gt;The pattern earns its keep when the documents are long, badly structured or scattered across systems, when the answer requires reading across several documents, when users do not know the internal terminology, or when the same questions arrive dozens of times a week. It does not help at all when your documents contradict each other. Fix that first, because retrieval will find both versions and the model will pick one.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/answering-from-your-own-documents-a-policies-and-procedures-assistant-for-any-business/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>businessoperations</category>
      <category>rag</category>
      <category>documentsearch</category>
    </item>
    <item>
      <title>When AI must not act alone: human-in-the-loop patterns for security automation</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 11 Sep 2026 00:53:24 +0000</pubDate>
      <link>https://dev.to/gatapia/when-ai-must-not-act-alone-human-in-the-loop-patterns-for-security-automation-53c2</link>
      <guid>https://dev.to/gatapia/when-ai-must-not-act-alone-human-in-the-loop-patterns-for-security-automation-53c2</guid>
      <description>&lt;p&gt;Most of the AI security work we do for clients ends up hinging on one question: what is this thing allowed to do on its own at 3am on a Sunday?&lt;/p&gt;

&lt;p&gt;Everything else follows from the answer. Model choice, prompt design, which vendor's agent framework you use, all of it matters less than where you draw the line between an action the system takes and an action a person takes. This post closes out &lt;a href="https://picnet.com.au/blog/practical-ai-in-cyber-security-the-series/" rel="noopener noreferrer"&gt;Practical AI in Cyber Security&lt;/a&gt; with the governance layer that the earlier posts assumed was there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Draw the line at reversibility
&lt;/h2&gt;

&lt;p&gt;The clean split is not "smart tasks versus dumb tasks". It is reversibility. If the worst outcome of the AI being wrong is a wasted five minutes of someone's reading time, automate it. If the worst outcome is a production system offline, a mailbox item gone, or a clinician locked out of a roster, a human decides.&lt;/p&gt;

&lt;p&gt;Safe to run unattended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collection. Pulling logs, endpoint telemetry, sign-in events, ticket history, config snapshots into one place.&lt;/li&gt;
&lt;li&gt;Enrichment. Resolving an IP to an ASN, looking up a hash, attaching asset ownership, pulling the last three changes to the affected host.&lt;/li&gt;
&lt;li&gt;Triage and correlation. Grouping alerts, suppressing known-benign patterns, ranking by likely impact, drafting the "what we think happened" summary.&lt;/li&gt;
&lt;li&gt;Reporting. Weekly posture summaries, control-drift reports, evidence packs for an audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behind a human decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blocking. Firewall rules, conditional access changes, disabling a user account.&lt;/li&gt;
&lt;li&gt;Deleting. Quarantining or purging mail, removing files, revoking tokens at scale.&lt;/li&gt;
&lt;li&gt;Isolating. Pulling a host off the network, suspending a VM, stopping a service.&lt;/li&gt;
&lt;li&gt;Anything that touches a system of record. Closing an incident, changing a risk rating, writing to an asset register.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vendors are pushing hard in the other direction. Cloudflare's own material on agentic AI describes systems that make decisions, execute code, query databases, send email and handle payments &lt;a href="https://www.reddit.com/r/AgentContext_dev/comments/1vys742/cloudflares_agentic_cloud_the_complete_platform/" rel="noopener noreferrer"&gt;without constant human prompting&lt;/a&gt;. That is a fair description of the capability. It is also a precise list of the things you want fenced off in a security context, because each one of them is hard to undo.&lt;/p&gt;

&lt;p&gt;The split is not new and it is not unique to security. Windmill's engineering write-up on their support pipeline lands in exactly the same place: AI does the volume work, drafting replies and even drafting the fix, but &lt;a href="https://www.windmill.dev/blog/support-automation" rel="noopener noreferrer"&gt;a human still presses send&lt;/a&gt; on anything customer-facing. Their reasoning is worth stealing wholesale. The system only works while people trust the drafts, and one wrong auto-sent reply burns that trust for every message after it. A SOC analyst who has been burned once by an automated block on a payment gateway will start ignoring the tool, and then you have paid for AI and got nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: agent proposes, human approves
&lt;/h2&gt;

&lt;p&gt;The most useful working implementation we have seen of this contract is in a repo, not a security product. The kube-agents project &lt;a href="https://github.com/gke-labs/kube-agents/pull/876" rel="noopener noreferrer"&gt;documented a protocol&lt;/a&gt; for AI agents contributing code: an agent claims work by self-assignment, escalates anything it cannot resolve with a &lt;code&gt;needs-human&lt;/code&gt; label, and cannot merge until a human &lt;code&gt;lgtm&lt;/code&gt; gate is satisfied. Three primitives. Claim, escalate, approve.&lt;/p&gt;

&lt;p&gt;That maps onto a security runbook almost directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claim    -&amp;gt; agent takes alert #4471, marks it in-progress so a second
            agent or analyst doesn't duplicate the work
enrich   -&amp;gt; pulls host, user, recent changes, threat intel, prior tickets
propose  -&amp;gt; writes the finding, the evidence and ONE recommended action
            ("isolate WKS-0412"), status = awaiting-approval
escalate -&amp;gt; if it cannot complete, status = needs-human with the reason
approve  -&amp;gt; named human approves, rejects or edits; only then does the
            action execute, under the human's authority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kube-agents authors made the contract deliberately framework-agnostic. That is the detail to copy. Your SOC processes will outlive your current model vendor by years. Keep the escalation and approval contract in your own orchestration layer, expressed in your own states, so swapping Claude for Gemini next year is a config change rather than a governance rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  A blind check must never look green
&lt;/h2&gt;

&lt;p&gt;The failure mode that costs real money is not the AI recommending the wrong action. It is the AI reporting "all clear" on a check that never ran.&lt;/p&gt;

&lt;p&gt;Alert and fault are different states and need different plumbing. An alert means the check ran and found something. A fault means the check could not run: the API token expired, the log source stopped shipping, the model call timed out, the agent hit a rate limit. Both are "not green", but they need different handlers, different dashboards, and often different people.&lt;/p&gt;

&lt;p&gt;Most monitoring code we inherit collapses the two. The check throws, something catches the exception, logs it at debug level and returns a default, and the tile stays green for eleven weeks while the log source has been dead the whole time. Adding an LLM to that pipeline makes it worse, because models are obliging. Ask one to summarise incomplete data and it will produce a confident, well-formatted summary of the subset it could see, with no flag that half the estate was missing.&lt;/p&gt;

&lt;p&gt;What we build into these pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every check returns one of three states, never two: &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;alert&lt;/code&gt;, &lt;code&gt;fault&lt;/code&gt;. There is no default value on the failure path.&lt;/li&gt;
&lt;li&gt;A fault renders as a distinct colour on the dashboard, not as a green tile with a footnote.&lt;/li&gt;
&lt;li&gt;Staleness is a fault. If a source has not reported inside its expected window, the check that depends on it is faulted, not passed.&lt;/li&gt;
&lt;li&gt;Coverage is reported alongside every AI-generated summary. "23 of 26 sources; 3 faulted" sits above the narrative, generated by the pipeline rather than written by the model.&lt;/li&gt;
&lt;li&gt;The model never sets its own coverage figure. That number comes from the orchestrator, which knows what it asked for and what came back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;needs-human&lt;/code&gt; label is the same idea at the agent level. An agent that hits something it cannot resolve emits an explicit escalation state rather than quietly returning success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit trail is the deliverable
&lt;/h2&gt;

&lt;p&gt;When someone asks how your AI-assisted decisions are governed, the honest answer is whatever your logs can show. Governance is a document exercise at the point it gets tested.&lt;/p&gt;

&lt;p&gt;That point is closer than it looks for organisations with any European exposure. The EU AI Act moved into enforcement on 31 August 2026 with the AI Office &lt;a href="https://tokenstead.ai/guides/eu-ai-act-first-enforcement-security-rfis" rel="noopener noreferrer"&gt;issuing formal requests for information&lt;/a&gt; to general-purpose model providers, covering model security, independent external evaluations and post-market monitoring. The requests went to providers rather than to deployers, so an Australian organisation using an API is not in the first wave. But the pattern is set: the questions are document-driven, the answers become part of a permanent supervisory record, and incorrect or incomplete replies carry penalties of up to 15 million euros or 3% of global turnover. Australian organisations serving EU users, or working under contracts that flow those obligations down, will be asked to evidence human oversight of AI-assisted decisions. You cannot backfill that evidence.&lt;/p&gt;

&lt;p&gt;For each AI-assisted decision we record: what proposed the action, which model and version, what evidence it saw (the actual query results, not a paraphrase), what it recommended, who approved or rejected it, when, and what executed. In practice the evidence snapshot is the hard part, because inputs change under you. Store it with the decision.&lt;/p&gt;

&lt;p&gt;One more control, learned from an open-source community rather than a compliance framework. The nixpkgs maintainers proposed &lt;a href="https://github.com/NixOS/nixpkgs/issues/551976" rel="noopener noreferrer"&gt;closing any pull request that removes the AI disclosure checkbox&lt;/a&gt;, on the basis that merging one would be a serious policy violation. The interesting part is the failure mode they found. The control existed, but it was a checkbox the submitter could delete. So in a security pipeline, AI-provenance metadata must be system-generated and not editable by the thing that generated the content. If the agent can write its own "reviewed by a human" flag, the flag means nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll it out in dry run, and stay there longer than feels necessary
&lt;/h2&gt;

&lt;p&gt;Every automation we deploy runs in dry run first. The agent does the full job, including selecting the action, and writes what it would have done to a log and a channel humans read. Nothing executes.&lt;/p&gt;

&lt;p&gt;We run this for a minimum of four weeks in most environments, longer for anything that touches identity or network paths. The dry-run period tells you three things you cannot get from testing: how often the agent recommends something an analyst would not do, how noisy it is at 2am when the batch jobs run, and whether the evidence it attaches is actually enough for a human to decide from. That last one fails more often than the first.&lt;/p&gt;

&lt;p&gt;Promotion out of dry run is per action type, never for the whole system. Enrichment and triage might graduate in week two. Automated blocking might never graduate, and for most clients it should not.&lt;/p&gt;

&lt;p&gt;An honest cost note. Human-in-the-loop is more expensive to run than full automation, because you are paying for the AI and keeping the analyst. The saving comes from what the analyst spends their time on: reviewing a drafted finding with evidence attached takes a couple of minutes, where building that finding from scratch takes twenty or thirty. The approval queue itself needs an owner and an SLA, or it becomes a place where alerts go to age quietly.&lt;/p&gt;

&lt;p&gt;For anything clinically adjacent, the sign-off is part of the design rather than a caveat bolted on the end. An AI can assemble a suspected-breach pack, correlate the access logs, draft the notification timeline. A named clinician or privacy officer approves what leaves the building, and their name is on the record.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/when-ai-must-not-act-alone-human-in-the-loop-patterns-for-security-automation/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>humanintheloop</category>
      <category>securityautomation</category>
    </item>
    <item>
      <title>Ad spend guardrails: automated Google Ads reporting and anomaly alerts</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:58:25 +0000</pubDate>
      <link>https://dev.to/gatapia/ad-spend-guardrails-automated-google-ads-reporting-and-anomaly-alerts-40jm</link>
      <guid>https://dev.to/gatapia/ad-spend-guardrails-automated-google-ads-reporting-and-anomaly-alerts-40jm</guid>
      <description>&lt;p&gt;Most organisations we work with have alerts on cloud spend. If an AWS account jumps 30% overnight, someone gets paged. The same organisation will spend a comparable amount on Google Ads and review it once a month, in a meeting, from a deck built by the agency that placed the ads.&lt;/p&gt;

&lt;p&gt;That gap is where money disappears. A campaign gets disapproved and stops serving for eleven days before anyone notices. A bid strategy switches to maximise clicks and burns three weeks of budget on traffic that never converts. A landing page 404s after a site release and the ads keep running. None of this is exotic. It is ordinary operational drift, and it is invisible until the monthly report, by which point the money is gone.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-marketing-the-series/" rel="noopener noreferrer"&gt;Practical AI in Marketing&lt;/a&gt; series, and it describes a small system we build for clients: a daily automated report on Google Ads spend and performance, with anomaly alerts, and a plain-English summary that a business owner will actually read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you instrument it yourself
&lt;/h2&gt;

&lt;p&gt;Two things happened in the last week that make the case better than I can. On 2 September a US court declined to force Google to sell its ad tech business, accepting behavioural remedies instead, &lt;a href="https://www.reuters.com/legal/litigation/google-defeats-us-bid-force-ad-tech-sale-2026-09-02/" rel="noopener noreferrer"&gt;the third time in recent years that US antitrust enforcers have failed to break up a Big Tech company&lt;/a&gt;. The buy side and the sell side of that market &lt;a href="https://www.nytimes.com/2026/09/02/technology/google-ad-tech-remedies.html" rel="noopener noreferrer"&gt;stay under one roof&lt;/a&gt;. The day before, the FTC and 22 state attorneys general alleged that Amazon &lt;a href="https://arstechnica.com/tech-policy/2026/09/ftc-alleges-amazon-illegally-made-20-billion-by-rigging-billions-of-ad-auctions/" rel="noopener noreferrer"&gt;made over US$20 billion by overriding its own ad auction results with higher prices&lt;/a&gt;, a claim Amazon disputes.&lt;/p&gt;

&lt;p&gt;Whatever the outcomes, the practical lesson for an Australian advertiser is the same. Auction transparency is not arriving from a regulator on a timetable that helps your budget this quarter. Newer AI-driven ad channels have their own problems; practitioners are already publicly complaining that &lt;a href="https://successfulsoftware.net/2026/09/02/chatgpt-ad-targeting-is-garbage/" rel="noopener noreferrer"&gt;ChatGPT ad targeting sends spend to irrelevant audiences&lt;/a&gt;. Pull your own spend and conversion data into a system you control, keep your own history, and reconcile platform-reported conversions against what your CRM says actually closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the system
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A scheduled job pulls campaign, ad group and keyword metrics from the Google Ads API each morning, read-only.&lt;/li&gt;
&lt;li&gt;Rows land in a database table you own, one row per entity per day, appended and never overwritten, so you keep your own history independent of platform reporting windows.&lt;/li&gt;
&lt;li&gt;An expectation layer computes what each campaign should have spent and returned.&lt;/li&gt;
&lt;li&gt;A deterministic rules engine compares actuals against expectations and raises anomalies.&lt;/li&gt;
&lt;li&gt;An LLM turns the day's numbers and any anomalies into six or eight sentences of plain English.&lt;/li&gt;
&lt;li&gt;Delivery goes to email and Slack or Teams, with the raw table available for anyone who wants to dig.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extraction query is unremarkable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;campaign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;campaign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;campaign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_micros&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;impressions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversions_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;campaign&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="n"&gt;DURING&lt;/span&gt; &lt;span class="n"&gt;LAST_7_DAYS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Read-only by default
&lt;/h2&gt;

&lt;p&gt;The credential the reporting job uses must not be able to change anything. No write scopes, no budget edits, no pausing campaigns, even though the API would happily allow all three.&lt;/p&gt;

&lt;p&gt;This is not a hypothetical concern. A recently merged open-source project, the KPA Traffic Stack, builds exactly this pattern for Meta and Google Ads diagnosis: read-only exports with &lt;a href="https://github.com/rodneicalixto-prog/KPA-calixto/pull/4" rel="noopener noreferrer"&gt;explicit guardrails against accidental writes and credential leaks&lt;/a&gt;, validators that reject exports containing secret-like keys, and a flag on every export asserting that platform writes are not allowed and human approval is required. The automated code review on that pull request found a real hole in it, where one validator trusted a self-reported "no credentials" flag rather than scanning nested fields for tokens. Worth reading if you are building something similar, because the same mistake is easy to make.&lt;/p&gt;

&lt;p&gt;Two reasons for the restriction. An agent or script with write access to a live ad account is a bad incident waiting for a bad day. And once the report can act, its findings stop being an independent record and start being a description of its own behaviour.&lt;/p&gt;

&lt;p&gt;If you do want programmatic control of campaigns, keep it in a separate, reviewed path. The agoraform project treats Google Ads as config-as-code, covering &lt;a href="https://github.com/dziblo-music/agoraform/issues/45" rel="noopener noreferrer"&gt;conversion goal, budget, campaign, ad group, targeting, keywords and responsive search ads in one declarative resource graph&lt;/a&gt; with a validate, plan, apply lifecycle. Campaign changes then get diffed and reviewed like any infrastructure change, by a person, on a different credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anomalies need an expected value, not last week's number
&lt;/h2&gt;

&lt;p&gt;The weak version of this system alerts when spend moves more than some percentage against the prior period. It produces noise every Monday, every long weekend and every end of financial year.&lt;/p&gt;

&lt;p&gt;The better version compares against a modelled expectation. The mizan target engine is a good illustration of the arithmetic: it &lt;a href="https://github.com/eslam21006-coding/mizan/pull/34" rel="noopener noreferrer"&gt;deterministically reverse-engineers required revenue, new customers, sales, qualified calls, shows, bookings, leads and ad spend from a monthly profit or margin goal&lt;/a&gt;, and computes a maximum sustainable acquisition CAC, maximum media CAC and maximum cost per lead along the way. That maximum sustainable ad spend is the ceiling a runaway-spend alert should be measured against. It also fails closed: missing months stay missing rather than becoming zero, and impossible targets return an explicit unattainable result instead of a plausible-looking number.&lt;/p&gt;

&lt;p&gt;Our rules end up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runaway_spend&lt;/span&gt;
  &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rolling_7d&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spend &amp;gt; 1.15 * max_sustainable_daily_spend&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dead_campaign&lt;/span&gt;
  &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rolling_3d&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spend &amp;gt; 2 * target_cpa and conversions == &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;silent_campaign&lt;/span&gt;
  &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rolling_2d&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status == "ENABLED" and impressions == &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpl_drift&lt;/span&gt;
  &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rolling_14d&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cost_per_lead &amp;gt; 1.3 * max_cpl and spend &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spend floors matter. A campaign running at $30 a day will breach any ratio-based rule constantly, so suppress alerts below a dollar threshold where the money at stake is smaller than the cost of reading the email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the LLM helps
&lt;/h2&gt;

&lt;p&gt;The LLM does one job: it writes the summary. It receives the computed table and the list of triggered anomalies as structured input and produces a short narrative. It does no arithmetic, no ranking, no attribution reasoning. Every number in its output must appear in the input, and we validate that before sending.&lt;/p&gt;

&lt;p&gt;The reason is boring and practical. A business owner will not open a dashboard daily, and will not read a 40-row table. They will read four paragraphs in an email that says spend was $4,180 against a $3,900 expectation, that the brand campaign is fine, and that a particular campaign has spent $612 in three days with no conversions and should be checked today. Prior to LLMs, we wrote that summary with templates, and the templates read like templates. This is the piece where the model genuinely earns its place.&lt;/p&gt;

&lt;p&gt;Costs are small. The Google Ads API is free to use, though you need a developer token and basic access approval. Daily summarisation for a mid-sized account runs to a few thousand tokens, which is cents per day. The build is the expense: a week or two of engineering for a first version covering one account, plus a smaller amount of maintenance because Google retires API versions regularly and you will be doing an upgrade at least annually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;Conversion lag is the big one. Yesterday's conversions are undercounted, so a dead-campaign rule reading a single day of data will produce false alarms. We compare on a lagged window and state the lag in the summary. Attribution is the second: Google's reported conversions and your CRM's closed deals will not agree, and the system should show both rather than pretending one is truth. Third, an alert nobody actions is worse than no alert, so cap the daily volume and give each rule an owner.&lt;/p&gt;

&lt;p&gt;One Australian-specific note. If your campaigns use customer match or remarketing audiences, the email addresses and identifiers you upload are personal information under the Privacy Act, and the handling, consent and retention questions belong with your privacy officer before the reporting project starts, not after.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/ad-spend-guardrails-automated-google-ads-reporting-and-anomaly-alerts/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
      <category>googleads</category>
      <category>marketingautomation</category>
    </item>
    <item>
      <title>A morning digest your team actually reads: AI-summarised news, spend and system health</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Mon, 07 Sep 2026 01:28:26 +0000</pubDate>
      <link>https://dev.to/gatapia/a-morning-digest-your-team-actually-reads-ai-summarised-news-spend-and-system-health-1lam</link>
      <guid>https://dev.to/gatapia/a-morning-digest-your-team-actually-reads-ai-summarised-news-spend-and-system-health-1lam</guid>
      <description>&lt;p&gt;Most Australian operations teams I talk to are subscribed to too much. Cloud billing alerts to one inbox, an uptime tool posting to a Slack channel, ad platform notifications to whoever set up the account three years ago, four vendor newsletters, and a deploy bot that fires on every merge. Nobody reads any of it. The item that actually mattered last Tuesday was sitting in a channel someone muted in March.&lt;/p&gt;

&lt;p&gt;The approach we keep coming back to is unglamorous: one email, one delivery list, sent at 7am, ordered by what needs a decision today. Industry news summarised by a model, yesterday's cloud and ad spend, and anything monitoring flagged overnight. This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-business-operations-the-series/" rel="noopener noreferrer"&gt;Practical AI in Business Operations&lt;/a&gt; series, and it is the cheapest useful thing on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one digest beats ten streams
&lt;/h2&gt;

&lt;p&gt;Notification channels rot quietly, and nothing tells you when they do. A good example sits in a push notification service where the send call returned a bare boolean, so a permanent APNS 410 (the token is dead, the app was uninstalled or push was disabled) looked identical to a transient network blip. Dead device tokens were never pruned and the failures kept accumulating with nobody watching (&lt;a href="https://github.com/trackrat-dev/TrackRat/pull/1802" rel="noopener noreferrer"&gt;TrackRat #1802&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That is the same failure mode as ten alert channels in an organisation. Each one is a small integration nobody owns. One digest with a known distribution list is a single thing to keep honest, and every recipient notices the day it stops arriving.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the digest contains, in the order it matters
&lt;/h2&gt;

&lt;p&gt;Order sections by what forces action, not by data source. Grouping by category buries the item that needs a response, which is exactly what happened to a changelog generator that grouped commits by their type prefix. Breaking changes were rendered under "Added" and "Changed", and one commit that deleted eight internal packages shipped in a published changelog under "Changed" where an auditor would never look for it. The fix was to give breaking changes their own section and sort it to the top (&lt;a href="https://github.com/go-kure/kure/pull/775" rel="noopener noreferrer"&gt;go-kure #775&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So the running order in ours is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overnight alerts. Anything monitoring raised between the last digest and this one, with the alert still open flagged separately from the ones that self-cleared.&lt;/li&gt;
&lt;li&gt;Spend. Yesterday's AWS or Azure spend against the trailing seven-day average, plus Google and Meta ad spend from the same window. Percentage moves in the heading, dollars in the row.&lt;/li&gt;
&lt;li&gt;Industry and vendor news. Summarised, triaged, and always last, because it is the section a reader is allowed to skip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;It is a scheduled task, not a platform. Ours runs on a cron trigger before business hours in Sydney and takes a few seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One collector per source, each returning &lt;code&gt;(rows, error)&lt;/code&gt;. Collectors never raise into the pipeline.&lt;/li&gt;
&lt;li&gt;One summariser call per section that needs prose. The alerts and spend sections are rendered from data with no model involved.&lt;/li&gt;
&lt;li&gt;One renderer producing HTML and a plain text alternative that says the same thing.&lt;/li&gt;
&lt;li&gt;One run log recording which sources answered, which failed, and how many items each contributed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;(rows, error)&lt;/code&gt; shape carries more weight than it looks like it should:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: COULD NOT READ (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: nothing overnight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty read and a failed read must render differently. A morning brief script learnt this the hard way: its calendar collector called the API with no &lt;code&gt;calendarId&lt;/code&gt;, so it read the account default and returned zero events for a week that had nine, including a weekly planning meeting. The brief printed "nothing" on days that had meetings, and the zero read as a pass. The prompt named the calendar in prose, which the author summarised as "prose is not an argument" (&lt;a href="https://github.com/assafkip/kipi-system/pull/299" rel="noopener noreferrer"&gt;kipi-system #299&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That is the archetypal silent digest failure. The email still arrives, still looks complete, and an entire data source is missing. Enforce data scope in code with an explicit parameter, and make source coverage a section of the digest itself so a missing feed is visible to every reader rather than to nobody.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt design that stays faithful to sources
&lt;/h2&gt;

&lt;p&gt;Three rules do most of the work.&lt;/p&gt;

&lt;p&gt;First, the model chooses, and code checks the choice. Give the summariser a small set of named admission rules and require it to cite which rule admitted each item it keeps. Then validate the citation in code and drop anything citing something else. A team digest at PostHog did exactly this with four named rules (contract, assumption, decision, customer) and went from keeping 21 of 26 merges to keeping zero to two (&lt;a href="https://github.com/PostHog/posthog/pull/88286" rel="noopener noreferrer"&gt;PostHog #88286&lt;/a&gt;). Their earlier attempt used a count target in the prompt, which squeezed out real news on busy days and admitted routine work on quiet ones.&lt;/p&gt;

&lt;p&gt;Second, no untrusted text reaches the prompt in a position where it can address the summariser. The same PostHog change removed contributor-written PR bodies from the prompt entirely, and made sure the remaining values could not close their own tag and continue as instructions. Vendor news is worse than internal commits here: press releases are written to be persuasive, and you are feeding them to a model whose job is to be sceptical about them.&lt;/p&gt;

&lt;p&gt;Third, ask for assessment, not paraphrase. A summary that repeats an item's own marketing copy is worse than the headline alone, because it launders the claim into your team's morning reading. A live automated feed we looked at as a format reference gets this right: a headline count ("20 new videos across your watchlist"), then per item a title, source, a triage verdict such as Skim or Skip, and one line of critical assessment (&lt;a href="https://github.com/projectbetterclass/youtube-digest/issues/10" rel="noopener noreferrer"&gt;youtube-digest #10&lt;/a&gt;). The triage label is what ten notification streams can never give you. It tells the reader what not to open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freshness rules, and the trap underneath them
&lt;/h2&gt;

&lt;p&gt;Decide deliberately what happens to an item the digest did not carry, because there are two separate loss paths and they need different answers. Something cut by a render cap is different from something the summariser read and rejected.&lt;/p&gt;

&lt;p&gt;Get this wrong and the digest quietly decays. PostHog's was carrying merges that had landed up to six days earlier with nothing marking them as old, because anything the cap cut or the summariser dropped was re-queued into the next morning's run. The rejected ones then produced the same empty result every day, forever, since the same input to the same prompt returns the same answer. Their fix was to let a run consume everything it claims, including an empty answer. Either age items out or label them explicitly as carried over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editorial rules as a binding contract
&lt;/h2&gt;

&lt;p&gt;Write the rules down once, in one referenced document, and treat them as constraints any agent revising the text must obey. Scattering them through ad-hoc prompts guarantees drift. A useful example of the form is an editorial supervision contract written for agent-generated prose: core rules, an explicit list of banned constructions, and a revision standard listing what every pass must actively search for (&lt;a href="https://github.com/jain-Igtm/janeco/issues/3" rel="noopener noreferrer"&gt;janeco #3&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For a digest, ours is short. Every claim links to its source. No item is described in the vendor's own adjectives. An item the model cannot summarise from the source text gets a link and no summary. Every section states its coverage. Nothing in the digest ever asserts something the collector did not read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits and costs
&lt;/h2&gt;

&lt;p&gt;The model spend is a rounding error next to the cloud bill the digest reports on. A handful of calls a day against a small model costs less than the coffee consumed while reading the output. The real cost is engineering time on the collectors, because ad platforms and cloud billing APIs change, and a broken collector that fails loudly still needs someone to fix it that week.&lt;/p&gt;

&lt;p&gt;The other limit is scope. A digest is a reading aid, not a control system. It should never take an action, close an alert, or approve spend. Every item links back to the console where a person does that. And it needs one owner who reads it every morning and notices the day a section goes quiet for the wrong reason.&lt;/p&gt;

&lt;p&gt;Build the smallest version first. One source, one section, one recipient, running for a fortnight before you add the second source. Most of the value shows up in week one.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/a-morning-digest-your-team-actually-reads-ai-summarised-news-spend-and-system-health/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>businessoperations</category>
      <category>llmsummarisation</category>
      <category>automation</category>
    </item>
    <item>
      <title>Cloud posture on autopilot: continuously auditing AWS, Azure and Microsoft 365</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:58:28 +0000</pubDate>
      <link>https://dev.to/gatapia/cloud-posture-on-autopilot-continuously-auditing-aws-azure-and-microsoft-365-49nh</link>
      <guid>https://dev.to/gatapia/cloud-posture-on-autopilot-continuously-auditing-aws-azure-and-microsoft-365-49nh</guid>
      <description>&lt;p&gt;Most of the cloud security incidents we get called into have nothing to do with a novel exploit. Someone opened a security group to 0.0.0.0/0 for a vendor's remote session and never closed it. A storage account got flipped to public so a contractor could pull a file. An admin was excluded from conditional access "temporarily" in March. A mailbox forwarding rule was added and nobody noticed because nobody was looking.&lt;/p&gt;

&lt;p&gt;None of that shows up in a vulnerability scan. It shows up in configuration, and configuration changes constantly, in three or four different consoles, made by people who had a good reason at the time. This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-cyber-security-the-series/" rel="noopener noreferrer"&gt;Practical AI in Cyber Security&lt;/a&gt; series, and it covers the service we run for clients: a scheduled job that reads the current state of AWS, Azure and Microsoft 365, compares it to a stored baseline, and emails a human when something has moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why point-in-time auditing stopped working
&lt;/h2&gt;

&lt;p&gt;The traditional pattern is an annual review where someone screenshots portal settings for the auditor. Cloud-first teams have been moving away from that towards continuous, code-driven checks that run against AWS and Azure on a schedule, so the audit is a job that runs rather than a scramble in the weeks before certification (&lt;a href="https://www.reddit.com/r/Information_Security/comments/1w3hlyo/how_are_cloudfirst_teams_adapting_iso_27001/" rel="noopener noreferrer"&gt;r/Information_Security discussion, Aug 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The control pattern that thread describes is worth stealing directly: state the requirement, tag the resource, run the check, report the result. It is boring and it works across every control domain we care about. Each of our checks is written that way, as a small independent unit with a stated requirement and a pass or fail, rather than as a giant "security score" that nobody can act on.&lt;/p&gt;

&lt;p&gt;There is a second reason to build this yourself rather than lean entirely on the native tools. Most Australian organisations we work with are not on one platform. They have Microsoft 365 for identity and email, some Azure, and AWS from an acquisition or a dev team that got there first, and the relative growth of the big three suggests that mix is not resolving itself any time soon (&lt;a href="https://www.reddit.com/r/Mag7shares/comments/1vwed0r/aws_was_lagging_microsoft_azure_and_google_cloud/" rel="noopener noreferrer"&gt;r/Mag7shares, Aug 2026&lt;/a&gt;). Three consoles with three different opinions about what "secure" means is how things get missed. One consolidated report, one owner, one weekly read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually check
&lt;/h2&gt;

&lt;p&gt;The check list is deliberately narrow. These are the things that drift, that matter, and that a person can fix in an afternoon.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity in AWS and Azure&lt;/strong&gt;: IAM users with console access and no MFA, long-lived access keys, keys that have not rotated, roles with wildcard permissions, orphaned service principals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network exposure&lt;/strong&gt;: security groups and NSGs allowing inbound from anywhere, especially on 22, 3389, 1433 and 3306. Public IPs on resources that had none last week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: S3 buckets and Azure blob containers with public access, anonymous read enabled, or encryption settings changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft 365 identity&lt;/strong&gt;: conditional access policies added, disabled or scoped down, break-glass exclusions, MFA registration state versus MFA enforcement (they are not the same thing and the gap is where people live), legacy authentication re-enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange Online&lt;/strong&gt;: mailbox forwarding rules to external domains, new transport rules, mailbox delegation changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entra ID app surface&lt;/strong&gt;: new enterprise app registrations, OAuth consent grants and the scopes they were granted, changes to admin role membership including eligible assignments in PIM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost and orphan drift&lt;/strong&gt;: resources that stopped being used but did not stop being billed. One operator running an identical 24/7 workload found AWS came in around 30% more expensive than the alternatives (&lt;a href="https://www.reddit.com/r/automation/comments/1w5a6fw/i_deployed_openclaw_on_aws_azure_and_google_cloud/" rel="noopener noreferrer"&gt;r/automation, Sep 2026&lt;/a&gt;), which is a fair reminder that an unnoticed left-running instance is a real cost, not just an untidy inventory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is not strictly a security check, but it uses the same snapshot and it is the item that most often gets a client to actually read the email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The design is simple enough to describe in a paragraph, which is part of the point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A scheduled worker (we run these hourly for identity checks, daily for everything else) authenticates to each tenant using a read-only role: an AWS IAM role assumed cross-account, an Azure reader plus a Graph app registration with directory and policy read scopes. No write permissions anywhere.&lt;/li&gt;
&lt;li&gt;Each collector pulls the relevant state and normalises it into a flat JSON document per platform. Resource identifier, control name, observed value, timestamp.&lt;/li&gt;
&lt;li&gt;That document is diffed against the last stored snapshot. Any difference is a drift event with a before value and an after value.&lt;/li&gt;
&lt;li&gt;Drift events go to a model that writes the summary a human reads: what changed, who changed it if the audit log says, and why it matters for this control.&lt;/li&gt;
&lt;li&gt;The report goes to email and to a Teams channel. Nothing is remediated automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where clients already have infrastructure as code, the diff is even cleaner, because the declared template is the baseline and live state is the thing that has wandered. Practitioners in that same ISO 27001 thread make the point about Bicep specifically. Full IaC coverage is rare in the environments we inherit, so snapshots plus drift detection give you the same signal without requiring a rebuild first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the AI part earns its keep
&lt;/h2&gt;

&lt;p&gt;The detection is deterministic. Rules, diffs, comparisons. We do not use a model to decide whether something is a finding, because a model that occasionally misses a public bucket is worse than no check at all.&lt;/p&gt;

&lt;p&gt;The model writes the explanation. Raw drift output is unreadable: a JSON blob showing that &lt;code&gt;conditionalAccessPolicy/7f3a...&lt;/code&gt; moved from &lt;code&gt;enabled&lt;/code&gt; to &lt;code&gt;enabledForReportingButNotEnforced&lt;/code&gt;. The summary the client gets says that the policy requiring MFA for admin sign-ins from outside Australia was switched to report-only mode on Tuesday afternoon, that it currently blocks nothing, and that the change was made by a named admin account. Same fact, but one of them gets actioned.&lt;/p&gt;

&lt;p&gt;The model also groups related drift. Twelve NSG changes from a single deployment become one paragraph rather than twelve alerts, which is the difference between a report people read and a filter rule people write.&lt;/p&gt;

&lt;p&gt;We keep the model on a short leash. It gets the drift record and the control description, and it writes prose from those. It does not query the tenant, it does not decide severity on its own, and every summary sits next to the raw before and after values so the reader can check it. When the model is wrong, it is wrong about wording, not about whether something changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect and notify, not auto-fix
&lt;/h2&gt;

&lt;p&gt;We deliberately do not remediate automatically. Experienced operators are sceptical of automated remediation for good reason, as one put it in that thread: "I'm personally not a fan of automated remediations as it's hard to do."&lt;/p&gt;

&lt;p&gt;Our experience matches. A rule that automatically closes an open security group will eventually close the one that a legitimate integration depends on, at 2am, in the environment where nobody documented the dependency. In tenants with clinical or health administration workloads, that risk is not acceptable at all. Any change to an operational system needs a person to sign it off before it happens. That is a design requirement for us, not a footnote.&lt;/p&gt;

&lt;p&gt;So the output is a decision point, not an action: here is what changed, here is what we think it means, here is the button that says "expected, add to baseline" or "not expected, raise a ticket".&lt;/p&gt;

&lt;h2&gt;
  
  
  The first run rule: never silently seed
&lt;/h2&gt;

&lt;p&gt;This one is worth stating on its own because it is the mistake we see most often in home-grown versions.&lt;/p&gt;

&lt;p&gt;The first time the service runs against a tenant, there is no prior snapshot, so nothing is technically "drift". The tempting behaviour is to quietly store that state as the baseline and start alerting from run two. That means every pre-existing misconfiguration, every public bucket and every stale global admin becomes invisible by definition, permanently blessed as normal.&lt;/p&gt;

&lt;p&gt;Our first run emails the complete baseline instead. Every public storage container, every account without MFA, every open security group, every external forwarding rule that exists on day one. It is usually an uncomfortable document, it is often the most valuable single report a client gets from us, and it forces an explicit decision on each item before anything becomes "normal".&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs and limitations
&lt;/h2&gt;

&lt;p&gt;Being honest about what this does not do:&lt;/p&gt;

&lt;p&gt;It only sees what the APIs expose. Graph rate limits mean a large tenant's full Microsoft 365 sweep takes longer than you would like, and some settings are only readable through interfaces that change without notice. Collectors break, so we monitor the collectors themselves and treat a silent run as a failure.&lt;/p&gt;

&lt;p&gt;It is noisy in environments with active deployment pipelines until the rules are tuned. Expect two to four weeks of tuning before the signal to noise ratio is good, and expect to keep tuning as the environment changes.&lt;/p&gt;

&lt;p&gt;New rules should never fire their first alert against production. Local emulators for AWS, Azure and GCP let you develop and regression-test drift rules offline (&lt;a href="https://flowg.cloud/blog/using-floci-local-emulators" rel="noopener noreferrer"&gt;flowg.cloud, Aug 2026&lt;/a&gt;), which we use for the AWS side in particular. The same write-up is candid that emulators do not validate real authentication, so the last test always happens against a real tenant, in read-only mode, before a rule goes live.&lt;/p&gt;

&lt;p&gt;Cost-wise, the compute is trivial. A scheduled worker plus snapshot storage runs at a few dollars a month. The model calls for summarisation are similarly small because they only fire when there is drift. The real cost is the human time in the first month, building the check set for your environment and clearing the day-one baseline. Budget for that honestly, because a posture service nobody triages is just an expensive mail rule.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/cloud-posture-on-autopilot-continuously-auditing-aws-azure-and-microsoft-365/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>cloudsecurity</category>
      <category>microsoft365</category>
    </item>
    <item>
      <title>Tracking search rankings and AI Overviews without an agency</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Wed, 02 Sep 2026 01:03:29 +0000</pubDate>
      <link>https://dev.to/gatapia/tracking-search-rankings-and-ai-overviews-without-an-agency-2ail</link>
      <guid>https://dev.to/gatapia/tracking-search-rankings-and-ai-overviews-without-an-agency-2ail</guid>
      <description>&lt;p&gt;The monthly agency deck lands, forty keywords are green, and somebody in the room asks the question the deck cannot answer: when a buyer asks ChatGPT or Gemini who does this work in Sydney, do we come up? Ranking well in the ten blue links and being absent from AI answers are now two different states, and a brand can sit in one while assuming the other (&lt;a href="https://www.reddit.com/r/geotoolsreview/comments/1w48g2u/radarkit_vs_peec_ai_vs_profound_compare_features/" rel="noopener noreferrer"&gt;RadarKit vs Peec AI vs Profound&lt;/a&gt;). That gap is the reason to run your own telemetry. This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-marketing-the-series/" rel="noopener noreferrer"&gt;Practical AI in Marketing&lt;/a&gt; series, and it covers the smallest thing that works: a scheduled job, a table you own, and a weekly report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record presence separately from position
&lt;/h2&gt;

&lt;p&gt;The main design decision is in the schema. Position and AI-surface presence are different measurements and collapsing them into one "visibility score" throws away the signal you actually want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;serp_check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;checked_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;keyword&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;locale&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- gl=au, hl=en-AU, Sydney or Melbourne&lt;/span&gt;
  &lt;span class="n"&gt;engine&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- google, google_ai_mode, chatgpt, perplexity&lt;/span&gt;
  &lt;span class="k"&gt;position&lt;/span&gt;    &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;-- null when absent, never 0&lt;/span&gt;
  &lt;span class="n"&gt;ai_present&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;-- was an AI answer rendered at all?&lt;/span&gt;
  &lt;span class="n"&gt;cited&lt;/span&gt;       &lt;span class="nb"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;-- were we named or linked in it?&lt;/span&gt;
  &lt;span class="n"&gt;competitors&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;                &lt;span class="c1"&gt;-- who was named instead&lt;/span&gt;
  &lt;span class="n"&gt;raw&lt;/span&gt;         &lt;span class="n"&gt;jsonb&lt;/span&gt;       &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;   &lt;span class="c1"&gt;-- the full response, kept forever&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter. Use null for absence rather than 0 or 101, because averages over a sentinel value quietly lie to you. And keep the raw payload. The tool market is churning hard enough that August 2026 alone produced round-ups of &lt;a href="https://www.reddit.com/r/geotoolsreview/comments/1w36r65/17_best_ai_rank_trackers_to_monitor_your/" rel="noopener noreferrer"&gt;17 AI rank trackers&lt;/a&gt; plus separate lists of LLM visibility tools and vendor alternatives. Any of those products may not exist in two years. Your history should live in your database, not theirs.&lt;/p&gt;

&lt;p&gt;Set the locale properly. Australian result sets differ from US ones, and a national keyword checked from a US data centre will tell you about a market you do not sell into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job itself
&lt;/h2&gt;

&lt;p&gt;The architecture is deliberately boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A keyword and prompt list in a config file in the repo, reviewed by whoever owns the site.&lt;/li&gt;
&lt;li&gt;A scheduled runner: cron on a small VM, an Azure Function on a timer, or a GitHub Actions workflow if you want the config and the history in one place.&lt;/li&gt;
&lt;li&gt;A SERP API call per keyword for classic Google, returning positions and any AI Overview block.&lt;/li&gt;
&lt;li&gt;Optional web-grounded calls to the LLM assistants your buyers actually use, one row per prompt per engine.&lt;/li&gt;
&lt;li&gt;An append-only write to Postgres or SQLite, plus a generated weekly markdown or HTML report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is novel work. A public &lt;a href="https://github.com/razz1000/ai-search-rank-tracking-example-repo" rel="noopener noreferrer"&gt;example repository&lt;/a&gt; posted in August 2026 does the whole loop with a prompt bank, four optional engines, JSONL history and a weekly workflow, and &lt;a href="https://lettertrace.com" rel="noopener noreferrer"&gt;Lettertrace&lt;/a&gt; ships an MIT-licensed bring-your-own-key version where your data sits in your own Supabase. The tracking layer is commoditised. Fork something, or write it in a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Per-query pricing is the easy part. SERP APIs bill per successful query in the fractions-of-a-cent range at the volumes a single company needs, so the arithmetic looks like this: 60 keywords checked daily is roughly 1,800 queries a month, which at half a cent a query is under $10 AUD. Add three AI surfaces on a weekly cadence and you are still in tens of dollars, not hundreds. Grounded LLM calls cost more per query than raw SERP fetches, so run those weekly rather than daily.&lt;/p&gt;

&lt;p&gt;Verify tier pricing against official sites before you budget. One August 2026 comparison listed Ahrefs at $199 per month per index and was corrected in its own comments to a $50 starting price, which the author acknowledged (&lt;a href="https://www.reddit.com/r/geotoolsreview/comments/1vtgkkf/9_best_llm_visibility_tracking_tools_to_monitor/" rel="noopener noreferrer"&gt;9 best LLM visibility tracking tools&lt;/a&gt;). Round-up pricing is unreliable in both directions.&lt;/p&gt;

&lt;p&gt;The real cost is engineering time: a day or two to build, then an hour or two a month keeping it alive. Budget that hour honestly, because it is the line item people forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits and failure handling
&lt;/h2&gt;

&lt;p&gt;Providers cap concurrency well below what a naive loop will attempt. Practical defaults we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queue the keywords and run them with bounded concurrency, not a parallel map over the whole list.&lt;/li&gt;
&lt;li&gt;Retry on 429 and 5xx with exponential backoff and a hard attempt limit.&lt;/li&gt;
&lt;li&gt;Add a few minutes of random jitter to the scheduled start so you are not hammering the API at the same second as everyone else's cron.&lt;/li&gt;
&lt;li&gt;Write a row for every failure with a status field. A missing row and a genuine absence must never look the same in the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point catches the worst failure mode. If your job dies halfway through Tuesday and you silently store nothing, next week's chart shows a ranking collapse that never happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsers break, and that is normal
&lt;/h2&gt;

&lt;p&gt;The AI surfaces change shape month to month. On 28 August 2026 Google AI Mode &lt;a href="https://www.seroundtable.com/google-ai-mode-travel-updates-41956.html" rel="noopener noreferrer"&gt;added flight price tracking, points and mile rates, and hotel booking&lt;/a&gt;. Layout changes of that kind break whatever selector or heuristic you use to decide "AI Overview present?".&lt;/p&gt;

&lt;p&gt;Two cheap defences. First, a canary: pick two or three keywords that reliably trigger an AI answer and alert if they all report absent on the same day, because a global drop is a parser bug far more often than a ranking event. Second, put a recurring maintenance task in the backlog rather than waiting for the dashboard to look wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the trend
&lt;/h2&gt;

&lt;p&gt;Daily numbers jitter for reasons that have nothing to do with your site: test buckets, location, index refreshes, and in the case of LLM answers, plain non-determinism between runs. Rules that keep people calm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Report a seven-day rolling median position, not yesterday's number.&lt;/li&gt;
&lt;li&gt;Treat AI presence as a rate, not an event: "cited in 9 of 28 checks this month" is a number you can act on.&lt;/li&gt;
&lt;li&gt;Only investigate a move that holds for four or more consecutive checks.&lt;/li&gt;
&lt;li&gt;Alert on the competitor field, not just your own. A rival appearing in six answers where you appear in none is the signal worth a meeting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where DIY wins, and what an agency still adds
&lt;/h2&gt;

&lt;p&gt;There are two jobs here and only one of them is a script. Counting how often you appear is a dashboard. Working out &lt;em&gt;why&lt;/em&gt; you are missing from AI answers across Google AI Overviews, ChatGPT Search, Perplexity, Copilot and Gemini is diagnosis, and it is where an agency or consultant still earns the fee (&lt;a href="https://www.reddit.com/r/jenova_ai/comments/1w2gpgs/what_is_the_best_ai_geo_strategist_for_brand/" rel="noopener noreferrer"&gt;AI GEO strategist thread&lt;/a&gt;). The method is not the moat either: search engines document their own systems and large agencies publish their manuals for free (&lt;a href="https://www.reddit.com/r/GenerativeOptimizers/comments/1w274ux/how_to_learn_seo_and_ai_search_for_free/" rel="noopener noreferrer"&gt;how to learn SEO and AI search for free&lt;/a&gt;), so what you are buying is judgement and follow-through, not secret knowledge.&lt;/p&gt;

&lt;p&gt;Judge your internal dashboard against the three questions agencies are being asked to answer in 2026: does the brand appear in AI-generated answers, which competitors are recommended instead, and what would change that (&lt;a href="https://www.reddit.com/r/aiseo_tips/comments/1vzliya/top_7_ai_visibility_trackers_for_agencies_2026/" rel="noopener noreferrer"&gt;AI visibility trackers for agencies&lt;/a&gt;). A script answers the first two well and the third not at all. If you want a template for splitting the work formally, an August 2026 engineering ticket ran a &lt;a href="https://github.com/creativeghq/material-kai-vision-platform/issues/349" rel="noopener noreferrer"&gt;gap analysis of an in-house visibility surface against a commercial product&lt;/a&gt; and produced an explicit build list. That is the right shape of decision: build the collection and the history, licence or outsource the analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;API results are not what a logged-in customer in Parramatta sees on their phone. LLM answers vary between identical runs, so any single check is close to meaningless and only the rate over weeks means anything. Coverage of AI surfaces varies by provider and lags each layout change. You own the maintenance, and telemetry does not connect to revenue on its own. What it does buy you is a defensible measurement you control, at a cost that rounds to nothing, so the conversation with your agency starts from shared numbers instead of their slide deck.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/tracking-search-rankings-and-ai-overviews-without-an-agency/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
      <category>seo</category>
      <category>aisearch</category>
    </item>
    <item>
      <title>Document hygiene at scale: finding stale, duplicated and over-shared files</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Mon, 31 Aug 2026 01:05:58 +0000</pubDate>
      <link>https://dev.to/gatapia/document-hygiene-at-scale-finding-stale-duplicated-and-over-shared-files-533o</link>
      <guid>https://dev.to/gatapia/document-hygiene-at-scale-finding-stale-duplicated-and-over-shared-files-533o</guid>
      <description>&lt;p&gt;Most Microsoft 365 tenants we are asked to look at have the same shape. A few thousand active documents that people use every week, and somewhere behind them a much larger pile that nobody has opened since the project closed. Old tender responses. Three versions of the same policy in three team sites. A OneDrive belonging to someone who left in 2022, still holding a CSV export of the customer database.&lt;/p&gt;

&lt;p&gt;None of that is a crisis on any given day. It becomes one when a mailbox is compromised and you have to work out what the attacker could reach, or when someone asks you to prove that personal information has actually been deleted rather than merely forgotten. The honest answer in most organisations is that nobody knows what is in there.&lt;/p&gt;

&lt;p&gt;This post is about how we build a scheduled audit that answers the question, and how we roll it out without deleting something a court later asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three signals worth checking on a schedule
&lt;/h2&gt;

&lt;p&gt;Rather than trying to classify every file, we look for three patterns that reliably indicate a problem.&lt;/p&gt;

&lt;p&gt;The first is subtree staleness. A single old file means nothing. A folder where nothing at any depth has been modified or opened in eighteen months, with no retention label and no legal hold, is a different animal. It is usually a finished project, and the whole subtree can be dealt with as one decision instead of four hundred.&lt;/p&gt;

&lt;p&gt;The second is sharing links that outlived their purpose. Anonymous links created for a tender that closed two years ago. Guest accounts from a consultancy engagement that ended. Links with edit rights where view rights would have done. These are cheap to find because the permission graph is right there in the API, and they are the findings that most often make an IT manager sit up.&lt;/p&gt;

&lt;p&gt;The third is personal-drive data that should not exist. System exports, copies of finance workbooks, HR documents in an individual's OneDrive rather than the site that governs them. This is the least popular part of the audit and usually the most valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The system is deliberately boring. Most of the work is enumeration and diffing, and only a thin slice of it needs a model.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A scheduled job walks every site and drive through the Microsoft Graph, using delta queries so subsequent runs only pull what changed. Metadata only at this stage: path, size, owner, timestamps, sensitivity and retention labels, version count, permissions and sharing links.&lt;/li&gt;
&lt;li&gt;Each run is written as a snapshot. An embedded store with JSON support is enough for this. SQLite will accept the raw Graph JSON and let you &lt;a href="https://dgl.cx/2020/06/sqlite-json-support" rel="noopener noreferrer"&gt;index fields out of it with generated columns&lt;/a&gt;, so you can add a new attribute to the audit without rebuilding the schema.&lt;/li&gt;
&lt;li&gt;Deterministic rules run over the snapshot first, because they are free. Exact duplicates by content hash, stale subtrees, links with no expiry, external sharing on sites marked internal, orphaned drives.&lt;/li&gt;
&lt;li&gt;Near-duplicate detection uses embeddings rather than hashes. Two copies of the same contract that differ by a filename and a date field will never match on a hash, but they sit next to each other in vector space. That is the same property behind dense retrieval work such as &lt;a href="https://arxiv.org/abs/2212.10496" rel="noopener noreferrer"&gt;HyDE&lt;/a&gt;: documents get compared by meaning, not by bytes.&lt;/li&gt;
&lt;li&gt;Content classification runs last and only over the ambiguous remainder. Files that need parsing before anything can be said about them go through a document parsing model, then a classifier that answers narrow questions: does this contain personal information, is it a record or a working copy, which business function does it belong to.&lt;/li&gt;
&lt;li&gt;Everything ends in a per-owner report and a proposed-actions table, which is where the rollout begins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Diffing snapshots is the part people underestimate. A single run tells you the tenant is messy. Two runs a month apart tell you which parts of the mess are growing, and that is what gets budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- folders where nothing in the subtree has changed in 18 months&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;folder_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_touched&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;retention_label&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;legal_hold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;folder_path&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-18 months'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where the model earns its place, and where it does not
&lt;/h2&gt;

&lt;p&gt;Most findings come out of rules and joins. The model is worth paying for on the slice where a human would otherwise have to open the file: is this scanned PDF an executed contract or a draft, does this spreadsheet hold customer records, are these two documents the same thing.&lt;/p&gt;

&lt;p&gt;Cost is manageable if you keep that slice small. Cohere prices its Parse model at &lt;a href="https://cohere.com/blog/parse" rel="noopener noreferrer"&gt;USD $1.50 per 1,000 pages&lt;/a&gt; through its API, which is a useful anchor for what tenant-scale parsing costs; vendors are now pitching document intelligence at whole organisational corpora rather than single files. At that order of magnitude, parsing a hundred thousand pages is a few hundred dollars, and re-parsing them every night is a waste. Parse once, cache by content hash, and only reprocess when the hash changes.&lt;/p&gt;

&lt;p&gt;Where the model does not belong is the delete decision. Classification is an input to a rule, not a substitute for one. If the pipeline cannot explain a proposed action in terms a records officer can read, it does not get to propose it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout: audit, dry run, then enforce
&lt;/h2&gt;

&lt;p&gt;We have never had a good outcome from turning deletion on early. The sequence that works:&lt;/p&gt;

&lt;p&gt;Audit only, for at least a month. The system reports and does nothing else. This is where you discover that the "stale" site is the one Finance opens each June, and that half the external links belong to an auditor who is still engaged.&lt;/p&gt;

&lt;p&gt;Dry-run deletes, for several weeks. Every action the system would have taken is written to a ledger with its rule, its evidence and its owner. Owners get a weekly digest of what would have happened to their content. The rules get tuned against real objections rather than imagined ones. This phase is deliberately long, because seasonal work is invisible in a two-week window.&lt;/p&gt;

&lt;p&gt;Enforcement, scoped narrowly and reversibly at first. Start with the actions nobody defends: expiring sharing links, removing guest access for closed engagements, archiving stale subtrees to cool storage rather than deleting them. Keep the recycle bin window generous and keep the ledger. Actual destruction should come last, run against retention labels, and generate a record of what was destroyed and under which rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and retention, in Australian terms
&lt;/h2&gt;

&lt;p&gt;Two things matter here for Australian organisations.&lt;/p&gt;

&lt;p&gt;The first is that the audit is the evidence. Retention policies are easy to write and hard to prove. A dated snapshot, a rule, a dry-run ledger and a destruction record together demonstrate that the policy is operating, which is a far better position than an assertion. If your organisation holds personal information it no longer needs for a permitted purpose, the audit is what tells you where it is, and the ledger is what shows you dealt with it. Take the specifics of retention periods from your privacy officer or legal counsel, not from the pipeline.&lt;/p&gt;

&lt;p&gt;The second is that the audit is itself a privacy surface. Paths and filenames leak content, sometimes including names and case identifiers. The index is a concentrated map of everywhere sensitive material lives, so it needs the same controls as the data it describes: Australian region, tenant-controlled keys, restricted access, its own retention period. If you send content to a hosted model for classification, know which jurisdiction it lands in and what the vendor retains. Private or on-premise deployment is a real option for regulated workloads.&lt;/p&gt;

&lt;p&gt;For health, legal and financial records, keep the pipeline administrative. Inventory, duplication, sharing exposure and retention status are safe ground. Any action touching clinical or case records goes to a named human for sign-off before it executes, and that sign-off is designed into the workflow, not bolted on as a warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations worth stating up front
&lt;/h2&gt;

&lt;p&gt;Access timestamps in Microsoft 365 are less reliable than they look, so treat staleness as a prompt for a human, not proof of abandonment. Near-duplicate detection has false positives on templated documents, where twenty tender responses are legitimately ninety per cent identical. Legal holds and matter-based retention must be resolved before any rule runs, because a wrong deletion there is far more expensive than a thousand stale files. And no audit fixes the behaviour that created the mess. Expiring sharing links by default and giving teams somewhere sensible to put finished work does more for hygiene over a year than any cleanup run.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-business-operations-the-series/" rel="noopener noreferrer"&gt;Practical AI in Business Operations&lt;/a&gt; series, which covers the unglamorous internal systems where this kind of work tends to pay off first. A document hygiene audit is a good first project precisely because it is measurable: you can count the links you closed and the gigabytes you retired.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/document-hygiene-at-scale-finding-stale-duplicated-and-over-shared-files/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>datagovernance</category>
      <category>sharepoint</category>
      <category>onedrive</category>
    </item>
    <item>
      <title>Supply-chain attacks: package cooldowns and policy checks for npm, PyPI and NuGet</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:50:59 +0000</pubDate>
      <link>https://dev.to/gatapia/supply-chain-attacks-package-cooldowns-and-policy-checks-for-npm-pypi-and-nuget-40p3</link>
      <guid>https://dev.to/gatapia/supply-chain-attacks-package-cooldowns-and-policy-checks-for-npm-pypi-and-nuget-40p3</guid>
      <description>&lt;p&gt;On 4 August 2026, a self-propagating worm called ChainDrop tore through npm. &lt;a href="https://www.bleepingcomputer.com/news/security/massive-chaindrop-npm-supply-chain-attack-infects-hundreds-of-packages/" rel="noopener noreferrer"&gt;BleepingComputer reported&lt;/a&gt; more than 1,300 compromised packages with about two billion monthly downloads between them, including keyv, cacheable, flat-cache and file-entry-cache. The poisoned releases carried a &lt;code&gt;"preinstall": "node setup.mjs"&lt;/code&gt; entry, so anyone who ran &lt;code&gt;npm install&lt;/code&gt; against an affected version executed the dropper before the install even finished. It collected environment variables, GitHub tokens, npm tokens, AWS credentials, Kubernetes secrets, Vault tokens and database credentials from developer machines and CI runners.&lt;/p&gt;

&lt;p&gt;The detail that matters for how you defend against this: the attacker got in through &lt;a href="https://www.reddit.com/r/pwnhub/comments/1vffibe/massive_npm_supplychain_attack_chaindrop/" rel="noopener noreferrer"&gt;a legitimate maintainer's compromised GitHub account&lt;/a&gt;, not a hole in npm or GitHub. The packages were built and published through their real GitHub Actions workflows, so the malicious releases carried valid provenance. Reputation checks, provenance attestation and "do we trust this maintainer" reviews all passed.&lt;/p&gt;

&lt;p&gt;So "is this package reputable?" is the wrong question. The question is whether this specific version is safe, and nobody can answer that at install time. Wiz was tracking the &lt;a href="https://www.reddit.com/r/SecOpsDaily/comments/1vf8im1/popular_npm_packages_in_the_keyv_and_cacheable/" rel="noopener noreferrer"&gt;keyv and cacheable compromise&lt;/a&gt; as an in-progress campaign, which means every organisation that installed during the window was exposed before any advisory existed to warn them.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-cyber-security-the-series/" rel="noopener noreferrer"&gt;Practical AI in Cyber Security&lt;/a&gt; series, and it covers the least glamorous control in it. There is no model here. Just two mechanical checks we run across every repository we touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control one: the cooldown window
&lt;/h2&gt;

&lt;p&gt;Never install a package version younger than N days. We use 14 days as the default.&lt;/p&gt;

&lt;p&gt;The logic is boring and that is the point. Nearly all of these compromises get detected and the malicious versions yanked within days of publication. The attacker's whole business model is speed: publish, get pulled into as many downstream lockfiles as possible, harvest credentials before anyone notices. A time delay between publication and installation closes exactly the window the attack depends on. You do not need to detect anything. You just need to arrive late.&lt;/p&gt;

&lt;p&gt;Fourteen days is not a number we invented. Security architects at a multi-national audit firm, after handling two supply-chain attacks themselves, gave &lt;a href="https://github.com/equationalapplications/curated-thoughts/pull/103" rel="noopener noreferrer"&gt;guidance of exactly two options&lt;/a&gt;: pin your versions, or only accept dependencies older than two weeks. We run both together.&lt;/p&gt;

&lt;p&gt;GitHub has moved in the same direction. Dependabot version updates now &lt;a href="https://github.blog/security/supply-chain-security/disrupting-supply-chain-attacks-on-npm-and-github-actions/" rel="noopener noreferrer"&gt;wait three days by default&lt;/a&gt; before opening a pull request, and security updates stay exempt so real fixes are not delayed. Three days is better than nothing. We think two weeks is a better fit for business systems, where a fortnight's delay on a minor version bump costs nothing.&lt;/p&gt;

&lt;p&gt;How you implement it depends on the ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm/pnpm: &lt;code&gt;minimumReleaseAge&lt;/code&gt; in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; (20160 minutes for 14 days), plus &lt;code&gt;cooldown.default-days: 14&lt;/code&gt; in &lt;code&gt;.github/dependabot.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: enforced at the resolver or proxy layer, since pip has no native age gate. If you run an internal index, the age check belongs there&lt;/li&gt;
&lt;li&gt;NuGet: same pattern, enforced through your feed rather than the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things about the cooldown are easy to get wrong. First, exclude your own first-party packages, otherwise you cannot ship your own code for a fortnight. Second, the gate governs lockfile regeneration, not installs from an existing lockfile. CI running &lt;code&gt;--frozen-lockfile&lt;/code&gt; is unaffected. That is correct behaviour: the regen moment is the attack surface, because that is when a floating range silently resolves to something new.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control two: policy checks on every manifest
&lt;/h2&gt;

&lt;p&gt;The cooldown handles timing. Policy checks handle everything else. We look for four things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lockfiles are required and committed.&lt;/strong&gt; No lockfile means every build resolves fresh, which means every build is a roll of the dice against whatever was published in the last hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install scripts are disabled.&lt;/strong&gt; ChainDrop ran through &lt;code&gt;preinstall&lt;/code&gt;. Attackers use install-time scripts because they fire immediately, before the package's code is ever called at runtime. npm v12 &lt;a href="https://github.blog/security/supply-chain-security/disrupting-supply-chain-attacks-on-npm-and-github-actions/" rel="noopener noreferrer"&gt;disables install scripts by default&lt;/a&gt; with an approval list for the packages that legitimately need them, and disables git and remote-URL dependencies too. Do not wait for the version bump to force it. Set it now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Floating versions are flagged.&lt;/strong&gt; A &lt;code&gt;^&lt;/code&gt; or &lt;code&gt;~&lt;/code&gt; range is a standing authorisation for code that does not exist yet and nobody has reviewed. The public pull request implementing this guidance is instructive: the project had &lt;a href="https://github.com/equationalapplications/curated-thoughts/pull/103" rel="noopener noreferrer"&gt;37 of 39 direct dependencies on flexible specifiers&lt;/a&gt;, with a frozen lockfile in CI as the only protection. Pinning direct dependencies to exact versions pushes every future bump into a reviewable &lt;code&gt;package.json&lt;/code&gt; diff instead of burying it in a lockfile where a poisoned transitive bump is easy to skim past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source mapping.&lt;/strong&gt; Every dependency maps to a declared upstream source. When an advisory lands, the first question is always "where do we have this, and in which versions?" If answering takes three days of grep, you have already lost the response window.&lt;/p&gt;

&lt;p&gt;One extension worth noting: a Python project template hardened in August 2026 added dependency scanning on the principle of &lt;a href="https://github.com/jakob1379/postmodern-python/pull/39" rel="noopener noreferrer"&gt;treating your scanners as untrusted software&lt;/a&gt;. The tools you install to inspect dependencies are themselves dependencies. Same cooldown, same pins, same install-script rules. Your scanner does not get an exemption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is monitored, not audited
&lt;/h2&gt;

&lt;p&gt;Two campaigns of this scale landed inside a single week in August 2026. An annual dependency audit misses both. A quarterly one probably misses both. The exposure window in the keyv/cacheable case was measured in hours.&lt;/p&gt;

&lt;p&gt;So we run recursive discovery across every repository in an organisation: walk the tree, find every &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;poetry.lock&lt;/code&gt;, &lt;code&gt;*.csproj&lt;/code&gt; and &lt;code&gt;packages.lock.json&lt;/code&gt;, including the ones inside sample directories, archived services and that internal tool one person maintains. Then apply the policy checks to all of them on a schedule and report the drift.&lt;/p&gt;

&lt;p&gt;The unglamorous finding is almost always the same. Organisations know their main product repositories reasonably well. They do not know about the twelve other repositories with manifests in them, and the abandoned build pipeline whose runner still holds a valid deployment credential is a better target than the flagship app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;A cooldown does not help against a slow attacker. If a malicious version sits undetected for three weeks, 14 days buys you nothing. It also does not protect you against a compromise you install on day 15 because no advisory ever appeared. These controls raise the cost and shrink the window. They do not close it.&lt;/p&gt;

&lt;p&gt;Pinning has a real cost too: you now own your upgrade cadence. Pinned dependencies do not update themselves, and a project that pins without a maintenance rhythm ends up on ancient, genuinely vulnerable versions, which is a worse position than where it started. Budget for the upgrade work.&lt;/p&gt;

&lt;p&gt;There is a per-upgrade review practice emerging as a complement, &lt;a href="https://blog.fidelramos.net/software/emacs-straight-ai-review" rel="noopener noreferrer"&gt;reading the actual diff of a package upgrade&lt;/a&gt; before accepting it, which is where AI assistance is starting to be useful in ecosystems without strong registry-side attestation. It is early, and it does not replace the cooldown.&lt;/p&gt;

&lt;p&gt;And if you ran an affected install, none of this is remediation. Treat the workstation or CI runner as compromised even if the package was removed: rebuild, rotate every token reachable from that environment, and check logs and repositories for unexpected access and commits.&lt;/p&gt;

&lt;p&gt;The rest is cheap. Two config settings and a scheduled job across your repositories will cost you a day of engineering time. Both of the August incidents would have been non-events for anyone running them.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/supply-chain-attacks-package-cooldowns-and-policy-checks-for-npm-pypi-and-nuget/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>supplychainsecurity</category>
      <category>npm</category>
    </item>
    <item>
      <title>An AI blog pipeline with a human editor: how this post was made</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Wed, 26 Aug 2026 02:00:47 +0000</pubDate>
      <link>https://dev.to/gatapia/an-ai-blog-pipeline-with-a-human-editor-how-this-post-was-made-5bmf</link>
      <guid>https://dev.to/gatapia/an-ai-blog-pipeline-with-a-human-editor-how-this-post-was-made-5bmf</guid>
      <description>&lt;p&gt;This post was drafted by a machine. A person read it, changed things, and merged it before you saw it. Since we spend this series describing how other organisations should apply AI to marketing work, it seemed reasonable to open up the pipeline that produces the series itself. This is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-marketing-the-series/" rel="noopener noreferrer"&gt;Practical AI in Marketing&lt;/a&gt; series, and it is the only post in it about us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backlog is a JSON file
&lt;/h2&gt;

&lt;p&gt;There is no content calendar in a spreadsheet and no monthly planning meeting. Topics live in a JSON file in the same repository as the site, and each entry carries enough context for an agent to write from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"an-ai-blog-pipeline-with-a-human-editor-how-this-post-was-made"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"series"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"practical-ai-in-marketing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"An AI blog pipeline with a human editor: how this post was made"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transparency meta-post. Backlog JSON, weekly research, humanised draft on a review branch, LinkedIn copy alongside, human edits and merges. Human gate is the point. Costs per post vs agency."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drafted"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notes field does most of the work. It is written by a person who knows what we actually do, which is why the pipeline can produce a post about our own process without me having to write the process down twice. Anyone at PicNet can add an entry. Adding one takes about two minutes, which matters more than it sounds: the old bottleneck was not writing, it was that good topic ideas evaporated between the conversation where they came up and the next time anyone opened the blog folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs each week
&lt;/h2&gt;

&lt;p&gt;The weekly job is deliberately boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick the next backlog item that is ready.&lt;/li&gt;
&lt;li&gt;Run web searches on the topic, scrape the promising results, and write everything into a research file next to the draft. Sources, notes, dates, full text where we can get it.&lt;/li&gt;
&lt;li&gt;Draft the post against a system prompt holding the house rules: word count, Australian English, byline, what may and may not be claimed, which client names are cleared for public use and which are not.&lt;/li&gt;
&lt;li&gt;Run a humanising pass that strips the stock AI phrasing.&lt;/li&gt;
&lt;li&gt;Draft the LinkedIn copy from the same research, in the same run, so the social post and the article do not drift apart.&lt;/li&gt;
&lt;li&gt;Commit the draft, the research file and the social copy to a review branch and open it for review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing in that list touches the live site. The last step is a person reading the branch and merging it, or not.&lt;/p&gt;

&lt;p&gt;Putting content through git rather than a CMS was the single best decision in the build. Every post has a diff, a branch and a merge, and every claim in it can be traced back to the research file sitting beside it in the same commit. Teams are already reworking their software lifecycle around agent output (&lt;a href="https://news.ycombinator.com/item?id=49275494" rel="noopener noreferrer"&gt;Ask HN, August 2026&lt;/a&gt;), and the review discipline transfers to a content repository without modification. The editor is doing a code review on prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the human gate is the point
&lt;/h2&gt;

&lt;p&gt;The clearest statement of the problem I have read comes from someone who built a marketing AI system for a 60-person team: "generation is free now... that sounds like good news, and for about three weeks it was." Their conclusion after the build was that the most valuable layer in the system is &lt;a href="https://www.reddit.com/r/Maestrix/comments/1vm6a7i/i_built_a_marketing_ai_operating_system_for_a/" rel="noopener noreferrer"&gt;the one that refuses to write&lt;/a&gt;. Once anyone can produce a competent blog post in ninety seconds, the constraint moves to the part of the process nobody had staffed, which is review and editorial judgement.&lt;/p&gt;

&lt;p&gt;Engineering hit the same wall first. Sylvain Kalache's framing, &lt;a href="https://www.sylvainkalache.com/blog/ai-writes-the-code-but-humans-cant-review-it-all" rel="noopener noreferrer"&gt;AI writes the code, but humans can't review it all&lt;/a&gt;, describes teams generating ten times faster while reviewing at human speed, and cites survey data showing high-adoption teams merging 98% more pull requests while review time rose 91%. Content has the same shape and worse consequences, because a bad merge in a repository fails in staging while a bad merge on a public blog is read by your clients.&lt;/p&gt;

&lt;p&gt;So we bounded the volume rather than the generation. The pipeline is capped at what one editor can genuinely read in a week, and the editor's job is not to tidy sentences. It is to answer three questions: is every statistic in here actually in the research file, is every client reference one we are allowed to make, and would I say this out loud to a client. Posts that fail the third question get deleted rather than fixed. That happens more often than the volume-obsessed version of this pipeline would tolerate, which is the point.&lt;/p&gt;

&lt;p&gt;The prompt also carries a list of things the model must never write: client names that are not cleared for publication, any claim we cannot source, any statistic without a link. The editor checks the list on every branch. Prompts are guidance, not controls, and treating them as controls is how organisations end up apologising publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Labelling what the machine wrote
&lt;/h2&gt;

&lt;p&gt;Beau Cronin's &lt;a href="https://beaucronin.com/tail/" rel="noopener noreferrer"&gt;TAIL scale&lt;/a&gt;, published on 18 August 2026, gives a 0 to 5 rating for how much of a piece AI wrote. TAIL 4 is "AI substantially authored, with my comprehension, review, and editing", and Cronin describes it as the last level that is meaningfully human in the loop. That is where this post sits, and where the series sits. It is a more honest label than a generic "AI-assisted" footer, because it tells you exactly which half of the work a person did.&lt;/p&gt;

&lt;p&gt;Publishing the mechanics is becoming its own small genre, including &lt;a href="https://treefortsystems.com/blog/is-this-blog-post-ai-generated/" rel="noopener noreferrer"&gt;technical blogs disclosing their own generation process&lt;/a&gt;. We would rather be in that group than pretend the drafts are typed by hand.&lt;/p&gt;

&lt;p&gt;One thing the humanising step is not for: beating detectors. AI writing detectors such as Pangram are now deployed everywhere and, as the Washington Post reported on 25 August 2026, &lt;a href="https://www.washingtonpost.com/technology/interactive/2026/08/25/ai-detectors-like-pangram-are-everywhere-arent-always-accurate/" rel="noopener noreferrer"&gt;they aren't always accurate&lt;/a&gt;. Their reporting notes a papal encyclical was flagged as partly AI-generated. A detector score is not a quality signal, and optimising a draft to pass one produces text that is neither honest nor better. We run the humanising pass because unedited model prose is full of sales phrasing and stock triads that no engineer would say in a meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Per post, the machine spend is search API calls plus model tokens, and it rounds to nothing against an hour of senior time. The real cost is the editor: roughly an hour per post to read the draft against the research file, cut what cannot be supported, and fix the parts where the model wrote something plausible and wrong.&lt;/p&gt;

&lt;p&gt;Compare that with an agency retainer, where you pay for both the writing and the research, and where the person writing about your systems has never seen them. The pipeline flips the ratio. We pay almost nothing for the drafting and spend the money on the judgement, which is the part an agency cannot do for us anyway because it depends on knowing what we have actually built.&lt;/p&gt;

&lt;p&gt;Volume is not the win, and it is worth being sceptical of pipelines sold on volume. A &lt;a href="https://www.reddit.com/r/AIGrowthHacks/comments/1vho2we/i_used_ai_content_platforms_for_12_months_my/" rel="noopener noreferrer"&gt;twelve-month practitioner review of AI content platforms&lt;/a&gt; starts from a B2B SaaS blog with 12 published articles and basically zero traffic, which is a fair reminder that publishing more of nothing produces more nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;The pipeline cannot interview anyone. It cannot visit a client site, sit through a workshop, or notice the thing a client said offhand that turns into a good post. Everything original in this series comes from a person putting it in the backlog notes first, and the quality of a post is capped by the quality of those notes.&lt;/p&gt;

&lt;p&gt;It also gets things wrong in a specific way: it will produce a confident sentence built on a source that does not quite say that. That is exactly what the research file and the branch review exist to catch, and it is why the editor's hour is not negotiable.&lt;/p&gt;

&lt;p&gt;If you want to build something similar, start with the review step. Decide who reads every piece, how long they get, and what they are allowed to reject. Then automate backwards from there. The generation was never the hard part.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/an-ai-blog-pipeline-with-a-human-editor-how-this-post-was-made/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
      <category>aicontent</category>
      <category>contentmarketing</category>
    </item>
    <item>
      <title>The robot that chases people: automated reminders, stale quotes and follow-ups</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:51:03 +0000</pubDate>
      <link>https://dev.to/gatapia/the-robot-that-chases-people-automated-reminders-stale-quotes-and-follow-ups-3d79</link>
      <guid>https://dev.to/gatapia/the-robot-that-chases-people-automated-reminders-stale-quotes-and-follow-ups-3d79</guid>
      <description>&lt;p&gt;Most operations teams have a version of the same list. Quotes sent three weeks ago with no answer. A job waiting on a client document since before the last public holiday. A renewal that needed a conversation in June and is now awkward. Nobody decided to drop these. They fell through a crack, and nobody was paid to watch the cracks.&lt;/p&gt;

&lt;p&gt;The usual fix is a person: an ops coordinator with a spreadsheet and a Monday morning ritual. That works until they take leave. The better fix is a scheduled task with an opinionated data model behind it, plus a language model doing the one part that genuinely needs judgment, which is writing a chase message that does not read like it came from a machine.&lt;/p&gt;

&lt;p&gt;This is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-business-operations-the-series/" rel="noopener noreferrer"&gt;Practical AI in Business Operations&lt;/a&gt; series, and it is deliberately the least glamorous entry. There is very little AI in it. That is the point: the value comes from the scheduler, and the model earns its place at the last step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table is most of the trick
&lt;/h2&gt;

&lt;p&gt;A working reminder system is smaller than people expect. An open design discussion on a tender tracking project sketches &lt;a href="https://github.com/Mikepeerawit-com/tender-tracker/issues/9" rel="noopener noreferrer"&gt;a &lt;code&gt;reminders&lt;/code&gt; table keyed on the record id plus &lt;code&gt;days_before&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt; and &lt;code&gt;sent_at&lt;/code&gt;, swept by a daily cron&lt;/a&gt;. The state columns are what stop the same nag firing twice. That is the whole mechanism.&lt;/p&gt;

&lt;p&gt;Ours look roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;reminders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;                &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_type&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- quote | job | invoice | renewal&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;         &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;milestone&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- first_follow_up | stale_quote | decision_chase&lt;/span&gt;
  &lt;span class="n"&gt;days_before&lt;/span&gt;       &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;-- exactly one of these two is set&lt;/span&gt;
  &lt;span class="n"&gt;remind_on&lt;/span&gt;         &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;due_date&lt;/span&gt;          &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- computed; the only thing the sweep looks at&lt;/span&gt;
  &lt;span class="n"&gt;assignee_id&lt;/span&gt;       &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;snoozed_until&lt;/span&gt;     &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;suppressed_reason&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;-- an honest reason to stop chasing&lt;/span&gt;
  &lt;span class="n"&gt;sent&lt;/span&gt;              &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sent_at&lt;/span&gt;           &lt;span class="n"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sweep is one query, run daily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reminders&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;current_date&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sent&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snoozed_until&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;snoozed_until&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;current_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;suppressed_reason&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;due_date &amp;lt;= current_date&lt;/code&gt;, not &lt;code&gt;= current_date&lt;/code&gt;. Exact date equality means one bad deploy day silently drops every reminder that fell on it. The same ticket works through that and two related traps, and lands on catch-up rather than skip, with reminders whose milestone has already passed suppressed in favour of a louder "this was missed" state. It also resets &lt;code&gt;sent&lt;/code&gt; when a deadline moves: a write-once boolean means pushing a due date back permanently silences the record.&lt;/p&gt;

&lt;p&gt;Timezones matter more here than they look. An organisation with staff in Perth and Sydney has a two or three hour window where "today" is two different dates. Pick an org-level timezone, compute &lt;code&gt;due_date&lt;/code&gt; in it, and stop treating server time as the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding when it fires is the same decision as deciding how someone hears about it
&lt;/h2&gt;

&lt;p&gt;The tender ticket treats notification delivery and the reminder model as &lt;a href="https://github.com/Mikepeerawit-com/tender-tracker/issues/9" rel="noopener noreferrer"&gt;one design question, not two&lt;/a&gt;, and that is right. A reminder that broadcasts to a shared channel has different semantics from one that reaches an individual. If your only channel posts to a group, then every reminder is public, and that constrains the content: no pricing, no margin, and a hard look at whether client names belong in a channel that may include contractors. Under the Australian Privacy Principles, "who can see this notification" is a design input, not a compliance review item at the end.&lt;/p&gt;

&lt;p&gt;Targeting also decides whether the system survives. Reminders that ping people who have already done the work train the whole team to mute the robot within weeks. Chase the assignees with nothing recorded against the milestone, not everyone attached to the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalation is a schedule, not a mood
&lt;/h2&gt;

&lt;p&gt;Escalation should be boring and configurable. For a client submission deadline, defaults of seven days, three days, one day and the morning of the due date work well. Two rules make it useful rather than noisy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A missed item does not drop off the list. It stays on the dashboard in a loud state until someone resolves it, and it posts once to the team channel when it is missed.&lt;/li&gt;
&lt;li&gt;Escalation changes audience, not just frequency. The third reminder goes to the assignee's manager. If your escalation only shouts louder at the same person, it is not escalation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For stale quotes, the trigger is different but the shape is the same: days since sent with no client response, days until the quote's validity expires, and a hard stop at 30 June for anything priced against a financial year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snooze, and the re-arm problem
&lt;/h2&gt;

&lt;p&gt;Snooze is where these systems go wrong, and the failure is not the trigger. It is when the system decides to re-arm after a human acts.&lt;/p&gt;

&lt;p&gt;There is a neat illustration of this from an unrelated field. A robot's heading-hold controller was counter-steering after a manual turn: the operator would turn, recentre, and the robot would swing back the other way. The hold released during the turn and &lt;a href="https://github.com/innate-inc/innate-os/pull/644" rel="noopener noreferrer"&gt;re-latched at the wrong point, against a target the machine had already rotated past&lt;/a&gt;. The fix was to shorten the hold's memory so the target caught up with reality faster.&lt;/p&gt;

&lt;p&gt;Reminders have the identical bug. Someone rings the client on Tuesday and snoozes for a week. On Wednesday the client replies and the quote status changes. If the snooze re-arms against the old &lt;code&gt;due_date&lt;/code&gt;, the reminder fires next Tuesday about a conversation that is already finished, and the person who did the right thing gets nagged for it. Recompute &lt;code&gt;due_date&lt;/code&gt; on every state change to the underlying record, and let the snooze expire against the recomputed value. The rule is simple: the reminder chases the current state of the record, never the state it had when the row was written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give people an honest way to stop the nag
&lt;/h2&gt;

&lt;p&gt;The most valuable column in that table is &lt;code&gt;suppressed_reason&lt;/code&gt;. Without it, "hasn't got to it" and "this cannot be actioned" look identical to the scheduler, and it chases people forever for work that is impossible. The tender ticket adds an explicit "no supplier found" record for exactly this reason. Ours are things like "client asked us to hold until the new financial year" or "waiting on their legal, no date". Each one silences the reminder and leaves a written trace of why, which is far more useful in a Monday meeting than a snoozed row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model actually goes
&lt;/h2&gt;

&lt;p&gt;Two places, both narrow.&lt;/p&gt;

&lt;p&gt;The first is classification. A rule that decides whether a quote is stale by looking at a status field alone will get it wrong, because status fields lag reality. Reading the actual email thread and the file notes is a better signal, and that is a job a model does well. One caution: read the whole record, not a sample. A scheduled job in another project fetched a partial version of its source text for years, and the correction was explicit, to &lt;a href="https://github.com/tamnd/kvant-solver/pull/58" rel="noopener noreferrer"&gt;pull all of the text rather than a sample of it&lt;/a&gt;. Reminder rules that peek at only the last two emails will confidently call a dead quote fresh.&lt;/p&gt;

&lt;p&gt;The second is drafting the chase message. The model gets the client name, what was sent, when, what has happened since, and two or three examples of how that account manager actually writes. It produces a draft. A person reads it, changes a line, and sends it from their own mailbox.&lt;/p&gt;

&lt;p&gt;We do not auto-send these, for two reasons. Clients can tell, and an unsupervised bot emailing your customers about commercial matters is a Spam Act 2003 problem waiting to happen rather than a productivity gain. The point of the automation was never to write the email. It was to make sure someone was asked to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design every state before you build
&lt;/h2&gt;

&lt;p&gt;Alert preferences deserve real screens, not a config file. One mobile build we looked at gives alerts &lt;a href="https://github.com/lucasturcuato-afk/breakingalpha/pull/624" rel="noopener noreferrer"&gt;a dedicated &lt;code&gt;/settings/alerts&lt;/code&gt; route alongside profile and saved items, and enumerates every screen at every lifecycle state&lt;/a&gt; for review. Copy that discipline. Pending, due, sent, snoozed, escalated, missed, suppressed and dismissed are eight states somebody has to design, and the ones teams skip are always suppressed and missed, which are the two that decide whether people trust the thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs and honest limits
&lt;/h2&gt;

&lt;p&gt;The model calls are the cheapest part. A few hundred short drafts a month is a rounding error next to the engineering time, and most of that time goes into failure semantics rather than prompts. Budget for a small build with a fortnight of tuning the thresholds afterwards, because the first set of defaults will be wrong.&lt;/p&gt;

&lt;p&gt;The real limit is consumption. Scheduled automation whose output nobody acts on is worse than none, because it costs attention and buys nothing. That same project found an extraction job that had run against roughly twelve articles a year, was compared against a model's reading, &lt;a href="https://github.com/tamnd/kvant-solver/pull/58" rel="noopener noreferrer"&gt;"and then used for nothing else"&lt;/a&gt;. The author's verdict was blunt: that is backwards. Before you build the chaser, decide who reads its output and what they do next. If the answer is thin, build the daily digest instead and stop there.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/the-robot-that-chases-people-automated-reminders-stale-quotes-and-follow-ups/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>businessoperations</category>
      <category>automation</category>
      <category>workflowautomation</category>
    </item>
    <item>
      <title>Cutting vulnerability noise: using AI and public data to demote CVEs that don't matter</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 21 Aug 2026 00:56:05 +0000</pubDate>
      <link>https://dev.to/gatapia/cutting-vulnerability-noise-using-ai-and-public-data-to-demote-cves-that-dont-matter-38n1</link>
      <guid>https://dev.to/gatapia/cutting-vulnerability-noise-using-ai-and-public-data-to-demote-cves-that-dont-matter-38n1</guid>
      <description>&lt;p&gt;A container image scan finishes and hands back a few hundred findings. A dozen are critical. Most of the rest are base image packages with a CVE ID, a CVSS score, and no patched version anywhere in the world. The team has one afternoon a fortnight for this work. Where do they start?&lt;/p&gt;

&lt;p&gt;Usually they start by scrolling, and that is the whole problem. When a report is 90% unactionable, people stop reading it, and the one finding that did have a fix available gets the same amount of attention as the 200 that did not. Noise is not just annoying. It is the mechanism by which real vulnerabilities get missed.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-cyber-security-the-series/" rel="noopener noreferrer"&gt;Practical AI in Cyber Security&lt;/a&gt; series, and it covers something we run for clients rather than something we think would be nice: cross-referencing scanner output against public vulnerability data so that findings with no available fix get demoted automatically, and findings with a released fix stay loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "no fix available" actually means
&lt;/h2&gt;

&lt;p&gt;Scanners report what a package version is vulnerable to. They are much less consistent about whether anyone has shipped a repaired version.&lt;/p&gt;

&lt;p&gt;The distinction matters because the two cases need completely different responses. If Debian, Alpine, Ubuntu or Red Hat has published a fixed package, the answer is to bump the version and rebuild, which is an hour of work. If the upstream maintainer has marked the issue as won't fix, disputed, or minor, or the affected code path is not compiled into the distro build at all, there is nothing to install. The only available responses are compensating controls, removing the package, or accepting the risk and writing it down.&lt;/p&gt;

&lt;p&gt;Both arrive in the report looking identical. A CVSS 7.5 with a fix and a CVSS 7.5 without one sit next to each other, sorted by severity, as though they are the same kind of work item.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;The approach is deliberately boring. Deterministic data does the demotion, and AI is confined to the parts that are genuinely unstructured.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normalise scanner output into one record per finding: CVE ID, package name, installed version, ecosystem, image or host, and where it runs.&lt;/li&gt;
&lt;li&gt;Query &lt;a href="https://osv.dev/" rel="noopener noreferrer"&gt;OSV.dev&lt;/a&gt; for each package and version. OSV aggregates advisories across language ecosystems and Linux distributions and returns affected version ranges, including whether a fixed version exists for that specific distro build.&lt;/li&gt;
&lt;li&gt;Apply the rule. A finding with a fixed version in the ecosystem the package actually came from stays at its original priority. A finding with no fixed version anywhere, and no runtime exposure flag, drops to a demoted queue that gets reviewed monthly rather than fortnightly.&lt;/li&gt;
&lt;li&gt;Everything demoted keeps its full record, with the reason, the advisory that justified it, and the timestamp of the data the decision was made on.&lt;/li&gt;
&lt;li&gt;Feed the promoted findings into the ticketing system the team already uses, one ticket per rebuild rather than one per CVE.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For clients with several environments, the fetch and reconciliation runs as a scheduled integration job. We use Centazio, our open source data integration platform, for this sort of thing, because the hard part is not the API call, it is keeping the state consistent across repeated runs and knowing which records are stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI helps, and where we keep it out
&lt;/h2&gt;

&lt;p&gt;The demotion decision itself is not an AI decision. Version comparison is a solved problem and it should stay deterministic, because a language model that occasionally hallucinates a fixed version is worse than no enrichment at all.&lt;/p&gt;

&lt;p&gt;Three things in this pipeline are messy enough that a model earns its place. The first is advisory prose. Distro advisories and upstream issue trackers explain in free text why something is not being fixed, and a model summarising twenty of those into "these six are all the same unfixed glibc locale issue" saves real reading time. The second is clustering: grouping 180 findings into the eight base image rebuilds that would resolve them. The third is drafting the risk acceptance note, which a human then edits and signs.&lt;/p&gt;

&lt;p&gt;Note the pattern. The model reads, groups and drafts. It never decides on its own that something is safe to stop looking at. Practitioners are still asking each other what tooling exists for reviewing AI-assisted work in engineering pipelines, with no settled answer as of &lt;a href="https://news.ycombinator.com/item?id=49321400" rel="noopener noreferrer"&gt;August 2026&lt;/a&gt;, so designing around a verification step rather than trust is the sensible default. It is also worth being explicit with stakeholders about where the model sits, because audiences in 2026 &lt;a href="https://www.reddit.com/r/nerdfighters/comments/1vc37aw/hanks_comment_about_his_ai_use_posted_here_as_its/" rel="noopener noreferrer"&gt;scrutinise disclosed AI use closely&lt;/a&gt;, and a security control nobody trusts does not get used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail noisy, not fail quiet
&lt;/h2&gt;

&lt;p&gt;The dangerous failure mode is obvious once you name it: the enrichment source is unreachable, every lookup returns nothing, "no fixed version found" is indistinguishable from "no answer received", and the whole report quietly demotes itself to zero. The dashboard goes green on the day it should go red.&lt;/p&gt;

&lt;p&gt;So the pipeline distinguishes the two cases at the type level and treats an unavailable source as a check failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;osv&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;advisory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;osv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ecosystem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;EnrichmentUnavailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;   &lt;span class="c1"&gt;# alerts, does not demote
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_CACHE_AGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;EnrichmentStale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixed_versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ORIGINAL&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runtime_exposed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REVIEW&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEMOTED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules follow from that. Findings that could not be enriched keep their original severity and are flagged as unenriched, so a broken run produces a louder report than a healthy one. And the cached advisory data carries a maximum age, because silently triaging today's images against a three week old snapshot is the same bug wearing a disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics for a lean team
&lt;/h2&gt;

&lt;p&gt;Most Australian organisations we work with have two or three people covering infrastructure, patching and everything else. Their real constraint is rebuild cycles, not knowledge. They know the base image is old.&lt;/p&gt;

&lt;p&gt;The value of demotion is that it makes the remaining queue small enough to finish, which is what turns patching into a habit instead of a quarterly panic. The ACSC's &lt;a href="https://www.cyber.gov.au/resources-business-and-government/essential-cyber-security/essential-eight" rel="noopener noreferrer"&gt;Essential Eight&lt;/a&gt; expects patches for applications and operating systems to be applied within defined windows and expects you to demonstrate it. A queue of fifteen fixable findings can be closed and evidenced. A queue of three hundred, mostly unfixable, cannot, and the auditor sees the same wall of red the engineers gave up on.&lt;/p&gt;

&lt;p&gt;Honest limitations. Version matching across distros is fiddly, because backported patches leave the version string looking unfixed when the vulnerability is already gone, so expect a tuning period and some manual overrides. Demoted is not deleted: the monthly review exists because a package with no fix today may get one next Tuesday, and a re-query catches that. Runtime reachability, which would let you demote far more aggressively, needs instrumentation most teams do not have, so we treat exposure as a coarse flag rather than a proof. And the LLM step costs a few dollars a month at typical volumes, which is nothing next to the engineering time to set the pipeline up and keep the version comparison honest. That build effort is the real cost, and it is worth it only if someone is going to work the shortened queue.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. &lt;a href="https://picnet.com.au/ai-services/" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about what a first project could look like.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://picnet.com.au/blog/cutting-vulnerability-noise-using-ai-and-public-data-to-demote-cves-that-don-t-matter/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>vulnerabilitymanagement</category>
      <category>cve</category>
    </item>
  </channel>
</rss>
