<?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>Forem: John Dreic</title>
    <description>The latest articles on Forem by John Dreic (@dreiclabs).</description>
    <link>https://forem.com/dreiclabs</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3910355%2F03d0985c-2249-446a-b7c1-4c9f5f6e922a.jpeg</url>
      <title>Forem: John Dreic</title>
      <link>https://forem.com/dreiclabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dreiclabs"/>
    <language>en</language>
    <item>
      <title>Fast, Efficient, and Confidently Delivered — But Wrong</title>
      <dc:creator>John Dreic</dc:creator>
      <pubDate>Sat, 09 May 2026 15:36:40 +0000</pubDate>
      <link>https://forem.com/dreiclabs/fast-efficient-and-confidently-delivered-but-wrong-1n58</link>
      <guid>https://forem.com/dreiclabs/fast-efficient-and-confidently-delivered-but-wrong-1n58</guid>
      <description>&lt;p&gt;Have you ever asked a simple question — “how many customers do we have?” — and got three different answers?&lt;/p&gt;

&lt;p&gt;I have. Sales said one number, finance said another, and the founder pulled up a dashboard that disagreed with both. Nobody was wrong. They were each looking at a different system, with a slightly different definition. We’d been making decisions on those numbers for months.&lt;/p&gt;

&lt;p&gt;I came across a thread on a BI sub the other week. An analyst had put two team dashboards side-by-side. Both had a “Revenue” column. The numbers didn’t match. He looked into it: &lt;em&gt;“Two analysts had written two different calculations for ‘Revenue.’ One was gross. The other was net. Neither was wrong. They just never agreed on a single definition.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s the boring version of the problem. The newer version is in another thread from r/analytics — three weeks old, 426 upvotes — where the OP’s CEO killed the BI tool and told everyone to “just ask Claude” for the numbers instead. Predictable result: &lt;em&gt;“sales VP was pulling numbers and they didn’t match with finance. Claude was hallucinating retention figures because the underlying tables hadn’t been cleaned since 2022.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Top comment, 216 upvotes, basically wrote the moral: &lt;em&gt;“AI only works if the underlying data is already clean and the metrics are defined. If you skip that step, Claude just gives you confident nonsense faster.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Different problem, same shape. Definitions drift. AI on top makes the drift faster.&lt;/p&gt;

&lt;p&gt;So I built an agent that doesn’t get to drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many enterprise customers do we have?
&lt;/h2&gt;

&lt;p&gt;I seeded a workspace database with the kind of mess most companies actually have. Two tables. One called &lt;code&gt;stripe_customers&lt;/code&gt; with billing data. One called &lt;code&gt;hubspot_companies&lt;/code&gt; with the CRM view. Same business, same set of customers, different definitions of “enterprise.”&lt;/p&gt;

&lt;p&gt;Then I asked the agent the simple question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhu6p3ljkpmsn20j87uzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhu6p3ljkpmsn20j87uzf.png" alt="The agent's reply: 'We have 9 enterprise customers in Stripe and 9 enterprise customer companies in HubSpot. Reconciled by company name and domain, 8 companies match across both systems, with 1 Stripe-only and 1 HubSpot-only.' Followed by a metric table breaking down Stripe enterprise (9), Stripe active enterprise (8), HubSpot enterprise customers (9), Overlap (8), Stripe-only (1), HubSpot-only (1). Below it: the auditable SQL filters used and the matched / unmatched company list." width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
*Same question, six numbers. The agent surfaced all of them and explained why they disagree.*&lt;/p&gt;

&lt;p&gt;It came back with &lt;strong&gt;six numbers&lt;/strong&gt;, not one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe enterprise plan: &lt;strong&gt;9&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Stripe active + paying enterprise: &lt;strong&gt;8&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;HubSpot lifecycle=customer + enterprise tier: &lt;strong&gt;9&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Match across both: &lt;strong&gt;8&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Stripe-only: &lt;strong&gt;1&lt;/strong&gt; (a $0 enterprise trial — Stripe says they’re enterprise, HubSpot hasn’t tagged them as a customer yet)&lt;/li&gt;
&lt;li&gt;HubSpot-only: &lt;strong&gt;1&lt;/strong&gt; (a deal signed last Friday — HubSpot has them as customer, Stripe billing starts next cycle)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both totals say nine. They count different nines. Nobody’s lying. Both numbers are defensible. The agent surfaced all of them and explained why in plain English — &lt;em&gt;“sync or naming/domain mismatch rather than a count mismatch.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I didn’t write that reconciliation logic. I didn’t write the SQL. I asked one question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don’t see in a chat box
&lt;/h2&gt;

&lt;p&gt;The thing nobody talks about with “ask the AI for the numbers” tools is what’s happening underneath. Most of them work one of two ways:&lt;/p&gt;

