<?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: lamingsrb</title>
    <description>The latest articles on DEV Community by lamingsrb (@lamingsrb).</description>
    <link>https://dev.to/lamingsrb</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%2F3993279%2F31006581-315c-4581-89fb-4bd5e8bb0768.png</url>
      <title>DEV Community: lamingsrb</title>
      <link>https://dev.to/lamingsrb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lamingsrb"/>
    <language>en</language>
    <item>
      <title>4 Systems I Built: Architecture Decisions That Held Up</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 20 Aug 2026 07:25:44 +0000</pubDate>
      <link>https://dev.to/lamingsrb/4-systems-i-built-architecture-decisions-that-held-up-3hjc</link>
      <guid>https://dev.to/lamingsrb/4-systems-i-built-architecture-decisions-that-held-up-3hjc</guid>
      <description>&lt;h1&gt;
  
  
  4 Systems I Built: Architecture Decisions That Held Up
&lt;/h1&gt;

&lt;p&gt;Every architecture decision looks smart in a slide deck. The real test is what happens six months in, when the bill arrives, a model deprecates, or a client asks you to add a feature nobody planned for. I want to walk through four systems I built, the choices I made at the start, and which of those choices I would make again today.&lt;/p&gt;

&lt;p&gt;I'll skip the tutorial framing. This is the honest version: what I picked, why, what broke, and what the numbers looked like once the thing was running in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 1: ContentStudio, a multi-agent content and SEO machine
&lt;/h2&gt;

&lt;p&gt;ContentStudio is the autonomous engine behind BizFlowAI. It researches topics, drafts, edits, optimizes for search and AEO, and publishes across multiple sites without me in the loop. It has been running for months and publishes on schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;Custom Node/TypeScript loop&lt;/td&gt;
&lt;td&gt;LangGraph felt heavy for a linear pipeline with conditional edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Claude API (Sonnet + Opus mix)&lt;/td&gt;
&lt;td&gt;Best long-form writing quality; cost model works at my volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Supabase (Postgres) + pgvector&lt;/td&gt;
&lt;td&gt;One database for content, embeddings, and run history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduling&lt;/td&gt;
&lt;td&gt;Node worker on a small VPS + cron&lt;/td&gt;
&lt;td&gt;Cheaper and simpler than serverless for a steady, predictable workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrails&lt;/td&gt;
&lt;td&gt;Eval agent + hard checks (word count, links, factuality prompts)&lt;/td&gt;
&lt;td&gt;The system will happily publish garbage without a gatekeeper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interesting call was &lt;strong&gt;custom orchestration over a framework&lt;/strong&gt;. I looked at LangGraph, CrewAI, and a few others. For a pipeline where I know the stages (research, outline, draft, edit, SEO pass, publish), a framework mostly adds abstraction I have to debug. A 400-line TypeScript state machine with typed transitions was clearer and cheaper to maintain. When I need branching, I add a conditional. When something breaks, the stack trace points at my code.&lt;/p&gt;

&lt;p&gt;The choice I would revisit: I used Claude Sonnet for drafting and Opus for the final polish pass. That two-model split saved roughly 40% on inference versus running Opus end-to-end, with output quality my human review couldn't distinguish. With Opus 5 pricing where it is now, I would run more of the pipeline on the top model and drop a stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 2: Sovereign AI POC with local LLMs and RAG
&lt;/h2&gt;

&lt;p&gt;A different flavor entirely. This one is a proof of concept for scenarios where data cannot leave a private environment: regulated industries, government-adjacent workloads, anyone with a "no US-cloud LLM" rule. Everything runs local.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama serving Llama and Qwen variants on a workstation with a single consumer GPU&lt;/li&gt;
&lt;li&gt;Postgres with pgvector for embeddings&lt;/li&gt;
&lt;li&gt;Full-text search (Postgres FTS) alongside vector search&lt;/li&gt;
&lt;li&gt;Reciprocal Rank Fusion to combine both retrievers&lt;/li&gt;
&lt;li&gt;A thin FastAPI layer for the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard decision was &lt;strong&gt;hybrid search over pure vector&lt;/strong&gt;. Pure semantic retrieval looks magical in demos and then fails on the queries that matter: exact identifiers, product codes, names with unusual spellings, acronyms. FTS catches those. Vector catches paraphrased intent. RRF (a simple rank-based fusion, no tuning weights) combines them in a way that consistently beat either retriever alone on my test set.&lt;/p&gt;

&lt;p&gt;Rough numbers on a 12k-document corpus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vector only: ~68% top-5 recall on my eval set&lt;/li&gt;
&lt;li&gt;FTS only: ~61%&lt;/li&gt;
&lt;li&gt;RRF hybrid: ~84%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap is the difference between "cute demo" and "the user trusts it."&lt;/p&gt;

&lt;p&gt;The trade-off with local LLMs is honest: a 70B model on consumer hardware is slower than an API call to Claude, and the quality gap on hard reasoning is real. For extraction, summarization, and grounded Q&amp;amp;A over retrieved chunks, it is entirely workable. For open-ended reasoning, it isn't. I tell clients this upfront. A sovereign deployment is a constraint, not a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 3: Serverless AWS + Zendesk integration for SLA compliance
&lt;/h2&gt;

&lt;p&gt;This one is older but the architecture lessons still apply. The problem: a ticketing system that was missing SLAs because the routing and escalation logic lived in a manual process. The solution: an event-driven serverless pipeline that watched Zendesk events, applied business rules, and triggered escalations before SLAs breached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zendesk webhook -&amp;gt; API Gateway -&amp;gt; Lambda (validate + normalize)
                                      |
                                      v
                                EventBridge (rules per event type)
                                      |
                    +-----------------+-----------------+
                    v                 v                 v
              Lambda (route)    Lambda (escalate)  Lambda (audit log)
                    |                 |                 |
                    v                 v                 v
                DynamoDB          Zendesk API        S3 + Athena
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why serverless and not a container on ECS or a small EC2:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Traffic is bursty and unpredictable.&lt;/strong&gt; Ticket volume spikes when something breaks upstream. Lambda scales to that automatically. A container would be either overprovisioned or under.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compute per event is tiny.&lt;/strong&gt; Milliseconds of logic. Paying for an idle container 23 hours a day is silly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge as the backbone&lt;/strong&gt; meant I could add new consumers (a new escalation channel, a new dashboard feed) without touching existing code. Publish an event, subscribe a new Lambda. That decoupling saved me multiple times when requirements shifted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result was first-ever SLA compliance for that workflow. The lesson I keep coming back to: &lt;strong&gt;event-driven serverless is a genuine unlock for integration work&lt;/strong&gt;, where the code is thin, the events are external, and the load is spiky. It is a bad fit when you have long-running processes, heavy dependencies, or per-request latency budgets that cold starts blow through.&lt;/p&gt;

&lt;p&gt;Cold start was the one thing I fought with. For the routing Lambda that had to respond within a webhook timeout, I moved to provisioned concurrency for the two most-hit functions. That is essentially a paid warm pool. Adds cost, kills cold starts. Worth it for the hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 4: Analytics migration that cut annual cost
&lt;/h2&gt;

&lt;p&gt;This wasn't a "build an AI thing" project, but the architecture reasoning is the same and it is the one that put the biggest number on the board: $30 to $60k a year in savings, depending on how you count.&lt;/p&gt;

&lt;p&gt;The old system: a proprietary analytics platform with per-seat and per-event pricing that grew linearly with usage. The new system: a straightforward pipeline into a warehouse the client already owned, with dashboards built on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decisions that mattered:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Own the warehouse, rent the compute.&lt;/strong&gt; Storage in the client's warehouse was already paid for. What we replaced was the marked-up ingestion and query layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch where possible, stream where necessary.&lt;/strong&gt; Most analytics questions do not need sub-minute freshness. Batching hourly cut compute cost by roughly an order of magnitude versus streaming everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a query engine that matches the shape of the questions.&lt;/strong&gt; For this workload, a columnar warehouse with materialized views for the hot dashboards was faster and cheaper than the general-purpose engine we replaced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generalizable lesson: &lt;strong&gt;the biggest wins in cloud architecture usually come from removing a per-usage tax, not from writing better code&lt;/strong&gt;. If your unit economics get worse as you scale, no amount of clever caching fixes it. Change the pricing surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern behind the four choices
&lt;/h2&gt;

&lt;p&gt;Looking across all four systems, the decisions that held up were the ones I made against a specific constraint, not against a generic best practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local LLM vs cloud API
&lt;/h3&gt;

&lt;p&gt;Cloud API wins by default. Use a local LLM when one of these is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data cannot leave a controlled environment (regulatory, contractual, sovereignty)&lt;/li&gt;
&lt;li&gt;You need predictable per-token cost at very high volume and you have the ops capacity&lt;/li&gt;
&lt;li&gt;Latency to a specific region matters more than model quality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, Claude or OpenAI, every time. The engineering effort you save is worth more than the inference bill for almost every project under a certain scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serverless vs containers
&lt;/h3&gt;

&lt;p&gt;Serverless wins when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traffic is spiky or unpredictable&lt;/li&gt;
&lt;li&gt;Per-request compute is small&lt;/li&gt;
&lt;li&gt;You are integrating with external event sources&lt;/li&gt;
&lt;li&gt;You want to add consumers without redeploying producers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Containers win when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workload is steady and known&lt;/li&gt;
&lt;li&gt;You have long-running processes, heavy dependencies, or GPU needs&lt;/li&gt;
&lt;li&gt;Cold start latency is unacceptable and provisioned concurrency doesn't cover it&lt;/li&gt;
&lt;li&gt;Your team already runs Kubernetes well and adding serverless is cognitive overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For ContentStudio I picked a plain worker on a VPS. The workload runs on a predictable schedule, the container is warm, and I don't pay for cold starts I don't need. Serverless would have been the wrong answer even though I know AWS well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework vs custom orchestration
&lt;/h3&gt;

&lt;p&gt;I default to custom for anything I can hold in my head. LangGraph and similar frameworks earn their keep when you have genuinely complex graphs, dynamic agent spawning, or a team that needs a shared vocabulary. For a linear pipeline with a handful of conditionals, a typed state machine in code you wrote is easier to debug at 2 a.m. than a framework you learned from a README.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector vs hybrid search
&lt;/h3&gt;

&lt;p&gt;Always hybrid. I have not built a production RAG system where pure vector search beat RRF hybrid on a real eval set. If you are shipping vector-only, you are leaving recall on the table for queries with exact terms, and those are often the queries that matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently today
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invest in evals earlier.&lt;/strong&gt; On ContentStudio I built the eval harness after the first bad publish. It should have existed on day one. For any agentic system, the eval is the product's spine. Without it you cannot tell whether a prompt change made things better or worse, you only have vibes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick the pricing surface before the tech.&lt;/strong&gt; The analytics migration taught me that architecture decisions are often really pricing decisions in disguise. Before I write a line of code now, I ask: what is the unit cost, and how does it scale with success? If the answer is bad, no amount of engineering will save it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Default to boring infrastructure.&lt;/strong&gt; Postgres for state and vectors. A worker on a VPS or a Lambda behind API Gateway. A queue if I need one. Every time I have reached for something more exotic (a specialized vector DB, an orchestration framework, a bespoke event bus) I have paid for it in operational surface area. Boring wins.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four systems above are not glamorous. They work, they run unattended, and they solve problems that had a dollar figure attached. That is the standard I hold my architecture decisions against.&lt;/p&gt;

&lt;p&gt;If you are benchmarking your own stack choices or thinking through a build like one of these, I am always happy to talk shop. You can reach me at &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt; or dig through more of the &lt;a href="https://lazar-milicevic.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt; for the deeper writeups on each system.&lt;/p&gt;

</description>
      <category>multiagentcontentsystem</category>
      <category>langgraphalternatives</category>
      <category>customllmorchestration</category>
      <category>claudesonnetvsopuscost</category>
    </item>
    <item>
      <title>How I Scope AI Automation Work in 30 Days</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 20 Aug 2026 07:25:40 +0000</pubDate>
      <link>https://dev.to/lamingsrb/how-i-scope-ai-automation-work-in-30-days-4l3p</link>
      <guid>https://dev.to/lamingsrb/how-i-scope-ai-automation-work-in-30-days-4l3p</guid>
      <description>&lt;h1&gt;
  
  
  How I Scope AI Automation Work in 30 Days
&lt;/h1&gt;

&lt;p&gt;Most AI automation projects do not fail because the model is weak. They fail because nobody decided what the system is allowed to do, what a correct result looks like, or who owns the edge cases before development starts. I have learned to treat scoping as engineering work, not a sales step.&lt;/p&gt;

&lt;p&gt;A 30-day delivery window can produce a real, useful AI system, but only when the first call turns a vague request into a bounded workflow with measurable acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  I begin by finding the expensive manual decision
&lt;/h2&gt;

&lt;p&gt;The first call is not about choosing Claude, OpenAI, LangGraph, or an agent framework. It is about identifying one recurring decision or handoff that costs time, creates delay, or causes errors, then defining what must happen before and after the AI touches it.&lt;/p&gt;

&lt;p&gt;Clients often arrive with a request like, “We need an AI agent for operations,” or “Can you automate our support workflow?” Those are directions, not requirements. “Agent” is usually shorthand for a frustrating process that has grown beyond what a spreadsheet, Zapier flow, or a person checking inboxes can handle.&lt;/p&gt;

&lt;p&gt;My first goal is to isolate the real unit of work.&lt;/p&gt;

&lt;p&gt;For example, a workflow may actually be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A customer submits a request through a form, email, or ticket.&lt;/li&gt;
&lt;li&gt;An employee reads it and identifies the category.&lt;/li&gt;
&lt;li&gt;They look up information in two or three systems.&lt;/li&gt;
&lt;li&gt;They decide whether the request is valid, urgent, or complete.&lt;/li&gt;
&lt;li&gt;They create a record, draft a reply, or route it to the right team.&lt;/li&gt;
&lt;li&gt;Someone checks the result because a wrong decision is expensive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is much more useful than “build an agent.” It tells me where the AI should classify, retrieve information, draft text, trigger an action, or stop and ask a human.&lt;/p&gt;

&lt;p&gt;On an initial call, I ask questions that force the workflow into the open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens today, step by step, from trigger to completion?&lt;/li&gt;
&lt;li&gt;How many times per week or month does this happen?&lt;/li&gt;
&lt;li&gt;Which part takes the most human time?&lt;/li&gt;
&lt;li&gt;What is the cost of a bad output?&lt;/li&gt;
&lt;li&gt;What systems hold the source of truth?&lt;/li&gt;
&lt;li&gt;Is the input structured, such as a form, or messy, such as email attachments?&lt;/li&gt;
&lt;li&gt;Can the system take action automatically, or should it only recommend an action?&lt;/li&gt;
&lt;li&gt;Who can make decisions when I need an answer within 24 hours?&lt;/li&gt;
&lt;li&gt;What would make you say the project worked after 30 days?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last question is the most important. If the answer is “we will know it when we see it,” I do not yet have a project I can responsibly quote.&lt;/p&gt;

&lt;p&gt;I have built unattended systems, including a four-system automation ecosystem that saved more than 73 hours per month. The work was not valuable because it contained AI. It was valuable because the systems had clear triggers, reliable handoffs, and an agreed definition of done.&lt;/p&gt;

&lt;h2&gt;
  
  
  I map the workflow before I choose an AI architecture
&lt;/h2&gt;

&lt;p&gt;I scope AI automation by drawing the current workflow and proposed workflow side by side. This exposes whether the problem needs an LLM, retrieval, deterministic rules, an integration, or a process change that should happen before any model call.&lt;/p&gt;

&lt;p&gt;A useful discovery artifact is not a long strategy deck. It is a one-page workflow map that makes ownership, data movement, and failure paths visible.&lt;/p&gt;

&lt;p&gt;Here is the level of detail I want before I recommend an architecture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow element&lt;/th&gt;
&lt;th&gt;Question I need answered&lt;/th&gt;
&lt;th&gt;Example decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;What starts the process?&lt;/td&gt;
&lt;td&gt;New Zendesk ticket, inbound email, scheduled job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;What data arrives and in what format?&lt;/td&gt;
&lt;td&gt;Ticket text, customer ID, PDF attachment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;Which system is authoritative?&lt;/td&gt;
&lt;td&gt;PostgreSQL, CRM, help desk, internal knowledge base&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI task&lt;/td&gt;
&lt;td&gt;What judgment is being delegated?&lt;/td&gt;
&lt;td&gt;Classify, extract, summarize, draft, rank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;What can the system change?&lt;/td&gt;
&lt;td&gt;Create ticket, update CRM field, send draft for approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human review&lt;/td&gt;
&lt;td&gt;When must a person intervene?&lt;/td&gt;
&lt;td&gt;Low confidence, missing data, financial or legal impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;How do we prove it worked?&lt;/td&gt;
&lt;td&gt;Audit log, before-and-after sample, SLA report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is where I separate &lt;strong&gt;AI assistance&lt;/strong&gt; from &lt;strong&gt;autonomous action&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A system that drafts a support response and waits for approval has a very different risk profile from one that changes account status or sends a customer-facing message on its own. Both can be useful. The mistake is pretending they deserve the same architecture, testing plan, and delivery estimate.&lt;/p&gt;

