Comp AI's CRM: An Agent That Prices Its Own Evidence Before Writing to Your Record

August 2, 2026

|repo-review

by Florian Narr

Comp AI's CRM: An Agent That Prices Its Own Evidence Before Writing to Your Record

trycompai/crm is a CRM where the agent isn't a chat box bolted onto a contacts table. It's a separate deployment, on eve, that leases its own work off a queue, decides what to look at next, and refuses to write anything to a contact record unless the evidence backing it clears a floor.

What it does

It's a self-hostable CRM — contacts, companies, deals — with a research agent that fills in the blanks: title, employer, LinkedIn URL, seniority. The agent runs on its own schedule against its own work queue, not in response to a rep opening a record.

Why I starred it

Most "AI CRM" products are a form with a chat widget next to it. The interesting decision here is architectural: the agent is a separate app (apps/agent) with its own deploy, its own sandbox, and its own database access pattern. And the rule baked into the tool layer — no tool accepts a confidence score — is a real answer to a real failure mode. A model asked to grade its own certainty will, and it'll be wrong in the direction that makes it look useful. This repo prices evidence instead of trusting self-reported confidence.

How it works

The work queue lives in apps/agent/agent/lib/tasks.ts. claimDue() is raw SQL, not Prisma's query builder:

UPDATE "agentTask" AS t
SET "leasedUntil" = ${until}, "attempts" = t."attempts" + 1
FROM (
  SELECT t2.id FROM "agentTask" AS t2
  WHERE t2."finishedAt" IS NULL
    AND t2."dueAt" <= ${now}
    AND (t2."leasedUntil" IS NULL OR t2."leasedUntil" < ${now})
    AND t2."attempts" < ${MAX_ATTEMPTS}
  ORDER BY t2."priority" DESC, t2."dueAt" ASC
  LIMIT ${limit}
  FOR UPDATE SKIP LOCKED
) AS due
WHERE t.id = due.id
RETURNING ...

FOR UPDATE SKIP LOCKED is the part that matters: two dispatchers can poll the same table at once and each walks away with a disjoint set of rows, no coordination service required. The cron entry in apps/agent/agent/schedules/dispatch.ts runs every minute and just calls drainAll over whatever's due — it doesn't decide what's due, the lease does. If a run dies mid-task, the lease expires and the row is up for grabs again. "Recheck this contact in 90 days" is a row with a dueAt, not a second cron job.

The part I actually stopped to reread is apps/agent/agent/lib/evidence.ts. Every fact the agent wants to write — a title, an employer, a LinkedIn URL — comes with a list of Evidence entries, each tagged with a kind (crm.signature-block, github.account-identity, linkedin.employer-and-name...) and a fixed weight:

export const WEIGHTS = {
  "profile.email-match": { weight: 0.95, primary: true, ... },
  "crm.signature-block": { weight: 0.8, primary: true, ... },
  "web.cited-claim": { weight: 0.4, primary: false, ... },
  "handle.name-form": { weight: 0.35, primary: false, ... },
  contradiction: { weight: 0, primary: false, ... },
} satisfies Record<EvidenceKind, Weighting>;

scoreEvidence() combines them the way you'd combine independent probabilities of detection — not by averaging, by treating each piece as a chance the claim is wrong and multiplying those chances down:

const combined = evidence.reduce(
  (remaining, item) => remaining * (1 - WEIGHTS[item.kind].weight),
  1,
);
let score = Math.min(CEILING, 1 - combined);

Stack a handle.name-form (0.35) on top of a search.cites-profile (0.35) and you get 1 - (0.65 × 0.65) = 0.5775 — enough to clear POSSIBLE (0.3) but nowhere near VERIFIED (0.85), and bandFor() in the same file won't grant VERIFIED at all without at least one primary: true entry, no matter how high the score climbs. Weak circumstantial evidence can pile up all it wants; it never becomes strong evidence. That's the actual engineering answer to "the agent hallucinated a job title" — not a bigger prompt, a scoring function two people could review in five minutes and agree on.

record_fact.ts (the tool the agent calls) and facts.ts (where it lands) enforce a second rule on top of the score: fillsBlank() checks humanOwns() first — if a rep already set the field, the agent's evidence doesn't matter, the write is refused. Strong evidence fills a blank. It never overwrites a human.

The sandbox is the other half of the honesty story. apps/agent/agent/sandbox/sandbox.ts is four real lines:

export default defineSandbox({
  backend: defaultBackend({
    vercel: { networkPolicy: "deny-all" },
    docker: { networkPolicy: "deny-all" },
    microsandbox: { networkPolicy: "deny-all" },
  }),
});

The agent gets bash, grep, glob, and a /workspace — enough to diff this month's enrichment against last month's — but no egress and, per the README, no DATABASE_URL either. Web fetches happen in the app runtime, not the sandbox, so there's no path by which a shell command could exfiltrate a customer's inbox. It's a small design choice that a lot of "give the agent a computer" projects skip.

Using it

git clone https://github.com/trycompai/crm.git && cd crm
cp .env.example .env
bun install
docker compose up -d      # Postgres on :5432
bun run db:deploy
bun run db:seed           # a believable pipeline to poke at
bun run dev

apps/agent/package.json lists seven dependencies total, and none of them is a model provider SDK — model calls route through the Vercel AI Gateway, so switching models is a config change, not a rewrite. schedule_recheck.ts is worth reading as a tool definition on its own: the reason field has a .min(10) and the description tells the model explicitly what a good one looks like ("a job change here would move the Acme deal," not "scheduled recheck") — that reason gets shown to the rep, so a vague one is visible as vague.

Rough edges

apps/agent has zero *.test.ts files. The repo overall carries about a hundred tests, but the scoring function and the task-leasing SQL — the two pieces this review spent the most time on — have none of them. docker compose plus Bun plus a Postgres seed is a heavier local setup than most repos I cover, and getting the agent tab to actually talk to you requires setting AGENT_BRIDGE_SECRET in two processes, which is easy to skip and get a silently unconfigured tab instead of an error. The repo is young enough (v1.14.0, commits landing multiple times a day as of this writing) that some of these tools — record_job_change.ts, write_workspace_profile.ts — read like they're still finding their final shape.

Bottom line

If you're building anything where an LLM writes facts into a system of record — not just chats about them — evidence.ts and facts.ts are worth reading even if you never run this CRM: the pattern of "weight the source, combine as independent detections, require a primary source to reach the top band, never overwrite a human" is more portable than the app around it.

trycompai/crm on GitHub
trycompai/crm