How to build a portable AI context layer outside your CRM
A working, CRM-independent context layer. The reusable knowledge and prompts live in git, an app runtime loads only the relevant rules, and a rep gets a grounded first-draft email.
This is the portable version of the CRM AI context layer. Same idea, different home. Instead of storing what your fields mean inside one CRM, you keep that knowledge in git or a small database and let the app runtime load the right slice when the model needs it. The CRM becomes just another source.
To keep it concrete, I built the whole thing: an agentic workflow that takes a lead, researches it, checks it against the CRM, builds context, and drafts the best next outreach email for a rep to review. Claude writes. Postgres stands in for the CRM. The runner is just code, which is the point: you don't need to buy or rent a separate automation layer for a simple context demo.
The context layer lives in git, not the CRM
The system runs on four folders in a repo. That is the context layer, plus the logic and prompts around it.
context/ reusable knowledge
icp · brand-voice · messaging-pillars · persona-rubrics ·
sequence-definitions · enrichment-policy · source-allowlist ·
measurement-spec · field-glossary
coordination/ decision logic
triggers · hygiene-rules · handoffs · qa-rubric
execution/ the ordered steps and the full prompts
01-research · 02a-hygiene · 02b-enrich · 03-build-context ·
04-draft-email · 05-qa-grade
memory/ run state and fallbacks
last-run-state · reply-performance · research-fallbacksThe context/ folder is the part that matches the pillar. It holds the field
glossary, the ICP, the brand voice, and the persona rubrics. That is the meaning an
AI needs and no CRM schema explains. The difference here is that it lives in the repo,
so the same definitions work whether the record came from Salesforce, HubSpot, or a
CSV someone dropped in.
My rule of thumb: adding an automation shouldn't mean copying the same giant prompt into a new place. The knowledge stays in one layer, and every workflow reads from it.
The runner is replaceable. The context layer isn't.
Keep the runner deliberately boring. It triggers steps, loads records, fetches the relevant rules, calls the model, and writes the result. Every prompt, decision rule, and piece of durable context lives outside the orchestration code.
App runtime
-> load CRM record
-> load relevant context entries
-> Claude
-> save generated artifact + context version
-> render the resultThis can be a Next.js API route, a queue worker, a cron job, a serverless function, or an automation tool like n8n, Make, or Zapier. The runner is plumbing. The expensive part is the business meaning encoded in the context layer.
Keep the provider in the context repo, not in the runner
One thing I got wrong in the first pass, and it undercuts the whole premise of this build: I hardcoded the vendor and the model tiers into the workflow. Opus for the draft, Sonnet for everything else, both written directly into the nodes.
The entire argument for this architecture is portability. A layer that can't change providers isn't portable, it's just decoupled from one CRM.
Model choice belongs in the context repo as configuration:
provider: anthropic
defaults:
research: { model: claude-sonnet-5, maxTokens: 2000 }
hygiene: { model: claude-haiku-4-5, maxTokens: 800 }
draft: { model: claude-opus-5, maxTokens: 1500 }
qaGrade: { model: claude-sonnet-5, maxTokens: 1200 }The runner reads the routing table and passes the model through. Now an org-wide provider switch is a pull request against one file, reviewable and reversible, rather than an afternoon of redeploying code or clicking through nodes. The first time cost forces that decision on you, you'll be glad it's a config change.
Grounding: every line traces to a real signal
The fastest way to lose a rep's trust is one made-up detail. So the rule is simple. Every personalized line in the email has to trace to a real signal (CRM, enrichment, research, the opportunity, an engagement) or an approved messaging pillar. Sources go in a citations list, never inline, so the email body stays clean and QA can check each claim on its own.
I pushed the same rule onto research. Sourced answers only, at most two web searches, JSON out, and one hard instruction: say "your warehouse" if you can't confirm the data stack. That single line is what stopped it from inventing a partnership that never existed.
Add two fields to every citation that I left out at first: when the signal was retrieved, and which workflow and context version produced it. A citation without a timestamp isn't verifiable. Six weeks later, "their careers page lists three data engineering roles" is either a live signal or an artifact, and there's no way to tell which from the citation alone.
{
"claim": "Hiring three data engineers in Q3",
"source": "https://example.com/careers",
"retrievedAt": "2026-08-18T14:22:09Z",
"workflowVersion": "sdr-core@1.4.2",
"contextVersion": "ctx@2026-08-17"
}Relevance-slicing keeps the prompt small
The draft step doesn't get the whole knowledge base. It gets three things: the contact's persona row, the messaging pillars that fit that persona, and a short overlay for whatever triggered the run. That is roughly 60% less prompt, and the writing gets sharper because the model isn't wading through everything you know.
Persona and its specific pain points come from a code map, not an extra AI call. Deterministic where I can be, AI where language actually needs it.
The pipeline, end to end
Event (Gong call · campaign · opp change · inbound · nightly cron)
-> 01 Research verify CRM signals, cite sources, abstain when thin
-> 02 Enrich provider lookup (no AI), before/after field deltas
-> 03 Build Context the AI context object, refreshed every run
-> 04 Draft Opus writes the rep-facing email, one CTA, sources cited
-> 05 QA grade check against a rubric; fail -> revise once -> human
-> write "AI Next Email" onto the contact
-> the outreach tool builds the send from that field
-> a rep reviews and sendsNothing sends on its own. The rep still reviews and hits send. The only change is they start from a grounded draft instead of a blank page.
New leads take one extra step first: a hygiene gate. Exact and clear duplicate matches get decided in code. The AI only ever sees the genuinely ambiguous band, and I told it to bias toward "review," never a wrong merge. Survivors join the same core pipeline, so there is one generation engine with two ways in.
What this needs before it runs on real volume
Everything above is the demo, and the demo is honest about being a demo. What it doesn't have is the set of controls that keep a generation pipeline from hurting you, and this is the part most AI workflow write-ups skip entirely.
A generation pipeline has a property that a retrieval pipeline doesn't: it costs money per run and it writes to the records that trigger it. Both of those turn into problems on real volume, and they turn into problems fast.
Here's the list, roughly in the order things break.
A recursion guard, before anything else. Generation triggers on record change. Generation writes to the record. That's a cycle, and the workflow above doesn't address it. Either write with an integration user you exclude from the trigger filter, or set a flag property the trigger checks, or compare the modifying user against your own service account and drop the run. Pick one and put it in the first step, not the fifth.
A per-record cooldown. Event-triggered generation on activity churn will refresh the same hot record several times in a day, because hot records are exactly the ones generating events. Without a cooldown window the pipeline quietly DDoSes itself, and the bill is usually the first symptom anyone notices. Put the real-time lane behind a cooldown and let a scheduled sweep catch anything that got skipped:
-- Drop the run unless the record is genuinely due for a refresh.
SELECT id
FROM contacts
WHERE id = $1
AND (ai_context_generated_at IS NULL
OR ai_context_generated_at < now() - interval '12 hours');Bulk-load suppression. One CSV import queues one generation per row. A 40,000-row list import at 2pm on a Tuesday is a very expensive afternoon. You need a global suppression switch you flip before a load and clear after, and it has to be checked by the trigger, not by the generation step. This isn't optional and it isn't a nice-to-have; every team I've watched build one of these learned it the same way.
A daily cap with a circuit breaker and an alert. Count generations per day, stop at the ceiling, and tell someone. A cap that silently stops is a different outage than a cap that pages you, and the silent one takes three days to notice.
Atomic write-back. A failed generation must never wipe a good prior value. Write the artifacts and the timestamp together in one transaction, and on failure write nothing at all. The naive version clears the field, calls the model, and writes the result, which means a timeout leaves the record worse than before it ran.
Idempotency keys. Retries are guaranteed. Jobs retry, webhooks get redelivered, someone re-runs a failed execution from the UI. Duplicate writes shouldn't happen. Derive a key from the record ID plus the triggering event ID, and make the write a no-op if that key already landed.
Append-only history. A snapshot generator with amnesia is far less useful than one with a ledger. Keep every generated artifact with its timestamp, its trigger, and its context version, then point the record's live field at the newest one. You get "what changed about this account since last month" for free, and if each entry records what triggered it, the ledger doubles as an audit trail of the pipeline itself.
CREATE TABLE ai_context_history (
id bigserial PRIMARY KEY,
record_id text NOT NULL,
record_type text NOT NULL,
generated_at timestamptz NOT NULL DEFAULT now(),
trigger_source text NOT NULL, -- gong_call | campaign | opp_change | inbound | sweep
idempotency_key text NOT NULL,
workflow_version text NOT NULL,
context_version text NOT NULL,
artifact jsonb NOT NULL,
UNIQUE (idempotency_key)
);The generated field is not a source of truth
The pipeline writes an "AI Next Email" onto the contact, and the pillar guide spends a section on why a generated field is more dangerous than a stale one. That applies to this build directly, so it's worth saying here.
Anything downstream that reads this field, including the next run of this same pipeline, needs to know it's non-authoritative. Two specific failures I've watched: a planned next step gets read later as a completed one, and a quiet record generates an optimistic summary because call notes describe intent rather than outcomes.
There's also a slower failure that only shows up after a few months. A generator that reads prior context to stay consistent will echo retired product names, dead system names, and old segment language forever, because each run treats the last run's output as background. One instruction in the generation prompt clears it out:
Build only from current signals. Prior context and history are for continuity only and may not introduce a fact the current data doesn't support.
That's what flushes dead vocabulary out of a self-referencing loop.
Freshness matters at the point of use, not the point of generation
The pipeline above ends at "the rep sends." The harder problem is what happens after.
A draft generated on Monday against Monday's signals gets sent on Thursday, and by Thursday the deal moved stages, the champion left, or the ticket that prompted the whole thing got resolved. Now you've shipped a confidently mismatched opener, which is worse than a generic one because it's specific and wrong.
Two things fix it. Regenerate at consume time rather than at generate time, or gate the send on context freshness: if the artifact is older than your threshold, the enrollment doesn't fire and the record goes back through the pipeline first. The freshness threshold belongs in the context repo alongside everything else, because it's a business decision.
The boring parts that aren't optional
Four things I'd want in place before pointing this at a real portfolio company:
Endpoint authentication. If your runner has a public endpoint, protect it. Anyone who can trigger generation against arbitrary record IDs can spend your model budget. Header auth at minimum.
Secret handling. Credentials live in environment variables or a secret store, not in prompt files, exported workflow JSON, or client-side code. Check what you're committing.
Workflow versioning. Stamp the runner version into every generated artifact. Without that, "why did this record get a weird draft in July" has no answer.
PII minimization. Send the model what the task needs and nothing else. A draft step needs the persona, the trigger, and a handful of signals. It doesn't need the full contact record, and every extra field is a field that ends up in a provider's logs.
The human gate is the pattern, not an implementation detail
I described the QA step as a rubric check that routes failures to a person, which made it sound like plumbing. It's the most reusable idea in the build.
The shape: code handles the confident cases, AI handles the ambiguous ones, and anything it can't resolve goes to a human queue rather than to a default. The hygiene gate uses it for duplicate matching, where exact matches merge in code, clear non-matches get dropped in code, and only the genuinely ambiguous band reaches the model with an instruction to bias toward "review." The QA step uses it for drafts.
That pattern transfers to anything where being wrong is expensive and being slow isn't. Bias the model toward abstaining, then make abstaining cheap by having somewhere for it to go.
Steal the pattern
The point isn't this exact workflow. It's the shape. Keep your context and prompts in git or a database. Let a small runner load the model. Ground every claim in a real signal, with a timestamp. Keep a human on the send. That works against any CRM, and you can rebuild it fast. I had about four hours in this one, and half of that was the demo UI.
The controls above took longer than the pipeline did. That ratio is normal.
Related
- The pillar explains the concept.
- The HubSpot version keeps the context layer closer to one CRM's properties.
- The Salesforce version uses an admin-owned custom object and Apex retrieval.
Get the next guide
New guides and the occasional note on GTM tooling. Don't worry, I won't drop you into a three-month nurture.