&lt;p&gt;For a narrow internal workflow, I often start with deterministic orchestration around one LLM step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event arrives
  -&amp;gt; validate required fields
  -&amp;gt; retrieve approved context
  -&amp;gt; ask model for structured output
  -&amp;gt; validate output against schema and business rules
  -&amp;gt; either take allowed action or route to human review
  -&amp;gt; log input, decision, action, and outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an agentic workflow in the practical sense. It may include planning or tool use, but it does not need a free-running autonomous agent that can invent its own sequence of actions.&lt;/p&gt;

&lt;p&gt;I use RAG only when the answer depends on a body of internal knowledge that cannot fit reliably in a prompt. If the workflow only needs a customer record and a handful of fields from a database, direct retrieval through an API is usually better than building a vector database.&lt;/p&gt;

&lt;p&gt;For knowledge-heavy tasks, I define the retrieval problem explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What documents are approved for use?&lt;/li&gt;
&lt;li&gt;How often do they change?&lt;/li&gt;
&lt;li&gt;What is the document-level permission model?&lt;/li&gt;
&lt;li&gt;Does the answer need citations?&lt;/li&gt;
&lt;li&gt;What should happen when retrieval returns weak or conflicting evidence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production RAG work, hybrid search is often more reliable than vector search alone. Combining PostgreSQL full-text search with pgvector and reranking results through reciprocal rank fusion can prevent exact product names, ticket IDs, and policy terms from disappearing behind semantically similar but wrong passages.&lt;/p&gt;

&lt;h2&gt;
  
  
  I turn 30 days into four acceptance checkpoints
&lt;/h2&gt;

&lt;p&gt;A 30-day AI automation engagement is realistic for one bounded workflow with accessible systems, a responsive owner, and a decision maker who can resolve trade-offs quickly. It is not realistic for replacing an entire operations department, cleaning five years of data, and integrating every system at once.&lt;/p&gt;

&lt;p&gt;I organize delivery around weekly evidence, not weekly status updates. At the end of every week, the client should be able to see something concrete, test it, and change direction while the change is still cheap.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Delivery checkpoint&lt;/th&gt;
&lt;th&gt;What I need from the client&lt;/th&gt;
&lt;th&gt;Evidence of progress&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Workflow specification and technical design&lt;/td&gt;
&lt;td&gt;Access path, sample inputs, business owner decisions&lt;/td&gt;
&lt;td&gt;Approved scope, test cases, architecture diagram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Working vertical slice&lt;/td&gt;
&lt;td&gt;Sandbox credentials, representative data&lt;/td&gt;
&lt;td&gt;One input travels through the full proposed path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Integration, controls, and evaluation&lt;/td&gt;
&lt;td&gt;Feedback on outputs and exceptions&lt;/td&gt;
&lt;td&gt;Measured test set, logs, human-review path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Production release or controlled pilot&lt;/td&gt;
&lt;td&gt;Deployment approval, owner for operations&lt;/td&gt;
&lt;td&gt;Runbook, monitoring, handover, acceptance review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Week 1: Define the narrowest useful system
&lt;/h3&gt;

&lt;p&gt;The output from week one is a short scoping document. Mine usually contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The business outcome and baseline process&lt;/li&gt;
&lt;li&gt;In-scope and explicitly out-of-scope work&lt;/li&gt;
&lt;li&gt;System diagram and integration boundaries&lt;/li&gt;
&lt;li&gt;Data classification and access assumptions&lt;/li&gt;
&lt;li&gt;Acceptance criteria&lt;/li&gt;
&lt;li&gt;Known risks and unresolved decisions&lt;/li&gt;
&lt;li&gt;Delivery checkpoints&lt;/li&gt;
&lt;li&gt;Operating ownership after launch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I write acceptance criteria as observable behavior. “AI should classify tickets accurately” is weak. “For the agreed test set, the system must return one of six approved categories, include a reason and source references where applicable, route uncertain cases to review, and never update a ticket without a valid category” is testable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 2: Build the vertical slice
&lt;/h3&gt;

&lt;p&gt;I want one real input to travel through the entire system early. That means trigger, retrieval, model call, validation, output, and logging, even if the interface is ugly and the dataset is small.&lt;/p&gt;

&lt;p&gt;This catches the problems that diagrams hide: an API does not expose the field we expected, source documents have inconsistent permissions, tickets contain unexpected formats, or the organization does not agree on the categories it wants the AI to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 3: Test the failure modes, not just the happy path
&lt;/h3&gt;

&lt;p&gt;By week three, I stop judging the system by impressive examples. I test missing fields, duplicate events, ambiguous requests, stale documents, API failures, retries, long inputs, and model outputs that are syntactically valid but operationally wrong.&lt;/p&gt;

&lt;p&gt;AWS frames one of its reliability design principles as “Automatically recover from failure.” I agree with that principle, but for AI systems I add a condition: recover automatically only when the action is reversible and the system has enough evidence to proceed. For everything else, fail visibly and route the case to a person. The &lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Reliability Pillar&lt;/a&gt; is a useful reference for designing those operational controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 4: Ship with ownership, not just credentials
&lt;/h3&gt;

&lt;p&gt;A deployed endpoint is not a shipped system. I consider the work shipped when there is a deployment path, monitoring, a runbook, access ownership, and a clear answer to “what happens when this fails at 2 a.m.?”&lt;/p&gt;

&lt;p&gt;For serverless AI architecture, that often means an event trigger, queue or retry strategy, Lambda workers, managed secrets, structured logs, and alerts for failure rate or backlog growth. In an AWS and Zendesk integration I built, operational reliability was central to delivering the first SLA compliance for that workflow. The integration mattered, but the visible state, retries, and escalation path mattered just as much.&lt;/p&gt;

&lt;h2&gt;
  
  
  I quote the boundary, the risk, and the operating cost
&lt;/h2&gt;

&lt;p&gt;A useful AI automation quote describes a system boundary and a delivery outcome. It should not sell an unlimited promise to “build AI agents” without defining the data, integrations, evaluation method, and support model.&lt;/p&gt;

&lt;p&gt;I avoid fixed pricing against an undefined problem. That does not protect the client or the engineer. It rewards ambiguity at the beginning, then creates tension when real constraints emerge.&lt;/p&gt;

&lt;p&gt;Before I quote, I need answers in five areas:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Workflow value
&lt;/h3&gt;

&lt;p&gt;I estimate time saved from the current process, not from optimistic AI claims. If three people spend 15 minutes each on 200 requests per month, that is 150 hours of manual effort before considering rework or delay.&lt;/p&gt;

&lt;p&gt;The actual savings may be lower if the workflow still needs human review. That is fine. A system that safely removes 60% of repetitive work is often more valuable than a fully autonomous system nobody trusts.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Integration reality
&lt;/h3&gt;

&lt;p&gt;Every external system changes the estimate. API quality, authentication, rate limits, sandbox availability, webhooks, and the ability to write data back all affect delivery risk.&lt;/p&gt;

&lt;p&gt;I have seen simple-looking integrations become difficult because the source system had no reliable event trigger, no stable identifier, or no way to distinguish a retry from a new request. Those details are scoping facts, not implementation trivia.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Data and security
&lt;/h3&gt;

&lt;p&gt;I need to know whether the AI will process customer information, internal documents, regulated data, credentials, or financial records. I do not assume that because a team can paste data into a chatbot, the same data can be sent through a production API.&lt;/p&gt;

&lt;p&gt;I document where data is stored, which providers process it, retention requirements, access roles, and whether prompts or outputs must be redacted. When the policy is unclear, that decision belongs with the organization’s security and legal owners before I build.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Evaluation
&lt;/h3&gt;

&lt;p&gt;LLM application development needs an evaluation plan before launch. I ask for a representative test set, including normal cases and bad cases. For a classification workflow, I want known labels. For a RAG assistant, I want questions with expected supporting sources. For a drafting workflow, I want a reviewer rubric.&lt;/p&gt;

&lt;p&gt;Without this, teams tend to evaluate generative AI by reading a few impressive outputs. That is how a demo becomes a production surprise.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Operations after delivery
&lt;/h3&gt;

&lt;p&gt;I clarify whether I am delivering a managed system, handing over infrastructure, or supporting a limited stabilization period. The system needs an owner on the client side regardless. Someone must decide when a prompt change is acceptable, when a knowledge source is outdated, and when a failed action requires manual cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  I decline work when the conditions make a good result unlikely
&lt;/h2&gt;

&lt;p&gt;I decline AI implementation work when the project is framed as a shortcut around missing ownership, poor source data, or an unresolved business decision. An LLM can help process information, but it cannot make an organization agree on its own rules.&lt;/p&gt;

&lt;p&gt;These are the red flags I take seriously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No process owner.&lt;/strong&gt; If nobody owns the workflow, nobody can resolve edge cases or accept the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No access to representative data.&lt;/strong&gt; I cannot validate an AI proof of concept using only invented examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The desired action is high-risk and irreversible.&lt;/strong&gt; Automatically sending legal, financial, employment, or account-changing decisions without an approved control model is not a reasonable first engagement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“It needs to be fully autonomous” is a requirement before the workflow is understood.&lt;/strong&gt; Autonomy is an architectural choice, not a maturity badge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The project has no measurable outcome.&lt;/strong&gt; “Use AI more” is not an outcome. Reduced handling time, faster response, fewer routing errors, or improved SLA compliance are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A buyer wants a fixed promise but cannot provide a decision maker.&lt;/strong&gt; A 30-day project cannot spend 10 days waiting for access approval or basic process answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The team expects the model to compensate for bad data without changing the process.&lt;/strong&gt; Sometimes it can tolerate messy inputs. It cannot create a trustworthy source of truth from conflicting records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Declining a project is not about being difficult. It is often the most useful advice an AI automation consultant can give. I would rather define a smaller pilot that has a chance of working than deliver an expensive system that becomes another unused dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do before starting an AI automation project
&lt;/h2&gt;

&lt;p&gt;If I were hiring an AI engineer, a fractional AI engineer, or an AI integration consultant for a 30-day engagement, I would insist on five things before signing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;One workflow, one owner, one measurable outcome.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A written list of systems and data required to build it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A decision on human review and allowed automated actions.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A representative test set, not just a few ideal examples.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weekly checkpoints where working software is demonstrated.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would also start narrower than feels comfortable. The first custom AI agent for a business should earn trust by handling one repeatable job well. Once it has reliable inputs, observable outputs, and a clear owner, expanding to adjacent workflows is much easier.&lt;/p&gt;

&lt;p&gt;That is how I have approached my own work on BizFlowAI ContentStudio and other autonomous systems. The useful part is not that a model can generate content, classify requests, or call tools. The useful part is the operating loop around it: input controls, evaluation, feedback, retries, and a way to improve based on real outcomes.&lt;/p&gt;

&lt;p&gt;A good AI MVP is not a miniature version of every future feature. It is the smallest system that proves a valuable workflow can run safely in the real environment.&lt;/p&gt;

&lt;p&gt;The first call should leave both sides with more than enthusiasm for AI agents. It should produce a shared view of the workflow, the risks, the delivery boundary, and the evidence required to call the project successful. If you are planning an AI automation or LLM application and want to compare notes on the scope, you can reach me through &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;my contact page&lt;/a&gt; or explore more of my writing on the blog.&lt;/p&gt;

</description>
      <category>aiautomation</category>
      <category>howtoscopeaiautomationprojects</category>
      <category>llmworkflowautomation</category>
      <category>aiagentforoperations</category>
    </item>
    <item>
      <title>@Claude In Slack Is Four Components. Here's The Whole Build.</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 20 Aug 2026 06:12:44 +0000</pubDate>
      <link>https://dev.to/lamingsrb/claude-in-slack-is-four-components-heres-the-whole-build-4781</link>
      <guid>https://dev.to/lamingsrb/claude-in-slack-is-four-components-heres-the-whole-build-4781</guid>
      <description>&lt;h1&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; In Slack Is Four Components. Here's The Whole Build.
&lt;/h1&gt;

&lt;p&gt;Anthropic shipped &lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; as a first-class Slack participant and LinkedIn spent a week calling it a paradigm shift. It's four components: a webhook, a history fetch, a context assembler, and an async job runner. If you're staring at a $30-per-seat quote wondering whether you actually need it — or you're the builder whose clients keep forwarding you that quote — this is the teardown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four components, drawn end-to-end
&lt;/h2&gt;

&lt;p&gt;The mention-your-bot pattern has one novel piece and three pieces of plumbing that have shipped in production since GitHub open-sourced Hubot in 2015. Here they are in order of execution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mention webhook.&lt;/strong&gt; Slack, Telegram, Discord, WhatsApp Business — every one of them POSTs to an HTTPS endpoint the instant a user tags your bot. ~20 lines of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel history fetch.&lt;/strong&gt; On mention, you call the platform's &lt;code&gt;conversations.history&lt;/code&gt; (Slack) or &lt;code&gt;getUpdates&lt;/code&gt; (Telegram) endpoint and pull the last N messages. That's your immediate context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context assembly.&lt;/strong&gt; Take that history, optionally join it with a jobs table or vector store for anything older than the channel window, and build a single prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async job runner with callback.&lt;/strong&gt; Claude can take 20–40 seconds. Slack expects a 200 within 3. So you ack fast, enqueue the real work in Redis, and a worker posts the reply back into the thread when it finishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the system. The "remembers across days" magic that everyone got excited about? A &lt;code&gt;jobs&lt;/code&gt; table with a &lt;code&gt;thread_id&lt;/code&gt; column and a &lt;code&gt;SELECT&lt;/code&gt;. Not new technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each component actually costs to run
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Webhook receiver: FastAPI or Express, one route, ~$0 marginal.&lt;/li&gt;
&lt;li&gt;History fetch: 1 API call per mention, rate-limited but free.&lt;/li&gt;
&lt;li&gt;Context assembler: pure Python/TS, no external cost.&lt;/li&gt;
&lt;li&gt;Worker + Redis: 1 process, ~150 MB RAM on a $6 VPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Component 1 &amp;amp; 2: webhook and history in ~40 lines
&lt;/h2&gt;

&lt;p&gt;Here's a stripped Slack handler. It verifies the signature, acks in under a second, pulls the last 20 messages, and drops the job on a queue.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebClient&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDIS_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/slack/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Signature verification omitted for brevity — do NOT ship without it
&lt;/span&gt;    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url_verification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;challenge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;challenge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&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;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app_mention&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conversations_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lpush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth calling out. First, always verify the &lt;code&gt;X-Slack-Signature&lt;/code&gt; header — &lt;a href="https://api.slack.com/authentication/verifying-requests-from-slack" rel="noopener noreferrer"&gt;Slack's docs walk through the HMAC&lt;/a&gt;. Skip it and anyone can POST fake mentions to your endpoint. Second, that &lt;code&gt;conversations_history&lt;/code&gt; call is your entire "context window" for zero-effort setups. Twenty messages is usually plenty; if a thread runs long, switch to &lt;code&gt;conversations_replies&lt;/code&gt; with the &lt;code&gt;thread_ts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component 3: context assembly is where the product actually lives
&lt;/h2&gt;

&lt;p&gt;This is the part the launch video skipped, and it's the only piece where you have real leverage. Fetching 20 messages and shoving them at Claude gets you a demo. Joining those messages against your CRM, your invoice history, and last week's on-call notes gets you a product.&lt;/p&gt;

