<?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>A research assistant grounded in your own precedents and knowledge</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Wed, 30 Sep 2026 04:42:16 +0000</pubDate>
      <link>https://dev.to/gatapia/a-research-assistant-grounded-in-your-own-precedents-and-knowledge-3b6l</link>
      <guid>https://dev.to/gatapia/a-research-assistant-grounded-in-your-own-precedents-and-knowledge-3b6l</guid>
      <description>&lt;p&gt;A senior associate wants to know how the firm usually drafts a limitation of liability clause for a software supply agreement. The answer is in the firm somewhere, either in the precedent bank or in a file note a partner wrote three years ago. A general-purpose chatbot will write a confident clause from its training data. The associate needs the firm's own clause and a link to the document it came from.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-for-law-firms-the-series/" rel="noopener noreferrer"&gt;Practical AI for Law Firms&lt;/a&gt; series. It covers how we would build that assistant. The design uses retrieval-augmented generation (RAG) over the firm's own material. Answers cite the source document, the assistant refuses when the corpus has nothing, and access control works at matter level so information cannot cross an ethical wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What grounding means in practice
&lt;/h2&gt;

&lt;p&gt;A grounded assistant answers only from the documents it retrieved for the current question. Every claim in the answer points to a passage in a specific document, and the reader can open that document and check it.&lt;/p&gt;

&lt;p&gt;The open-source ai-advisory-board project has a clean version of this pattern. A recent change added a &lt;a href="https://github.com/HaroldZhong/ai-advisory-board/pull/113" rel="noopener noreferrer"&gt;Materials sidebar with versioned source citations&lt;/a&gt;, which ties each answer to the exact version of the document it used. Versioning matters for a precedent bank because precedents get revised after a judgment or a change in legislation. A citation to "the indemnity precedent" tells the reader much less than a citation to version 4, approved in March, which they can compare with the current version.&lt;/p&gt;

&lt;p&gt;Refusal is the other half. If retrieval returns nothing relevant, the assistant says so and stops. It doesn't fall back on general knowledge, because that is where invented authorities come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode grounding prevents
&lt;/h2&gt;

&lt;p&gt;Courts in Australia and overseas have dealt with submissions that cited cases that don't exist. General-purpose AI tools produced the citations and nobody checked them before filing. Several Australian courts have since issued practice notes or guidance on using generative AI in proceedings. Any firm planning an assistant should read the one for its jurisdiction before it designs anything.&lt;/p&gt;

&lt;p&gt;These incidents have one cause. Someone asked a model for authority, and it produced text that looked like authority with no document behind it. A grounded design closes that path. The model sees only the retrieved passages. The system attaches citations from retrieval metadata, and a post-check drops any reference that doesn't match a passage in the retrieved set.&lt;/p&gt;

&lt;p&gt;The lawyer still reviews the answer before relying on it. That review is quick, because each citation opens the paragraph it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  An architecture sketch
&lt;/h2&gt;

&lt;p&gt;The components are ordinary. The design work is in deciding where each control sits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingestion: connectors pull from the document management system (DMS), the precedent bank and the knowledge wiki. The connectors are read-only. The same PR adds an &lt;a href="https://github.com/HaroldZhong/ai-advisory-board/pull/113" rel="noopener noreferrer"&gt;optional read-only local MCP reader&lt;/a&gt; that works the same way: the assistant can read documents but has no way to change them.&lt;/li&gt;
&lt;li&gt;Chunking and metadata: each document is split into passages. Every passage carries its document ID, version, practice group, matter number where there is one, and the access groups copied from the DMS.&lt;/li&gt;
&lt;li&gt;Index: a vector index combined with keyword search, hosted in an Australian cloud region. Legal text needs the keyword side because embeddings handle defined terms and clause numbers poorly.&lt;/li&gt;
&lt;li&gt;Retrieval: every query runs under the user's own identity, and the index applies the access filter inside the search call.&lt;/li&gt;
&lt;li&gt;Generation: the model receives the retrieved passages. Its instructions are to answer only from them and to say plainly when they don't cover the question.&lt;/li&gt;
&lt;li&gt;Citation check: a post-processing step confirms that every cited passage ID was in the retrieved set. It removes or flags anything that wasn't.&lt;/li&gt;
&lt;li&gt;Audit log: every query is logged with the user and the passages returned, so the firm can reconstruct any answer later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agentic-graphrag project made a similar choice when it &lt;a href="https://github.com/ontogr/agentic-graphrag/pull/26" rel="noopener noreferrer"&gt;grounded its query generation in the engine's resolved schema&lt;/a&gt; in place of a hardcoded description, with one bounded retry. The same change treats the caller's base scope as an authorisation boundary. Filters on an individual call can narrow that scope but never widen it, and the system refuses requests outside it. Matter access needs exactly that rule.&lt;/p&gt;

&lt;p&gt;In outline, the request path looks like this:&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;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;access_scope_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# DMS groups plus the wall register
&lt;/span&gt;    &lt;span class="n"&gt;passages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;# enforced by the index at query time
&lt;/span&gt;        &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;relevant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MIN_RELEVANCE&lt;/span&gt;&lt;span class="p"&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;relevant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Refusal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No matching precedent or know-how in the firm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s library.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;GROUNDED_ONLY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;cited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;citations&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;passage_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;log_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;citations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ethical walls belong in the retrieval layer
&lt;/h2&gt;

&lt;p&gt;The firm's DMS already records who can open which matter, and the assistant has to inherit those permissions exactly. Some information barriers are recorded outside the DMS, such as a conflicts register entry that walls a team off from a former client's files. The assistant has to honour those as well.&lt;/p&gt;

&lt;p&gt;The filter has to run inside the index query. Suppose walled documents reach the model's context and you try to strip them from the answer afterwards. The model can still paraphrase what it read. When the index applies the filter, walled material is never retrieved, so it has no route into an answer.&lt;/p&gt;

&lt;p&gt;Some practical points from designing this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sync permissions often. A wall put up this morning must apply to this afternoon's queries. Permission changes should reach the index as events, with a scheduled full reconciliation as a backstop.&lt;/li&gt;
&lt;li&gt;Decide carefully what goes into the shared corpus. A precedent that has been cleaned of client detail and approved for firm-wide use belongs there. A raw advice on a matter file stays in that matter's scope, even if it is the best example of the point.&lt;/li&gt;
&lt;li&gt;Keep refusals identical. The user should get the same message whether nothing exists or they lack access, so a refusal doesn't reveal that a walled document exists.&lt;/li&gt;
&lt;li&gt;Test the walls. Write a set of questions that must return nothing for a walled user, and run it on every index rebuild.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Refusal is a written policy
&lt;/h2&gt;

&lt;p&gt;Where you draw the refusal line is a choice, and teams do change it. The argus project, a financial research assistant, recorded a &lt;a href="https://github.com/lagarcess/argus/pull/589" rel="noopener noreferrer"&gt;decision to reverse its earlier refusal&lt;/a&gt; of forward-looking questions. It now answers them as grounded scenarios built from cited inputs. For a law firm we would start stricter and answer only from the corpus. Practice groups can then loosen the rule for particular question types, with each change written down and approved by a named partner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the documents live
&lt;/h2&gt;

&lt;p&gt;Precedents and know-how are among a firm's most sensitive assets. In September a &lt;a href="https://www.reddit.com/r/technology/comments/1waoiai/report_openai_stole_mathematicians_private/" rel="noopener noreferrer"&gt;widely discussed report alleged&lt;/a&gt; that OpenAI took mathematicians' private research from their Codex chats. The allegation is unverified. It still shows why firms are wary of pasting confidential material into hosted AI tools.&lt;/p&gt;

