The model that writes the CRM briefing never sees a customer
A CRM is full of names, phone numbers and emails. The briefing’s model runs on the same machine as the job and only ever reads totals, so no customer record goes to an outside AI service.
The problem
Posting CRM records to a hosted AI service was the easy route, and the wrong one.
The fix
Give a local model totals only, and list every place the data does go.
- 0
- customer records sent to an outside AI service
- 0
- customer names, emails or phone numbers in the prompt
- 2
- services besides Zoho that receive any CRM data
A CRM is a list of people: names, phone numbers, email addresses and what they want to buy. AHA’s brief asked for an AI-written daily briefing, and the quickest route was to post the day’s records to a hosted model. I didn’t want a single customer record going to an outside AI service.
Two halves of the problem
Keeping data away from a model has two halves: what the model is given, and where it runs. I closed both.
The model only reads totals
Python builds one input for the model from the Postgres functions’ results:
- the day’s new leads against the seven-day average, with the change as a percentage;
- open pipeline, pipeline created that day, and won and lost totals;
- counts by status and by source, with the junk share per source;
- active leads and pipeline per salesperson;
- the anomaly sentences written by two fixed rules.
There is no lead name, email address or phone number in it. The only names are salespeople’s, because the briefing has to say whose pipeline is thin. Even if this input leaked, it would be a page of totals.
The model runs on the same machine
Ollama serves Llama 3.2 locally, and the job calls it through LangChain. The first commit used Mistral 7B; by the second, the same day, it was Llama 3.2. There’s no account with a model provider and no per-run fee.
The cost is a dependency. Ollama has to be running before the job starts. If the model call fails, the job stops and sends nothing. That’s the right failure: no message beats a wrong one.
Where the data does go
The project’s README promised “zero data leaves the building”. That was too broad. Here is every hop:
- Zoho to a hosted Postgres database (Supabase). Full records, custom fields included. The finished dashboard report is saved there too.
- Totals to the local model. Same machine, nothing sent out.
- The WhatsApp summary to Twilio, which delivers it. Figures and salespeople’s names, no customer records.
So the claim I make is narrower, and true: no customer record goes to an outside AI service. If the database also had to stay in-house, that’s the hop to change, and now it’s written down.
A small model needs a tight harness
Small local models don’t always follow a format to the letter, so the job doesn’t depend on it. One prompt asks for two tagged sections, a dashboard report and a WhatsApp summary, and nothing outside the tags. Then Python:
- accepts a missing closing tag;
- falls back to everything after the opening tag, and then to the dashboard text;
- strips any stray tags the model left behind.
The anomalies arrive as finished sentences, so the model’s job is to report them, not to find them.
Rules I’d keep
- Send totals, not records. The safest data is data the model never receives.
- Run the model where the data already is.
- List every hop before calling anything private.
- Fail silent, not wrong. No answer from the model means no message.