Santia: citizen WhatsApp bot
63,360 conversations. 9.63M messages. 7 months operating the largest citizen assistant in San Nicolás.
San Nicolás has 170,000 residents. Before Santia, handling a municipal procedure meant going in person: waiting in line, bringing copies, coming back if something was missing. Santia is the attempt to change that.
The bot operates 24/7 over WhatsApp (the channel residents already have installed and use every day), handling everything from booking a medical appointment to checking a traffic fine. It doesn't replace government: it makes it more accessible.
Architecture and services
Santia is two layers with opposite responsibilities. One is a deterministic state machine that collects data step by step, validates it and writes it into municipal systems. The other is an AI layer that figures out what the resident wants when they write in free text. That is the rule that orders the whole system: AI understands, the bot executes. When a procedure intent is detected, the AI layer does not ask for the data. It fires the deterministic flow and steps aside until the procedure is done.
Procedures are classified into three types by complexity. Pure AI for open informational queries, where the answer comes from RAG over municipal documentation indexed in Qdrant. Hybrid for procedures where AI understands and classifies but the critical steps are deterministic, such as medical appointments or license renewals. Pure deterministic for forms, payments and external API validations, where there is nothing to interpret. ID card scanning is resolved with vision and OCR within the same turn.
Until early 2026 the decision layer lived entirely in a no-code orchestrator: a main agent that classified and delegated to per-domain sub-agents, each with its own access to the vector store. It carried 63,000 conversations and it worked. The cost was structural: every query chains several model calls, conversational state ends up split across systems, and business rules live inside prompts. That layer is being replaced by code.
Where the decision lives: before and now
Renderizando diagrama…
This is not a vendor swap, it is a change of responsibility. Before, an agent decided and called other agents. Now the model only classifies and the decision is made by code that can be tested without booting anything.
- BuilderBotProcedure state machine: WhatsApp session and deterministic flows
- GupshupWhatsApp Business Service Provider (BSP)
- Native agentIn-house decision layer in Node: structured-output router + deterministic policy. Progressively replacing the orchestrator
- n8nOriginal AI-layer orchestrator. Serves the domains left to migrate
- QdrantVector DB for RAG: auto-updated municipal documentation (source: AWS S3)
- OpenAI / OpenRouterClassification and copy, vision and OCR for documents, audio transcription
- PostgreSQLPersistence for conversations, messages, escalations and agent session
- AWS S3Media storage: photos, PDFs, audio
- PuppeteerPDF generation for bills and certificates
- CRM PanelInterface for human operators handling escalations
Volume and operations
Since the mass launch in November 2025, the bot has processed over 9.6 million messages. The monthly peak was November (17,768 conversations), the launch month. The daily message average in April 2026 is ~57,000.
There's a strong weekly pattern: Sundays and Mondays concentrate the most traffic (~250 to 317 conversations/day), while Fridays and Saturdays are at the minimum (~65 to 114). The peak-to-valley ratio is approximately 4:1.
Automated resolution vs human operator
Every conversation goes through n8n, the central orchestrator. n8n classifies the intent of each message and decides the path: deterministic flows for procedures with known steps, natural language LLM responses for open queries, or escalation to a human operator when the procedure requires it.
The result: 85% is resolved without any human involvement. The remaining 15% is routed to operators for personalized attention.
- Resolved automatically: 85%
- With human operator: 15%
Migrating the agent: the LLM proposes, the code disposes
The redesign rests on four rules. No model performs a side effect: it returns a structure and the code validates it and decides whether to act. There is a single LLM call in the decision path, not an orchestrator calling agents that call agents. If the resident is already inside a procedure, the model does not intervene. And deciding and conversing are separate layers, so the conversational framework can be swapped without touching anything above it.
The turn comes in, gets normalized, the session is loaded, and only then is it classified. The router makes one structured-output call and returns domain, action, entities, confidence and which fields are missing. A pure function decides from there. What moves cost the most is not which model is used: it is that when an intent is left waiting for a field, the next message is resolved by a validator and the model is never called.
The policy covers 17 domains and every branch is code that can be tested without booting anything. The confidence threshold depends on the domain and also on the action: asking a tourism question should not have to clear the same bar as opening a municipal case file. If the model returns an action the domain does not allow, the verb gets corrected instead of throwing away the topic. And if every field is present but one of them is remembered from an earlier turn, it gets confirmed before anything is recorded.
The full turn, and the shortcut that avoids most calls
Renderizando diagrama…
In a procedure spanning 8 to 15 turns the model intervenes exactly once. The validator and the state machine handle the rest.
The policy tree: the model picks none of these branches
Renderizando diagrama…
Every diamond is code, the model decides none of them. The two shortcuts on the left never even call it. Details that do not fit the drawing: the threshold depends on the domain AND the action, and if the model returns an action the domain does not allow, the verb gets corrected instead of dropping the topic.
Answers with sources, or no answer
The anti-hallucination guardrail is not a prompt line: it is a branch in the code. Before answering an informational query, fragments are retrieved from the domain collections. If none clears the minimum score, the model is not called at all. The bot says it does not have that information and asks the resident to rephrase.
The reasoning is simple: you cannot ask a model not to make things up if you give it nothing to answer with. When there is material, synthesis happens strictly over those fragments, and the prompt prioritizes not dropping any requirement over being brief. An address or an opening time that is not in the material does not get written.
The case that justified the design was real and happened in production: questions like "hi, where is the town hall" came in as small talk, were answered without sources, and the model returned an address that does not exist. Today that message is redirected to the institutional domain, which does have indexed material.
The score cutoff comes before the model
Renderizando diagrama…
If there is no material above the threshold, the model is never called. The guardrail is structural, not an instruction the model can ignore.
How you replace a live layer
No big bang. Both systems coexist: a global flag, an allowlist of numbers and a per-domain gate decide turn by turn who answers. Anything the new layer does not cover falls back to the previous path without the resident noticing.
That makes it possible to enable one domain, measure it against the previous behavior, and move on to the next. To test without burning WhatsApp messages there is a sandbox that runs the real pipeline and shows the decision, the turn stages and every model call side by side, plus a per-conversation trace viewer.
The native agent already handles real conversations over WhatsApp. All 17 domains are implemented and measured, and the rollout advances domain by domain behind the flag, which is exactly how you replace a layer that 63,000 conversations depend on. The pace of a full cutover is not set by the architecture: it is set by the LLM provider's requests-per-minute ceiling against peak traffic.
Both layers coexisting, domain by domain
Renderizando diagrama…
Three gates before the new agent answers. Any of them failing sends the turn back to the previous path, so the worst case of the migration is business as usual.
Findings
The real savings are not in the model, they are in not calling it
The first instinct is to cut costs by switching models. Measured against the real pipeline, what moves the needle is the deterministic shortcut: when an intent is left waiting for a field, the next message is resolved by a validator and the model is never called. Combined with the fact that a procedure in progress never touches AI on any turn, the LLM intervenes exactly once in a conversation of 8 to 15 messages.
Juzgado and Corralón are the next automation targets
Two departments account for 55% of human escalations (Juzgado 33%, Corralón 22%). They're the clearest candidates for specialized AI sub-flows: high volume, repetitive intents, and procedures with identifiable steps. A per-department agent can cut operational load without losing the precision human operators currently bring.