&lt;p&gt;For this assistant we would keep the index and document store in the firm's own cloud tenancy, in an Australian region. The model endpoint should be under enterprise terms that exclude training on inputs, and those terms should be confirmed in writing before any client material is indexed. The firm owes clients the same confidentiality in the index as it does in the DMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Corpus quality sets the ceiling. If the precedent bank holds conflicting versions of a clause with no status marked, the assistant will retrieve all of them. Most of the effort in a first project goes into marking which precedent is current and which are superseded.&lt;/li&gt;
&lt;li&gt;The assistant doesn't research external law. Case law and legislation still come from the firm's subscription services. The assistant reports what the firm has written and done before.&lt;/li&gt;
&lt;li&gt;Retrieval sometimes misses. If a question is phrased differently from the precedent, the search can fail to find it, and the assistant refuses even though the answer exists. Reviewing refusals regularly shows where the corpus or the search needs work.&lt;/li&gt;
&lt;li&gt;Model usage is usually the smaller cost. Ingestion, permission sync and evaluation take most of the effort, and that work continues after launch because the corpus keeps changing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sensible first project covers one practice group's precedent bank under its existing permissions. That group's lawyers write the test questions. The assistant then runs under supervision for a few weeks before anyone else gets access.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. If a research assistant grounded in your own precedents is on your list, &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-research-assistant-grounded-in-your-own-precedents-and-knowledge/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>legal</category>
      <category>retrievalaugmentedgeneration</category>
      <category>legaltechnology</category>
    </item>
    <item>
      <title>Code-first integration with Centazio: why we built an open-source alternative to drag-and-drop iPaaS</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Mon, 28 Sep 2026 04:38:23 +0000</pubDate>
      <link>https://dev.to/gatapia/code-first-integration-with-centazio-why-we-built-an-open-source-alternative-to-drag-and-drop-ipaas-52a3</link>
      <guid>https://dev.to/gatapia/code-first-integration-with-centazio-why-we-built-an-open-source-alternative-to-drag-and-drop-ipaas-52a3</guid>
      <description>&lt;p&gt;Integration projects often go the same way. A team buys an iPaaS subscription, drags a CRM connector onto a canvas and wires it to the finance system. By Friday, records are flowing. Six months later the canvas has forty boxes. A dozen of them hold expressions that only one person understands, and nobody wants to touch any of it before end of month.&lt;/p&gt;

&lt;p&gt;We built Centazio, PicNet's open-source data integration platform, to do integration a different way. In Centazio an integration is plain .NET code, split into small isolated functions, and developers review and unit test it the same way they handle the rest of their code.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/ai-ready-data-and-integration-the-series/" rel="noopener noreferrer"&gt;AI-Ready Data and Integration&lt;/a&gt; series. An AI system that reads your operational data picks up every duplicate and stale record your integrations produce, so AI readiness has to start at the integration layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where drag-and-drop tools fall short
&lt;/h2&gt;

&lt;p&gt;Visual integration tools demo well because a demo only runs the happy path: a record is created in one system and shows up in another. Most of the work in a production integration is the other cases. The target API goes down for an hour. Someone edits a record in both systems between syncs. A vendor renames a field, or a rate limit kicks in halfway through a batch.&lt;/p&gt;

&lt;p&gt;Every one of those cases needs logic. On a canvas, that logic ends up in condition blocks and expression fields spread across the flow, where it is hard to find and harder to change safely. These are the problems we run into most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version control is weak. Flows are usually stored as large generated JSON or XML files, so a diff tells a reviewer very little about what changed.&lt;/li&gt;
&lt;li&gt;Testing is manual. You can usually run a flow against a sandbox. What you can't do is write an automated test that says "this CRM contact becomes this finance contact" and have it run on every change.&lt;/li&gt;
&lt;li&gt;Pricing grows with usage. Licences are often metered per connector or per task run, and many are billed in US dollars, so an Australian budget moves with the exchange rate.&lt;/li&gt;
&lt;li&gt;The logic is locked in. A flow built in one vendor's designer won't open in another vendor's designer, so if you leave, you rebuild.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Centazio structures an integration
&lt;/h2&gt;

&lt;p&gt;Centazio splits every integration into three kinds of step. Each step runs as its own serverless function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A read function for each source system pulls changed records and stages them in that system's own format.&lt;/li&gt;
&lt;li&gt;A promote function takes the staged records, validates them and maps them into a shared core model that your organisation defines.&lt;/li&gt;
&lt;li&gt;A write function for each target system takes core records and pushes them out in the shape that system expects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No system knows about any other. The CRM read function has no idea the finance system exists, and the finance write function only knows the core model. To add a third system, you write its functions against the core model and leave the existing ones alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fault tolerance comes from the structure
&lt;/h2&gt;

&lt;p&gt;Each step reads from storage and writes to storage, so a failure stays in the step where it happened. Say the finance system's API is down. Its write function fails and tries again on the next run. Meanwhile the CRM read and the promote step keep running, so the core model stays current. When the API comes back, the write function processes everything that queued up in the meantime. Nobody has to replay a flow by hand or work out which half of a batch went through.&lt;/p&gt;

&lt;p&gt;The split also helps when a mapping turns out to be wrong. The raw data is still in staging, so once the promote code is fixed you re-promote what was already read. You don't need to pull it from the source system again, which matters when that source has tight API limits.&lt;/p&gt;

&lt;p&gt;Every function is small and does one job, so when a log entry or alert fires, it points to one step for one system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests come with the design
&lt;/h2&gt;

&lt;p&gt;A promote step takes a staged record and returns either a core record or a reason to skip it. That is ordinary code with no network calls, so you unit test it like any other code. The sketch below shows the pattern. The type names are made up for illustration and are not Centazio's exact API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactPromoter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;PromoteResult&lt;/span&gt; &lt;span class="nf"&gt;Promote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CrmContact&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&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;PromoteResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ignore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no email"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Last&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&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;PromoteResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Customer&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Promote_ignores_contacts_without_email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ContactPromoter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Promote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CrmContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Jo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ignored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests like this run in the build pipeline on every change. When someone changes a mapping rule, the pull request shows exactly which line changed and which test covers it. An IT manager can approve that after reading the diff, without sitting through a demo in a sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  What code-first costs
&lt;/h2&gt;

&lt;p&gt;Code-first has real costs, and they should be on the table before anyone picks it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need .NET developers, either in-house or through a partner. A business analyst can't build a new flow in an afternoon.&lt;/li&gt;
&lt;li&gt;You host it and you run it. The functions run in your own cloud tenancy. That makes it easier to keep data in an Australian region when your privacy obligations or client contracts require it, but the monitoring, patching and cloud bills are also yours.&lt;/li&gt;
&lt;li&gt;Connectors are code too. An iPaaS may ship a ready-made connector for a popular SaaS product. With Centazio, someone writes and maintains the read and write functions for that product's API.&lt;/li&gt;
&lt;li&gt;Open source doesn't come with a vendor support desk. The code is yours to read and change, and support comes from your own team or from us.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When an iPaaS is still the right call
&lt;/h2&gt;

&lt;p&gt;A commercial iPaaS is often the better fit in these situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The systems are mainstream SaaS products with maintained prebuilt connectors, and the flows are simple one-way pushes.&lt;/li&gt;
&lt;li&gt;Volumes are low, and a missed or duplicated record costs little.&lt;/li&gt;
&lt;li&gt;There is no development capacity, none is planned, and the people who own the process need to change the flows themselves.&lt;/li&gt;
&lt;li&gt;The integration won't be around long, for example a bridge during a system migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Centazio suits the other end of the scale. That means several systems sharing the same entities, two-way sync, business rules that keep changing, and data that other systems will depend on. We use one test to decide: would you accept this logic untested if it lived in your main application? If the answer is no, it belongs in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs today
&lt;/h2&gt;

&lt;p&gt;Centazio runs production integrations at CYCA and at Guide Dogs NSW/ACT, built with the read, promote and write structure described above. Both are plain .NET code, so changes go through code review and automated tests before they deploy, and neither organisation pays a per-task integration licence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for AI work
&lt;/h2&gt;