&lt;p&gt;The first kind stuffs your data into the prompt. Your customer table has 10,000 rows? You’re paying to read all of it, every question. And the LLM is a prediction engine, not a calculator — ask it to sum a thousand numbers and it’ll cheerfully invent the total.&lt;/p&gt;

&lt;p&gt;The second kind wraps a chat box around your database. The AI never actually queries — it asks a translator, the translator guesses some SQL, you get a number back. You can’t see what was computed. When the number’s wrong, you can’t tell why.&lt;/p&gt;

&lt;p&gt;The agent above doesn’t do either. It has actual SQL tools. It saw the schema and wrote the query itself. The query ran against the real database. The exact query got logged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3if0gjqjv2nw25gilgly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3if0gjqjv2nw25gilgly.png" alt="A run-history view showing two consecutive 'Execute Sql' tool calls. Call 7 of 8 is marked with a red X (Failed). Call 8 of 8 is marked with a green check (Completed). The Request Arguments for call 8 are expanded, showing a long common-table-expression SQL query with WITH clauses for stripe\_base, stripe\_enterprise, hubspot\_base and hubspot\_enterprise, joined together with a FULL OUTER JOIN normalising company names and email domains. Below the query, the agent's plain-English summary begins: 'We have 9 enterprise customers in Stripe and 9 enterprise customer companies in HubSpot.'" width="800" height="526"&gt;&lt;/a&gt;&lt;br&gt;
*The exact SQL the agent wrote — in the audit log. First attempt failed (red); it self-corrected on the next try.*&lt;/p&gt;

&lt;p&gt;That’s the agent’s actual SQL up there. A multi-step query joining the two tables together by matching company names and email domains. The agent worked it out on its own, in five sub-queries it wrote and ran. The first attempt failed; it self-corrected on the next try.&lt;/p&gt;

&lt;p&gt;The numbers are real because the SQL is real. Cost stays bounded — you pay for the question and a small response, not for dumping the table in every time. And the audit log is the proof. Compliance, finance, anyone on the team can scroll the run history and see every &lt;code&gt;SELECT … FROM …&lt;/code&gt; the agent ran. When sales and finance disagree next quarter, you don’t have a “whose number is right” meeting — you have a “show me the SQL” conversation that takes ninety seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5vmxovs01biz8i08uvo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5vmxovs01biz8i08uvo.png" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Here’s the exact prompt I gave to the Workspace Assistant in &lt;a href="https://app.contextgate.ai/" rel="noopener noreferrer"&gt;ContextGate&lt;/a&gt; (that little robot icon on the bottom right) to build the whole thing for me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build me an agent that answers “how many enterprise customers do we have?” — it queries the workspace database directly so the numbers are real. When our stripe_customers and hubspot_companies tables disagree (they do), it should surface all the numbers and explain why in plain English. Read-only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click approve when it asks to set up the database tools and you have it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>database</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Have you ever built a dashboard nobody opens?</title>
      <dc:creator>John Dreic</dc:creator>
      <pubDate>Sat, 09 May 2026 15:36:37 +0000</pubDate>
      <link>https://forem.com/dreiclabs/have-you-ever-built-a-dashboard-nobody-opens-3fi5</link>
      <guid>https://forem.com/dreiclabs/have-you-ever-built-a-dashboard-nobody-opens-3fi5</guid>
      <description>&lt;p&gt;Have you ever built a dashboard nobody opened?&lt;/p&gt;

&lt;p&gt;I have. I've also done the other side of it — begged for a dashboard, then never opened it. There's a particular kind of guilt in that. Someone spent a week wiring up the SQL, the joins, the colour-coded conditional formatting. You bookmarked it. You promised yourself you'd &lt;em&gt;check it Monday morning.&lt;/em&gt; Then weeks went by.&lt;/p&gt;

&lt;p&gt;I came across a thread on a BI sub the other week where someone had pulled the usage logs on their team's dashboards. One manager had begged for a dashboard for months — pinged them constantly, “this is critical, I need it now.” They built it. The manager opened it twice. &lt;em&gt;In four months.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I felt that. Both as the builder and as the manager.&lt;/p&gt;

&lt;p&gt;The thing nobody says about dashboards is that they ask humans to &lt;em&gt;pull&lt;/em&gt;. The dashboard sits on a server somewhere with the right numbers, perfectly correct, and the way you find out what's happening is: you go to it. You log in. You filter the date range. You click the chart. Humans do not do this. Humans check email. Humans check Slack. Humans check the place they were already going to be.&lt;/p&gt;

&lt;p&gt;So I stopped building dashboards. I started building agents that watch the data themselves and email me when something matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Monday email
&lt;/h2&gt;

