One CRM question, traced end to end
What actually happens between a question and an answer: phase-1 payload, rule index, selection, query plan, tool-boundary validation, and the audit log. Plus the same question failing on purpose.
Everything else on this site describes the parts. This page runs one question through all of them and shows the actual artifacts at each step, then runs a second question that's supposed to fail.
The setup is the seven starter rules against a Salesforce org, with access through an MCP server. The user is a regional AE who can see their own team's accounts.
The question
How much qualified pipeline did we create last quarter?
Three ways that goes wrong without any of this: calendar quarter instead of fiscal, CPQ artifacts counted as real, and "qualified" defined on the fly by whatever the model thinks it means.
Step 2: what always loads
No decisions here. This payload is identical for every question, which is what makes it cacheable and what makes the exclusions impossible to miss.
# Global instructions (guidance v2026.08.3)
- Fiscal year starts 1 February, named for the January end-month. [R-001]
- Convert to USD before summing any amount across records. [R-002]
- Never read a field whose API name starts with DNU_. [R-005]
- Load guidance once per session. Do not re-fetch on every turn.
- State the rules applied, the exclusions applied, the exact date range,
and whose permissions the query ran under.
# Always-on exclusions
- [R-003] Before any query on Opportunity, add:
Type != 'Internal Test' AND IsSystemGenerated__c = false
# Anti-patterns
- [R-007] Never group or filter on a multi-select or formula field.
# Allowed objects and fields
Account: Id, Name, OwnerId
Opportunity: Id, Name, AccountId, OwnerId, StageName, Amount, CloseDate, CreatedDateThat's roughly 200 tokens and it never changes between questions. It sits in the cached prefix.
Step 3: the index
Every active rule, titles only. This is the part that lets the library grow past what you could afford to send in full.
R-001 | * | temporal | v1 | Use the configured fiscal calendar for every quarter, year, and comparison
R-002 | * | currency | v1 | Never sum amounts across currencies without converting first
R-003 | Opportunity | exclusion | v1 | Exclude system-generated, test, and internal opportunities from reporting
R-004 | Opportunity | attribution | v1 | Credit the AE from owner and the development rep from contact role
R-005 | * | authority | v1 | Treat any field whose API name begins with DNU_ as dead
R-006 | * | security | v1 | Query only allowed objects and fields; refuse rather than substitute
R-007 | * | anti_pattern | v1 | Never group or filter on a multi-select or formula field
R-024 | Opportunity | definition | v2 | Count only Stage 3+ opportunities with a future close date as qualified pipeline
R-031 | Account | attribution | v1 | Credit the account owner at time of close, not the current owner
R-052 | Opportunity | definition | v1 | Read Amount as ACV on New Business and total project fee on Professional ServicesStep 4: what gets fetched, and why
The question says "qualified pipeline," so R-024's title matches and its body gets
pulled. R-024 declares dependsOn: [R-003], which is already loaded.
R-052 also gets fetched: the question will sum Amount, and R-052's title says
Amount means different things on different record types. That's the fetch that
saves the answer, and it happens because the title says what it says. Titled
Opportunity amount notes, it would have been skipped.
R-031 is not fetched. Attribution isn't what was asked.
[R-024 v2] Count only Stage 3+ opportunities with a future close date as qualified
pipeline. "Qualified" means StageName in (Evaluation, Proposal, Negotiation).
Filter: StageName IN ('Evaluation','Proposal','Negotiation') AND CloseDate >= TODAY
Depends on: R-003
[R-052 v1] Amount is not comparable across record types. Filter to a single
RecordType before summing, and state which record type the total covers. Never
present a blended Amount total across New Business and Professional Services.Total loaded: phase 1 plus two rule bodies. Not the library.
Step 5: the plan, and the gate
The model proposes a query. Then code checks it, after the reasoning is finished, which is the only moment that matters.
{
"object": "Opportunity",
"fields": ["Id", "Amount", "StageName", "CloseDate", "CreatedDate", "RecordTypeId"],
"filters": [
"CreatedDate >= 2026-05-01 AND CreatedDate <= 2026-07-31",
"StageName IN ('Evaluation','Proposal','Negotiation')",
"CloseDate >= TODAY",
"Type != 'Internal Test'",
"IsSystemGenerated__c = false"
],
"groupBy": "RecordTypeId"
}The date range is the one R-001 produces. Fiscal Q2 here runs 1 May to 31 July, not the calendar quarter a model would reach for unprompted. The two exclusion filters came along without the question ever mentioning them, because R-003 always loads.
assertAllowed(Opportunity, [Id, Amount, StageName, CloseDate, CreatedDate, RecordTypeId])
→ object Opportunity: allowed
→ fields: all allowed
→ PASSStep 6 and 7: rows, then the answer
The query runs as the AE, so their sharing rules apply underneath everything above.
You created $4.2M in qualified pipeline in fiscal Q2 (1 May – 31 July 2026),
across 38 opportunities.
That figure is New Business only. Amount means annual contract value on New
Business and total project fee on Professional Services, so the two can't be
summed (R-052). Professional Services added $610K on 9 opportunities if you
want it separately.
Rules applied: R-001 fiscal calendar, R-024 qualified-pipeline definition,
R-052 record-type amount semantics.
Exclusions applied: R-003, removing 14 system-generated and test opportunities.
Date range: 2026-05-01 to 2026-07-31 (fiscal Q2).
Permissions: ran as your user. Accounts outside your team are not included.The last four lines make this checkable in two minutes. They're there because a global instruction requires them, not because the model felt like being thorough.
The audit log
Written whether the answer was right or not. This is what makes the next corrective rule possible.
{
"runId": "r_01J9X2",
"askedBy": "user_4471",
"guidanceVersion": "2026.08.3",
"phase1RuleIds": ["R-001", "R-002", "R-003", "R-005", "R-006", "R-007"],
"fetchedRuleIds": ["R-024", "R-052"],
"plannedObjects": ["Opportunity"],
"queriedObjects": ["Opportunity"],
"plannedFields": ["Id", "Amount", "StageName", "CloseDate", "CreatedDate", "RecordTypeId"],
"gateResult": "pass",
"rowsReturned": 47,
"rowsAfterExclusions": 38,
"outcome": "answered",
"latencyMs": 3120,
"cache": "hit",
"tokensIn": 6840,
"tokensOut": 410
}plannedFields versus what was actually queried is the pair people leave out and
then wish they had. When an answer looks wrong six weeks later, the first question
is always "what did it read?"
Now the same architecture, failing
Second question, deliberately outside the lines:
Which support tickets are open on our at-risk accounts, and what's their sentiment score?
Case isn't an allowed object here. Watch where it stops.
assertAllowed(Case, [Id, Subject, Status, Sentiment_Score__c])
→ object Case: NOT IN ALLOWLIST
→ throw BlockedByGuidance("Case is not an allowed object for agent queries.")Nothing queried. The exception surfaces to the agent, which reports it rather than routing around it:
I can't answer this one. Case is not an allowed object for agent queries, so I
have no access to support tickets or sentiment scores.
I can tell you which accounts are flagged at risk from Account and Opportunity,
which are allowed. Want that instead?
Permissions: ran as your user.{
"runId": "r_01J9X3",
"guidanceVersion": "2026.08.3",
"plannedObjects": ["Account", "Case"],
"queriedObjects": [],
"gateResult": "blocked",
"gateReason": "object_not_allowlisted:Case",
"outcome": "refused",
"latencyMs": 890
}Three things to notice.
The circuit breaker is code, not text. assertAllowed threw. The model didn't
decide to be well-behaved, it was handed an exception it had to deal with. If the
only defence had been a sentence in the prompt, the likely outcome is an answer
assembled from Task records and Opportunity.Description with no error anywhere
and no way to tell.
The refusal names what's missing. "Case is not an allowed object" is actionable: someone can decide whether to allowlist it. "I don't have access to that information" is not.
It's logged as a refusal, not an error. gateResult: blocked with a reason.
Refusal rate and tool-boundary rejection rate are worth tracking as metrics. A rate
of zero usually means the boundary isn't wired up, rather than that nobody ever asks
for anything out of scope.
The other failure worth showing
Guidance itself failing to load. Say the rules store times out after phase 1:
{
"runId": "r_01J9X4",
"guidanceVersion": "2026.08.3",
"phase1RuleIds": ["R-001", "R-002", "R-003", "R-005", "R-006", "R-007"],
"fetchedRuleIds": [],
"fetchError": "timeout: rules store unavailable after 2 retries",
"outcome": "routed_to_review",
"latencyMs": 5400
}The tempting behavior is to answer anyway. Phase 1 loaded, the exclusions are there, the query would run fine. The correct behavior is to stop, because the question mentioned qualified pipeline and the definition of "qualified" never arrived. An answer built on a guessed definition is exactly what this architecture exists to prevent, and it would look completely normal on the way out.
That's what GQ-22 tests.
- How to build an AI context layer for your CRM — the concept and full build
- The minimum viable context layer — the seven rules in this trace
- Debugging wrong CRM AI answers — when the trace shows something wrong
- MCP vs. CRM context layer — where
assertAllowedlives in an MCP server
FAQ
- What does a CRM AI context layer actually do at runtime?
- It runs three phases before the model touches data. Phase 1 loads global instructions plus every always-load rule (exclusions, anti-patterns, temporal, currency, security). Phase 2 loads a compact index of every active rule as id, title, scope, and category. Phase 3 fetches full rule bodies for the handful the question touches. Then the finished query plan is validated in code at the tool boundary, and only then does a query run.
- How does the agent decide which rules to fetch?
- From the titles in the phase-2 index, which is why titles have to state the condition under which a rule applies rather than name a category. In this trace the agent fetches R-024 because its title mentions qualified pipeline, and never sees a reason to fetch R-031 because attribution is not what was asked.
- What should happen when an agent asks for a field it isn't allowed to read?
- The tool boundary should throw before any query runs, and the agent should report what it could not access rather than substituting a similar field. If your only defence is a sentence in the prompt asking it not to, the failure is silent: the agent answers from a lookalike field and nothing errors.
- What belongs in the audit log for an AI CRM answer?
- Guidance version, the rule ids actually loaded, the objects and fields planned versus queried, the tool calls made, the answer or refusal, latency, and cache hit or miss. Without the rule ids and guidance version you cannot attribute a bad answer to a rule revision, which means you cannot write the corrective rule.
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.