AI Privacy Pro Team20 min read

How to set up Private AI Automation

Self-hosted n8n-based automation with private AI integrations. Step-by-step flows for expense tracking and LinkedIn lead outreach, designed for privacy and simplicity.

n8nPrivate AIAutomationSelf-hostedOllamaLinkedInReceipts

Executive Summary

This guide shows how to build a privacy-first automation stack using self-hosted tools like n8n (workflow automation) and Ollama (local LLM API), keeping all sensitive data inside your network. You will deploy the stack in minutes with Docker, then build two pragmatic automations:

  • Expense tracking automation: Parse receipts from email or files, extract amounts/categories with a local LLM, and store to Postgres + export monthly CSV.
  • LinkedIn lead scraper & outreach: Collect leads via compliant inputs, enrich with a local LLM, deduplicate, and schedule rate-limited outreach—without sending data to third-party AI.

We prioritize privacy, simplicity, and maintainability: minimal components, local inference, strict egress controls, and clear rollback.

Private Automation Stack Overview

Core Tools

  • n8n (Self-hosted): Visual workflow builder with triggers, HTTP, queues, credentials vault, code nodes.
  • Postgres: Durable storage for workflow state and results.
  • Ollama: Local LLM server for extraction, classification, prompt templating.

Alternatives (optional)

  • Node-RED: Lightweight flows; fewer turnkey AI nodes.
  • Huginn: Agent-style event automation; more DIY.
  • Windmill: Dev-centric jobs; great for TypeScript/Python-heavy tasks.

Privacy Capabilities

  • Local inference: Use Ollama HTTP API instead of cloud LLMs.
  • Egress control: Restrict n8n outbound traffic; allow-list internal services.
  • Secret handling: Store API keys/session tokens in n8n Credentials (encrypted at rest).
  • Data minimization: Log only necessary fields; short retention windows.

Quickstart Deployment (Docker Compose)

Create a new folder (e.g., private-automation) and save this as docker-compose.yml. Adjust volumes/ports for your environment.

version: "3.8"
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: n8n_pass
      POSTGRES_DB: n8n
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks: [private]

  n8n:
    image: n8nio/n8n:1.71.0
    depends_on: [postgres]
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: n8n_pass
      N8N_HOST: n8n.local
      N8N_PORT: 5678
      N8N_PROTOCOL: http
      N8N_LOG_LEVEL: info
      N8N_PERSONALIZATION_ENABLED: false
      EXECUTIONS_DATA_SAVE_ON_SUCCESS: none
      EXECUTIONS_DATA_SAVE_ON_ERROR: reduced
      # Optional basic auth
      N8N_BASIC_AUTH_ACTIVE: true
      N8N_BASIC_AUTH_USER: admin
      N8N_BASIC_AUTH_PASSWORD: strong_password_here
    ports:
      - "5678:5678"
    volumes:
      - ./data/n8n:/home/node/.n8n
      - ./dropbox:/workspace/dropbox  # for CSV/receipt inputs
    networks: [private]

  ollama:
    image: ollama/ollama:latest
    restart: unless-stopped
    volumes:
      - ./data/ollama:/root/.ollama
    ports:
      - "11434:11434"
    networks: [private]

networks:
  private:
    driver: bridge

Then run:

docker compose up -d
# Pull a small general model
curl -s http://localhost:11434/api/pull -d '{"name":"mistral:7b"}'

Open http://localhost:5678 to access n8n. Create Credentials for Postgres and any integrations.

Security Hardening

  • Bind ports to 127.0.0.1 and expose via Tailscale/WireGuard if remote admin is needed.
  • Place n8n behind a reverse proxy (Traefik/Caddy) with HTTPS + Basic Auth.
  • Firewall egress: deny-all, then allow postgres and ollama only.
  • Rotate credentials and audit n8n Executions retention settings.

Connect n8n to a Local LLM (Ollama)

Use n8n's HTTP Request node to call Ollama's API. Example JSON request for structured extraction:

POST http://ollama:11434/api/generate
{
  "model": "mistral:7b",
  "prompt": "Extract receipt fields (date, vendor, amount, currency, category suggestions) as strict JSON without commentary.
TEXT:
{{ $json.text }}",
  "stream": false
}

In n8n, set Response Format to JSON and parse the response field.

Automation 1: Expense Tracking (Email/Folder → LLM → DB → CSV)

Goal

Automatically capture expenses from receipts, categorize locally, and export monthly reports.

Design

  • Ingestion: IMAP Email (receipts@yourdomain) or a watched folder (./dropbox/receipts).
  • Parsing: Convert email/attachment text, then LLM JSON extraction.
  • Storage: Postgres table expenses.
  • Reporting: Monthly CSV to ./dropbox/reports and optional summary message.

Postgres schema

CREATE TABLE IF NOT EXISTS expenses (
  id SERIAL PRIMARY KEY,
  tx_date DATE NOT NULL,
  vendor TEXT NOT NULL,
  amount_cents INTEGER NOT NULL,
  currency TEXT DEFAULT 'USD',
  category TEXT,
  source TEXT,
  original_ref TEXT,
  created_at TIMESTAMP DEFAULT now()
);