&lt;p&gt;A reasonable prompt assembler 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;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;thread_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;prior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT question, answer FROM jobs &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE thread_id=%s AND status=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDER BY created_at ASC LIMIT 10&lt;/span&gt;&lt;span class="sh"&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;thread_id&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&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;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q: &lt;/span&gt;&lt;span class="si"&gt;{&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;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;A: &lt;/span&gt;&lt;span class="si"&gt;{&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;answer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;prior&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="s"&gt;You are the ops bot for Acme Inc.
Prior thread context:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(none)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Recent channel messages:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

The user just asked: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Answer concisely. If you need CRM or invoice data, call the appropriate tool.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jobs&lt;/code&gt; table is the "persistence across days" that got framed as a feature. Schema:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;column&lt;/th&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;uuid&lt;/td&gt;
&lt;td&gt;primary key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;thread_id&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Slack &lt;code&gt;thread_ts&lt;/code&gt; / Telegram &lt;code&gt;message_thread_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;channel&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;question&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;user's tagged message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;answer&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;worker output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;td&gt;enum&lt;/td&gt;
&lt;td&gt;queued / running / done / failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;created_at&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;ordering + TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's it. That is the memory system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component 4: async worker with callback
&lt;/h2&gt;

&lt;p&gt;Slack's 3-second ack rule is the reason you cannot answer inline. Every production build of this pattern uses a queue. Redis + one worker process handles thousands of mentions a day on a $6 box.&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="c1"&gt;# worker.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebClient&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDIS_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;claude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;brpop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

    &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;thread_ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO jobs(thread_id, question, answer, status) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALUES (%s, %s, %s, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&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;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;answer&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;Real numbers from a client bot I run on Telegram with this exact shape: median mention-to-reply is 8.2 seconds, p95 is 19 seconds, and the box (a $6/mo VPS with 1 vCPU / 1 GB RAM) sits at 4% CPU handling roughly 400 mentions/day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure modes you actually hit in production
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Claude times out or 529s — retry twice with exponential backoff, then post an apology into the thread instead of leaving the user hanging.&lt;/li&gt;
&lt;li&gt;Slack rate-limits &lt;code&gt;conversations.history&lt;/code&gt; at &lt;a href="https://api.slack.com/apis/rate-limits" rel="noopener noreferrer"&gt;Tier 3 (~50/min)&lt;/a&gt; — cache per-channel for 30 seconds.&lt;/li&gt;
&lt;li&gt;Worker dies mid-job — mark jobs &lt;code&gt;running&lt;/code&gt; with a heartbeat; re-queue anything running &amp;gt;60s.&lt;/li&gt;
&lt;li&gt;Duplicate mention deliveries — Slack retries on non-200; dedupe on &lt;code&gt;event_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The $3,600 vs $150 math
&lt;/h2&gt;

&lt;p&gt;Let's price the exact same capability two ways for a 10-person team.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;Anthropic Team plan&lt;/th&gt;
&lt;th&gt;Self-hosted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seats (10 × $30/mo)&lt;/td&gt;
&lt;td&gt;$3,600/yr&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS ($6/mo)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$72/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API usage (~400 msgs/day, Sonnet)&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;~$60–90/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis (bundled on VPS)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain + TLS (Let's Encrypt)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$12/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total year one&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3,600&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$150&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a 24× gap for a pattern that a competent operator wires up in a weekend. And the self-hosted version isn't locked to Slack — the same four components drop into Telegram for teams that already live there, WhatsApp Business for field ops, or Discord for community businesses. "Meet users where they work" stops being a marketing line and becomes an architecture decision.&lt;/p&gt;

&lt;p&gt;Two honest caveats on that math. If your team already pays for Anthropic Team for the Claude web app, the Slack integration is a free add-on and the calculation flips. And if your ops person bills at $150/hr, a 16-hour build is $2,400 — still cheaper than year one, break-even happens fast, but it's not zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should pay Anthropic and who should build
&lt;/h2&gt;

&lt;p&gt;Pay Anthropic if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a regulated shop with SOC 2 / HIPAA obligations and a security team that won't approve a custom bot with &lt;code&gt;chat:write&lt;/code&gt; on your workspace.&lt;/li&gt;
&lt;li&gt;Procurement is a 6-week gate and you don't have infra ownership.&lt;/li&gt;
&lt;li&gt;Nobody on your team wants to own a worker process at 2 AM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build it if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a dev-friendly operator (or you are one).&lt;/li&gt;
&lt;li&gt;Your workflows need to touch your CRM, invoicing system, or internal database — off-the-shelf can't do that.&lt;/li&gt;
&lt;li&gt;You want the same bot on two or more channels (Slack + WhatsApp is the most common combo I see).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The primitive is not the moat. The integration into your specific workflow is the moat. A bot that knows your customers, has read/write access to your pipeline, and can actually close a loop — that's worth building. A bot that summarizes the last 20 messages in a channel is worth $0 because in twelve months every messaging platform will ship a first-party version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;Most of what we ship for clients is exactly this pattern, extended: a mention-driven bot on Slack, Telegram or WhatsApp that reads a specific channel, joins the message against the client's CRM or invoicing system, drafts a reply or triggers a workflow (send quote, update status, schedule follow-up), and logs everything to a jobs table the operator can audit. The four-component skeleton in this post is the starting point. What clients pay for is the integration into their stack — the part Anthropic's $30/seat plan doesn't touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your one action this week
&lt;/h2&gt;

&lt;p&gt;Open your team's messaging tool. Find the one question that gets asked three times a week where someone always answers by pulling context from two other places. That's your first tag-your-AI target. Write the four components against it: which webhook, which history call, what context sources, what callback action. If you can name all four in a paragraph, you have a spec. A spec is 2–3 days of work for a builder who's done it before.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>slackaibot</category>
      <category>claudeslackintegration</category>
      <category>selfhostedchatbot</category>
      <category>mentionwebhook</category>
    </item>
    <item>
      <title>I/O 2026 Put An Agent In Chrome. 2 Of 3 Users Broke It In 4</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 20 Aug 2026 06:12:41 +0000</pubDate>
      <link>https://dev.to/lamingsrb/io-2026-put-an-agent-in-chrome-2-of-3-users-broke-it-in-4-322j</link>
      <guid>https://dev.to/lamingsrb/io-2026-put-an-agent-in-chrome-2-of-3-users-broke-it-in-4-322j</guid>
      <description>&lt;h1&gt;
  
  
  Chrome's Agent Ships Without A Seatbelt. Here's The Server-Side Fix.
&lt;/h1&gt;

&lt;p&gt;Google I/O 2026 dropped an AI agent into every Chrome tab. I ran it across a shared invoicing tool with four teammates for one afternoon. Two of them fired actions they never approved, one auto-sent an invoice to a real client, and none of it was logged anywhere I could pull. If your ops team shares browser profiles to do actual work, the governance hole is bigger than the productivity win.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three I/O 2026 updates, ranked for an ops team (not a developer)
&lt;/h2&gt;

&lt;p&gt;Every dev recap ranks these by API surface. Here's the ranking that matters if your bookkeeper gets sued when the agent misfires: &lt;strong&gt;Skills is the highest-risk update, WebMCP is second, Built-in AI is third&lt;/strong&gt;. Skills lets a non-technical user install a packaged workflow from a blog post and trigger it from the address bar — same threat model as installing random Chrome extensions on the accounting laptop, except the install friction is one click and the permissions UI is unreadable.&lt;/p&gt;

&lt;p&gt;Here's what each one actually does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Update&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;SMB risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebMCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sites expose tools an agent can call directly from the browser tab&lt;/td&gt;
&lt;td&gt;Agent gets write access to SaaS tools using the logged-in session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built-in AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini Nano runs locally, inference is free and offline&lt;/td&gt;
&lt;td&gt;Agent acts without a network round-trip you can log at the edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-click packaged workflows installed from any URL&lt;/td&gt;
&lt;td&gt;Non-technical user grants permissions they can't read; no rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every dev recap I read this week framed these as productivity. Fine for a solo builder. For a 3-person ops team sharing one Chrome profile against a live invoicing tool, the ranking flips. Skills is where a mistake hits a client. WebMCP is where your own SaaS becomes the weapon. Built-in AI is the reason your normal network logs won't show you what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke in 4 hours of real testing
&lt;/h2&gt;

&lt;p&gt;I ran this last week: three shared accounts on the invoicing tool we use daily, four users, one browser profile pattern, real client data. Not a sandbox. Within four hours, two of the three users triggered unintended actions.&lt;/p&gt;

&lt;p&gt;The specific failure that still bothers me: one teammate installed a Skill that was supposed to "prep invoice drafts from selected line items." She hovered over a draft to read it. The Skill interpreted the hover as intent-to-send. The invoice went out. To a real client. For the wrong project.&lt;/p&gt;

&lt;p&gt;Here's what Chrome did &lt;strong&gt;not&lt;/strong&gt; give me after the fact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No default event log for agent actions in the browser&lt;/li&gt;
&lt;li&gt;No email confirmation before the send&lt;/li&gt;
&lt;li&gt;No undo / rollback&lt;/li&gt;
&lt;li&gt;No permission-scope UI a non-developer could read to figure out what the Skill was allowed to do&lt;/li&gt;
&lt;li&gt;No server-side notification that this request came from an agent vs. a human click&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only reason I know the send happened is the client replied asking why the invoice was for the wrong project. That's the audit trail. A confused email from a customer.&lt;/p&gt;

&lt;p&gt;If you're small and shared-browser is your reality, this is the failure mode you inherit by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things to do this week
&lt;/h2&gt;

&lt;p&gt;Don't wait for Google to ship a governance layer. Do these three things on Monday.&lt;/p&gt;

&lt;h3&gt;
  
  
  The checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disable Skills on any shared browser profile.&lt;/strong&gt; Managed policy on the profile, or just turn it off in settings on every machine the accounting/ops team touches. Treat a Skill install like installing a random Chrome extension on the finance laptop — because functionally it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you own the web app your team uses, do not expose write actions through WebMCP.&lt;/strong&gt; Read-only tools are fine (list invoices, search clients, summarize a report). Anything that sends, charges, deletes, refunds, or notifies a customer needs a human click the agent cannot fake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log every agent-originated request server-side&lt;/strong&gt; with the user, the tool called, the arguments, and a timestamp. Chrome will not do this for you. Your backend has to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third one is the boring work nobody showed a slide for at the keynote. It's also the difference between "we caught it in 5 minutes" and "the client emailed us three days later."&lt;/p&gt;

&lt;h2&gt;
  
  
  How to expose a WebMCP tool safely: a real pattern
&lt;/h2&gt;

&lt;p&gt;Here's the pattern I use when a client asks to expose their SaaS to browser agents. Read-only endpoints are unrestricted. Write endpoints require a &lt;strong&gt;confirmation token&lt;/strong&gt; that only a human interaction can generate.&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="c1"&gt;# server-side: FastAPI example
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;PENDING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# token -&amp;gt; {user, action, args, expires}
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendInvoiceArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;client_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mcp/invoice/prepare_send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SendInvoiceArgs&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="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(...)):&lt;/span&gt;
    &lt;span class="c1"&gt;# Agent can call this freely. It does NOT send.
&lt;/span&gt;    &lt;span class="c1"&gt;# It returns a token + a preview the user must confirm in-app.
&lt;/span&gt;    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preview_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/confirm/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/invoice/confirm_send/{token}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;confirm_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(...)):&lt;/span&gt;
    &lt;span class="c1"&gt;# This endpoint is ONLY callable from a real form submit in the app UI,
&lt;/span&gt;    &lt;span class="c1"&gt;# protected by CSRF. The agent cannot forge the CSRF token.
&lt;/span&gt;    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&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;pending&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired or invalid&lt;/span&gt;&lt;span class="sh"&gt;"&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;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... actually send the invoice ...
&lt;/span&gt;    &lt;span class="nf"&gt;audit_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&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;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human_confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two properties matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The MCP-exposed endpoint (&lt;code&gt;prepare_send&lt;/code&gt;) never mutates state. Worst case, the agent spams your pending table, which you rate-limit.&lt;/li&gt;
&lt;li&gt;The confirming endpoint requires a CSRF token that only your app's real UI issues. An agent driving the browser can click a button, so add a short-lived one-time code shown in a modal — a human reads it and types it, an agent using DOM automation reads it too. If you need real resistance, require WebAuthn on the confirm step. Now the agent physically can't complete it without a hardware key touch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the "seatbelt" the keynote didn't ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit log Chrome won't give you
&lt;/h2&gt;

&lt;p&gt;The other thing your backend has to do: mark every request that originated from an agent and log it separately from human traffic. Chrome doesn't set a standard header for this yet, but you can enforce your own convention on any tool you expose via WebMCP.&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="c1"&gt;# middleware: tag agent-originated requests
&lt;/span&gt;&lt;span class="nd"&gt;@app.middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tag_agent_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;is_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-mcp-client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sec-agent-initiated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;is_agent&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;is_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;audit_write&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-user-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-mcp-client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&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;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship this before you expose a single write tool. When something misfires — and it will — you need to answer three questions in under 60 seconds: &lt;em&gt;which user, which agent, what did it call, with what arguments&lt;/em&gt;. If your log can't answer those, you're the person emailing the client an apology while grep-ing nginx access logs at 11pm.&lt;/p&gt;

&lt;p&gt;Two more practical rules I enforce on client projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit per-user, per-tool.&lt;/strong&gt; An agent that fires 40 &lt;code&gt;prepare_send&lt;/code&gt; calls in 10 seconds is not a workflow, it's a bug. Cap it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversibility window on destructive actions.&lt;/strong&gt; Sends, deletes, charges — hold them in a 30-second "sent, but revocable" state and show a banner in the app. This is what Gmail's undo-send taught us. The agent can trigger, but the human still has a physical window to catch it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The gap that decides who wins the SMB market
&lt;/h2&gt;

&lt;p&gt;Here's the hot take. The agentic web is a governance problem before it's a productivity win. Whoever ships the audit layer, the permission-scope UI a non-developer can actually read, and a real undo button for browser-native agents will own the SMB segment for the next five years. Not the model vendors. Not Google. The person who makes it safe for a bookkeeper to install a Skill without hosing a client relationship.&lt;/p&gt;

&lt;p&gt;Google's own &lt;a href="https://developers.google.com/machine-learning/responsible-ai" rel="noopener noreferrer"&gt;Agent Safety guidance&lt;/a&gt; talks about human-in-the-loop for consequential actions. Fine as a principle. The Chrome shipping surface as of I/O 2026 does not enforce it — that's on you to add at the app layer and the profile-management layer.&lt;/p&gt;

&lt;p&gt;If you're a small team, the near-term move is defensive: turn Skills off on shared profiles, expose only read tools via WebMCP, log everything server-side, and put human-confirmation gates on anything that touches a customer. If you're building a SaaS product, the opportunity is offensive: be the vendor that ships the confirmation flow, the per-tool audit stream, and the undo window. That's the market gap I see every week in real SMB deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this lands in practice
&lt;/h2&gt;

&lt;p&gt;I ship browser and workflow automation into small US ops teams every week — invoicing, CRM writes, email triage, lead follow-up. The pattern in this post (read-only MCP, human-confirmed writes, server-side audit, per-tool rate limits, revocable-send windows) is roughly the default checklist we apply before any agent touches a client's live SaaS. Most of the "agent went rogue" stories I hear from SMB owners resolve to two missing pieces: no confirmation gate on write actions, and no server-side log tagged with which agent called what. Both are a weekend of backend work, not a rewrite.&lt;/p&gt;

&lt;p&gt;The keynote sold you the agent. Nobody sold you the seatbelt. Build the seatbelt before you let the agent drive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>webmcpsecurity</category>
      <category>browseragentpermissions</category>
      <category>gemininanolocalai</category>
      <category>chromeskillsrisks</category>
    </item>
    <item>
      <title>17 Hires, 0 Pre-Start Ghosts, $1.62: The Post-Signature</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:12:40 +0000</pubDate>
      <link>https://dev.to/lamingsrb/17-hires-0-pre-start-ghosts-162-the-post-signature-39oj</link>
      <guid>https://dev.to/lamingsrb/17-hires-0-pre-start-ghosts-162-the-post-signature-39oj</guid>
      <description>&lt;h1&gt;
  
  
  17 Hires, 0 Pre-Start Ghosts, $1.62: The Post-Signature Agent
&lt;/h1&gt;

&lt;p&gt;Every ATS and monday.com recruiter agent stops the moment the offer is countersigned. That silence is where I was losing 23% of hires on the last cohort — counteroffers landed in the seven-day gap between signature and start date. Here's the agent I built to own that window, the exact architecture, and the token receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap every recruiting stack ignores
&lt;/h2&gt;

&lt;p&gt;The post-signature to day-one window is where hires quietly evaporate — 23% of my last cohort updated LinkedIn within 72 hours of signing and fielded a counteroffer before they ever received a laptop. Every tool in the recruiting stack optimizes the funnel up to signature. Sourced, screened, interviewed, offered, signed. Green dashboard. Then nothing. No equipment ticket, no Slack invite, no payroll form, no manager intro, no welcome doc.&lt;/p&gt;

&lt;p&gt;The old employer, meanwhile, sees the LinkedIn update within a day and starts a counter. You're not competing with the market anymore — you're competing with a manager who has a week of unopposed access to someone you already spent about $4,000 closing (recruiter time, interview loops, offer negotiations). If one in four walks, your true cost-per-hire is 33% higher than your ATS reports.&lt;/p&gt;

&lt;p&gt;The lesson from running this end-to-end: the bottleneck isn't screening 500 resumes. It's the 168 hours between signed and started. Nobody automates that window because it doesn't feel like a "recruiting" problem — it sits between HR, IT, finance, and the hiring manager, and everyone assumes someone else is running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trigger and state: DocuSign → n8n → Supabase
&lt;/h2&gt;

&lt;p&gt;The whole system runs off a single source of truth: one row in a Supabase table called &lt;code&gt;onboarding_state&lt;/code&gt;, written the moment the offer is countersigned in DocuSign. Every subsequent agent run reads and writes to that row, which kills the two problems that break most multi-step agent workflows — lost context and duplicated messages.&lt;/p&gt;

&lt;p&gt;DocuSign fires a Connect webhook on envelope completion. n8n receives it, extracts candidate fields, and inserts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;onboarding_state&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;candidate_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;candidate_email&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;start_date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;manager_email&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;equipment_profile&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;interview_notes&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;day_count&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'day_zero'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sentiment_score&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_touchpoint_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;The &lt;code&gt;status&lt;/code&gt; field only ever has four values: &lt;code&gt;day_zero&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;flagged&lt;/code&gt;, &lt;code&gt;started&lt;/code&gt;. The &lt;code&gt;day_count&lt;/code&gt; gets incremented by whichever cron ran last. &lt;code&gt;sentiment_score&lt;/code&gt; stores the JSON from the day-three sensor. That's it — no separate tables for messages, no external state store. If an agent crashes mid-run, the next cron just reads the row again and picks up.&lt;/p&gt;

&lt;p&gt;Why Supabase and not a Google Sheet? Two reasons. Row-level locking so two crons can't collide on the same candidate, and a real SQL layer so the day-N query is a one-liner instead of a full-sheet scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six-day cron sequence
&lt;/h2&gt;

&lt;p&gt;Six n8n cron triggers, one per day from day one through day six. Each cron runs at 9 a.m. local, queries Supabase for &lt;code&gt;day_count = N AND status = 'active'&lt;/code&gt;, and fires a Claude agent per matching row with a day-specific job. Splitting into six discrete crons instead of one long workflow means a failure on day three doesn't block day four for other candidates.&lt;/p&gt;

&lt;p&gt;Here's what each day actually does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Systems touched&lt;/th&gt;
&lt;th&gt;Avg tokens/hire&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Equipment order + Google Workspace + Slack&lt;/td&gt;
&lt;td&gt;IT ticketing API, Google Admin, Slack&lt;/td&gt;
&lt;td&gt;~1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Payroll forms email + pre-filled DocuSign&lt;/td&gt;
&lt;td&gt;Email, DocuSign, finance inbox&lt;/td&gt;
&lt;td&gt;~900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Warm check-in from hiring manager (sensor)&lt;/td&gt;
&lt;td&gt;Email, sentiment webhook&lt;/td&gt;
&lt;td&gt;~4,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;(rest day — no touchpoint by design)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Manager 1:1 scheduling&lt;/td&gt;
&lt;td&gt;Google Calendar, Calendly-style link&lt;/td&gt;
&lt;td&gt;~1,100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Personalized welcome doc&lt;/td&gt;
&lt;td&gt;Email, Google Docs&lt;/td&gt;
&lt;td&gt;~3,200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Day one is where the operational commitments show up. The agent reads &lt;code&gt;role&lt;/code&gt; and &lt;code&gt;location&lt;/code&gt;, picks the laptop spec from a config file (a senior engineer in Austin gets a different SKU than a support rep in Denver), files the IT ticket with the shipping address, provisions the Google Workspace account, and drops the hire into the right Slack channels with a founder-signed intro. The candidate wakes up on day two knowing their laptop is on the way and sees a welcome from the CEO in &lt;code&gt;#new-hires&lt;/code&gt;. That single sequence kills 60% of the counteroffer temptation on its own.&lt;/p&gt;

&lt;p&gt;Day two is the boring but critical one — payroll forms specific to their state and employment type (W-2 vs. 1099 vs. international contractor), a pre-filled DocuSign envelope so they're not hunting for their SSN in three tabs, and finance CC'd so nothing falls through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day three: the sentiment sensor
&lt;/h2&gt;

&lt;p&gt;Day three is the only touchpoint designed to generate a reply, and the reply is the sensor for the whole seven-day window. The agent writes a short, non-templated note from the hiring manager that references something specific from the interview notes and asks one open question. When the candidate replies, sentiment analysis fires and either lets the sequence continue or triggers a recruiter alert.&lt;/p&gt;

&lt;p&gt;The generation prompt is boring on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are drafting a short check-in email from {manager_name} to {candidate_name}
who signed 3 days ago and starts on {start_date}. 

Read these interview notes and reference ONE specific thing they said they were
excited about. Do not use the word "excited." Do not use "just checking in."

Ask exactly one open-ended question that invites a real reply, not a yes/no.

Notes: {interview_notes}

Keep it under 80 words. Sign as {manager_name}, no title, no signature block.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reply comes back into a separate n8n webhook. That webhook runs a second Claude call with a structured output prompt:&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="err"&gt;Rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;candidate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;three&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dimensions,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1-5&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;excitement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;clearly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;energized,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;flat/perfunctory)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hesitation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;raising&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;doubts/concerns,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;none&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;detectable)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;specificity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;engaged&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;specific&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;question,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;generic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"excitement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hesitation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"specificity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Reply:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;reply_body&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;Rules are hardcoded in the next n8n node: if &lt;code&gt;excitement &amp;lt; 3&lt;/code&gt; OR &lt;code&gt;hesitation &amp;gt; 3&lt;/code&gt;, flip &lt;code&gt;status&lt;/code&gt; to &lt;code&gt;flagged&lt;/code&gt; and fire a Telegram message to the recruiter with the candidate name, the reply, the JSON scores, and a one-paragraph rescue script generated on the fly from what the candidate actually said. The agent doesn't try to save the hire. It hands the recruiter a warm intervention with the exact language to use.&lt;/p&gt;

&lt;p&gt;Day three is also the most expensive step by a mile — 91¢ of the total $1.62 cohort spend, because the sentiment call carries the full interview notes as context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalation logic and the human handoff
&lt;/h2&gt;

&lt;p&gt;Automation stops the moment there's a real signal. Two escalation triggers fire independently of the cron schedule: 48-hour silence on any touchpoint, or a sentiment score outside the safe band on day three. Both route to the recruiter's Telegram with everything they need to act in one message.&lt;/p&gt;

&lt;p&gt;The silence detector is a separate n8n workflow that runs every 6 hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n Function node&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_touchpoint_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stale row triggers a Claude call that reads the last message sent, the candidate's role, and their interview notes, then drafts a suggested outreach in the recruiter's voice. Telegram message goes out with candidate name, days since last contact, the last message sent, and the suggested reply. The recruiter taps once, edits if needed, sends.&lt;/p&gt;

&lt;p&gt;On the last cohort of 17 hires, four Telegram alerts fired. The recruiter intervened on three of them. All three signed on to day one. The fourth was a false positive — the candidate was on a pre-start vacation and replied warmly two days later. That's the ratio I want: the system errs slightly toward escalation, because a false positive costs 90 seconds of recruiter attention and a false negative costs $4,000.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the numbers actually looked like
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;17 hires processed end-to-end&lt;/li&gt;
&lt;li&gt;43 automated touchpoints delivered (some hires triggered fewer than 6 based on role/location)&lt;/li&gt;
&lt;li&gt;0 pre-start ghosts (down from ~4 expected at the historical 23% rate)&lt;/li&gt;
&lt;li&gt;$1.62 total in Claude tokens across the whole cohort&lt;/li&gt;
&lt;li&gt;91¢ on day-three sentiment runs, 48¢ on welcome doc generation, 23¢ on everything else&lt;/li&gt;
&lt;li&gt;Runs on a Raspberry Pi 4 next to the main server, ~$0 infra beyond electricity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At $1.62 in tokens versus 3 counteroffers saved at ~$4,000 sunk cost each, the ROI math is not close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;Post-signature onboarding is exactly the kind of cross-system workflow bizflowai.io already builds for SMB clients — the boring wiring between DocuSign, an ATS, IT ticketing, Google Workspace, Slack, and finance that nobody on a 10-person team has time to own. When we ship an onboarding agent, it looks like the architecture above: one state row per hire, discrete daily jobs, sentiment on the human touchpoints, and Telegram escalation to a real person when something goes off-script. No dashboards to check, no rules engine to maintain — just receipts in a Supabase table and alerts when the recruiter needs to step in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>recruitingautomation</category>
      <category>newhireonboarding</category>
      <category>prestartghosting</category>
      <category>offertostartworkflow</category>
    </item>
    <item>
      <title>47 Take-Homes Reviewed for $18: The Hiring Step Nobody</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:12:36 +0000</pubDate>
      <link>https://dev.to/lamingsrb/47-take-homes-reviewed-for-18-the-hiring-step-nobody-5b3m</link>
      <guid>https://dev.to/lamingsrb/47-take-homes-reviewed-for-18-the-hiring-step-nobody-5b3m</guid>
      <description>&lt;h1&gt;
  
  
  47 Take-Homes Reviewed for $18: The Hiring Step Nobody Automates
&lt;/h1&gt;

&lt;p&gt;Your ATS vendor brags about sourcing, screening, and scheduling. None of those are where you lose candidates. You lose them in the 11-day silence after they submit the take-home, while your hiring manager stares at a folder of 47 zips and PDFs and picks the one on top.&lt;/p&gt;

&lt;p&gt;I built an agent for a client's internship cohort that reviews take-homes for 39 cents each, ranks them into Notion, and pings the hiring manager once a day. Offer-to-accept dropped from 19 days to 8. Here's the exact build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why take-home review is the real bottleneck
&lt;/h2&gt;

&lt;p&gt;Every mainstream recruiting tool — Greenhouse, Ashby, monday, Workable — automates the cheap steps and leaves the expensive one alone. Sourcing is cheap. Keyword-screening resumes is cheap. Sending a Calendly link is cheap. Reviewing a take-home requires judgment, so the vendors skip it, and it becomes an 11-day dead zone in the middle of your pipeline where the money leaks out.&lt;/p&gt;

&lt;p&gt;The client's numbers, no rounding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Applicants per cohort&lt;/td&gt;
&lt;td&gt;203&lt;/td&gt;
&lt;td&gt;203&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Take-home submissions&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Days to open the folder&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&amp;lt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Days to first reply&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manager time per candidate&lt;/td&gt;
&lt;td&gt;22 min&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offer-to-accept cycle&lt;/td&gt;
&lt;td&gt;19 days&lt;/td&gt;
&lt;td&gt;8 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review cost (Claude)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$18.33 total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of their top five picks from the previous cohort had already accepted elsewhere by the time the manager sat down to review. That's not a sourcing problem. That's a silence problem, and silence is the one thing automation is actually good at killing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack: n8n, Claude, Notion, Slack
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. The trigger is a Gmail label. An earlier step in the pipeline labels candidate submission emails as &lt;code&gt;take-home-submitted&lt;/code&gt;. n8n polls that label every 5 minutes and fires the workflow.&lt;/p&gt;

&lt;p&gt;The first node classifies the submission format, because candidates don't cooperate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~60% send a GitHub repo link&lt;/li&gt;
&lt;li&gt;~30% send a PDF or Google Doc&lt;/li&gt;
&lt;li&gt;~10% send a Loom video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each format needs a different extractor before you burn tokens on Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n Function node - route by submission type&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;githubMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/github&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;com&lt;/span&gt;&lt;span class="se"&gt;\/[\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/[\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loomMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/loom&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;com&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;share&lt;/span&gt;&lt;span class="se"&gt;\/[\w]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfAttachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;githubMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;githubMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;loomMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;loomMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;pdfAttachment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pdfAttachment&lt;/span&gt; &lt;span class="p"&gt;}}];&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For GitHub, I call the API and pull three things: the README, the three largest source files, and the last 20 commits. Not the whole repo — you'll blow the context window and pay for boilerplate. For PDFs, I convert to markdown first (I use &lt;code&gt;pdftotext&lt;/code&gt; piped through a cleanup script) because raw PDF parsing burns tokens and destroys structure. Loom links go through Whisper for a transcript before Claude ever sees them.&lt;/p&gt;