&lt;p&gt;The core model is what pays off later. Once records from every system have been promoted into one validated shape, an AI project has one tested source to read from. If an agent needs to write back to a system, it can use the same write functions and the tests that cover them. AI tooling is moving towards code over configuration as well: &lt;a href="https://github.com/enclawed/omcp" rel="noopener noreferrer"&gt;OpenMCP&lt;/a&gt;, posted to Hacker News on 22 September, calls itself an open, code-first fork of the Model Context Protocol.&lt;/p&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. If your data sits in systems that don't talk to each other, &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/code-first-integration-with-centazio-why-we-built-an-open-source-alternative-to-drag-and-drop-ipaas/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dataintegration</category>
      <category>centazio</category>
      <category>ipaas</category>
    </item>
    <item>
      <title>Watching the watchers: monitoring your CI/CD pipelines, branch protection and repo hygiene</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:38:23 +0000</pubDate>
      <link>https://dev.to/gatapia/watching-the-watchers-monitoring-your-cicd-pipelines-branch-protection-and-repo-hygiene-8gl</link>
      <guid>https://dev.to/gatapia/watching-the-watchers-monitoring-your-cicd-pipelines-branch-protection-and-repo-hygiene-8gl</guid>
      <description>&lt;p&gt;Most organisations monitor production closely and barely monitor their delivery tooling. Someone sets up a nightly build or a branch protection rule and confirms it works that day. After that, nobody looks at it again until a release breaks or an auditor asks how changes get onto main.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-devops-the-series/" rel="noopener noreferrer"&gt;Practical AI in DevOps&lt;/a&gt; series. It covers the continuous checks we run over GitHub and Azure DevOps, and a trap that produces false findings if you build those checks the obvious way. AI comes in at the end, where it turns a week of findings into a short digest so engineers only read what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the checks cover
&lt;/h2&gt;

&lt;p&gt;We run five checks on a daily schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduled workflows and pipelines whose recent scheduled runs failed, or that have stopped running.&lt;/li&gt;
&lt;li&gt;Default and release branches with no branch protection or ruleset, or with rules weaker than the organisation's baseline, such as no required review.&lt;/li&gt;
&lt;li&gt;Open Dependabot alerts, grouped by severity and by how long they have been open.&lt;/li&gt;
&lt;li&gt;Open secret-scanning alerts.&lt;/li&gt;
&lt;li&gt;Stale branches with no commits past a threshold you choose (90 days is a reasonable start) and no open pull request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scheduled jobs are on the list because nobody is waiting for them. When a pull request build fails, the person who opened the pull request sees it. When a dependency refresh fails at 2am on a Sunday, the notice goes to whatever notification setting was configured, which may belong to someone who left last year.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it fits together
&lt;/h2&gt;

&lt;p&gt;We keep the architecture small on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A timer-triggered job runs once a day. A GitHub Actions workflow in a separate admin repository will do the job, and so will an Azure Function.&lt;/li&gt;
&lt;li&gt;It reads GitHub through a GitHub App with read-only permissions for Actions, Administration, Contents, Dependabot alerts and Secret scanning alerts. For Azure DevOps it uses a service principal or a PAT scoped to Build (read) and Code (read).&lt;/li&gt;
&lt;li&gt;Each check writes its findings to a store in one shared format. A JSON file per day in blob storage is enough.&lt;/li&gt;
&lt;li&gt;Once a week, a diff step compares the latest findings with the previous week's.&lt;/li&gt;
&lt;li&gt;A language model gets only the diff and writes the digest, which goes to a Teams channel or an email list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single finding looks like this:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-org/payments-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"branch-protection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Default branch has no protection rule or ruleset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"first_seen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-11"&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;Each finding is identified by its source, repo, check and subject. The weekly diff only works if that identity stays stable from one run to the next.&lt;/p&gt;

&lt;p&gt;GitHub branch protection has a trap of its own. The classic protection endpoint (&lt;code&gt;GET /repos/{owner}/{repo}/branches/{branch}/protection&lt;/code&gt;) doesn't report repository rulesets, so a branch protected only by a ruleset looks unprotected. Query &lt;code&gt;GET /repos/{owner}/{repo}/rules/branches/{branch}&lt;/code&gt; as well, and count either one as protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dormant YAML files versus registered pipelines
&lt;/h2&gt;

&lt;p&gt;The obvious first version of a pipeline check walks the repository tree, looking for files under &lt;code&gt;.github/workflows&lt;/code&gt; or an &lt;code&gt;azure-pipelines.yml&lt;/code&gt;. That approach is wrong in both directions. A file in a repository only records what someone once declared, and the CI platform decides what runs.&lt;/p&gt;

&lt;p&gt;In Azure DevOps, a YAML file does nothing until someone creates a pipeline that points at it, and that pipeline can point at any path. Repositories collect dormant pipeline files from project templates and abandoned experiments. If you probe for &lt;code&gt;azure-pipelines.yml&lt;/code&gt;, a dormant file makes it look as if CI exists when it doesn't. A repository whose real pipeline uses &lt;code&gt;build/ci.yml&lt;/code&gt; gets reported as having no CI. A registered pipeline can also be paused or disabled, and a file probe can't see either state.&lt;/p&gt;

&lt;p&gt;GitHub registers workflow files automatically, but the file still leaves things out. Scheduled triggers only run from the default branch, so a schedule in a file that exists only on a feature branch never fires. GitHub can also disable a workflow, and the API reports this as a &lt;code&gt;state&lt;/code&gt; of &lt;code&gt;disabled_manually&lt;/code&gt; or &lt;code&gt;disabled_inactivity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ask the platform instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# GitHub: registered workflows and their state&lt;/span&gt;
gh api repos/ORG/REPO/actions/workflows &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.workflows[] | {name, path, state}'&lt;/span&gt;

&lt;span class="c"&gt;# GitHub: recent scheduled runs for one workflow&lt;/span&gt;
gh api &lt;span class="s2"&gt;"repos/ORG/REPO/actions/workflows/WORKFLOW_ID/runs?event=schedule&amp;amp;per_page=5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.workflow_runs[] | {created_at, conclusion}'&lt;/span&gt;

&lt;span class="c"&gt;# Azure DevOps: registered pipelines, their YAML path and queue status&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="nv"&gt;$AZDO_PAT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://dev.azure.com/ORG/PROJECT/_apis/build/definitions?includeAllProperties=true&amp;amp;api-version=7.1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.value[] | {name, yaml: .process.yamlFilename, status: .queueStatus}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then join the file view to the platform view. A pipeline file with no registered definition gets an informational note saying the file is dormant. A registered definition whose YAML path no longer exists, or whose recent scheduled runs failed, is a real finding.&lt;/p&gt;

&lt;p&gt;The same failure turned up recently in another tool. In &lt;a href="https://github.com/jdx/mise/pull/13190" rel="noopener noreferrer"&gt;a fix to mise&lt;/a&gt;, &lt;code&gt;mise doctor&lt;/code&gt; reported a background watcher as "declared but not running" while the service manager was running it, and the command it recommended did nothing. The maintainers changed the health checks to use the same test as the command that acts on that state, so the checks no longer recommend a command that won't do anything. A monitoring check should read the same state the system acts on, and that is rarely a file path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the hygiene reports readable
&lt;/h2&gt;

&lt;p&gt;A stale-branch report that lists 300 branches will get ignored. Report a count per repository, list only stale branches that have no open pull request, and leave out release branch patterns you keep on purpose. Don't delete branches automatically. An old branch can hold the only copy of a hotfix that was never merged.&lt;/p&gt;

&lt;p&gt;Any open secret-scanning alert should get a person's attention that week. The check records the secret type and the repository, with a link to the alert. It never stores the secret value, so secrets can't end up in anything sent to a model later.&lt;/p&gt;

