The problem

Enquiries, job threads and tenders arrive through shared inboxes with no CRM. Work gets lost, a stale thread looks like a dead one, and nobody can see who owes a reply.

What we built

A read-only pipeline: Microsoft Graph fetch → dedupe → batched Claude classification (enquiry / job thread / contractor / supplier / admin / junk) → tenant domain rules → a single Postgres ingest RPC → a board grouped by company, with a weekday n8n digest of items needing review.

Engineering

  • Fail-closed classification — a failed model call files the message as a 0-confidence enquiry for human review rather than dropping it. Anything under 0.7 confidence is never auto-filed.
  • Operator data is untouchable — a Postgres trigger freezes staff-entered fields (stage, notes, next action) against pipeline re-runs, with the actor set inside the RPC transaction so pooled connections can't leak it.
  • Row-level security on every tenant-scoped table; multi-tenant from day one.
  • Dedupe on message ID before classification, so the same email seen through two mailboxes is classified once.
  • Money in Decimal, VAT from a fixed decision table that refuses unsupported cases, CIS applied to labour only, rounding once at the total.
  • Guarded live runs — dry-run by default; a live run needs an explicit approval flag.
  • 551 automated tests, all offline.