Moosa Memon
← Index of work

An ops hub where the AI can't certify its own claims

OpsRelay: calls, forwarded emails and typed notes go in; deduplicated, confidence-tagged tasks come out, behind a Slack approval gate a human has to pass before anything is sent or marked confirmed. Built for real-estate offices, agencies and dispatch desks.

Type
Agents
Stack
FastAPI / LangGraph / Claude API / n8n / Supabase / Slack / Notion
Status
Complete; 117 tests run the real pipeline on scripted model responses, no credentials needed
Measured
A fact is 'verified' only if it matches a field the upstream platform actually sent; everything read from a transcript, email body or note stays 'unconfirmed' until a human approves it. Enforced in code and tested as a property, including the case where the model claims a source it doesn't have

Problem

Small service teams (real-estate offices, agencies, dispatch desks) have their operational reality scattered across a phone system, an inbox, a CRM and someone’s notes app. They want AI to organise that. They do not want AI speaking for them: sending a confirmation the customer never agreed to, or attaching a call to the wrong client. Most “AI ops automation” demos are an LLM with a webhook. The interesting parts of this one are the three places it refuses to guess.

Constraints

System

n8n orchestrates: webhooks, cron, retries, error routing. A FastAPI service decides: idempotency claim, persist the raw event before any model call, a three-hop LangGraph pipeline (classify → extract → assess), fact adjudication, contact matching, task creation, and then the Slack gate. Supabase is the record; Notion is the surface the team works from; Airtable stands in for the client’s real CRM.

Slack interactivity points directly at the service rather than through n8n, because signature verification and the approval state transition belong in the same place. An approval that can be replayed or forged is worse than no gate at all.

Decisions

The model cannot certify its own output. The extraction graph proposes a provenance for each claim, and the fact layer throws that proposal away and re-derives it from whether the value matches the event’s structured block. A note, which has no structured block, can never produce a verified fact. This is what makes the approval gate load-bearing instead of decorative.

Dedupe keyed on source + external_id, not the timestamp. The original spec included the delivery timestamp. Folding it in would mean the same logical event redelivered five seconds later hashes differently and sails through, reopening the exact hole dedupe exists to close. Typed notes, which have no upstream id, fall back to source + occurred_at + digest(content), normalised so reformatting a note isn’t a new event.

Matching returns a decision, not a best guess. Three passes (exact identifier, lexical name similarity, optional embeddings), and anything below the accept threshold creates the task flagged needs_contact_link with candidates attached. Two Castellanos siblings both scoring 1.00 against “this is Castellanos” is a flag, not a coin flip. Company alone never links.

Three hops instead of one prompt. Cheap triage terminates non-actionable events (thanks, FYIs) before the expensive hops. Extraction carries the verbatim span for every item: no quote, no claim. Assessment sees the extraction as data rather than its own prior output, which makes it far more willing to mark claims low-confidence.

Outcome

117 tests drive the real pipeline end to end on scripted model responses with no network. The replay script fires each fixture twice so you can watch the second delivery get dropped. /healthz reports exactly which integrations are live and which are running dry, and outside development the service refuses to serve the Slack interactions endpoint without a signing secret, because it can approve outbound messages.

Have a workflow that looks like this?

Most of these start as a messy, manual process someone got tired of. Twenty minutes is usually enough to sketch how I'd approach yours.