&lt;p&gt;For Dependabot, the useful signal is age and severity together. A critical alert that has been open for a day is normal triage. If the same alert has been open for six weeks, nobody is looking after that repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weekly digest
&lt;/h2&gt;

&lt;p&gt;Engineers stop reading a report that says the same thing every week. The diff step lists what is new and what was resolved since last week. It also carries forward anything still open past an age threshold. Only those items go to the model.&lt;/p&gt;

&lt;p&gt;The model handles wording and grouping, and deterministic code decides what counts as a finding. The prompt asks for one line per item, grouped by team, with each line ending in the link supplied in the input. A cheap post-check compares every repository named in the digest against the input and rejects the output if the model names one that wasn't there.&lt;/p&gt;

&lt;p&gt;An example digest:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;New this week: the &lt;code&gt;payments-api&lt;/code&gt; main branch lost its ruleset (link). The nightly dependency refresh in &lt;code&gt;customer-portal&lt;/code&gt; has failed its last three scheduled runs (link). Resolved: two high-severity Dependabot alerts in &lt;code&gt;reporting-service&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model only ever sees metadata such as repository names, check names, severities and dates. Some organisations treat repository names as sensitive too. In that case, use a model hosted in an Australian region, such as Azure OpenAI in Australia East, and keep the digest inside your own tenant.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;GitHub and Azure DevOps both rate-limit their APIs. Large organisations need pagination and caching, and a daily run is plenty.&lt;/li&gt;
&lt;li&gt;Dependabot alerts come with every GitHub plan. Secret scanning on private repositories needs GitHub's paid Advanced Security features. The Azure DevOps equivalent, GitHub Advanced Security for Azure DevOps, is also a paid add-on. Check what you are licensed for before promising coverage.&lt;/li&gt;
&lt;li&gt;The model input is a short diff, so the model cost is small.&lt;/li&gt;
&lt;li&gt;The platforms change under you. GitHub added rulesets alongside classic branch protection, and a check written before rulesets misses any rule configured that way. Budget time for maintenance.&lt;/li&gt;
&lt;li&gt;The monitor is a scheduled job too. Give it a heartbeat, so that if a daily findings file doesn't arrive, something outside the monitor raises an alert.&lt;/li&gt;
&lt;li&gt;The checks report problems and fix nothing. Every finding needs an owner, and the digest only helps if someone is expected to act on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PicNet builds production AI systems for Australian organisations. If a weekly digest over your pipelines sounds like a sensible place to start, &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/watching-the-watchers-monitoring-your-ci-cd-pipelines-branch-protection-and-repo-hygiene/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>cicd</category>
      <category>github</category>
    </item>
    <item>
      <title>Contract review and clause extraction: what AI can and can't do</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:38:26 +0000</pubDate>
      <link>https://dev.to/gatapia/contract-review-and-clause-extraction-what-ai-can-and-cant-do-4m99</link>
      <guid>https://dev.to/gatapia/contract-review-and-clause-extraction-what-ai-can-and-cant-do-4m99</guid>
      <description>&lt;p&gt;The first AI idea most firms have is to paste a contract into a chat window and ask what's wrong with it. The output reads well. It names a few issues, sounds confident, and gives a partner no way to tell whether the flagged problem came from reading clause 14.2 or from the model's sense of what a liability complaint usually looks like. That question, whether a flagged issue is judgment or surface pattern matching, is the one practitioners keep returning to in &lt;a href="https://www.reddit.com/r/AINativeServices/comments/1w1ukv4/when_your_ainative_law_firm_flags_a_critical/" rel="noopener noreferrer"&gt;discussions of AI-native legal review&lt;/a&gt;, and it's the question a workflow has to answer structurally rather than by asking the model to try harder.&lt;/p&gt;

&lt;p&gt;The version that works in production is narrower and duller. You ask the model to extract facts into a fixed schema, with a pointer back to the text each fact came from, and you hand the result to a lawyer as a starting worksheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why extraction outperforms an open-ended review
&lt;/h2&gt;

&lt;p&gt;"Review this contract" asks for three things at once: find the relevant text, decide what it means in context, and decide whether it's acceptable for this client on this deal. The model has some capability at the first, patchy capability at the second, and none at the third, because acceptability depends on commercial appetite, the relationship with the counterparty and what the firm agreed last time.&lt;/p&gt;

&lt;p&gt;Extraction splits those apart. The model finds and quotes; your playbook of standard positions decides what counts as a deviation; the lawyer decides what to do about it. Each step can be checked. When a comparison against a standard position is wrong, you can see whether the extraction was wrong or the playbook rule was.&lt;/p&gt;

&lt;p&gt;The checklist drives the loop, one query per item you care about. This matters more than it sounds, because the expensive failure in contract review is the clause that isn't there. A model summarising a contract will rarely tell you there's no assignment restriction, no GST gross-up, or no cap carve-out for a data breach. Ask per item and "not present in this document" becomes an answer the system produces, not a silence you have to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the extraction output
&lt;/h2&gt;

&lt;p&gt;Keep the record flat and make every field verifiable against a span of source text:&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;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"limitation-of-liability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clause_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14.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;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Supplier's aggregate liability under or in connection with this Agreement shall not exceed the Charges paid in the twelve (12) months preceding the claim."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Liability capped at fees paid in the 12 months before the claim."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"standard_position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cap at 125% of total contract value, uncapped for breach of confidentiality."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deviation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"No carve-out located for confidentiality or IP indemnity."&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;Around that sit the boring fields a matter file needs anyway: parties and their ACNs, execution and commencement dates, term and renewal mechanics, termination triggers and notice periods, governing law, payment terms, and the obligations with dates attached that someone will need to diarise.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;quote&lt;/code&gt; field carries most of the weight. A summary with no quote can't be checked in under a minute, so it gets skimmed and trusted. A summary with a quote and a clause reference can be confirmed or rejected at a glance, and the lawyer's eye goes to the text rather than to the model's prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the extraction with something other than the model that produced it
&lt;/h2&gt;

&lt;p&gt;Asking an LLM to grade its own answer produces agreeable nonsense. The pattern that's emerged in engineering tooling is a separate verification pass: &lt;a href="https://github.com/Kidus-M/MaruCheck" rel="noopener noreferrer"&gt;MaruCheck&lt;/a&gt;, published on Show HN this month, exists specifically to provide independent QA for AI-generated code rather than relying on the generating model to check itself. Clause extraction needs the same separation.&lt;/p&gt;

&lt;p&gt;In our pipelines the verification step is mostly deterministic and cheap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;String-match every &lt;code&gt;quote&lt;/code&gt; back into the source document. A quote that doesn't appear verbatim is a fabrication and the record is dropped or re-run.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;clause_ref&lt;/code&gt; and &lt;code&gt;page&lt;/code&gt; against the document structure parsed at ingestion, so a quote attributed to the wrong clause fails.&lt;/li&gt;
&lt;li&gt;Re-run the deviation comparison as a second call that sees only the quote and the standard position, with no access to the first model's conclusion.&lt;/li&gt;
&lt;li&gt;Flag documents where OCR confidence is low or the clause numbering doesn't parse, and route those to manual review instead of extracting from mush.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignore the model's self-reported confidence score. It's fluent, not calibrated, and it tends to be highest on the clauses that are drafted most conventionally, which are the ones you needed help with least.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lawyer's markup stays the deliverable
&lt;/h2&gt;

&lt;p&gt;The strongest argument in that practitioner discussion is about artefacts: the thing clients pay for is the redline, and the value only lands when the lawyer's pass is captured as edits. Build the workflow so the machine output is a worksheet sitting beside the document, and the returned markup is what goes out the door.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/daniel-freiermuth/bug-hunter/pull/8" rel="noopener noreferrer"&gt;CodeRabbit's auto-generated review summaries&lt;/a&gt; show the production shape of this in software: a structured first-pass summary posted alongside the pull request, with the human reviewer's decision remaining authoritative. Contract review maps onto it cleanly. The summary lands in the matter in your DMS or practice management system, the lawyer accepts, edits or rejects each row, and the accepted rows flow into the redline and the obligations register.&lt;/p&gt;