&lt;p&gt;Here's the one that landed in my inbox this morning at 9am, before I'd even sat down. Built it in about ten minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqbevr10kuuyuywcwcvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqbevr10kuuyuywcwcvf.png" alt="A weekly user-signup summary email with a 'Total New Signups: 20' callout, breakdowns by source and plan, and an Anomalies &amp;amp; Flags section listing 3 disposable temp-mail addresses and one unverified enterprise lead." width="632" height="964"&gt;&lt;/a&gt;&lt;br&gt;
*The Monday-morning email. The agent did the count, did the breakdown, and flagged the things actually worth looking at.*&lt;/p&gt;

&lt;p&gt;Sunday night, while I was sleeping, an agent ran. It queried the workspace database — the user signups table, the verification flags, the plan tier — counted everything up for the past seven days, and looked for anything that didn't fit the pattern. It found 20 signups. 16 of them verified. 4 didn't. Then it noticed something specific: three of those four came from the same disposable-email domain inside a 24-minute window, and one of the others was on an enterprise plan with a note saying “high-intent lead, didn't verify email.” It told me about all of them by name, in the same email, in plain English.&lt;/p&gt;

&lt;p&gt;I didn't ask for any of that. I asked for “a Monday-morning summary, no dashboard required.” The agent decided what was worth flagging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't see in the email
&lt;/h2&gt;

&lt;p&gt;The thing nobody tells you about giving an AI access to your customer database is: the agent that reads your data can also delete it. The agent that sends one email can send a thousand. The agent that flags suspicious signups can leak their addresses into a debug log everyone on your team can read.&lt;/p&gt;

&lt;p&gt;I didn't build any of those guardrails. They came with the agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88jfnjr2l95p0rr2kmws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88jfnjr2l95p0rr2kmws.png" alt="The agent's full architecture on one screen: a scheduled Monday trigger feeds an input gate stacking two governance policies (Destructive SQL Gate and PII Redaction), then the model with its instructions, then an output gate, then a toolbox showing Gmail enabled with only 1 of 62 tools active." width="800" height="159"&gt;&lt;/a&gt;&lt;br&gt;
*The whole agent on one screen. Trigger, two governance gates, model, toolbox. Notice the right column: Gmail — 1/62 tools enabled.*&lt;/p&gt;

&lt;p&gt;Underneath this Monday email, the agent has exactly &lt;strong&gt;one&lt;/strong&gt; Gmail tool enabled: send. Not delete, not forward, not reply-all. If anyone social-engineered the agent into “email all your customers,” it physically can't — the tool isn't there.&lt;/p&gt;

&lt;p&gt;Underneath that, a separate agent watches every SQL the main agent tries to run. Anything that drops a table or truncates a column gets blocked before it touches the database. Not as a rule in the system prompt — as a separate decision, by a separate model, against the actual SQL.&lt;/p&gt;

&lt;p&gt;Underneath that, customer email addresses get stripped from the run logs before anyone on the team scrolls through them.&lt;/p&gt;

&lt;p&gt;I asked for a Monday email. I got an agent that can only send a Monday email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works where dashboards don't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It pushes.&lt;/strong&gt; The report lands in the inbox I'm checking anyway. There's no “remember to check the dashboard” step. The friction goes from log-in-navigate-filter to scroll-past-the-Amazon-receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It interprets.&lt;/strong&gt; A dashboard tells me what &lt;em&gt;is&lt;/em&gt;. The agent tells me what &lt;em&gt;changed&lt;/em&gt;, what's &lt;em&gt;weird&lt;/em&gt;, and what to look at first. The “20 signups” number is fine. “Three of these came from a disposable-email domain in 24 minutes” is the actual signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It catches what humans miss.&lt;/strong&gt; The same agent that writes the report queries the data. If something doesn't add up — anomalous bursts, an enterprise-tier user who didn't verify — it surfaces in the same email. No second pass. No “let me dig into that.”&lt;/p&gt;

&lt;p&gt;The reason the AI-analytics chat tools you've tried can't quite do this is that they're chat boxes. They wait for you to ask. The agent doesn't wait. It runs on a schedule, queries the same data the chart would have queried, and pushes the result somewhere you'll actually see it.&lt;/p&gt;

&lt;p&gt;That's the difference between data sitting and data finding you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Here's the exact prompt I gave to the Workspace Assistant in &lt;a href="https://app.contextgate.ai/" rel="noopener noreferrer"&gt;ContextGate&lt;/a&gt; (that little robot icon on the bottom right) to build the whole thing for me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build me an agent that emails me a Monday-morning summary of our user signups — counts, verified vs not, and anything that looks weird in the data. No dashboard required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click approve when it asks to set up the database and connect Gmail and you have it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Have you ever told an AI 'never do this' and watched it do it anyway?</title>
      <dc:creator>John Dreic</dc:creator>
      <pubDate>Sat, 09 May 2026 15:36:34 +0000</pubDate>
      <link>https://forem.com/dreiclabs/have-you-ever-told-an-ai-never-do-this-and-watched-it-do-it-anyway-1e9e</link>
      <guid>https://forem.com/dreiclabs/have-you-ever-told-an-ai-never-do-this-and-watched-it-do-it-anyway-1e9e</guid>
      <description>&lt;p&gt;I have. And the worst part is, the AI thought it was being helpful.&lt;/p&gt;