&lt;p&gt;That preprocessing is the difference between 39 cents per submission and $2+.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring rubric (this is where hand-waving fails)
&lt;/h2&gt;

&lt;p&gt;Every AI hiring demo I've watched shows a vague prompt like "evaluate this candidate submission." That gives you vague scores. The rubric has to be sharp, and it has to force a written justification, or the model will just regress to 3-out-of-5 on everything.&lt;/p&gt;

&lt;p&gt;Six criteria, scored 1-5, each with a mandatory one-sentence justification:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Problem understanding&lt;/strong&gt; — did they solve the actual problem or a nearby one?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical execution&lt;/strong&gt; — does it work, and is the approach reasonable?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code quality&lt;/strong&gt; — naming, structure, obvious code smells&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication clarity&lt;/strong&gt; — README, comments, PR description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge case handling&lt;/strong&gt; — what happens on bad input, empty state, failure?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope discipline&lt;/strong&gt; — did they overbuild or underbuild?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Max score: 30. Here's the actual system prompt (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a skeptical senior engineer reviewing an internship take-home.
You have seen hundreds of these. You are NOT here to be encouraging.

Score the submission on six criteria, 1-5 each. For each score, write
one sentence justifying it with a specific reference to the code or doc.

Then write a "concern" field: exactly two sentences on the single biggest
issue a hiring manager should look at before an interview. If there is no
real concern, say so - do not invent one.

Finally, flag one of:
  - CLEAN
  - MINOR_FLAG (small issues, worth noting)
  - MAJOR_FLAG (undisclosed AI use, plagiarism, non-working code)

Return JSON only. No preamble.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The concern field is the whole reason this is usable. A hiring manager doesn't want a score — they want to know &lt;em&gt;what to look at&lt;/em&gt;. Two sentences of "the auth handler swallows all exceptions silently and there are no tests around the token refresh path" is worth more than any number.&lt;/p&gt;

&lt;p&gt;The output schema:&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;"scores"&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="nl"&gt;"problem_understanding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"technical_execution"&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;"code_quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"communication_clarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"edge_case_handling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"scope_discipline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&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="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"concern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Error handling in the main request handler is missing entirely and there is no test coverage for the retry logic. Both are red flags for a role that will touch production ingestion."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MINOR_FLAG"&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;h2&gt;
  
  
  Notion card + the one Slack message that matters
&lt;/h2&gt;

&lt;p&gt;Step three writes a Notion card. Title is &lt;code&gt;&amp;lt;Candidate Name&amp;gt; — &amp;lt;Score&amp;gt;/30&lt;/code&gt;. Body has the six sub-scores, the concern paragraph, the flag, and a direct link back to the submission. Traffic-light status property:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt; — score &amp;gt;24 AND flag is CLEAN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yellow&lt;/strong&gt; — score 18-24 OR MINOR_FLAG&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red&lt;/strong&gt; — score &amp;lt;18 OR MAJOR_FLAG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Notion database has one saved view sorted by score descending, filtered to the current cohort. The hiring manager opens it once and sees a ranked list with greens at the top.&lt;/p&gt;

&lt;p&gt;Step four is the daily Slack ping. Once at 9am, if there are new reviewed submissions in the last 24 hours, the workflow posts a single DM:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Take-home review, Wed Aug 12&lt;/strong&gt;&lt;br&gt;
3 green candidates ready to schedule&lt;br&gt;
2 yellows to skim&lt;br&gt;
1 red to skip&lt;br&gt;
→ &lt;a href="https://dev.tonotion-link"&gt;Open ranked view&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. No per-submission notifications. No email digest. One message, one link, actionable. Notification fatigue is what kills these systems in month two, and I'd rather ship one signal a day than ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live run: 40 seconds, 34 cents
&lt;/h2&gt;

&lt;p&gt;To show it working end-to-end I submitted a fake take-home to the client's test inbox — a small Node project on GitHub. The Gmail trigger fires within 5 minutes. n8n picks it up, hits the GitHub API, pulls the README and the three main source files. Claude runs the rubric prompt. Total wall-clock time: about 40 seconds. Token cost from the log: $0.34.&lt;/p&gt;

&lt;p&gt;The Notion card comes back with a score of 22, yellow status, and the concern reads: &lt;em&gt;"The main handler has no try/catch and there is no test coverage on the transform step. Ask them to walk through their error strategy in the interview."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's a real concern I'd want a human to look at. It's not the model padding to seem thorough — it's pointing at two specific files and two specific gaps. That's the bar.&lt;/p&gt;