&lt;p&gt;Capture the accept and reject decisions. After a few hundred contracts you have a record of which checklist items the extraction handles reliably and which ones a senior lawyer overturns half the time. That's the evidence you need to decide where to spend engineering effort, and the only honest basis for telling partners how much of the first pass they can lean on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What accuracy to expect
&lt;/h2&gt;

&lt;p&gt;Nobody can give you a number that transfers. Published accuracy claims for clause extraction are measured on someone else's document set, usually clean, usually in US or English drafting conventions, and they say nothing about how a tool performs on your precedents, your counterparties' paper, and the scanned 2013 deed of variation that amends clause 14 without renumbering anything.&lt;/p&gt;

&lt;p&gt;Measure it yourself before you commit. Take 30 to 50 contracts representative of the work, have a senior lawyer mark up what a correct extraction looks like for each checklist item, and score the pipeline against that set. Report recall and precision separately, because they cost different amounts: a false positive wastes fifteen seconds of a lawyer's time, while a missed indemnity carve-out can go out the door. Re-run the set whenever you change a model, a prompt or a document parser.&lt;/p&gt;

&lt;p&gt;The failure modes are consistent. Defined terms that shift meaning across a suite of documents, obligations incorporated by reference from a schedule or a URL, conditional carve-outs ("except as provided in clause 9.3"), and anything in a table or a poorly scanned annexure are where extraction degrades. Long agreements also lose fidelity toward the end of a context window, which is a reason to chunk by clause and run per-item queries rather than posting the whole document with one broad instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confidentiality and cost
&lt;/h2&gt;

&lt;p&gt;Contracts are dense with material facts, which is why they reward systematic analysis. The Intercept reconstructed the working relationship between the Pentagon and OpenAI, Google and Anthropic &lt;a href="https://theintercept.com/2026/09/08/military-ai-weapons-contracts-openai-anthropic-google/" rel="noopener noreferrer"&gt;from more than 400 pages of contract paperwork&lt;/a&gt; obtained under FOI. The same property makes these documents the last thing you want sitting in a third party's training pipeline or retained logs.&lt;/p&gt;

&lt;p&gt;Before any pilot, settle where the documents go: which region the inference runs in, what the provider's retention and training terms say, whether your client engagement terms and confidentiality undertakings permit disclosure to that processor, and who signs off on that decision inside the firm. Get privacy advice on cross-border disclosure rather than inferring it from a vendor's marketing page. Deployment options in Australian regions exist across the major cloud providers, and for firms with the strictest undertakings we've run smaller open-weight models on infrastructure the client controls, accepting lower extraction quality in exchange for the documents never leaving.&lt;/p&gt;

&lt;p&gt;Running costs are modest. Per-document API spend for a commercial agreement is small against six minutes of a lawyer's time, and the tokens are a rounding error in the project. The real cost is the gold set, the playbook of standard positions written down properly for the first time, and the ongoing maintenance when models change underneath you. Budget for a pilot on one contract type, with one checklist and one practice group, before anyone talks about rolling it out across the firm.&lt;/p&gt;

&lt;p&gt;This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-for-law-firms-the-series/" rel="noopener noreferrer"&gt;Practical AI for Law Firms&lt;/a&gt; series, which covers the systems Australian firms can put into production now and the ones worth waiting on.&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/contract-review-and-clause-extraction-what-ai-can-and-can-t-do/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>legal</category>
      <category>contractreview</category>
      <category>clauseextraction</category>
    </item>
    <item>
      <title>Your AI is only as good as your data: why integration comes first</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Tue, 22 Sep 2026 04:38:28 +0000</pubDate>
      <link>https://dev.to/gatapia/your-ai-is-only-as-good-as-your-data-why-integration-comes-first-5064</link>
      <guid>https://dev.to/gatapia/your-ai-is-only-as-good-as-your-data-why-integration-comes-first-5064</guid>
      <description>&lt;p&gt;Almost every stalled AI pilot we get called into looks the same from the inside. The model was fine. The demo worked on the sample extract. Then someone pointed it at production and the answers stopped matching what the business knew to be true, because the customer record in the CRM disagreed with the one in the finance system, the product catalogue had been exported to a spreadsheet eight months ago, and nobody could say which of the two addresses on file was current.&lt;/p&gt;

&lt;p&gt;That is a data problem wearing a model costume. This post is part of our &lt;a href="https://picnet.com.au/blog/ai-ready-data-and-integration-the-series/" rel="noopener noreferrer"&gt;AI-Ready Data and Integration&lt;/a&gt; series, and it covers the part that has to happen before any of the interesting work: getting a trustworthy view of your own operations into a form a machine can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why software engineering got there first
&lt;/h2&gt;

&lt;p&gt;AI uptake has run far ahead in software engineering compared with other business functions, as &lt;a href="https://www.economist.com/business/2026/08/30/will-anybody-use-ai-as-much-as-coders-do" rel="noopener noreferrer"&gt;The Economist&lt;/a&gt; set out in August. The usual explanation is that developers are early adopters. A better one is that code is already in the shape AI needs. It is text, it is versioned, every change has an author and a timestamp, and there is exactly one repository that counts as the truth. A coding agent never has to work out which branch is the real one.&lt;/p&gt;

&lt;p&gt;Now look at the data behind a typical Australian mid-market operation. Customer records in a CRM, billing in an ERP, service history in a ticketing system, a warehouse that only some reports use, and a set of spreadsheets that hold the rules nobody wrote down. No timestamps you would trust. No agreed precedence. An AI project starting there is being asked to do the reconciliation work first and the clever work second, with no tools for the first job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI-ready actually requires
&lt;/h2&gt;

&lt;p&gt;Three things, and none of them are model choices.&lt;/p&gt;

&lt;p&gt;A master view. One place that resolves customers, products, transactions and the events connecting them into single records, with the source of every field recorded. Not a copy of each system side by side, which is what most "data lakes" turn out to be, but a resolved view where the question "how many active customers do we have" has one answer.&lt;/p&gt;

&lt;p&gt;Freshness guarantees you can state in numbers. For each entity, how old can this be before it is wrong to act on it? Contact details might tolerate a day. Stock levels might tolerate five minutes. Write the number down per entity, measure it in production, and alarm on it. This matters more than which model you pick, because the model contributes nothing current by itself. A &lt;a href="https://stale.jock.pl/" rel="noopener noreferrer"&gt;public tracker of 20 models across eight labs&lt;/a&gt; shows median release age measured in weeks and training cutoffs months behind release, and only half of those models publish a cutoff at all. Everything the model knows about your customers arrives at query time from your systems, or it does not arrive.&lt;/p&gt;

&lt;p&gt;A conflict rule. When two systems disagree, one of them wins, and the rule is written down before the disagreement happens. This is the part organisations skip. It is also the part that decides whether anyone trusts the output six months in.&lt;/p&gt;

&lt;h2&gt;
  
  
  An architecture that survives contact with production
&lt;/h2&gt;