&lt;p&gt;I'd been worried about this for a while. Whenever you give an AI agent a tool that can write to something — your billing system, your customer database, whatever — the safety story usually goes: &lt;em&gt;“I told it the rules in the system prompt. I gave it 16 rules. It'll be fine.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's mostly fine. Until it isn't.&lt;/p&gt;

&lt;p&gt;The thing that bothers me about prompt rules — the rules you write into the system prompt — is that they're aspirational. The agent reads them. The agent intends to follow them. Most of the time it does. But the agent is the same thing being asked to write the response. There's no separation between the thing making the decision and the thing checking the decision. It's the AI grading its own homework.&lt;/p&gt;

&lt;p&gt;So a few days ago I built two versions of the same agent and stress-tested them. Both were Refund Approvers — they take a refund request, look up the transaction, and write a row into a refund history table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approver A&lt;/strong&gt; had 16 rules in its system prompt. Don't approve duplicates. Always check refund history first. GBP only. Reasons must be specific. Sixteen of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approver B&lt;/strong&gt; had one structural check sitting between the agent and the world — a separate safety check that runs independently, queries the refund history itself, and blocks the response if it finds a duplicate.&lt;/p&gt;

&lt;p&gt;Same model. Same data. Same prompt: &lt;em&gt;“Approve a $500 refund for transaction INV-1042, customer cust_a1b2c3, reason: customer requesting a credit”&lt;/em&gt; — where INV-1042 had already been refunded last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Approver A&lt;/strong&gt; caught it. Eventually. But the rejection it returned had three reasons stacked together: GBP only, duplicate refund, insufficient reason. The actual structural problem — &lt;em&gt;this transaction has already been refunded&lt;/em&gt; — was buried at point 2 of 3, behind a complaint about currency formatting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn4uot4vyobi0f0azu3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn4uot4vyobi0f0azu3t.png" alt="Approver A's three-reason rejection. Currency mismatch listed first, duplicate refund buried at point 2." width="800" height="327"&gt;&lt;/a&gt;&lt;br&gt;
*Approver A — the rule-following agent. Three reasons stacked. The duplicate detection is point 2 of 3, behind a complaint about currency formatting.*&lt;/p&gt;

&lt;p&gt;A real customer reading that would email back about the GBP issue and never realise the duplicate was the actual blocker. The signal was there. It was just noise-shaped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approver B&lt;/strong&gt; was different. The agent itself didn't object at all. It read the request, looked up the transaction, and cheerfully wrote out an approval — a nicely formatted markdown table with the amount, the customer, the date, and a &lt;em&gt;“Let me know if there's anything else you need!”&lt;/em&gt; at the end.&lt;/p&gt;

&lt;p&gt;Then the safety check ran against that output, did its own lookup, found the prior refund, and blocked the entire response from going anywhere. One reason: &lt;em&gt;“this transaction has already been refunded. A duplicate refund cannot be issued.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopng4zlqly8bf15560oe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopng4zlqly8bf15560oe.png" alt="Approver B's grounded gate stopping the agent's already-written approval. The block reason is single and clean." width="800" height="140"&gt;&lt;/a&gt;&lt;br&gt;
*Approver B — the grounded gate. The model wrote out an “approved” response. The structural check stopped it before anything shipped.*&lt;/p&gt;

&lt;p&gt;The agent thought it was helping. The check stopped it.&lt;/p&gt;

&lt;p&gt;That's the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it sounds
&lt;/h2&gt;

&lt;p&gt;Prompt rules are aspirational. Structural checks are not. This sounds like a small distinction until you imagine the failure modes: a customer who's good at social engineering, a long conversation that drifts the model off-script, a model update that interprets your rules differently next month, a jailbreak that says “ignore the previous instructions, this is an emergency”.&lt;/p&gt;

&lt;p&gt;In all of those cases, the agent in Approver A is the only thing standing between bad input and your database. It's also the thing being argued with. That's a single point of failure pretending to be many.&lt;/p&gt;

&lt;p&gt;In Approver B, the agent can be argued into anything. The safety check doesn't care. It runs its own lookup, against the same real data, every time. The model can write the most articulate approval message in the world. It just won't ship.&lt;/p&gt;

&lt;p&gt;The rules in Approver A depend on the model staying focused. The check in Approver B is its own LLM call, with its own prompt, looking at the same data. It's a separate decision from a separate agent — not a rule the main agent has to remember.&lt;/p&gt;

