Last month I needed to run a lead enrichment pipeline that pulled data from a public scraper, scored each prospect with a local LLM, and sent personalized outreach—all without leaking data to a third‑party API. I built a self hosted AI stack using Ollama, n8n workflows, and a small PostgreSQL store, and after a week of tweaking I had a repeatable workflow that cost me nothing in API fees. After reading this you’ll be able to assemble the same pieces, spot where the pipeline stalls, and decide whether to keep the DIY route or grab a ready‑made blueprint.
Why off‑the‑shelf SaaS APIs become a liability for solo operators
When you rely on a hosted LLM provider you hand over every prompt and every piece of context to a server you don’t control. That creates two problems: first, any data you send could be logged or used for model training, which is a deal‑breaker for client‑facing work. Second, the provider can change pricing, rate limits, or even shut down the endpoint with little notice, leaving your automation dead in the water. I once had a campaign halted because the vendor raised the per‑token price from $0.0004 to $0.0012 overnight, blowing my budget.
Running the model locally removes that exposure. Your prompts stay on your machine, your data never leaves your network, and you control the upgrade schedule. The trade‑off is you need to manage the hardware and the software stack, but for a solo operator the cost is usually lower than a paid API once you pass a few hundred calls per day.
How do you keep model updates from breaking your workflow?
One of the biggest worries with a self hosted model is that a new version might change its output format or its token handling, causing silent failures in downstream steps. I treat the model like any other dependency: I pin a specific version and run it in a container that I only update after I have verified the new release against a test suite.
Here’s the routine I follow:
- Pull the desired Ollama model tag, for example
ollama pull mistral:7b-instruct-v0.2-q4_K_M. - Run the container with a fixed port:
docker run -d -p 11434:11434 --name ollama ollama/ollama serve. - Point your n8n HTTP request node to
http://localhost:11434/api/generateand include the model name in the payload. - Keep a small JSON file that records the exact tag you used; when you want to test a newer tag, spin up a second container on a different port and run your test suite against it.
- Only switch the production container after the test suite passes with zero differences in the scored output.
This adds a few minutes of overhead but saves hours of debugging when a model update silently changes the way it formats JSON.
What most guides get wrong about chaining LLMs with automation tools
Many tutorials show you how to call an LLM from a workflow, then they stop. They assume the model will always return clean JSON and that the next node can parse it directly. In reality, LLMs love to add explanatory text, stray commas, or even markdown fences around their answer. If you feed that straight into a database insert or an email template you’ll get errors that are hard to trace.
The fix is to wrap the LLM call in a validation step that forces the output into a known schema. In n8n I use a Function node that does three things:
- Strip everything outside the first curly‑brace pair.
- Parse the remaining string with
JSON.parseand catch any exception. - If parsing fails, return a fallback object that flags the error so the workflow can send a Slack alert instead of proceeding with bad data.
I learned this the hard way after a week of “mystery” failed emails that turned out to be caused by the model adding a polite “Here is the result:” prefix.
A concrete named example: scoring leads with Mistral 7B via Ollama and n8n
Let’s walk through the exact nodes I use to turn a raw LinkedIn URL into a prospect score.
- Scraper – a simple HTTP Request node that calls a public scraper API (I use
scraperapi.comwith a free tier) and returns the raw HTML of a profile. - Extract text – a Function node that strips tags and keeps the visible text, limiting it to 3000 characters to stay within the model’s context window.
- Build prompt – another Function node that creates the following prompt:
{
"model": "mistral:7b-instruct-v0.2-q4_K_M",
"prompt": "You are a sales assistant. Given the following LinkedIn profile text, output a JSON object with two fields: \"score\" (integer 0‑100) indicating how likely the person is to buy a SaaS automation tool, and \"reason\" (short string) explaining the score.\n\nProfile:\n" + $json["text"] + "\n\nJSON:"
}" - Call Ollama – an HTTP Request node POST to
http://localhost:11434/api/generatewith the JSON body from the previous node, headerContent-Type: application/json, and a 30‑second timeout. - Validate output – the Function node described in the previous section that strips wrappers, parses JSON, and returns {score, reason} or an error flag.
- Store result – a PostgreSQL node that inserts the score, reason, and original URL into a
leadstable. - Send outreach – if score > 70, an Email node fires a templated message that includes the reason line.
The whole chain runs in under five seconds per lead on a modest laptop with an RTX 3060. The only recurring cost is the electricity for the GPU; the software stack is free.