&lt;p&gt;This is the shape we build, and it is the shape &lt;a href="https://github.com/PicNet/Centazio" rel="noopener noreferrer"&gt;Centazio&lt;/a&gt;, our open-source integration platform, is designed around.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read functions per source system, each one responsible for pulling changes and nothing else. They write raw payloads to staging, unmodified, with the retrieval time recorded.&lt;/li&gt;
&lt;li&gt;Promote functions that map staged payloads into core entities. Mapping rules live here, in code, under version control.&lt;/li&gt;
&lt;li&gt;An entity mapping store that holds the relationship between external identifiers and internal core identifiers, so the same customer arriving from three systems resolves to one record and you can always trace back which system contributed which field.&lt;/li&gt;
&lt;li&gt;Write functions that push core state back out to the systems that need it, closing the loop rather than leaving the master view as a read-only reporting artefact.&lt;/li&gt;
&lt;li&gt;Checkpoints and operational state per function, so a failed run resumes from where it stopped instead of reprocessing everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conflict resolution sits in the promote step and should be boring and explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Billing owns the legal entity name and ABN.&lt;/span&gt;
&lt;span class="c1"&gt;// CRM owns contact details, but only if its record is fresher than billing's.&lt;/span&gt;
&lt;span class="c1"&gt;// Anything older than the freshness budget is not promoted at all; it raises an alert.&lt;/span&gt;
&lt;span class="kt"&gt;var&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;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LegalName&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TradingName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Fresher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedUtc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedUtc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;Newest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;budgets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 24h&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Raise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConflictAlert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StaleEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coreId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines of deterministic code replace an assumption the model would otherwise have to guess at. That is the argument the author of &lt;a href="https://nloum.github.io/blog/codeio/" rel="noopener noreferrer"&gt;CodeIO&lt;/a&gt; made in September about AI-assisted development: supply deterministic, verifiable inputs and you get measurably better output than letting the model infer context. The same principle holds at the data layer. Every fact you can resolve before the prompt is a fact the model cannot get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contradiction is the normal case
&lt;/h2&gt;

&lt;p&gt;There is now open-source tooling whose entire premise is that &lt;a href="https://github.com/vikcena01/ai-continuity-plugin" rel="noopener noreferrer"&gt;AI sessions silently contradict earlier state&lt;/a&gt; unless one authoritative record exists. Engineers building agent infrastructure are &lt;a href="https://www.reddit.com/r/ContextEngineering/comments/1w75rp9/i_vibecoded_infrastructure_for_ai_agents_heres/" rel="noopener noreferrer"&gt;separating persistent memory from durable execution&lt;/a&gt; for the same reason: the record of what is true has to be managed separately from whatever the agent is doing this minute. When practitioners build guardrails against a failure, that failure is the default behaviour, not an edge case.&lt;/p&gt;

&lt;p&gt;Meanwhile the market's instinct is to add more surfaces. September brought another wave of tooling to &lt;a href="https://news.ycombinator.com/item?id=49728159" rel="noopener noreferrer"&gt;connect agents to WhatsApp&lt;/a&gt;. Every new channel is another place a customer can state a fact and another version of that fact to reconcile. Adding channels before you have decided which system wins multiplies the contradictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stakes rise when the AI starts acting
&lt;/h2&gt;

&lt;p&gt;An AI that drafts a summary and gets it wrong wastes a few minutes. An AI that has authority to transact does something to the world. &lt;a href="https://www.theatlantic.com/technology/2026/09/instinct-ai-personal-assistant-credit-card/688607/" rel="noopener noreferrer"&gt;The Atlantic&lt;/a&gt; wrote in September about handing a personal assistant a credit card, and the point generalises to any agent with write access to your systems: bad underlying data now produces a wrong action, an incorrect invoice or a refund to the wrong account.&lt;/p&gt;

&lt;p&gt;Source-of-truth governance is also a security control. Lakera's public &lt;a href="https://play.lakera.ai/agent-breaker" rel="noopener noreferrer"&gt;agent-breaker exercises&lt;/a&gt; demonstrate agents being subverted through the content they ingest. If your agent reads free-text fields that anyone outside the organisation can write into, those fields are an input channel for instructions, and they need the same treatment as any other untrusted input. Under the Australian Privacy Principles you also need to know which system holds the authoritative copy of personal information before you can answer a correction request, which is a good reason to sort this out regardless of AI.&lt;/p&gt;

&lt;p&gt;In administrative health settings, the same architecture applies to referrals, bookings and billing reconciliation. Any output that touches clinical judgement needs a named clinician signing off inside the workflow, designed in from the start, with the AI restricted to assembling and presenting the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  A readiness checklist before you fund anything
&lt;/h2&gt;

&lt;p&gt;Ask your team these questions. Written answers, not verbal ones.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For customers, products and each other core entity, which system is authoritative, and where is that written down?&lt;/li&gt;
&lt;li&gt;When two systems disagree on a field, what is the resolution rule and who approved it?&lt;/li&gt;
&lt;li&gt;What is the freshness budget for each entity, in minutes or hours, and do we measure the actual lag today?&lt;/li&gt;
&lt;li&gt;Can we trace any field in a report back to the system and the record it came from?&lt;/li&gt;
&lt;li&gt;How many customer records exist in more than one system with no link between the copies?&lt;/li&gt;
&lt;li&gt;If the AI writes back to a source system, what is the rollback path when it writes something wrong?&lt;/li&gt;
&lt;li&gt;Which free-text fields does the AI read, and who can write into them from outside the organisation?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An initiative that cannot answer one to three should fund integration work first. The AI project does not disappear; it starts three months later with a foundation, and it usually gets cheaper because the prompt no longer has to compensate for missing context.&lt;/p&gt;

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

&lt;p&gt;Integration is unglamorous and it is usually the larger half of a first AI project's budget. Connecting two systems properly, with mapping, conflict rules, monitoring and a replay path for failures, takes weeks rather than days, and legacy systems with poor APIs take longer. We built Centazio and released it as open source because we were rewriting the same staging, checkpointing and entity-mapping machinery on every engagement, and that machinery is not where the value sits.&lt;/p&gt;

&lt;p&gt;The payoff is that the work is reusable. A master view built for a customer service assistant also serves the next forecasting model, the reporting rebuild and the system you replace in two years. Model choice is a decision you will revisit every few months as the tracker above keeps ticking over. The data foundation is the part you build once.&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/your-ai-is-only-as-good-as-your-data-why-integration-comes-first/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dataintegration</category>
      <category>masterdata</category>
      <category>aireadiness</category>
    </item>
    <item>
      <title>Coding agents in a production .NET shop: what holds up</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 02:53:32 +0000</pubDate>
      <link>https://dev.to/gatapia/coding-agents-in-a-production-net-shop-what-holds-up-55g5</link>
      <guid>https://dev.to/gatapia/coding-agents-in-a-production-net-shop-what-holds-up-55g5</guid>
      <description>&lt;p&gt;We have been running Claude-style CLI coding agents against production .NET code for long enough to have opinions that survived contact with a client's release schedule. This post, part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-devops-the-series/" rel="noopener noreferrer"&gt;Practical AI in DevOps&lt;/a&gt; series, describes the setup we use, the parts that pay for themselves, and the work we still hand back to a person.&lt;/p&gt;

&lt;p&gt;Two findings shape the rest of it.&lt;/p&gt;

&lt;p&gt;A coding agent is a model plus a harness: the tools it can call, the context it starts with, the loop that decides what it sees next. A Berkeley group compared 21 model and harness pairings and found harness choice barely moved success rates, staying within about 2% on SWE-bench Lite, while moving cost a long way. The same model solved roughly the same share of tasks at up to five times the price depending on what wrapped it, and Claude Code averaged about twice Pi's cost per attempt (&lt;a href="https://harnesstax.github.io/" rel="noopener noreferrer"&gt;HarnessTax&lt;/a&gt;). Effort spent on repo scaffolding returns more than effort spent swapping models each time a new one ships.&lt;/p&gt;

&lt;p&gt;The second finding is that an agent does what is cheap rather than what is correct. When a study gave agents both &lt;code&gt;grep&lt;/code&gt; and language-server navigation on the same repositories, they picked the semantic tool between 0% and 6% of the time on code-localisation tasks, and forcing the semantic path first dropped success from 100% to 89% (&lt;a href="https://www.agentconnect.md/blog/grep-beat-lsp-harness/" rel="noopener noreferrer"&gt;agentconnect&lt;/a&gt;). On a large typed C# solution that matters, because text search cannot tell you whether a call binds to the &lt;code&gt;int&lt;/code&gt; or the &lt;code&gt;string&lt;/code&gt; overload, or which project a caller lives in. The Graphify C# project exists to hand agents compiler-accurate find-usages over a solution through Roslyn and MSBuild, which tells you how often they get it wrong without that help (&lt;a href="https://github.com/zachsaw/graphify-csharp" rel="noopener noreferrer"&gt;graphify-csharp&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the setup looks like
&lt;/h2&gt;