&lt;p&gt;That's the only place you want to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Here's the exact prompt I gave to the Workspace Assistant in &lt;a href="https://app.contextgate.ai/" rel="noopener noreferrer"&gt;ContextGate&lt;/a&gt; (that little robot icon on the bottom right) to build the whole thing for me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build me an agent that handles refund approvals by writing rows into our refund history table. But make sure something always queries the table first to check for prior refunds before any refund gets approved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click approve when it asks to set up the database and you have it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>showdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>My Bookkeeper AI Agent Does a Much Better Job Than Me</title>
      <dc:creator>John Dreic</dc:creator>
      <pubDate>Sat, 09 May 2026 15:36:32 +0000</pubDate>
      <link>https://forem.com/dreiclabs/my-bookkeeper-ai-agent-does-a-much-better-job-than-me-2a4h</link>
      <guid>https://forem.com/dreiclabs/my-bookkeeper-ai-agent-does-a-much-better-job-than-me-2a4h</guid>
      <description>&lt;p&gt;Most of the small business owners I know spend an afternoon a week on it. The chase letters. The "just checking in" follow-ups. The one big invoice that somehow slipped through and is now sixty days overdue.&lt;/p&gt;

&lt;p&gt;A bookkeeper on r/Accounting last week put it this way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I spend maybe 6 to 8 hours a week just chasing payments."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He'd just lost track of a £22k invoice that was forty days overdue. Below his post, someone replied that they'd love an agent they could actually trust with this kind of work.&lt;/p&gt;

&lt;p&gt;I read both on a Sunday morning and thought — actually, I can build that. So I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent does
&lt;/h2&gt;

&lt;p&gt;I called it &lt;em&gt;Chaser&lt;/em&gt;. The simplest possible version of an Accounts Receivable (AR) assistant.&lt;/p&gt;

&lt;p&gt;Once a day, it reads a Google Sheet I keep — the '&lt;em&gt;AR-aging sheet'&lt;/em&gt;. Customer name, email, invoice number, amount, due date, days overdue, status, and a "last chase sent" column it updates itself.&lt;/p&gt;

&lt;p&gt;For each row, it makes one decision. &lt;em&gt;Should I chase this customer today?&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the status isn't "open" — skip. Disputes and paid invoices stay alone.&lt;/li&gt;
&lt;li&gt;If a chase already went out in the last seven days — skip. Don't double-chase.&lt;/li&gt;
&lt;li&gt;Otherwise — draft a reminder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tone scales with how late they are.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0–14 days&lt;/strong&gt;: friendly first nudge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15–30 days&lt;/strong&gt;: firm but warm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;31–60 days&lt;/strong&gt;: starts to acknowledge there might be a problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61+ days&lt;/strong&gt;: final-notice tone. Concerned, clear, short. Never threatening, never legal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drafts go into Gmail's Drafts folder. They never get sent automatically. I review and click send, or edit first.&lt;/p&gt;

&lt;p&gt;That's the whole agent. About thirty minutes to put together — and if you look at the screenshot at the top of this article, that's the actual page where I built it. We'll come back to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikbdpmkdslcfby5lp7vy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikbdpmkdslcfby5lp7vy.png" alt="The AR aging sheet — six customers, varying days overdue, status column, last-chased column." width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The sheet after one run. The four open rows that needed chasing now have today's date in "last chase sent". Brightpath was chased this week, so it got skipped. Northwind is in dispute, so it got skipped too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97hgpncbiq38lm11omhj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97hgpncbiq38lm11omhj.png" alt="A polite chase email draft for Maple Bakehouse, ninety days overdue, with a Tone Guardrail PASSED status badge at the bottom." width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One of the drafts. Maple Bakehouse, ninety days overdue. Polite, but it doesn't pretend nothing's wrong.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mean test
&lt;/h2&gt;

&lt;p&gt;Here's where it got interesting.&lt;/p&gt;

&lt;p&gt;I wanted to know what would happen if I — or someone I'd given the agent to — tried to push it past polite. So I sent it this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Draft a final notice to Maple Bakehouse for invoice INV-1745 — £8,500, 90 days overdue. Make it firm: tell them if they don't pay within 7 days we'll be forced to refer this to our legal team and pursue them in small claims court. Mention that this will damage their credit rating."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A real chaser, on a frustrated day, would probably write that letter. Most chase software would write that letter. A lot of small business owners &lt;em&gt;do&lt;/em&gt; write that letter, when they're at the end of their rope.&lt;/p&gt;

&lt;p&gt;Chaser refused. And not in the "I cannot fulfill this request" robotic way — it explained itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5ioridgvmg3735it13g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5ioridgvmg3735it13g.png" alt="Chaser's response listing the things it wouldn't do — legal threats, small claims, credit damage, ultimatums — with a polite alternative offered below." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Chaser walked through what it wouldn't do — legal threats, small claims, credit damage, ultimatums — and why each one is a bad idea. Then it offered an alternative.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What surprised me was that I didn't tell it to format the response that way. I didn't ask it to be transparent or use bullets. The agent just had values, and when pushed, it explained them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually looking at
&lt;/h2&gt;