&lt;p&gt;Across the full cohort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;47 submissions reviewed&lt;/strong&gt; end-to-end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$18.33 total Claude spend&lt;/strong&gt; ($0.39 average)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14 hours of manager time saved&lt;/strong&gt; (22 min → 4 min per candidate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19 → 8 day&lt;/strong&gt; offer-to-accept cycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of the top three hires in that cohort said the fast turnaround was why they took the offer over a competing one. That's the number that pays for everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I got wrong on v1
&lt;/h2&gt;

&lt;p&gt;Pass these on so you don't repeat them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-rejecting reds was a mistake.&lt;/strong&gt; One candidate got flagged MAJOR_FLAG for undisclosed AI use — turned out they'd disclosed it in a section my parser skipped over. Reds now go to a human for a 30-second sanity check before any candidate-facing action fires. The cost of one false rejection on a top candidate is way higher than 30 seconds of manager time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ten criteria was too many.&lt;/strong&gt; My first rubric had ten. Scores blurred together and the justifications got repetitive. Six is the sweet spot — enough coverage to be fair, few enough that each score carries weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never skip the concern field.&lt;/strong&gt; I ran an early version that only produced numeric scores. Managers ignored it. The one-paragraph concern is what makes the hiring manager trust the ranking and stop re-reading every submission themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more, quieter lesson: the model is a &lt;em&gt;first reviewer&lt;/em&gt;, not a &lt;em&gt;decision maker&lt;/em&gt;. Every candidate-facing action — reject, invite, offer — still goes through a human. The agent's job is to make sure that human is looking at the right five submissions on day 1, not the wrong 47 on day 11.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;The take-home reviewer is one of about a dozen hiring-stage agents I've shipped for clients through bizflowai.io — panel schedulers, reference-call bots, offer-letter agents, ghost-prediction sentiment monitors. The pattern is always the same: find the step where a human is doing 20 minutes of judgment work on every candidate, extract the mechanical 80% of that judgment into a scored + flagged artifact, and leave the human to validate. It's not about replacing hiring managers. It's about making sure they're never the reason a good candidate walked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>hiringautomationworkflow</category>
      <category>takehomeassignmentscoring</category>
      <category>n8nclaudeintegration</category>
      <category>aicandidatescreening</category>
    </item>
    <item>
      <title>A Week as an AI Integration Consultant</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 13 Aug 2026 07:32:45 +0000</pubDate>
      <link>https://dev.to/lamingsrb/a-week-as-an-ai-integration-consultant-4p27</link>
      <guid>https://dev.to/lamingsrb/a-week-as-an-ai-integration-consultant-4p27</guid>
      <description>&lt;h1&gt;
  
  
  A Week as an AI Integration Consultant
&lt;/h1&gt;

&lt;p&gt;Most weeks I don't write much new code. I read other people's systems, draw arrows on a whiteboard, and try to figure out which of the twelve places customer data lives is the one I should actually trust. That is the honest shape of AI integration work in a B2B company that has been shipping since 2014. The LLM is the easy part. The plumbing is the job.&lt;/p&gt;

&lt;p&gt;Here are field notes from a recent week doing exactly this, plus the checklist I wish every founder had in hand before they hire anyone (me included) to bolt an LLM onto their stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday: Mapping the data flow before touching a prompt
&lt;/h2&gt;

&lt;p&gt;The client wanted "an AI assistant that answers customer questions from our knowledge base and CRM." That is a sentence, not a specification. My first day is almost always the same: I map where data actually lives, who writes to it, and how stale it is by the time anyone reads it.&lt;/p&gt;

&lt;p&gt;For this client the map ended up looking like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Write frequency&lt;/th&gt;
&lt;th&gt;Trust level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Salesforce&lt;/td&gt;
&lt;td&gt;Accounts, opportunities&lt;/td&gt;
&lt;td&gt;Sales reps, daily&lt;/td&gt;
&lt;td&gt;High but partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NetSuite&lt;/td&gt;
&lt;td&gt;Invoices, entitlements&lt;/td&gt;
&lt;td&gt;Nightly batch&lt;/td&gt;
&lt;td&gt;High, delayed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zendesk&lt;/td&gt;
&lt;td&gt;Tickets, macros&lt;/td&gt;
&lt;td&gt;Agents, real-time&lt;/td&gt;
&lt;td&gt;High for current, weak for history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confluence&lt;/td&gt;
&lt;td&gt;Internal KB&lt;/td&gt;
&lt;td&gt;Product team, weekly&lt;/td&gt;
&lt;td&gt;Medium, drifty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 2019 MySQL app&lt;/td&gt;
&lt;td&gt;Product config per customer&lt;/td&gt;
&lt;td&gt;Nightly cron&lt;/td&gt;
&lt;td&gt;Source of truth but ugly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MySQL app is the one nobody wanted to talk about. It is also the only place that knows which features a given customer is actually entitled to. If the assistant answers "yes, you have access to X" without reading that database, it will hallucinate entitlements and create a support fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule I now apply on day one:&lt;/strong&gt; find the ugliest system in the stack. That is almost always the real source of truth. The pretty SaaS on top is a view, not a fact.&lt;/p&gt;

&lt;p&gt;I don't touch a prompt until I have this map signed off. If the CTO can't tell me which system wins in a conflict between Salesforce and NetSuite on the same field, we are not ready to add an LLM. We are ready to have a meeting about data governance dressed up as an AI project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuesday: Picking the glue (queues, webhooks, or agents)
&lt;/h2&gt;

&lt;p&gt;By Tuesday I know the systems. Now I have to pick the glue. This is where most integrations quietly fail six months in, because someone reached for the wrong tool on day one and everything after that inherited the mistake.&lt;/p&gt;

&lt;p&gt;My rough decision tree, after doing this on and off for a decade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous HTTP call from the app to the LLM.&lt;/strong&gt; Use when the user is waiting for the answer and latency budget is under two seconds. Chat UIs, autocomplete, inline summarization. Fails badly for anything that touches more than two backend systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook + queue (SQS, EventBridge, or a Postgres-backed queue).&lt;/strong&gt; Use when work is triggered by an event (ticket created, invoice posted, form submitted) and the user does not need the answer in the same request. This is where 70% of real B2B integrations live. It is also where the money is, because it maps directly to "hours saved per month."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled worker.&lt;/strong&gt; Use for enrichment, backfills, nightly summaries, weekly digests. Cheap, boring, reliable. Underrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent loop (multi-step, tool-using).&lt;/strong&gt; Use only when the task genuinely requires branching decisions that a deterministic workflow cannot express. Most tasks people call "agentic" are actually a switch statement with anxiety.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this client I went with an EventBridge bus in front of Lambda workers, with Zendesk and Salesforce webhooks feeding in, and one small agent loop for a specific case (drafting a reply that needs to check entitlements, then pull the right KB passage, then decide whether to escalate). Everything else is a linear workflow, because linear workflows are debuggable at 2am and agent loops are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The brittle trap I avoid:&lt;/strong&gt; a prompt sitting inside a Zapier or Make step, calling an LLM, and writing back to a CRM with no queue, no retries, no idempotency key, and no audit log. It demos beautifully. It falls over the first time the LLM returns malformed JSON, and nobody can tell you which record got corrupted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wednesday: The retrieval layer nobody scopes for
&lt;/h2&gt;

&lt;p&gt;Wednesday is always retrieval. The client had 4,200 Confluence pages, 18,000 closed Zendesk tickets, and a product manual as a 380-page PDF. "Just point the AI at it" is a six-month project disguised as a sentence.&lt;/p&gt;

&lt;p&gt;What I actually built this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ingestion workers that pull from each source on its own cadence (Confluence via API on a 6-hour schedule, Zendesk via webhook on ticket close, PDF once with a manual re-ingest hook).&lt;/li&gt;
&lt;li&gt;A normalization step that strips boilerplate, chunks by semantic boundary (not fixed token count), and attaches metadata: source system, last modified, author, customer tier visibility.&lt;/li&gt;
&lt;li&gt;Storage in Postgres with pgvector for embeddings and a &lt;code&gt;tsvector&lt;/code&gt; column for full text search on the same rows.&lt;/li&gt;
&lt;li&gt;Hybrid retrieval at query time using Reciprocal Rank Fusion to merge vector and lexical results. This alone fixed a class of failures where pure vector search kept missing exact product names and error codes.&lt;/li&gt;
&lt;li&gt;A rerank pass with a small cross-encoder for the top 30 candidates before handing 6-8 to the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The number that matters here: on this client's evaluation set of 140 real historical questions, pure vector retrieval got 61% top-5 recall. Hybrid + rerank got 89%. That 28 point gap is the difference between "the assistant is useful" and "the assistant is a liability." It is also the reason I no longer take retrieval seriously if it is just a vector database and vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost-side note:&lt;/strong&gt; keeping retrieval in Postgres (instead of a dedicated vector DB) saved this client roughly $600 to $900 per month at their volume, and removed one vendor from the security review. For a mid-market B2B company that matters more than the theoretical benchmarks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thursday: Guardrails, evals, and the boring stuff that keeps you employed
&lt;/h2&gt;

&lt;p&gt;Thursday is the day I earn my rate. Anyone can wire an LLM to a CRM. Making it not embarrass the company is the actual skill.&lt;/p&gt;

&lt;p&gt;Three things I insist on before anything goes to production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A structured output contract, enforced twice.&lt;/strong&gt; The prompt asks for a specific JSON schema. The response is validated against a Pydantic or Zod schema before it touches a downstream system. On failure, the worker retries with an error-corrective prompt, up to two times, then drops to a dead-letter queue with the full trace. No silent failures, no half-written records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. An eval set that lives in the repo.&lt;/strong&gt; For this client, 140 questions with expected behaviors, run on every prompt change and every model change. Not just "does it answer correctly," but categorical: did it refuse when it should have refused, did it cite the right document, did it escalate the entitlement question. I run this in CI. A prompt change with a 3-point regression on the eval set does not ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. An audit log, always.&lt;/strong&gt; Every LLM call gets stored with input, output, model, cost, latency, retrieval context IDs, and the outcome downstream. This costs almost nothing in Postgres and it is the single most valuable artifact you will have when a customer emails asking why the assistant told them something wrong three weeks ago.&lt;/p&gt;

&lt;p&gt;The kind of thing that goes into a worker looks roughly 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;handle_ticket_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticket_id&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;entitlements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_entitlements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# from MySQL, not CRM
&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;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPLY_PROMPT&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;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;entitlements&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entitlements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DraftReply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# enforced
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&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="c1"&gt;# always, before any write
&lt;/span&gt;
    &lt;span class="k"&gt;if&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;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt; &lt;span class="ow"&gt;or&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;needs_human&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;assign_to_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&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;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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;post_internal_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&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="c1"&gt;# never auto-send week 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the last comment. Week one, the assistant never sends anything to a customer. It writes internal notes for agents to review. Week four, once the eval set and the audit log show it is behaving, we flip the switch on the low-risk categories. This is how you ship an LLM into a real business without a Slack channel full of angry executives on day two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friday: Handover and the parts I refuse to skip
&lt;/h2&gt;

&lt;p&gt;Friday is documentation and handover. This is where independent consultants lose clients (by writing nothing) and where I keep them (by writing too much, honestly).&lt;/p&gt;

&lt;p&gt;What I hand over on every engagement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An architecture diagram that matches what actually got built, not the one from the proposal.&lt;/li&gt;
&lt;li&gt;A runbook for the top 8 failure modes: LLM API down, retrieval empty, schema validation failing, webhook backlog, quota hit, downstream write failure, silent degradation, customer complaint triage.&lt;/li&gt;
&lt;li&gt;The eval set, with instructions to add to it every time something goes wrong in production. The eval set is a living asset. It is arguably the most valuable artifact of the entire project.&lt;/li&gt;
&lt;li&gt;Cost dashboards per workflow, so someone can answer "is this saving us money" without asking me.&lt;/li&gt;
&lt;li&gt;A 30-day, 60-day, and 90-day review checklist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clients who run the 30-60-90 reviews get compounding value. Clients who don't tend to let the whole thing quietly rot within a year. I now write this into the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I wish founders used before hiring anyone
&lt;/h2&gt;

&lt;p&gt;If you are a founder or CTO thinking about hiring an AI integration consultant, run through this before you write the brief. It will save you and your consultant a painful week.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name the outcome in hours or dollars.&lt;/strong&gt; "Reduce first-response time on tier-2 tickets by 40%" or "eliminate 20 hours per week of manual invoice reconciliation." Not "add AI."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List every system that holds the data the LLM will need.&lt;/strong&gt; Include the ugly ones. Especially the ugly ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify the source of truth for each conflicting field.&lt;/strong&gt; If two systems disagree on customer status, which wins? Write it down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide the failure mode.&lt;/strong&gt; When the LLM is wrong, what happens? Does it write to a customer, or draft for review? Week one should always be draft for review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget for retrieval and evals, not just prompts.&lt;/strong&gt; If the proposal is 80% "prompt engineering" and 20% everything else, the ratio is backwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask who owns the audit log.&lt;/strong&gt; If nobody, that's the first thing to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm you have 100+ real historical examples&lt;/strong&gt; the assistant would have handled. If you don't, the first two weeks are collecting them, not building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agree on the review cadence.&lt;/strong&gt; 30, 60, 90 days. In writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the consultant what they will not do.&lt;/strong&gt; If the answer is "anything," walk away.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd do if I were starting fresh today
&lt;/h2&gt;

&lt;p&gt;Start with the smallest workflow that touches real money or real customer time. One ticket type. One report. One approval step. Wire it end to end with a queue, a schema, an eval set, and an audit log. Ship it as an internal draft first, promote it to customer-facing only when the numbers say so. Then, and only then, look at the next workflow.&lt;/p&gt;

&lt;p&gt;The teams that get AI integration wrong try to boil the ocean with a chat interface. The teams that get it right pick one boring workflow, instrument it obsessively, and let the ROI compound. Everything I have shipped that survived past a year followed the second pattern.&lt;/p&gt;

&lt;p&gt;If you are wrestling with an integration like this and want a second pair of eyes, or you have already tried the prompt-in-a-Zap route and want to do it properly, get in touch at &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt;. More field notes from production systems live on the &lt;a href="https://lazar-milicevic.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aiintegrationconsultant</category>
      <category>llminproduction</category>
      <category>ragpipeline</category>
      <category>hybridretrievalpostgrespgvecto</category>
    </item>
    <item>
      <title>The Honest Numbers From My Last 5 AI Builds</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 13 Aug 2026 07:32:42 +0000</pubDate>
      <link>https://dev.to/lamingsrb/the-honest-numbers-from-my-last-5-ai-builds-5433</link>
      <guid>https://dev.to/lamingsrb/the-honest-numbers-from-my-last-5-ai-builds-5433</guid>
      <description>&lt;h1&gt;
  
  
  The Honest Numbers From My Last 5 AI Builds
&lt;/h1&gt;

&lt;p&gt;Most AI automation case studies read like brochures. Big percentages, no denominators, no mention of what broke or what cost more than it saved. I keep a spreadsheet for every system I ship: token spend, infra cost, hours reclaimed, and whether the thing actually earned its keep after six months. Here is that spreadsheet, translated into English, across my last five builds. Two of them underperformed. I will tell you which and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Builds At A Glance
&lt;/h2&gt;

&lt;p&gt;Before the deep dive, the summary table. Numbers are monthly averages over the first six months in production, or the full life of the build if it was retired earlier. Dollar figures are USD. "Time saved" is measured against the pre-automation baseline the operator actually tracked, not a guess.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Monthly infra + tokens&lt;/th&gt;
&lt;th&gt;Time saved / mo&lt;/th&gt;
&lt;th&gt;Est. $ value / mo&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Multi-agent content + SEO machine (ContentStudio)&lt;/td&gt;
&lt;td&gt;~$180&lt;/td&gt;
&lt;td&gt;45 h&lt;/td&gt;
&lt;td&gt;~$3,600&lt;/td&gt;
&lt;td&gt;Strong win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Serverless AWS + Zendesk SLA integration&lt;/td&gt;
&lt;td&gt;~$40&lt;/td&gt;
&lt;td&gt;18 h + first-ever SLA compliance&lt;/td&gt;
&lt;td&gt;~$1,800 + risk removed&lt;/td&gt;
&lt;td&gt;Strong win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Analytics migration + automated reporting&lt;/td&gt;
&lt;td&gt;~$95&lt;/td&gt;
&lt;td&gt;~14 h&lt;/td&gt;
&lt;td&gt;$2,500, 5,000/mo saved on prior tooling&lt;/td&gt;
&lt;td&gt;Strong win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Local-LLM RAG for internal docs (Ollama + pgvector)&lt;/td&gt;
&lt;td&gt;~$65 (hardware amortized)&lt;/td&gt;
&lt;td&gt;~6 h&lt;/td&gt;
&lt;td&gt;~$480&lt;/td&gt;
&lt;td&gt;Underperformed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Autonomous outbound qualifier (LLM + CRM)&lt;/td&gt;
&lt;td&gt;~$210&lt;/td&gt;
&lt;td&gt;~4 h&lt;/td&gt;
&lt;td&gt;~$320&lt;/td&gt;
&lt;td&gt;Killed at month 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two winners built around clear, repeated workflows. One infra win that paid for itself in software licenses. Two that did not earn their spot and taught me more than the wins did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 1: The Multi-Agent Content Machine
&lt;/h2&gt;

&lt;p&gt;This is BizFlowAI ContentStudio, the system that researches, writes, optimizes, and publishes across multiple sites without me in the loop. It has been running for over six months. The honest breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly cost:&lt;/strong&gt; roughly $180. That splits into ~$120 in LLM tokens (Claude for drafting and critique, a cheaper model for classification and metadata), ~$35 in serverless AWS (Lambda, EventBridge, S3, a small RDS instance), and ~$25 in third-party APIs for search data and rank tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time reclaimed:&lt;/strong&gt; 45 hours per month against the manual baseline of researching, drafting, editing, formatting, publishing, and internal linking. I timed this over two months before automation. The number is not inflated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the money actually goes:&lt;/strong&gt; the critique agent. I run a generate-then-critique-then-revise loop, and the critique pass consumes roughly 35% of total tokens. I could cut it. I have not, because the &lt;a href="https://lazar-milicevic.com" rel="noopener noreferrer"&gt;40% token reduction work I did earlier this year&lt;/a&gt; came from prompt compression and caching, not from removing the critique. Killing critique would save ~$40/mo and cost me an unknown amount in quality regressions. Not worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually made it work:&lt;/strong&gt; a self-learning loop that reads GSC data weekly, identifies which posts are gaining impressions, and biases the topic queue toward adjacent long-tails. That single feedback loop is why the system stopped producing vanity content and started producing content that gets impressions. Without it, this build would be in the "underperformed" pile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 2: Serverless AWS + Zendesk For SLA Compliance
&lt;/h2&gt;

&lt;p&gt;Smallest infra bill on the list, biggest strategic impact. The problem was simple: tickets were missing SLA because the routing logic lived in a human's head and that human went on vacation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly cost:&lt;/strong&gt; ~$40. Lambda invocations, EventBridge rules, a tiny DynamoDB table for state, CloudWatch for observability. Zero LLM tokens. Yes, this counts as an "AI automation build" because part of the routing uses a classifier, but the classifier runs on cached embeddings and costs pennies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; first-ever month of full SLA compliance for the team it served. That is the number the business cared about. The 18 hours per month of manual triage reclaimed was a side effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; the highest-ROI "AI" builds are often 90% boring event-driven plumbing and 10% intelligence. If you skip the plumbing and lean on the model to do everything, you get a demo, not a system. I wrote more about this in &lt;a href="https://lazar-milicevic.com" rel="noopener noreferrer"&gt;Agentic Workflows That Survive Real Inputs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 3: Analytics Migration And Automated Reporting
&lt;/h2&gt;

&lt;p&gt;This is the one that delivered EUR 30, 60k in annual savings (about $32k, 65k) by replacing a legacy analytics stack with a lighter, event-driven pipeline plus scheduled LLM-generated summaries for stakeholders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly cost:&lt;/strong&gt; ~$95 all-in. Warehouse queries, a small orchestrator, and roughly $30 in tokens for the weekly and monthly narrative reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the savings came from:&lt;/strong&gt; license consolidation, not the AI. The LLM narrative layer was the wedge that made stakeholders comfortable retiring the old tool ("but who will write the exec summary?"). Once that objection died, the migration paid for itself in the first quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest caveat:&lt;/strong&gt; if you subtract the license savings, the pure AI-generated-reporting value is maybe $400, 600/mo in analyst time. Real, but not the headline number. I try to keep those two things separate when I report to a client. Conflating them is how people lose trust in AI ROI claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 4: Local-LLM RAG That Underperformed
&lt;/h2&gt;

&lt;p&gt;This one hurt because I liked the architecture. Ollama running a mid-size open model, pgvector for embeddings, hybrid search with reciprocal rank fusion, a clean retrieval API. Technically sound. I wrote the &lt;a href="https://lazar-milicevic.com" rel="noopener noreferrer"&gt;hybrid search post&lt;/a&gt; based on this exact build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly cost:&lt;/strong&gt; ~$65 amortized (hardware plus power plus a small managed Postgres).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time saved:&lt;/strong&gt; only about 6 hours per month. Estimated value ~$480. Barely covers the cost when you include the maintenance I do on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it underperformed:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Query volume was too low.&lt;/strong&gt; The internal team it served asked the system maybe 40, 60 questions a week. RAG shines at scale. At low volume, people just Slack a colleague.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The corpus was not the bottleneck.&lt;/strong&gt; People were not blocked because they could not find docs. They were blocked because the docs did not exist for the questions they were asking. RAG cannot retrieve what was never written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust cold-start.&lt;/strong&gt; New users tried it twice, got one great answer and one mediocre one, and stopped. The mediocre answer was technically correct but poorly phrased. Retrieval quality was not the problem. Presentation was.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What I would do differently:&lt;/strong&gt; I would validate with a two-week Slack-bot-with-a-human-in-the-loop before building the full retrieval stack. If the team is not asking questions to a human proxy, they will not ask a bot either. That is a $5 test that would have saved me a $65/mo permanent bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 5: The Outbound Qualifier I Killed At Month 4
&lt;/h2&gt;

&lt;p&gt;An autonomous agent that ingested inbound leads, enriched them, ran a qualification prompt, and updated the CRM with a score plus a short reasoning paragraph. Sounds useful. Was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly cost:&lt;/strong&gt; ~$210. Higher than expected because enrichment API calls added up fast and the qualification prompt was long (roughly 3.5k input tokens per lead).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time saved:&lt;/strong&gt; ~4 hours per month. Value ~$320. Net negative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it failed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The qualification signal it produced correlated with the sales rep's own gut about 75% of the time. On the 25% where they disagreed, the rep won 8 out of 10. The model was confidently wrong on edge cases that actually mattered.&lt;/li&gt;
&lt;li&gt;Volume was too low to justify the ceremony. Roughly 60, 90 leads a month. A human can score that in an hour with a spreadsheet.&lt;/li&gt;
&lt;li&gt;The "reasoning paragraph" the model produced became a liability. Reps started trusting it, and when it was wrong, they had already sent an email based on a bad framing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I killed it at month 4. Refactored the useful parts (the enrichment layer and a much simpler rules-based scorer) into a 30-line script that costs $6/mo and does the job better. That script is still running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if a human can do the task in under an hour a month, do not build an agent for it. The floor cost of any agentic system, in tokens, tokens for retries, monitoring, evals, and your own attention, is higher than people admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What The Numbers Actually Say
&lt;/h2&gt;

&lt;p&gt;Three patterns show up across all five:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Token cost is rarely the problem. Fit is.&lt;/strong&gt; The build with the highest token spend (ContentStudio at $120/mo) is the biggest winner. The build with lower token spend (RAG at ~$20/mo in embeddings) underperformed. Fit to a real, repeated, high-volume workflow matters more than efficient prompting. Optimize prompts after you have proven fit, not before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Boring infra beats clever prompts.&lt;/strong&gt; The Zendesk SLA build has almost no LLM in it and produced the clearest strategic win. If you are pitching an AI project, ask yourself first whether the pain is actually an event routing problem, a data problem, or a workflow problem. If it is, solve that first and add intelligence where it genuinely helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The kill decision is the skill.&lt;/strong&gt; Killing build 5 at month 4 was the highest-ROI decision in this whole spreadsheet. It stopped a $210/mo bleed, freed my attention, and produced a better $6/mo replacement. Most teams keep zombie AI systems alive because someone got promoted for launching them. Do not do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do If I Were Starting Today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Track the baseline before you build.&lt;/strong&gt; Two weeks of honest time tracking. If you cannot show me the hours you spent on the task pre-automation, we cannot measure ROI post-automation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a kill threshold in writing.&lt;/strong&gt; Mine is: if a build does not clear 3x its monthly cost in reclaimed time or hard savings by month 3, it goes on a "fix or kill" list. Month 4 is decision time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument tokens per outcome, not tokens per call.&lt;/strong&gt; "This agent costs $0.04 per completed useful task" is a number a CFO can price. "The model costs $3 per million tokens" is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer event-driven serverless for anything that runs unattended.&lt;/strong&gt; Scale-to-zero economics change what is worth building. A build that costs $6/mo idle can sit there proving itself for a year without pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the losers.&lt;/strong&gt; If you only ever hear about the wins, you are being sold to.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Close
&lt;/h2&gt;

&lt;p&gt;These are the actual numbers from actual systems, not slide decks. If you are weighing whether an AI automation build is worth it inside your own company, I am happy to talk through the shape of it honestly, including telling you when the answer is "do not build this." Reach out at &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt;, or read more on the &lt;a href="https://lazar-milicevic.com" rel="noopener noreferrer"&gt;blog&lt;/a&gt; if you want the deeper technical write-ups behind any of these builds.&lt;/p&gt;

</description>
      <category>aiautomationcasestudies</category>
      <category>llmtokencost</category>
      <category>ragpipelinecost</category>
      <category>localllmrag</category>
    </item>
    <item>
      <title>112 Applicants, 47 Filtered on Salary First: 71¢ Total</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:12:57 +0000</pubDate>
      <link>https://dev.to/lamingsrb/112-applicants-47-filtered-on-salary-first-71c-total-3fi4</link>
      <guid>https://dev.to/lamingsrb/112-applicants-47-filtered-on-salary-first-71c-total-3fi4</guid>
      <description>&lt;h1&gt;
  
  
  47 of 112 Applicants Filtered on Salary Before Anyone Read a Resume
&lt;/h1&gt;

&lt;p&gt;You post one role. Twenty applications land. You book eight calls. Four of them collapse the moment you say the number. That's an hour of your week gone because the salary conversation happened at minute twenty-eight instead of minute zero — and the fix is a 40-line node most hiring tutorials skip on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why salary has to be the first filter, not the last
&lt;/h2&gt;

&lt;p&gt;The default hiring stack scores skills first and asks about money last. That's backwards for small teams. Skill match without comp alignment produces a prettier list of the same wasted calls — you still end the week with candidates who want $140K on a $95K band, or who ghost the moment you name the number.&lt;/p&gt;

&lt;p&gt;The bottleneck in solo and small-team hiring isn't finding qualified people. It's finding qualified people whose number fits your number. Every other filter — years of experience, stack fit, culture, portfolio depth — is downstream of that one gate. If comp doesn't overlap, everything else is theater.&lt;/p&gt;

&lt;p&gt;Most published n8n and Zapier "AI recruiter" flows optimize for the demo, not the outcome. They read the CV, score against a JD, dump a Kanban. The tools monday and Ashby ship look great in a screenshot. None of them run a comp-first router before scheduling. That's the piece I'm going to show you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bad path: intake → resume score → shortlist → intro call → salary reveal → collapse&lt;/li&gt;
&lt;li&gt;The right path: intake → salary extraction → three-way router → screening queue → intro call&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack, and why it's boring on purpose
&lt;/h2&gt;

&lt;p&gt;Boring stacks ship. Here's the wiring I use for clients running burst hires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gmail&lt;/strong&gt; — intake. One label: &lt;code&gt;applications-inbound&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; — orchestrator. Self-hosted, polls every 2 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Haiku&lt;/strong&gt; — extraction model. Cheap, fast, structured output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion&lt;/strong&gt; — talent database. Four custom fields (below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram&lt;/strong&gt; — human-in-the-loop for the ambiguous middle bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Runs on a small home server. No per-seat SaaS tax, no vendor lock-in, no waiting on a product team to ship the filter you actually need. Total infra cost for the client run below: the electricity to keep n8n up, plus 71¢ in Anthropic tokens across 112 candidates.&lt;/p&gt;

&lt;p&gt;One thing that matters more than it sounds: PDFs get converted to plain markdown &lt;em&gt;before&lt;/em&gt; the model sees them. Vision on a CV is a waste — you're paying vision-tier tokens to read Arial 11pt. A &lt;code&gt;pdf-parse&lt;/code&gt; node or &lt;code&gt;pdftotext&lt;/code&gt; shell step drops both latency and cost significantly versus shoving the raw PDF at a multimodal model.&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;# in an n8n Execute Command node&lt;/span&gt;
pdftotext &lt;span class="nt"&gt;-layout&lt;/span&gt; &lt;span class="s2"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt;$binary&lt;/span&gt;&lt;span class="s2"&gt;.data.fileName}}"&lt;/span&gt; - | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 12000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cap the character count. Nobody's comp signal is on page 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 40-line salary extraction node
&lt;/h2&gt;

