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.