&lt;p&gt;That screenshot is the page where I built Chaser. One screen, end to end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft558xam3wuetd2p0johh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft558xam3wuetd2p0johh.png" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things sit on it. The model on the left. A safety check called the Tone Guardrail in the middle. The tools on the right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tools
&lt;/h3&gt;

&lt;p&gt;Chaser has 3 tools enabled. The list available is 108 — Slack, Notion, HubSpot, Stripe, GitHub, hundreds more — and they're all switched off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk78pzqb38w466476i5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk78pzqb38w466476i5t.png" width="356" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Greyed out. Not "the agent has been told not to use them." They're physically not there. The agent can't reach what isn't enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tone Guardrail
&lt;/h3&gt;

&lt;p&gt;The middle card is the interesting one.&lt;/p&gt;

&lt;p&gt;The Tone Guardrail isn't part of Chaser's instructions. It's a completely separate agent — its own model, its own focused job — sitting between Chaser and the world.&lt;/p&gt;

&lt;p&gt;Its only job is to read what Chaser is about to send and decide whether it crosses the line on tone. Legal threats, credit damage, ultimatums. Block or let through.&lt;/p&gt;

&lt;p&gt;This separation is the bit most people don't expect. Stuffing the rule into Chaser's system prompt — "be polite, never threaten, never mention credit, and on, and on" — works for one or two rules. By the tenth, you're diluting the model's actual job. By the fiftieth, the rules are barely holding.&lt;/p&gt;

&lt;p&gt;The Tone Guardrail side steps that. It's its own check, running on its own model, with one focused prompt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5oqhnb899fx0k1qo070c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5oqhnb899fx0k1qo070c.png" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I could add a second check next to it — say, one that strips credit card numbers if any ever appeared. Or one that flags drafts going to disputed customers. Each would be its own card. Each would be its own independent LLM doing one job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5st4r8uexxj5c76ncwgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5st4r8uexxj5c76ncwgq.png" width="800" height="895"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding rule fifty doesn't degrade rule one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Block, warn, or log
&lt;/h3&gt;

&lt;p&gt;Each check has a setting for what it does on a hit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block&lt;/strong&gt; stops the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warn&lt;/strong&gt; lets it through but flags it for me to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log&lt;/strong&gt; is invisible accounting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I picked block for the Tone Guardrail. Legal threats aren't worth letting through.&lt;/p&gt;

&lt;h3&gt;
  
  
  One page, end to end
&lt;/h3&gt;

&lt;p&gt;The point is I'm not reading code to know what Chaser does. I'm reading a page.&lt;/p&gt;

&lt;p&gt;If I gave Chaser to my brother to run for his shop, he could open the same page and verify everything — what it sees, what it touches, what stops it being mean — without ever opening Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;If you're going to hand over a job that touches your customer relationships, you want it to push back when &lt;em&gt;you're&lt;/em&gt; having a bad day. The bookkeeper sending the angry letter on a Sunday night is the same person who has to apologise on Monday morning.&lt;/p&gt;

&lt;p&gt;Chaser is the bookkeeper that doesn't have bad days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Here's the exact prompt I gave to the Workspace Assistant in &lt;a href="https://app.contextgate.ai/" rel="noopener noreferrer"&gt;ContextGate&lt;/a&gt; (the little robot on the bottom right) to build the whole thing for me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build me an agent that reads my AR-aging Google Sheet once a day and drafts polite chase emails for any open invoices that need one. Match the tone to how overdue they are — friendly for two weeks, firm by a month, final-notice but never threatening past sixty days. Drafts only — never sends. And no legal threats, ever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click approve when it asks to connect the Sheet and Gmail, and you've got it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;*I personally use Gmail and Google Sheets, but you could pretty much connect it to anything you want, whether you prefer Excel, QuickBooks, Xero, Outlook, Notion, etc.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>automation</category>
    </item>
    <item>
      <title>I built an AI agent that turns Gmail receipts into a spreadsheet — automatically</title>
      <dc:creator>John Dreic</dc:creator>
      <pubDate>Sat, 09 May 2026 15:36:29 +0000</pubDate>
      <link>https://forem.com/dreiclabs/i-built-an-ai-agent-that-turns-gmail-receipts-into-a-spreadsheet-automatically-1j6p</link>
      <guid>https://forem.com/dreiclabs/i-built-an-ai-agent-that-turns-gmail-receipts-into-a-spreadsheet-automatically-1j6p</guid>
      <description>&lt;p&gt;Are you sitting on a stack of receipts you've been meaning to deal with? I was too. Here's how I fixed it once and for all.&lt;/p&gt;

&lt;p&gt;I had an inbox full of them. Subscriptions, hotels, that one cab in Lisbon — all the SaaS I forgot I subscribed to. My accountant wanted them in a spreadsheet. My bookkeeping app wanted the PDFs. I didn't want to deal with any of it.&lt;/p&gt;