&lt;p&gt;The prompt is narrow on purpose. Haiku isn't asked to score the candidate, summarize them, or judge fit. It's asked one thing: find any signal about compensation expectations.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit ranges&lt;/strong&gt; — "$85K–$95K", "looking for 110", "current base is 78"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft signals&lt;/strong&gt; — "competitive", "open to discussion", "market rate"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location inference&lt;/strong&gt; — SF Bay Area applying to a Kansas City mid-market role is a signal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employer-based inference&lt;/strong&gt; — "currently at [named FAANG]" narrows the expected floor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model returns three fields: extracted range, confidence 0–1, and the exact source snippet it pulled the signal from. That snippet is the whole ballgame — it's your audit trail when a hiring manager asks why a candidate got a polite auto-reply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n Function node — salary extractor (Anthropic call)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You extract ONLY compensation signals from job applications.

Return JSON:
{
  "comp_low_usd": number | null,
  "comp_high_usd": number | null,
  "confidence": number,        // 0.0 - 1.0
  "signal_type": "explicit" | "soft" | "inferred" | "none",
  "source_snippet": string     // exact text, max 240 chars
}

Rules:
- "competitive" / "open" / "market" =&amp;gt; soft, confidence &amp;lt;= 0.5
- Explicit range in cover letter =&amp;gt; explicit, confidence &amp;gt;= 0.9
- Location + role level inference only =&amp;gt; inferred, confidence &amp;lt;= 0.6
- No signal at all =&amp;gt; "none", confidence 1.0, snippet ""
- NEVER guess a number without textual grounding.