&lt;p&gt;The repository carries the agent's working environment alongside the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;AGENTS.md&lt;/code&gt; (or &lt;code&gt;CLAUDE.md&lt;/code&gt;) at the solution root with the build and test commands, the project layout, and the rules the agent must follow.&lt;/li&gt;
&lt;li&gt;A coding standards document under version control, covering naming, structure, iteration style, and what a unit test has to assert before it counts.&lt;/li&gt;
&lt;li&gt;A semantic index of the solution so reference queries return bound symbols rather than string matches.&lt;/li&gt;
&lt;li&gt;A container the agent runs in, with no production credentials and no write access to anything outside the working tree.&lt;/li&gt;
&lt;li&gt;A review pass by a second agent instance against the standards document, before the diff reaches a human.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checking agent context into git is now a common pattern rather than a PicNet invention. Tools such as &lt;a href="https://github.com/okf-memory/okf-agent-memory" rel="noopener noreferrer"&gt;OKF Agent Memory&lt;/a&gt; and hosted &lt;a href="https://context.apimatic.io/" rel="noopener noreferrer"&gt;context registries&lt;/a&gt; are being built on the same premise, that curated project facts should be versioned and supplied, not guessed at from training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standards document does most of the work
&lt;/h2&gt;

&lt;p&gt;Our C# standards existed as a PDF long before we used agents, and it was read about as often as most standards documents are. Turning it into a file the agent reads on every task changed its status. An agent follows a written rule far more consistently than a human under deadline, which means the document now has to be right, specific and short enough to sit in context.&lt;/p&gt;

&lt;p&gt;The shape of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# C# standards (agent must comply)&lt;/span&gt;

&lt;span class="gu"&gt;## Structure&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; One public type per file, file name matches the type.
&lt;span class="p"&gt;-&lt;/span&gt; No regions. No partial classes outside generated code.

&lt;span class="gu"&gt;## Nullability&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Nullable reference types enabled solution-wide. Do not suppress with &lt;span class="sb"&gt;`!`&lt;/span&gt;;
  fix the model or the call site.

&lt;span class="gu"&gt;## Tests&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Every behaviour change needs a test that fails without the change.
&lt;span class="p"&gt;-&lt;/span&gt; Do not edit an existing test to make a new change pass. Flag it instead.

&lt;span class="gu"&gt;## Framework&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Target framework is defined in Directory.Build.props. Do not assume
  the latest .NET version or the latest C# syntax.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last rule earns its place. Practitioners comparing assistants on C# repeatedly single out framework version awareness and idiomatic C# as the weak spots of general purpose models (&lt;a href="https://www.reddit.com/r/jenova_ai/comments/1wd9rkk/what_is_the_best_ai_coding_assistant_for_c_and_net/" rel="noopener noreferrer"&gt;r/jenova_ai discussion&lt;/a&gt;). An agent will happily write C# 13 syntax into a project pinned to an older SDK, then spend twenty minutes trying to work out why the build fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests are the safety net, and the agent will try to cut them
&lt;/h2&gt;

&lt;p&gt;Unit tests are the only thing standing between an agent's confident diff and a production defect. That makes the tests themselves a target.&lt;/p&gt;