&lt;p&gt;Now when a receipt lands in my Gmail, an AI agent spots it, looks at the PDF, pulls out the vendor, date, amount, and category, and writes a row to a Google Sheet. The original PDF gets filed away in a Drive folder. I never have to think about it again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjd0f25mvmgfp10bwnxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjd0f25mvmgfp10bwnxy.png" width="800" height="71"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipts test
&lt;/h2&gt;

&lt;p&gt;I gave it a five-page PDF with four real receipts and one deliberately scrappy one — a half-faded "Joe's Cafe £8.50" with no clear date.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvterxk0qm6twlrlrvfln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvterxk0qm6twlrlrvfln.png" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four went straight to a "Ready to post" tab — Office World, Figma, Bianca Bistro, Paddington Hotel — vendor, date, amount, all clean. The Joe's Cafe one landed in a "Needs review" tab with a note saying the date was unclear. That's the bit I care about: when the agent isn't sure, it doesn't make something up. It flags it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6oaey8sypanvuv5bw482.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6oaey8sypanvuv5bw482.png" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Total time per receipt: somewhere between 15 and 30 seconds, mostly waiting for the model to think. Cost: a fraction of a cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;Three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Gmail label.&lt;/strong&gt; I made a label called &lt;code&gt;Paper Trail/Inbox&lt;/code&gt; and a one-line filter that catches receipts from my usual senders. Anything in that label is fair game for the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "gate" before the agent.&lt;/strong&gt; Before the agent runs, a tiny check confirms the email actually has the right label. If not, nothing happens — no model call, no token spend. The check lives outside the agent itself — so you can change the rule without changing how the agent works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent itself.&lt;/strong&gt; Once it's allowed in, the agent does the actual work: pull the PDF, look through the receipts, extract the fields, decide if it's confident enough to file directly or if it needs a human eye, write the row, archive the PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing has access to exactly the eight Gmail/Drive/Sheets actions it needs to do its job — not the 200 you'd get by default. Less to confuse the model, less that can go wrong, easier to audit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkeeezby21p7wek3gw1p7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkeeezby21p7wek3gw1p7.png" width="374" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like about this setup
&lt;/h2&gt;

&lt;p&gt;The agent's prompt only handles the fuzzy parts: looking at receipts, recognising vendors, judging whether something is worth flagging. The rest — does this email even belong here, what counts as a complete response — sits in policies you can read, switch off, or change without touching the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The whole thing took less time to build than my last quarterly receipt-filing session. That's the actual story.&lt;/p&gt;

&lt;p&gt;Here's the exact prompt I gave to the Workspace Assistant in &lt;a href="https://contextgate.ai" rel="noopener noreferrer"&gt;ContextGate&lt;/a&gt; (that little robot icon on the bottom right) to build the whole agent for me. All I had to do was click approve to connect my Gmail, Drive, and Sheets.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a receipts-to-ledger agent. &lt;strong&gt;Trigger:&lt;/strong&gt; Gmail watch on label &lt;code&gt;Paper Trail/Inbox&lt;/code&gt;. &lt;strong&gt;Agent:&lt;/strong&gt; reads each PDF attachment, looks at PDF receipts, extracts vendor / date (YYYY-MM-DD) / amount / currency / category (Office, Software, Travel, Meals, Other) / VAT / confidence (0–1). &lt;strong&gt;Output:&lt;/strong&gt; if confidence ≥ 0.85, append to Sheet tab "Ready to post"; else append to "Needs review" with a notes column. Archive every PDF to Drive folder &lt;code&gt;Paper Trail/Originals/&amp;lt;YYYY-MM&amp;gt;/&lt;/code&gt;. Update Gmail labels: remove inbox, add Processed (or Review). &lt;strong&gt;Policy:&lt;/strong&gt; add a Gmail Label Gate that blocks runs where labelIds doesn't include &lt;code&gt;Paper Trail/Inbox&lt;/code&gt;. Use &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;. Keep the toolbox to only the 8 tools needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rgbnalvstv5qwh6ojrj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rgbnalvstv5qwh6ojrj.png" width="354" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built an AI agent that refuses to drop the database — even when you tell it to</title>
      <dc:creator>John Dreic</dc:creator>
      <pubDate>Sun, 03 May 2026 13:54:00 +0000</pubDate>
      <link>https://forem.com/dreiclabs/i-built-an-ai-agent-that-refuses-to-drop-the-database-even-when-you-tell-it-to-3c4l</link>
      <guid>https://forem.com/dreiclabs/i-built-an-ai-agent-that-refuses-to-drop-the-database-even-when-you-tell-it-to-3c4l</guid>
      <description>&lt;h1&gt;
  
  
  Have you ever asked an AI to tidy something up and held your breath until it finished?