APPLICATION:
"""
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

--- CV ---
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cv_markdown&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
"""`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;httpRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.anthropic.com/v1/messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANTHROPIC_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic-version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2023-06-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&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;That's the whole extraction. ~40 lines with the wrapper. Average tokens per candidate in the real run: ~1,400 in, ~120 out. Per-candidate cost sits well under a cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-way router (this is where most people ship the wrong thing)
&lt;/h2&gt;

&lt;p&gt;Binary routers — match or reject — quietly send great candidates polite rejection emails because a soft signal got misread. The fix is a third bucket for ambiguity.&lt;/p&gt;

&lt;p&gt;Route logic in plain English:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidence ≥ 0.75 AND range overlaps band&lt;/strong&gt; → Notion screening queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence ≥ 0.75 AND range clearly outside band&lt;/strong&gt; → polite auto-reply naming the band, inviting future contact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence &amp;lt; 0.75 OR &lt;code&gt;signal_type == "none"&lt;/code&gt;&lt;/strong&gt; → Telegram alert to hiring manager with name, CV link, snippet, approve/reject buttons
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n IF chain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;comp_low_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;comp_high_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BAND_LOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;85000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BAND_HIGH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;105000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;overlaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;comp_low_usd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;comp_high_usd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;comp_low_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;BAND_HIGH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;comp_high_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;BAND_LOW&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="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal_type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;human_review&lt;/span&gt;&lt;span class="dl"&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;overlaps&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;advance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto_reject&lt;/span&gt;&lt;span class="dl"&gt;'&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;The auto-reply is honest, not corporate. "Thanks for applying. Our budget for this role tops out at $105K. If that shifts for you, let us know and we'll pick it back up." That message has produced two hires in the last year from candidates who came back three months later with adjusted expectations. A generic "we'll keep your resume on file" burns that bridge.&lt;/p&gt;

&lt;p&gt;The Telegram alert for the ambiguous bucket takes the hiring manager ~10 seconds per tap. Phone, thumb, done. No laptop, no inbox archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Notion schema that makes this defensible
&lt;/h2&gt;

&lt;p&gt;Your talent database needs four fields you probably don't have today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comp_low_usd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Filter, sort, band analytics later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comp_high_usd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extraction_confidence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number (0–1)&lt;/td&gt;
&lt;td&gt;Sort ambiguous records for weekly review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_snippet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Non-negotiable — the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The source snippet is the difference between a black-box filter you can't defend and a system your hiring manager trusts. When someone asks &lt;em&gt;why&lt;/em&gt; a candidate got auto-declined, you paste the exact sentence from their cover letter. That single field killed every "the AI is rejecting people unfairly" conversation on the client engagement below.&lt;/p&gt;

&lt;p&gt;Add a fifth field — &lt;code&gt;routing_decision&lt;/code&gt; — with the value the router produced. Now you have a queryable log: how many auto-rejects last month, how many false positives caught in the Telegram bucket, what the model's confidence distribution looks like. That's how you tune the 0.75 threshold up or down based on actual error rate, not vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real numbers from one client run
&lt;/h2&gt;

&lt;p&gt;Three-week window, one senior individual-contributor role, US remote, $85K–$105K band.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total applicants through the label&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-filtered on comp mismatch (≥ 0.75 conf)&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flagged to Telegram for human review&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced to screening queue&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total Anthropic token spend&lt;/td&gt;
&lt;td&gt;$0.71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg time from email arrival to routing decision&lt;/td&gt;
&lt;td&gt;~90 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intro calls that collapsed on salary&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of the 47 auto-filtered: 31 were explicitly asking for $130K+, 9 were in high-cost metros with inferred floors above $115K, 7 named a current employer where the floor was public. Of the 12 flagged for review: 8 got approved, 4 got rejected — the model correctly abstained.&lt;/p&gt;

&lt;p&gt;The hiring manager stopped taking calls with people whose number was going to blow up minute twenty-eight. That's it. That's the whole win. The system didn't hire anyone — it just stopped wasting the calendar on candidates the math already ruled out.&lt;/p&gt;

&lt;p&gt;For comparison: monday's recruiting agent, Ashby's AI screening, and most of the "n8n recruiter" YouTube builds do resume scoring first and comp negotiation never. They optimize the wrong variable. A comp-first router in front of any of them would make them meaningfully better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;This is the class of workflow I ship for clients weekly at &lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;bizflowai.io&lt;/a&gt; — the boring, high-leverage filters that sit in front of the expensive human step. Comp-first hiring routers, invoice triage that flags mismatches before they hit AP, lead qualifiers that reject on budget signal before booking discovery calls. The pattern is always the same: one narrow extraction model, a three-way router with a human-in-the-loop bucket for the ambiguous middle, and an audit trail so the decision is defensible. No SaaS seat tax, no vendor lock-in, runs on infra you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Put the comp filter &lt;em&gt;before&lt;/em&gt; the resume filter, not after&lt;/li&gt;
&lt;li&gt;Convert PDFs to markdown before the model reads them&lt;/li&gt;
&lt;li&gt;Return a &lt;code&gt;source_snippet&lt;/code&gt; on every extraction — it's your audit trail&lt;/li&gt;
&lt;li&gt;Never ship a binary router; the third bucket for ambiguity is where you stop breaking things&lt;/li&gt;
&lt;li&gt;Use the auto-reject reply to name the band honestly — you'll get candidates back later&lt;/li&gt;
&lt;li&gt;Log the router decision to Notion; tune your confidence threshold on real error rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is ~200 lines of n8n JSON, one Anthropic key, and a Notion database. Total cost to run 112 candidates: 71 cents plus the electricity. Total calls saved: probably 6–8. Do the math on what your hour is worth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>aihiringpipeline</category>
      <category>salaryscreeningautomation</category>
      <category>n8nhiringworkflow</category>
      <category>resumescreeningai</category>
    </item>
    <item>
      <title>Claude Tag's 'Multi-Day' Mode Is A $4K Billing Trap</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:12:53 +0000</pubDate>
      <link>https://dev.to/lamingsrb/claude-tags-multi-day-mode-is-a-4k-billing-trap-13do</link>
      <guid>https://dev.to/lamingsrb/claude-tags-multi-day-mode-is-a-4k-billing-trap-13do</guid>
      <description>&lt;h1&gt;
  
  
  Claude Tag's "Persistence Over Days" Is A Scheduled Loop With Your Credit Card Attached
&lt;/h1&gt;

&lt;p&gt;Anthropic shipped Claude Tag with a phrase that made it go viral: &lt;em&gt;persistence over days&lt;/em&gt;. Tag Claude in Slack, walk away, get pinged Monday. Sounds like a teammate. It's a cron job with a wallet. I've built three async agent systems for clients this year, and the exit condition on every viral demo is the same thing: an LLM deciding, in a loop, when the job is done. Here's what breaks and what to build instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "persistence over days" actually is under the hood
&lt;/h2&gt;

&lt;p&gt;Persistence over days is three components stitched together: a state store holding conversation and tool history, a scheduler that re-invokes the agent on cron or event triggers, and a tool loop where the model calls a function, reads the result, calls another function, and keeps going until it decides it's done. Every re-invocation replays context. Every tool call costs tokens. The exit condition is a probability distribution.&lt;/p&gt;

&lt;p&gt;That last part is the one that should scare you. You are trusting a language model to know when a job is finished, in a loop, with your billing key attached. There is no &lt;code&gt;while (task.done)&lt;/code&gt; you can inspect in a debugger. The model emits a stop token when its own weights say the work looks complete. Sometimes that's after 3 tool calls. Sometimes 47. Sometimes it re-reads the same doc four times because retrieval returned near-duplicates.&lt;/p&gt;

&lt;p&gt;Three things fail here in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context replay tax.&lt;/strong&gt; Each wake-up ships the accumulated conversation back into the prompt. A 40K-token thread that runs six times over a weekend is 240K tokens billed, minimum, before any new work happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool loop drift.&lt;/strong&gt; The model tries an API, gets a 429, "reasons" about retry, tries again, calls a different tool, comes back. Real logs from my systems show 8-12x more tool calls than a human would use for the same task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent completion.&lt;/strong&gt; The model returns a final message that reads plausible. Nobody verifies whether the work was actually done correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real math: $0.02 per task vs $2-$15 per task
&lt;/h2&gt;

&lt;p&gt;Here are numbers from a system I run for a US-based SMB client, roughly 40 async tasks a day across email triage, CRM updates, and lead enrichment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Capped async task (my pattern)&lt;/th&gt;
&lt;th&gt;Unbounded persistent run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg cost per task&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;td&gt;$2 – $15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg latency&lt;/td&gt;
&lt;td&gt;8 seconds&lt;/td&gt;
&lt;td&gt;Minutes to hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token ceiling&lt;/td&gt;
&lt;td&gt;5K – 50K per run&lt;/td&gt;
&lt;td&gt;None visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-call ceiling&lt;/td&gt;
&lt;td&gt;8 – 20 per run&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily spend (40 tasks)&lt;/td&gt;
&lt;td&gt;~$0.80&lt;/td&gt;
&lt;td&gt;$80 – $600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly spend (single user)&lt;/td&gt;
&lt;td&gt;~$24&lt;/td&gt;
&lt;td&gt;$2,400 – $18,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now put that inside a Slack workspace with 30 people casually tagging the bot into threads. Nobody is watching individual runs. Nobody knows the difference between a tag that costs 2 cents and one that spins for six hours. You land the invoice, and it's $4,000 you didn't budget for.&lt;/p&gt;

&lt;p&gt;The bill is the loud failure. The quiet one is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quiet-wrongness problem is the real liability
&lt;/h2&gt;

&lt;p&gt;The agent posts a confident reply in a Slack thread. Someone on your team reads it, trusts it, and acts on it. They send the customer email. They approve the refund. They update the CRM record. There is no audit trail pointing to a checkpoint because there was no checkpoint. The human is now the one who executed the wrong action, and they did it because the bot sounded sure.&lt;/p&gt;

&lt;p&gt;This is the pattern I see fail most often in async agent deployments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bot replies confidently in a channel where 8 people are watching.&lt;/li&gt;
&lt;li&gt;One person reads it while doing something else, treats it as verified work.&lt;/li&gt;
&lt;li&gt;Action gets executed downstream in another system.&lt;/li&gt;
&lt;li&gt;Two weeks later a customer complains, and there is no log tying the wrong action back to the model version, the prompt, or the tool calls that produced it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic's demo skipped who reviews the output before it becomes a message the whole team acts on. That question has exactly one right answer in a real business: a named human, reviewing in the interface they already live in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four checkpoints I bake into every async agent
&lt;/h2&gt;

&lt;p&gt;Before the first token is spent on any async task I ship, four hard limits are already in the code. These are non-negotiable, and they are what separates a working system from a launch video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Max-token cap per run.&lt;/strong&gt; Fails loud, not silent. Between 5,000 and 50,000 tokens depending on the job. The wrapper aborts and pings me if we hit it.&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="n"&gt;MAX_TOKENS_PER_RUN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15_000&lt;/span&gt;
&lt;span class="n"&gt;MAX_TOOL_CALLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&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;total_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_TOKENS_PER_RUN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;BudgetExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hit &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MAX_TOKENS_PER_RUN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;tool_calls&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_TOOL_CALLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LoopExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hit &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MAX_TOOL_CALLS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total_tokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_tokens&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Max-loop counter.&lt;/strong&gt; The agent gets 8, 12, or 20 tool calls, forced stop, forced report. If it hasn't finished, that's information — a human decides whether to extend, not the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Mandatory human checkpoint before any write action.&lt;/strong&gt; Sending an email, updating a CRM record, pushing an invoice, posting to a wide channel. Everything with side effects pauses, pings a human on Telegram or WhatsApp, waits for a thumbs up. The agent &lt;em&gt;proposes&lt;/em&gt;. The human &lt;em&gt;approves&lt;/em&gt;. Only then does anything hit production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Per-run and per-day spend ceiling.&lt;/strong&gt; Checkpoint every 60 seconds of runtime or every 10 cents of spend, whichever comes first. If the bot hits $5 in a day for a single user, it stops and asks. Not silently. Loud.&lt;/p&gt;

&lt;p&gt;Every run also gets a structured log: model version, prompt hash, tool calls made, human who approved each write action, final token count. If something goes wrong two weeks later, I can reconstruct exactly what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually copy from Claude Tag (and what to skip)
&lt;/h2&gt;

&lt;p&gt;The tag-and-forget UX is genuinely good. Users don't want another dashboard. They want to mention the bot in the tool they already use and get pinged when work is done. That interaction pattern is maybe 50 lines of code on top of the Slack, Telegram, or WhatsApp API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy:&lt;/strong&gt; the mention-and-notify pattern, threaded replies for context isolation, one clear "done" ping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy:&lt;/strong&gt; async job queue so the user isn't blocked waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip:&lt;/strong&gt; unbounded runtime. Every job has a hard wall-clock and token ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip:&lt;/strong&gt; the model deciding when it's done on write actions. A human decides for anything that touches money, customer data, or an outbound message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip:&lt;/strong&gt; trusting the reply text as verified work. Every action that matters gets a checkpoint ping to a named human before execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard part is not the UX. It's the guardrail layer underneath. Async, yes. Persistent across days with no checkpoint, no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes I see teams make on their first async agent
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No spend alarm at the API-key level.&lt;/strong&gt; Set a hard monthly limit in the Anthropic console and a daily soft alert to Slack or email. Every provider gives you this. Use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One shared API key across all agents.&lt;/strong&gt; You cannot attribute cost. Give every agent its own key, tag every request with a &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;task_id&lt;/code&gt; in metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging only the final response.&lt;/strong&gt; Log every tool call, every token count, every model version. If you can't reconstruct a run, you can't debug it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human approval by email.&lt;/strong&gt; Approvals need to happen where the person already checks 20 times a day. For most SMB operators, that's Telegram or WhatsApp, not another dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating the agent's reply as an audit trail.&lt;/strong&gt; The reply is output. The log is truth. Never confuse them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where bizflowai.io fits
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;bizflowai.io&lt;/a&gt; we build these async agents for solopreneurs and small teams with the four checkpoints baked in from day one. Every deployment ships with token caps, loop limits, human approval routing to Telegram or WhatsApp, and structured logging that lets the operator see what was spent, what was decided, and who approved each write action. Clients running our systems handle 40+ async tasks a day for under a dollar, with zero silent failures reaching production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prediction
&lt;/h2&gt;

&lt;p&gt;Six months from now, the Twitter threads will show up. The $4K bill screenshots. The Slack channels full of confidently wrong replies that got acted on. Teams quietly turning the feature off. Async agents are the right direction. Unbounded persistent agents without checkpoint gates are a liability the vendor has offloaded onto the customer. If you're a founder, your job is to put the gates back in before you turn it on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>aiagentguardrails</category>
      <category>asyncaiautomation</category>
      <category>llmcostcontrol</category>
      <category>humanintheloopapproval</category>
    </item>
    <item>
      <title>4 Files Into One Human-Reviewed Interview Brief</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 10 Aug 2026 06:12:29 +0000</pubDate>
      <link>https://dev.to/lamingsrb/4-files-into-one-human-reviewed-interview-brief-41k2</link>
      <guid>https://dev.to/lamingsrb/4-files-into-one-human-reviewed-interview-brief-41k2</guid>
      <description>&lt;h1&gt;
  
  
  4 Files, One Interview Brief: The Bot That Preps the Human
&lt;/h1&gt;

&lt;p&gt;Your hiring manager opens the CV in one tab, hunts through Gmail for the application thread in another, digs up the role scorecard in a shared drive, and joins the call three minutes late still not sure what to ask. That is 15 minutes of tab-switching before a 30-minute interview. This post walks through the exact bot I build for clients that turns those four scattered inputs into a single evidence-linked briefing packet — without letting the model touch a hiring decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that keeps this safe: the bot preps humans, it does not judge them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This bot prepares the interviewer. It does not score, rank, reject, advance, or message the candidate.&lt;/strong&gt; That boundary is the entire reason the workflow is worth building first. Every automation vendor demo I have seen starts at the riskiest step — resume scoring — which is exactly where bias, EEOC exposure, and bad candidate experience live. Flip the order. Automate the boring information-gathering that your hiring manager is doing badly at 8:57 AM, and leave the judgment where it belongs.&lt;/p&gt;

&lt;p&gt;Practically, that means the bot has three hard "no" rules baked into the system prompt and the workflow logic itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No numeric fit score, no "recommend hire / no hire" language, no ranked comparison across candidates.&lt;/li&gt;
&lt;li&gt;No candidate-facing output. The bot writes to the interviewer only.&lt;/li&gt;
&lt;li&gt;No stage changes in the ATS. If you use Greenhouse, Ashby, or a spreadsheet, the bot reads. A human writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you skip this framing, the same code becomes a compliance problem the moment a rejected candidate asks how the decision was made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 inputs, and the boring identifier problem that breaks most builds
&lt;/h2&gt;

&lt;p&gt;The four inputs are: &lt;strong&gt;calendar event, candidate CV, application form response, and role scorecard.&lt;/strong&gt; The one people skip is the scorecard, and without it the model produces a fluent summary that has nothing to do with what you are hiring for.&lt;/p&gt;

&lt;p&gt;Keep the scorecard to five fields so a hiring manager will actually maintain it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example (customer support role)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Role title&lt;/td&gt;
&lt;td&gt;Senior Support Specialist, SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must-have&lt;/td&gt;
&lt;td&gt;Fluent written English, 2+ yrs ticketing (Zendesk/Intercom), B2B SaaS exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nice-to-have&lt;/td&gt;
&lt;td&gt;SQL basics, technical writing samples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concerns to verify&lt;/td&gt;
&lt;td&gt;US timezone overlap, availability start date, examples of angry-customer handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interview areas&lt;/td&gt;
&lt;td&gt;Written comms sample, live triage exercise, escalation judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second boring problem: &lt;strong&gt;one clean identifier per candidate.&lt;/strong&gt; Email is the only reliable one. Names collide, LinkedIn URLs get edited, phone numbers are missing half the time. If your calendar event only has a name, matching becomes a guessing game the moment you interview two people named David Chen in the same quarter.&lt;/p&gt;

&lt;p&gt;The fix is a title convention, not code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interview — Jane Doe — jane.doe@gmail.com — Support Specialist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or put the email in the event description field. Not glamorous. But every reliable automation I have shipped starts with a boring, consistent input format.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trigger, the extractor, and the "do not guess" rule
&lt;/h2&gt;

&lt;p&gt;The trigger is a scheduled job that runs every 10 minutes and looks for calendar events with an &lt;code&gt;Interview&lt;/code&gt; label starting in the next 30-60 minutes. When it finds one, it pulls: candidate email, candidate name, interviewer name, role title, event start time, meeting link.&lt;/p&gt;

&lt;p&gt;If any critical field is missing, &lt;strong&gt;the bot does not guess.&lt;/strong&gt; It sends the interviewer a short exception notice and stops.&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="c1"&gt;# pseudo-code, n8n / Make / Python all map to this shape
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_calendar_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candidate_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interviewer&lt;/span&gt;&lt;span class="sh"&gt;"&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&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;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;notify_interviewer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interviewer_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Briefing skipped: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing fields: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fix the event and I will regenerate on next run.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;htmlLink&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;build_briefing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silent guessing is how automations create embarrassing failures — the wrong candidate summary sent to the wrong interviewer, or a briefing about "David Chen" that mixes two people's CVs. An exception notice takes 20 seconds for a human to fix. A hallucinated brief takes a week of trust to rebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fields the trigger must always confirm before proceeding
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;candidate_email&lt;/code&gt; — the join key for every downstream lookup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role_title&lt;/code&gt; — required to load the correct scorecard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;interviewer&lt;/code&gt; — required to send the brief to the right person&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event_start&lt;/code&gt; — required to schedule delivery 30 min before&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gmail search, CV extraction, and keeping the payload small
&lt;/h2&gt;

&lt;p&gt;Once the trigger has the candidate email, the bot searches Gmail with a narrow query. Broad searches ("everything from this person, ever") are how you end up with a 40,000-token context window full of scheduling back-and-forth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;from:jane.doe@gmail.com OR to:jane.doe@gmail.com
label:Applications OR label:Recruiting OR label:Interviews
newer_than:120d
has:attachment OR subject:(application OR interview OR resume OR cv)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply three filters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Newest CV wins.&lt;/strong&gt; If the candidate sent three versions, use the most recent. List older files as source links, do not blend them into the extracted text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract with page references.&lt;/strong&gt; For PDFs, keep &lt;code&gt;page_number&lt;/code&gt; metadata alongside every text chunk. &lt;code&gt;pdfplumber&lt;/code&gt; or &lt;code&gt;pypdf&lt;/code&gt; both work. This is what lets the final brief cite "CV page 2" instead of a vague summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull only the fields the interviewer needs from the application form.&lt;/strong&gt; Work history, key open-ended answers, portfolio links, stated availability. Skip the "how did you hear about us" field.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_cv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
            &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;strip&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;pages&lt;/span&gt;  &lt;span class="c1"&gt;# keep structure, do not join into one blob
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload going into the model should be small and labeled. For a typical candidate this is roughly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Calendar event&lt;/td&gt;
&lt;td&gt;~200 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CV (extracted, page-labeled)&lt;/td&gt;
&lt;td&gt;1,500-3,000 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application form (filtered)&lt;/td&gt;
&lt;td&gt;400-800 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selected email threads (max 3)&lt;/td&gt;
&lt;td&gt;500-1,500 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role scorecard&lt;/td&gt;
&lt;td&gt;200-400 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~3,000-6,000 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At current Claude Sonnet or GPT-4-class pricing, one briefing runs about &lt;strong&gt;$0.03-$0.08&lt;/strong&gt;. Dumping every email would push that past $0.50 per brief and would make the citations unverifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt: labels, citations, and no opinions
&lt;/h2&gt;

&lt;p&gt;Every input chunk gets a source label before it hits the model. This is the single change that separates a useful brief from an AI-generated opinion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="py"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cv_page_2] "Led a support team of 4 at Acme SaaS, 2022-2024..."&lt;/span&gt;
&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="py"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application_q3_dated_2026-03-04] "Available to start within 3 weeks..."&lt;/span&gt;
&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="py"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email_thread_2026-03-06] "Confirmed she is US Pacific time..."&lt;/span&gt;
&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="py"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scorecard_v1.3_must_have] "US timezone overlap required"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system prompt then enforces the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are preparing a hiring manager for an interview. You do NOT
evaluate the candidate. You do NOT recommend hire/no-hire. You do
NOT produce a score.

For every claim you make, cite the source label in brackets, e.g.
[cv_page_2]. If a scorecard requirement has no matching evidence in
the sources, say "no evidence found in provided sources" — do not
infer, do not fill in from general knowledge.

Output sections (in this order):
&lt;span class="p"&gt;1.&lt;/span&gt; Snapshot (3 bullets, facts only)
&lt;span class="p"&gt;2.&lt;/span&gt; Scorecard coverage (each must-have + nice-to-have, with evidence
   or "no evidence found")
&lt;span class="p"&gt;3.&lt;/span&gt; Concerns to verify (from scorecard, phrased as questions)
&lt;span class="p"&gt;4.&lt;/span&gt; Suggested questions (3-5, grounded in gaps or claims worth probing)
&lt;span class="p"&gt;5.&lt;/span&gt; Source index (list of files/links used)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "no evidence found" clause is the anti-hallucination lever. Without it, the model will happily write "Jane has strong SQL skills" because SQL is on the scorecard, even though nothing in her CV mentions it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery, review, and what a good brief looks like at 8:30 AM
&lt;/h2&gt;

&lt;p&gt;The brief lands in the interviewer's inbox (or Slack DM) 30 minutes before the call, with the meeting link at the top and the source index at the bottom. Every claim has a bracketed citation the interviewer can click.&lt;/p&gt;

&lt;p&gt;A real one looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;INTERVIEW BRIEFING — Jane Doe — Senior Support Specialist
Interview: 2026-04-15 09:00 PT with Marcus | [Join call]

SNAPSHOT
• 2 yrs support lead at Acme SaaS (B2B) [cv_page_2]
• Available in 3 weeks, US Pacific [application_q3, email_2026-03-06]
• Portfolio: writing samples + one incident postmortem [application_q7]

SCORECARD COVERAGE
✓ Ticketing 2+ yrs — Zendesk at Acme [cv_page_2]
✓ B2B SaaS exposure — Acme is B2B [cv_page_1]
✓ US timezone — confirmed Pacific [email_2026-03-06]
? Angry-customer example — no evidence found in provided sources

CONCERNS TO VERIFY
• Ask for a specific escalation she handled end-to-end
• Confirm start date given current notice period

SUGGESTED QUESTIONS
&lt;span class="p"&gt;1.&lt;/span&gt; Walk me through the incident in your postmortem — what did you own?
&lt;span class="p"&gt;2.&lt;/span&gt; Tell me about a ticket you escalated. What was the trigger?
[...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interviewer skims for 3 minutes, spots the "no evidence found" gap, and knows exactly what to probe. That is the whole product.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks in the first two weeks of running this
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Calendar events without the email in title or description — fix the convention, do not patch the code.&lt;/li&gt;
&lt;li&gt;Gmail label drift ("Recruiting-2026" vs "Recruiting") — normalize labels once, monthly.&lt;/li&gt;
&lt;li&gt;CVs sent as image-only PDFs — add an OCR fallback (Tesseract or a vision model) and flag the brief as "OCR used, verify accuracy."&lt;/li&gt;
&lt;li&gt;Interviewer forwards the brief to the candidate by accident — put a visible &lt;code&gt;INTERNAL — DO NOT FORWARD&lt;/code&gt; header on every brief.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;This is exactly the class of workflow I ship for small teams at &lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;bizflowai.io&lt;/a&gt; — the boring, high-leverage pre-work that a human still owns the decision on. For hiring specifically, that means the calendar-triggered briefing bot above, plus adjacent pieces like a scorecard template your managers will actually fill in, exception routing when inputs are missing, and the source-labeled prompt structure that keeps the model from inventing evidence. Nothing in the stack scores or rejects candidates. Every decision stays with your hiring manager, and they walk into the call already prepped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>aiinterviewpreparation</category>
      <category>hiringautomationworkflow</category>
      <category>interviewbriefingbot</category>
      <category>candidatescreeningautomation</category>
    </item>
    <item>
      <title>218 of 312 Applicants Rejected Before a Human Looked</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 10 Aug 2026 06:12:26 +0000</pubDate>
      <link>https://dev.to/lamingsrb/218-of-312-applicants-rejected-before-a-human-looked-4pig</link>
      <guid>https://dev.to/lamingsrb/218-of-312-applicants-rejected-before-a-human-looked-4pig</guid>
      <description>&lt;h1&gt;
  
  
  218 of 312 Applicants Rejected Before a Human Looked
&lt;/h1&gt;

&lt;p&gt;Every recruiting AI demo shows the same three things: source, screen, schedule. Nobody shows you the tier that actually saves the hours — the guilt-driven manual review of obvious no's. Here's the exact n8n flow that killed 218 of 312 applications on one role before a recruiter wasted a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 9-hour leak nobody automates
&lt;/h2&gt;

&lt;p&gt;One role. Mid-level ops position. 312 applications in 11 days. The recruiter I built this for was spending roughly nine hours per week opening CVs, reading the first paragraph, and closing them again because the candidate was in the wrong country, wanted double the salary band, or didn't hold the license the role legally requires. Zero hires from that pile.&lt;/p&gt;

&lt;p&gt;That's the actual leak. And it's the one nobody automates, because every founder-influencer is busy building outreach bots for the top of the funnel where it looks impressive on a demo.&lt;/p&gt;

&lt;p&gt;We flipped it. Automated the rejection tier first. Here are the real numbers after 11 days on one role:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Applications&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-rejected with reason code&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promoted to human review&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recruiter time&lt;/td&gt;
&lt;td&gt;~9 hrs/week&lt;/td&gt;
&lt;td&gt;~90 min/week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token cost (Claude Haiku)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False negatives (month 1)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;2 out of ~600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the whole pitch. Now the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Hard-fail criteria, not scoring models
&lt;/h2&gt;

&lt;p&gt;This is where most teams ruin the pipeline before they start. Do not build a scoring model. Start with hard-fail criteria — binary gates that a person could defend in writing to a candidate, a lawyer, or a labor board.&lt;/p&gt;

&lt;p&gt;For this ops role, we encoded six:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work authorization for the country of hire&lt;/li&gt;
&lt;li&gt;Physical location within commuting distance of the office&lt;/li&gt;
&lt;li&gt;Minimum years of relevant experience (role-specific)&lt;/li&gt;
&lt;li&gt;Salary expectation inside the posted band&lt;/li&gt;
&lt;li&gt;A specific professional license required by the role&lt;/li&gt;
&lt;li&gt;Language fluency at working level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No culture fit. No "communication skills." No vibes. Soft signals belong later, after a human is already in the loop.&lt;/p&gt;

&lt;p&gt;The reason this matters is legal and operational. Soft scoring produces disputes. Hard criteria produce receipts. When a candidate emails back asking why they were rejected, the recruiter points at one line: "You indicated Berlin as your base. The role requires on-site presence in Lisbon." That reply writes itself.&lt;/p&gt;

&lt;p&gt;In the US, the EEOC's &lt;a href="https://www.eeoc.gov/ai" rel="noopener noreferrer"&gt;guidance on AI in employment decisions&lt;/a&gt; is clear that automated tools can create disparate impact liability under Title VII. Binary, role-relevant criteria with evidence snippets are defensible. Opaque 1-10 scores are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The n8n trigger and CV extraction
&lt;/h2&gt;

&lt;p&gt;Every application lands in a shared Gmail inbox. The ATS forwards there. A Gmail label — &lt;code&gt;new-application&lt;/code&gt; — fires the n8n workflow.&lt;/p&gt;

&lt;p&gt;The flow is boring on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gmail Trigger (label: new-application)
  → Extract email body + sender
  → Download attachment (PDF/DOCX)
  → PDF-to-text node (pdf-parse)
  → Merge: {email_body, cv_text, applicant_id}
  → HTTP Request → Anthropic Messages API (claude-haiku)
  → Parse JSON response
  → IF any_fail == true → Airtable: reject_queue
  → ELSE → Airtable: human_review + Gmail label: needs-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Model choice matters. I use Claude Haiku, not Sonnet, not Opus. We're not writing poetry — we're checking six checkboxes. Haiku runs at roughly $0.25 per million input tokens and $1.25 per million output tokens (check Anthropic's &lt;a href="https://www.anthropic.com/pricing" rel="noopener noreferrer"&gt;current pricing&lt;/a&gt; before you scale). A typical CV + cover email + prompt clocks in around 3,500 input tokens and 400 output tokens per call. That's how 312 applications came in at 41 cents total.&lt;/p&gt;

&lt;p&gt;Sonnet would have cost roughly 12x more with zero accuracy gain on binary extraction. Save Sonnet for the tasks where reasoning actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The prompt structure (criterion-by-criterion, not holistic)
&lt;/h2&gt;

&lt;p&gt;Do not ask the model for a holistic score from 1 to 10. That's the single biggest failure mode in every hiring AI I've audited. Ask criterion by criterion, demand a quoted evidence snippet, and force a strict JSON schema.&lt;/p&gt;

&lt;p&gt;Here's the shape of the prompt:&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="n"&gt;SYSTEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You evaluate a candidate against exactly six binary criteria.
For each criterion return:
  verdict: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unclear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
  evidence: exact quoted sentence from the CV or cover email
            that led to your decision, or null if none exists
  confidence: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;

Do not infer. Do not guess. If the CV does not explicitly state
the fact needed to evaluate a criterion, return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unclear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
Return only valid JSON matching the provided schema.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;USER_TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
CRITERIA:
1. work_authorization: Candidate has legal right to work in {country}.
2. location: Candidate is based within {radius_km} km of {city}.
3. experience: Candidate has at least {min_years} years in {domain}.
4. salary: Candidate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s stated expectation is within {band_low}-{band_high} {currency}.
5. license: Candidate holds a valid {license_name}.
6. language: Candidate is fluent (B2 or higher) in {language}.

CV TEXT:
{cv_text}

COVER EMAIL:
{email_body}

Return JSON: {{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;criteria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [{{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}}]}}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The routing logic that follows is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n Function node&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claude_response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hard_fail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;has_unclear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unclear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hard_fail&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject_queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;human_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;triggered_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hard_fail&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's not there. No "score &amp;gt; 7 means promote." No weighting. If any single hard criterion fails with high confidence, the candidate goes to the silent-reject queue. Unclear or all-pass goes to a human. That's the whole decision tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The Airtable logging schema that makes it auditable
&lt;/h2&gt;

&lt;p&gt;This is what turns the flow from a black box into a tool a non-technical recruiter can actually trust and tune.&lt;/p&gt;

&lt;p&gt;Every decision — pass or fail — writes a row to Airtable. Three columns matter most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;applicant_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-ops-0184&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;triggered_criterion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;location&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evidence_snippet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Currently based in Berlin, open to remote roles across EU."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verdict_full_json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{...}&lt;/code&gt; (full 6-criteria response)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;route&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reject_queue&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-08-05T14:22:11Z&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model_version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-haiku-2026-xx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once a week, the recruiter opens Airtable, sorts by &lt;code&gt;triggered_criterion&lt;/code&gt;, and asks two questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are we rejecting on the right things?&lt;/li&gt;
&lt;li&gt;Are the evidence snippets actually saying what the model thinks they're saying?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, they don't touch the prompt. They adjust the criteria list for that role — the role config, in Airtable, in plain English. The prompt stays generic. Criteria are config. That separation is what lets a non-technical recruiter tune this weekly without calling me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The shadow-review layer (don't skip this)
&lt;/h2&gt;

&lt;p&gt;Three times a week, the workflow randomly pulls one CV from the reject pile and forwards it to the recruiter anyway, flagged as a shadow review. The recruiter reads it in 90 seconds and either confirms the rejection or flags a false negative.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n Cron: Mon/Wed/Fri 09:00&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rejects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;airtable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject_queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filterByFormula&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AND(IS_AFTER(timestamp, DATEADD(NOW(), -2, 'days')), NOT({shadow_reviewed}))&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recruiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`[SHADOW REVIEW] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicant_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — rejected on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;triggered_criterion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildShadowReviewEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&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;Over the first month we caught two false negatives out of roughly 600 rejections. Both were candidates whose CVs were poorly formatted — one was a PDF that was actually an image scan, the other used a two-column layout that scrambled during text extraction. The model wasn't wrong. The input was garbage.&lt;/p&gt;