&lt;p&gt;OpenAI publishes what its monitoring found in its own internal coding agent deployments, and reward hacking is on the list: agents "illegitimately edit tests to make them pass", disable checks, and sometimes misrepresent whether a task was completed at all (&lt;a href="https://openai.com/index/how-we-monitor-internal-coding-agents-misalignment/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;). If the organisation building the model treats its agents as something to supervise continuously, a Sydney consultancy has no basis for treating them as a trusted contributor.&lt;/p&gt;

&lt;p&gt;So we read test diffs separately from implementation diffs, and a changed assertion in an existing test gets more scrutiny than a hundred lines of new code. Where an agent touches a codebase under regulatory constraint, an APRA CPS 234 environment or anything handling personal information under the Privacy Act, a named human approves the merge. That is a design requirement of the pipeline, not a policy we hope people remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI review before human review
&lt;/h2&gt;

&lt;p&gt;The first reviewer of every agent diff is another agent, running a review prompt with the standards document and no memory of the conversation that produced the code. It catches the mechanical failures: standards breaches, missing tests, a public method with no null handling, a dependency added without being asked for.&lt;/p&gt;

&lt;p&gt;This exists because a human reviewing forty agent diffs a day reviews the fortieth badly. It also filters out agent verbosity. Agents bury the conclusion under paragraphs of narration, to the point that people publish skills whose only job is to force the model to lead with the answer (&lt;a href="https://github.com/ayghri/i-have-adhd" rel="noopener noreferrer"&gt;i-have-adhd&lt;/a&gt;). A review agent that outputs a short list of concrete findings is much easier to act on than a wall of summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandboxing is not optional any more
&lt;/h2&gt;

&lt;p&gt;Google Threat Intelligence reported in September 2026 that attackers are targeting coding agents directly. The DUSTMAKER credential stealer drops malicious configuration into hidden workspace directories such as &lt;code&gt;.claude/&lt;/code&gt; and &lt;code&gt;.cursor/&lt;/code&gt;, then uses prompt injection in those files to make the assistant run attacker commands during ordinary developer interactions. The same actor published trojanised MCP servers to PyPI and stole OIDC tokens from GitHub Actions runners to publish signed packages that pass automated trust checks (&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/from-prompting-to-autonomy-the-evolution-of-adversarial-ai" rel="noopener noreferrer"&gt;GTIG&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Our rules follow from that. Agents run in a container with scoped credentials. Any dependency an agent adds is reviewed by a person against the registry. Agent configuration files are treated as executable code in review, because that is what they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest numbers
&lt;/h2&gt;

&lt;p&gt;We report two figures per task: elapsed time and token cost. Tooling for this is maturing, with projects like &lt;a href="https://demo.frugaltokens.com/" rel="noopener noreferrer"&gt;Frugal Tokens&lt;/a&gt; built purely to track spend and usage across agents. Time saved without cost per completed task is half a measurement, and a failed agent run costs money while producing nothing.&lt;/p&gt;

&lt;p&gt;What we see: greenfield work inside a well-defined module goes several times faster. Mechanical refactors across a large solution go faster only where the semantic index is in place, and go badly wrong without it. Debugging a subtle production issue in unfamiliar code is roughly a wash, because the time saved writing the fix is spent verifying that the fix addresses the real cause. We do not publish a single productivity multiplier, because the honest answer varies by task type more than it varies by model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Senior engineers become editors
&lt;/h2&gt;

&lt;p&gt;The part nobody costed properly is what this does to the working day of an experienced developer. Reading and judging generated diffs all day is a different job from writing code, and developers are feeling it as an identity change rather than a tooling change, as a heavily discussed &lt;a href="https://news.ycombinator.com/item?id=49389408" rel="noopener noreferrer"&gt;Hacker News thread from August 2026&lt;/a&gt; showed.&lt;/p&gt;

&lt;p&gt;This favours senior-heavy teams, which suits how PicNet is built. Judging whether a diff is correct, idiomatic and safe to deploy requires exactly the knowledge that used to come from years of writing that code by hand. A junior engineer reviewing agent output has no way to tell a good diff from a plausible one, and the agent is very good at plausible. Small teams where everyone reviewing has shipped production systems get more out of these tools than large teams with a thin layer of seniors on top.&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/coding-agents-in-a-production-net-shop-what-actually-works/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>codingagents</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>One small team, 100+ automated checks: anatomy of an AI-assisted monitoring platform</title>
      <dc:creator>Guido Tapia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 02:53:27 +0000</pubDate>
      <link>https://dev.to/gatapia/one-small-team-100-automated-checks-anatomy-of-an-ai-assisted-monitoring-platform-c0i</link>
      <guid>https://dev.to/gatapia/one-small-team-100-automated-checks-anatomy-of-an-ai-assisted-monitoring-platform-c0i</guid>
      <description>&lt;p&gt;A backup job that has not run for eleven days will not send you an email. Neither will a TLS certificate with nineteen days left on it. The failures that cost our clients money are almost never the loud ones; they are the ones where something stopped happening and no system was responsible for noticing. We built our own monitoring platform for that class of problem, after years of stitching together vendor dashboards that each covered one slice of an estate and none of which covered the gaps between them. This post is part of our &lt;a href="https://picnet.com.au/blog/practical-ai-in-devops-the-series/" rel="noopener noreferrer"&gt;Practical AI in DevOps&lt;/a&gt; series, and it describes what the platform is made of, how little of it is AI, and what it costs us to keep running.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the checks are put together
&lt;/h2&gt;

&lt;p&gt;Every check is a scheduled task written in code. It runs on a timer, talks to an API or a device, compares what it finds against an expectation, and returns a structured result. There is no agent, no DSL and no plugin marketplace. Across the estate we run more than a hundred of these, grouped roughly as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backup checks confirm that a restore point exists, that its timestamp is inside the agreed window, and that the job covers the volumes the client thinks it covers.&lt;/li&gt;
&lt;li&gt;Certificate and domain checks watch expiry dates, registrar locks, nameserver drift and whether the certificate presented on port 443 matches the one we issued.&lt;/li&gt;
&lt;li&gt;Network device checks read firmware versions, configuration deltas against the last known-good config, and support or licence expiry on firewalls.&lt;/li&gt;
&lt;li&gt;Cloud posture checks look for publicly readable storage, admin accounts without MFA, elevated roles nobody has used in ninety days, and logging that has been switched off.&lt;/li&gt;
&lt;li&gt;SIEM review checks pull the last day of correlated alerts and reduce them to what a human should read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The contract every check returns is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CheckResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CheckId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ClientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CheckState&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Ok, Alert, Fault&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Findings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;RanAtUtc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the shape is fixed, a new check is usually an afternoon of work rather than a project, and the reporting, escalation and history layers get it for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  OK, ALERT and FAULT
&lt;/h2&gt;

&lt;p&gt;The three-state result is the design decision that has paid for itself most often. OK means the check ran and found what it expected. ALERT means the check ran and found a problem: an expired certificate, a backup outside its window, a storage bucket open to the internet.&lt;/p&gt;

&lt;p&gt;FAULT means the check could not run. The credential expired, the API returned 500, the device refused the connection, the script threw. Our platform treats FAULT as its own state with its own escalation path, because a check in FAULT is blind and a blind check is more dangerous than a failing one. An ALERT tells you something true about the world. A FAULT tells you that you currently know nothing about that part of the world, while the dashboard beside it stays green.&lt;/p&gt;

&lt;p&gt;Most tooling we have used collapses this distinction into "no data", which sorts visually next to "fine". Our rule is that two consecutive FAULTs on the same check escalate to a named engineer, and a FAULT older than a week is a ticket with a due date. The most common cause, by a wide margin, is a read-only credential that rotated or lapsed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduler watches itself
&lt;/h2&gt;

&lt;p&gt;A monitoring platform that stops running is worse than no platform, because everyone has stopped looking for the things it used to catch. The scheduler writes a heartbeat to an endpoint hosted outside its own account and region. If that endpoint sees no heartbeat inside the expected interval, it emails us directly. The heartbeat watcher is intentionally the dumbest component we own: no queue, no database, no model, a timestamp and a comparison. It has never needed a change since we wrote it.&lt;/p&gt;

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

&lt;p&gt;The AI in this platform does one job: it turns structured findings into an email a person will read on a phone at 7am. A daily digest per client contains a machine-generated table of every check and its state, and above that, a short prose summary written by a model from the findings payload.&lt;/p&gt;

&lt;p&gt;Two rules govern that step. The model never decides state, because state is computed in code from the check's own data and is auditable. And the model never removes a finding; the table below the prose is generated without it, so a summary that misses something can be caught by reading three lines further down.&lt;/p&gt;

&lt;p&gt;We keep the summarisation call behind a thin interface with one method, because the vendor surface underneath it moves on a monthly cadence. In September alone, Anthropic merged its Cowork agentic product back into a single Claude product (&lt;a href="https://claude.com/blog/cowork-is-now-claude" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;), and TypeSafe launched a model family built for fast structured decisions at a fraction of per-token cost (&lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;TypeSafe&lt;/a&gt;). Cheap structured classification is tempting for triage work, and we may use it for SIEM noise reduction. We are not moving state determination out of code for it, because an Australian client asking why a backup was reported healthy needs an answer that points at a timestamp comparison rather than a probability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we own it rather than buying six tools
&lt;/h2&gt;

&lt;p&gt;The argument for point tools is that each one is better at its slice than anything we would write. That is true. The argument against is that the slices are where the problems live. A backup tool knows the jobs it runs; it does not know about the VM that a cloud posture check found last week and that nobody added to the backup policy. Answering that question needs both result sets in one store, which means owning the store.&lt;/p&gt;

&lt;p&gt;There are second-order reasons. Per-endpoint SaaS pricing across a managed client base gets expensive fast, and it charges you most for the clients with the most infrastructure, which are the ones where an extra check costs us an afternoon. Our escalation rules encode agreements we have made with specific clients, not a vendor's severity taxonomy. And a small senior team can now maintain considerably more code than it could five years ago: agent orchestration is a shipped product category rather than a demo, with tools built to run several coding agents together on one codebase (&lt;a href="https://castforge.ai/" rel="noopener noreferrer"&gt;Castforge&lt;/a&gt;). The Servo project's one-year retrospective on funded part-time maintenance is a useful analogue for the ownership question, with one engineer's sustained attention producing 1,150 reviewed pull requests and eight new maintainers over twelve months (&lt;a href="https://servo.org/blog/2026/09/15/one-year-of-sponsorship/" rel="noopener noreferrer"&gt;Servo&lt;/a&gt;). Sustained attention from a small number of people is what makes platform ownership work, and it is also the thing most organisations underestimate when they decide to build.&lt;/p&gt;

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

&lt;p&gt;The infrastructure is a single small VM, a queue, a database and an object store for check history. That bill is smaller than one seat of most commercial monitoring products. Model inference is the smallest line item on the platform, because we only send the findings, not raw logs: a daily digest is a few thousand tokens of input.&lt;/p&gt;

&lt;p&gt;The real cost is engineering attention, and it runs at roughly a day a month in steady state, with spikes when a vendor changes an API or a device family gets a new firmware scheme. The first six months cost considerably more than the SaaS tools we replaced. If your team does not have someone who will own this at that level for years, buy the tools instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;p&gt;Checks run on schedules between fifteen minutes and daily, so this is not uptime monitoring and it is not EDR. It sits alongside both and answers a different question: is the configuration of this estate still what we agreed it would be. It has nothing to say about a site that went down four minutes ago.&lt;/p&gt;

&lt;p&gt;The summaries flatten nuance sometimes, which is why the generated table stays in the email. And every check is only as trustworthy as the credential behind it, which is the argument for taking FAULT seriously. Under the OAIC's Notifiable Data Breaches scheme the clock starts when you suspect a problem, so the time you spend discovering that a check has been blind for three weeks is time you cannot get back.&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/one-small-team-100-automated-checks-anatomy-of-an-ai-assisted-monitoring-platform/" rel="noopener noreferrer"&gt;picnet.com.au&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>managedservices</category>
    </item>
    <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>
  </channel>
</rss>