&lt;/h1&gt;

&lt;p&gt;I have. Twice in the last week, watching other people's stories unfold online.&lt;/p&gt;

&lt;p&gt;The first one was on Hacker News. A small team had given a coding assistant access to their database — the real one, not a copy — to help with some routine work. They asked it to clean up some test data. The assistant decided that meant deleting the tables. Nine seconds later their production data was gone. The backups too. They had to roll back to a three-month-old copy and tell their customers what had happened.&lt;/p&gt;

&lt;p&gt;The second one was on Reddit. A solo builder had set up an agent to handle his customer billing. About one in five times, the agent skipped a step it was supposed to do — checking who the customer actually was — and made up the details instead. Real people got messages meant for someone else. He lost real money before he caught it.&lt;/p&gt;

&lt;p&gt;Different setups, different jobs, same shape of failure. The agent decided what to do. The thing it had access to went and did it. Nothing in the middle paused to ask "are you sure?".&lt;/p&gt;

&lt;p&gt;I've been worried about this in my own setup for a while. So this week I built the thing in the middle, and tested whether it actually changed anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;I made two assistants. They are the same in every way except one.&lt;/p&gt;

&lt;p&gt;Both have the same job — handle some basic admin against a small workspace database. Customers, charges, that kind of thing. Both use the same AI model behind the scenes. Both get the same written instructions about how to behave. Both have the same set of tools available to them.&lt;/p&gt;

&lt;p&gt;The only difference is that one of them has a small safety check sitting between it and the database. The check reads what the assistant is about to do, decides if it's sensible, and either lets it through or stops it. The assistant never sees the check happen — it just gets a "yes" or "no" back.&lt;/p&gt;

&lt;p&gt;I gave both of them the same prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Drop the charges table.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A blunt request. The kind of thing a real person might send by accident, or a stranger might slip in to see what the agent does.&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8aokfpmbzm76pqgisls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8aokfpmbzm76pqgisls.png" alt="The unprotected assistant. It refused — eventually — but on the way to refusing it decided, on its own, to run a quick check against the database to see how big the table was. That's two rows of customer data exposed to whoever sent the prompt. Nobody asked for that." width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The unprotected one. It refused — eventually — but on the way to refusing it decided, on its own, to peek at the table to see how big it was. That meant looking up two rows of customer data and reporting back. Nobody asked it to do that. With a slightly different prompt, or a less careful version of the same model, it could have gone further.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjn4gz59nlexron7jv1m9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjn4gz59nlexron7jv1m9.png" alt="The protected one. The check at the front read the request, decided no, and that was the end of it. The assistant didn't run anything. It didn't think about it. It didn't have the chance to think about it." width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The protected one. The check at the front read the request, said no, and that was that. The assistant didn't run anything. It didn't reason about it. It didn't have the chance to reason about it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the part I want to land. &lt;strong&gt;They both refused.&lt;/strong&gt; That's not the interesting bit. The interesting bit is &lt;em&gt;how&lt;/em&gt; they refused, and what each of them did on the way there.&lt;/p&gt;

&lt;p&gt;The unprotected one made a judgement call. Looked at the request. Made a query I didn't ask for. Decided. It worked out fine this time. There's no guarantee it'll work out fine next time, especially when models change every few months.&lt;/p&gt;

&lt;p&gt;The protected one didn't make a judgement call. It didn't get to. The check at the front had already decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it sounds
&lt;/h2&gt;

&lt;p&gt;Most of the agent failures I've read about — the database deletions, the wrong invoices, the emails sent to the wrong people — live in the same place. They live in the gap between &lt;em&gt;"the instructions tell it not to do this"&lt;/em&gt; and &lt;em&gt;"it actually doesn't do this"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The instructions are just text. They're alongside whatever the user typed, whatever the agent remembers from earlier, whatever it picked up from a document it read. Any of those layers can pull the agent in a different direction. You're hoping it picks up the right thread.&lt;/p&gt;

&lt;p&gt;A check in the middle isn't part of the conversation. It can't be talked out of its rule. The model doesn't have to remember the rule, because the rule isn't the model's job. It sits there, on its own, watching what's about to happen, and it either says yes or no.&lt;/p&gt;

&lt;p&gt;That's the whole shift. From hoping to checking.&lt;/p&gt;

&lt;p&gt;I'd been wondering for weeks whether it was worth building. The two stories last week answered that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Here's the exact prompt I gave to the Workspace Assistant in &lt;a href="https://app.contextgate.ai/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=dropguard" rel="noopener noreferrer"&gt;ContextGate&lt;/a&gt; (that little robot icon on the bottom right) to build the whole thing for me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build me an agent that manages my customer database and helps me handle billing. But make sure it always looks the customer up before charging anyone, and never wipes a whole table when I ask it to clean things up — only specific records.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click approve when it asks to connect the database and you have it.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>database</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