n8n Workflow (nodes)

  1. Cron: Every 10 minutes.
  2. IMAP Email (or Read Binary File):
    • IMAP: pull unread messages from receipts mailbox; mark processed.
    • Folder: iterate ./dropbox/receipts for new files.
  3. Function: Produce a text field:
    • Email path: prefer plain-text body; fallback to HTML-to-text.
    • PDF/JPG: optional OCR via a local microservice; else skip attachments at first.
  4. HTTP Request → Ollama:
    Prompt:
    Extract and return ONLY this JSON, no commentary:
    {
      "tx_date": "YYYY-MM-DD",
      "vendor": "string",
      "amount_cents": number,
      "currency": "USD|EUR|...",
      "category": "one of [food, travel, software, office, other]",
      "source": "email|file",
      "original_ref": "message-id or filename"
    }
    TEXT:
    {{ $json.text }}
    
  5. IF: Validate required fields; fallback category to other.
  6. Postgres (Insert): Upsert into expenses based on original_ref.
  7. Once/month CronPostgres (Query): Aggregate by month.
  8. Spreadsheet File (n8n node): Write CSV to ./dropbox/reports/2025-10-expenses.csv.
  9. Matrix/Email (optional): Send summary totals.

Privacy Notes

  • Disable execution data persistence for successful runs to avoid storing raw receipts.
  • Hash or omit PII fields you don't need; prefer amounts, vendors, dates.
  • Keep OCR local if you add it; never upload receipts to cloud OCR.

Automation 2: LinkedIn Lead Collection & Outreach (Compliant-first)

Important

Respect LinkedIn's Terms of Service and applicable laws. Prefer consented, first-partydata and official exports. If you choose browser automation, run it locally, store cookies securely, and rate-limit aggressively. This guide provides two privacy-respecting options.

Two input options

  • Option A (Recommended): Import a CSV you exported (e.g., connections or Sales Navigator lists).
  • Option B: Local headless browser (Playwright) script you run on your machine to capture visible page results you view.

Design

  • Ingestion: Watch ./dropbox/leads/ for new CSVs (Option A) or call a local HTTP endpoint that your Playwright script exposes (Option B).
  • Enrichment: Use a local LLM to summarize profile lines and propose a single personalized opener.
  • Deduplication: Postgres leads table with unique constraint on profile URL.
  • Outreach: Queue 10–15 messages/day; send via email (own SMTP) or a local browser automation to LinkedIn messaging UI you control.

Postgres schema

CREATE TABLE IF NOT EXISTS leads (
  id SERIAL PRIMARY KEY,
  full_name TEXT,
  title TEXT,
  company TEXT,
  profile_url TEXT UNIQUE,
  email TEXT,
  opener TEXT,
  status TEXT DEFAULT 'queued',
  created_at TIMESTAMP DEFAULT now()
);

n8n Workflow (Option A – CSV import)

  1. Cron: Every hour.
  2. Read Binary File: Glob ./dropbox/leads/*.csv.
  3. Spreadsheet File: Convert CSV → JSON rows.
  4. Function: Normalize fields (full_name, title, company, profile_url, email).
  5. HTTP Request → Ollama: Generate a short opener based on title/company.
    Prompt:
    You write concise, respectful first messages (<= 280 chars) with 1 personalized insight.
    Return ONLY text. Inputs:
    - Name: {{ $json.full_name }}
    - Title: {{ $json.title }}
    - Company: {{ $json.company }}
    - Our value: "privacy-first AI automations; zero data leaves client network"
    Output: message in sentence case, no emojis.
    
  6. Postgres (Upsert): Insert rows with status='queued'. Skip duplicates by profile_url.
  7. Rate Limit: 15 items/day.
  8. Email Send (or custom HTTP to your local message-sender): Dispatch messages; update statussent.

n8n Workflow (Option B – Local browser automation)

Keep automation on your machine with a local Playwright script. Expose POST /send on http://localhost:3001 that logs in using your session cookie (stored locally) and sends a message on a given profile URL.

# Minimal Python (fastapi + playwright) sketch
# Run locally; never share cookies with n8n. n8n just calls http://127.0.0.1:3001
  1. CronPostgres (Query): Fetch next 10 queued leads.
  2. HTTP Request: POST each to http://127.0.0.1:3001/send with { profile_url, opener }.
  3. IF: On success, mark sent; on failure, retry with exponential backoff.
  4. Wait: 3–5 minutes between sends; random jitter to mimic human cadence.

Privacy & Compliance

  • Prefer manual CSV exports you initiated; avoid mass scraping.
  • Run browser automation only on your device; store cookies in OS keychain or .env file with strict perms.
  • Never upload lead data to a third-party LLM. Keep enrichment local via Ollama.

Observability, Backups, and Upgrades

  • Logs: n8n execution logs set to minimal; forward container logs to a local file sink.
  • Backups: Nightly pg_dump; snapshot ./data volumes; archive CSV reports.
  • Model hygiene: Pin model tags (mistral:7b); test upgrades in staging.
  • Access: SSO via reverse proxy or strong basic auth; rotate secrets quarterly.

Troubleshooting

  • LLM timeouts: Reduce prompt size; try a smaller model; ensure Ollama has RAM/VRAM headroom.
  • Email parsing: Start with plain-text receipts; add local OCR later if needed.
  • Duplicate leads: Ensure unique index on profile_url; dedupe before insert.
  • Rate limits: Use n8n Rate Limit + Wait nodes; spread sends across the day.

Next Steps

  • Add a local OCR microservice (Tesseract container) for PDFs/JPG receipts.
  • Create a small UI to approve or correct extracted expenses before insert.
  • Integrate with your accounting system via local connectors or SFTP drops.
  • Extend outreach with reply detection and automatic follow-ups (local only).