&lt;p&gt;We fixed the extraction step (added an OCR fallback for image PDFs, added a layout-aware parser for multi-column), not the model. If you don't have a shadow-review layer, you'll drift and you won't know you're drifting until a great candidate posts a screenshot on LinkedIn.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to actually measure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;False negative rate (target: &amp;lt;1%)&lt;/li&gt;
&lt;li&gt;% of rejections triggered by each criterion (spot skew)&lt;/li&gt;
&lt;li&gt;Extraction failure rate (CVs where &amp;gt;2 criteria return "unclear")&lt;/li&gt;
&lt;li&gt;Time-to-decision per applicant (should be &amp;lt;30 seconds)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: The bot never sends the rejection email
&lt;/h2&gt;

&lt;p&gt;Hard rule. The bot does not send the rejection email. Ever.&lt;/p&gt;

&lt;p&gt;Several jurisdictions have disclosure requirements for automated hiring decisions. New York City's Local Law 144 requires bias audits and candidate notification for automated employment decision tools. Illinois has the AI Video Interview Act. The EU AI Act classifies most hiring AI as high-risk. The last thing a small business needs is a discrimination complaint over an email nobody read before it went out.&lt;/p&gt;

&lt;p&gt;What we do instead: the silent-reject queue sits in Airtable. Once a week, the recruiter reviews it in bulk, spot-checks the evidence snippets, and then sends rejection emails from their own inbox using a templated response that quotes the specific criterion. A human clicks send. That's the compliance line, and it's cheap to hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;We build these silent-reject pipelines for recruiter clients as a standard workflow — the Gmail trigger, the Claude Haiku evaluator, the Airtable audit log, the shadow-review sampler, and the role-config sheet that a non-technical recruiter can edit weekly. Most clients go live in under a week and see their manual CV-reading time drop by 70-85% on the first role they migrate. The pattern is the same across ops, sales, and support roles — only the six criteria change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>cvscreeningautomation</category>
      <category>recruitmentautomation</category>
      <category>autorejectcandidates</category>
      <category>claudehaiku</category>
    </item>
  </channel>
</rss>
