L2A Decision Code Reference
Every code emitted by g-gremlin salesforce l2a, with meaning and apply behavior. Eighteen decision codes, three skip codes, ten apply result codes, five safe actions, and the explicit non-goals that keep this a governed matcher instead of a routing platform.
Published April 18, 2026 • The Salesforce L2A module ships in the Gremlin CLI private beta
Taxonomy at a glance
Scout conclusions, skip reasons, and apply outcomes are intentionally separated.
- decision_code — what scout concluded (18 codes: 5 safe, 7 review, 6 blocked)
- skip_code — why evaluation was intentionally skipped (3 codes)
- apply_result_code — what happened when apply ran (10 codes)
- safe actions — the only mutations the bounded write surface permits (5 actions)
Safe-to-apply decision codes (5)
Deterministic, single-account outcomes eligible for the bounded write surface.
| Code | Meaning | Behavior |
|---|---|---|
| L2A_SAFE_KNOWN_ACCOUNT_MAP_SINGLE_ACCOUNT | Lead email domain matches an entry in known_account_map and that entry resolves to exactly one active account. | Eligible for lead.set_gremlin_match under gremlin_owned or lead.set_shared_match_if_empty under approved shared_existing. |
| L2A_SAFE_EXACT_DOMAIN_SINGLE_ACCOUNT | Normalized lead email domain maps to exactly one active account; no parent/child collision. | Eligible for the bounded write surface. Non-personal email domains only. |
| L2A_SAFE_EXACT_DOMAIN_PLUS_NAME_SINGLE_ACCOUNT | Normalized domain and normalized company name both resolve to the same single account, reinforcing the match. | Eligible for the bounded write surface. Highest deterministic band below known_account_map overrides. |
| L2A_SAFE_EXACT_NON_PERSONAL_EMAIL_DOMAIN_SINGLE_ACCOUNT | Email domain is verified non-personal (not gmail, yahoo, outlook, etc.) and resolves to a single active account. | Eligible for the bounded write surface. Consumer email domains never land here. |
| L2A_SAFE_APPROVED_SHARED_FIELD_EMPTY | Under shared_existing, the approved customer field is empty and the deterministic evidence is strong enough to populate it. | Eligible for lead.set_shared_match_if_empty only. Requires acknowledged risk audit hash at apply time. |
Review-only decision codes (7)
Ambiguity that an operator resolves, not a heuristic. Routed into grouped_exceptions/.
| Code | Meaning | Behavior |
|---|---|---|
| L2A_REVIEW_DOMAIN_MULTI_ACCOUNT | Normalized email domain matches two or more active accounts with comparable evidence. | Routed into grouped_exceptions/domain_multi_account/. Operator picks or encodes an override in known_account_map. |
| L2A_REVIEW_NAME_MULTI_ACCOUNT | Normalized company name matches multiple candidate accounts; domain signal is insufficient to break the tie. | Routed to review. Never auto-applied. |
| L2A_REVIEW_PARENT_CHILD_COLLISION | Two or more candidate accounts in the same hierarchy match the lead with comparable evidence. | Routed into grouped_exceptions/parent_child/. Encode the owning level in known_account_map. |
| L2A_REVIEW_EXISTING_NON_GREMLIN_VALUE | Under observe_existing or shared_existing, the incumbent field already holds a non-Gremlin value that disagrees with the deterministic decision. | Never overwrites. Surfaces the disagreement in disagreements.csv. |
| L2A_REVIEW_CONSUMER_EMAIL_ONLY | Only signal available is a consumer email domain (gmail, yahoo, outlook, etc.). Governed matcher never auto-matches consumer domains. | Routed to review. Known_account_map override is the only safe-match path. |
| L2A_REVIEW_INSUFFICIENT_SIGNAL | Lead has no domain signal, no company-name signal strong enough to match, and no known_account_map hit. | Routed to review. Typical for incomplete or early-stage leads. |
| L2A_REVIEW_ACCOUNT_GRAPH_AMBIGUOUS | Candidate accounts form an ambiguous graph — for example, multiple sibling accounts under a parent with no clear owning node. | Routed to review. Requires explicit policy before known_account_map is safe. |
Blocked decision codes (6)
Hard stops: manual hold, mode unresolved, risk unsigned, field not writable, downstream risk, or target account missing.
| Code | Meaning | Behavior |
|---|---|---|
| L2A_BLOCK_MANUAL_HOLD | Gremlin_Match_Status__c is set to manual_hold. Operator or script has explicitly paused re-evaluation for this lead. | Row is skipped on every run until the hold is cleared. |
| L2A_BLOCK_MODE_UNRESOLVED | Mode could not be resolved from CLI flag, environment variable, gremlin.l2a.yaml, or default. Apply refuses to run. | Re-run init or pass --mode explicitly. Apply will not proceed without a resolved mode. |
| L2A_BLOCK_SHARED_FIELD_NONEMPTY | Under shared_existing with overwrite_mode=if_empty_only, the approved customer field already holds a value. | Never overwrites. Clear the field manually or switch to gremlin_owned to proceed. |
| L2A_BLOCK_FIELD_NOT_WRITABLE | The configured write field is not writable under the current permission context (FLS, profile, or rule restriction). | Fix permissions or FLS. Typically the Gremlin permission set covers this when the metadata pack is installed. |
| L2A_BLOCK_DOWNSTREAM_AUTOMATION_RISK | The configured shared field has unacknowledged downstream automation risk. The risk audit hash is missing from the apply command line. | Re-run init to refresh risk_audit.json and pass --ack-risk-audit on apply. |
| L2A_BLOCK_TARGET_ACCOUNT_MISSING | Candidate account exists in the plan but is missing, inactive, or inaccessible at apply time. | Re-run scout to refresh the candidate set. Inactive accounts drop out of eligibility. |
Skip codes (3)
Why evaluation was intentionally skipped. Not a verdict.
| Code | Meaning | Behavior |
|---|---|---|
| SKIP_NO_RELEVANT_CHANGE | Input fingerprint (lead inputs + matcher version + config version + known_account_map version + account corpus watermark) matches the previous evaluation. Nothing actionable has changed. | Row is skipped to avoid flapping. Pass --full-refresh to bypass the fingerprint cache. |
| SKIP_RECENTLY_EVALUATED | Gremlin_Last_Evaluated_At__c is within the configured re-evaluation window. | Row is skipped until the window elapses. |
| SKIP_OUTSIDE_SCOUT_FILTER | Lead falls outside the --lead-filter expression or the configured scout scope. | Row is excluded from the current run; widen --lead-filter to include. |
Apply result codes (10)
What happened when apply ran. Separate from decision code for a clean audit trail.
| Code | Meaning | Behavior |
|---|---|---|
| APPLY_DRY_RUN_OK | Dry-run succeeded: plan revalidated, preconditions hold, write was not executed. | Emitted for every row in a dry-run. Receipts are still written. |
| APPLY_SUCCESS_SET_GREMLIN_MATCH | Live apply succeeded in gremlin_owned mode. Gremlin_Matched_Account__c updated. | Receipt captures before/after and the plan hash. |
| APPLY_SUCCESS_SET_SHARED_MATCH | Live apply succeeded in shared_existing mode. Approved customer field populated under if_empty_only. | Requires both --confirm-plan-hash and --ack-risk-audit. |
| APPLY_SUCCESS_STATE_ONLY | Live apply updated only Gremlin state fields (status, decision code, review flag) without touching a matched-account lookup. | Typical for review-only or blocked decision codes promoted into state tracking. |
| APPLY_SKIPPED_PRECONDITION_CHANGED | Lead or candidate account changed between scout and apply. Revalidation detected drift. | Row skipped. Re-run scout to refresh the plan. |
| APPLY_SKIPPED_FIELD_NOW_NONEMPTY | Under shared_existing if_empty_only, the target field became non-empty between scout and apply. | Row skipped. No overwrite. |
| APPLY_FAILED_PLAN_HASH_MISMATCH | The plan hash passed via --confirm-plan-hash does not match the plan at apply time. | Apply refuses to run. Regenerate the plan or pass the correct hash. |
| APPLY_FAILED_PERMISSION_DENIED | Salesforce rejected the write due to profile, permission set, or FLS restriction. | Install or update the Gremlin permission set; retry. |
| APPLY_FAILED_VALIDATION_RULE | A Salesforce validation rule blocked the write. The write field or a dependent field failed validation. | Adjust the validation rule to accept the Gremlin write pattern, or mark the row manual_hold. |
| APPLY_FAILED_WRITE_REJECTED | Generic Salesforce write rejection — duplicate rule, lock contention, or transient failure. | Retry with exponential backoff. Persistent failures should be traced individually. |
The 5 safe actions
The only mutations the bounded write surface permits. Everything else is an explicit non-goal.
lead.set_gremlin_match
Set Gremlin_Matched_Account__c when the decision is safe and the field is empty or Gremlin-owned.
lead.set_gremlin_state
Set Gremlin_Match_Status__c, Gremlin_Decision_Code__c, and Gremlin_Review_Required__c.
lead.write_gremlin_provenance
Set Gremlin_Last_Evaluated_At__c, Gremlin_Last_Input_Fingerprint__c, Gremlin_Receipt_Id__c, and Gremlin_Apply_Result_Code__c.
lead.set_shared_match_if_empty
In approved shared_existing mode, write the approved customer field when it is empty and the decision is safe.
lead.mark_manual_hold
Mark a lead as manual_hold when Gremlin detects manual clear, manual set, or an operator hold condition.
Evaluation fingerprint
The deterministic input set that drives SKIP_NO_RELEVANT_CHANGE. If any component changes, the lead is re-evaluated.
- Normalized lead match inputs (email, domain, company name, website)
- Matcher version
- Config version (gremlin.l2a.yaml hash)
- known_account_map version
- Account corpus watermark or scout snapshot version
Explicit non-goals
The boundaries that keep this a governed matcher instead of a routing platform.
- No OwnerId writes, no territory writes
- No lead status mutation, no lead conversion
- No Account, Contact, Opportunity, or Case writes
- No account create, merge, or delete
- No Apex deployment, no Flow deployment
- No auto-deploy of AI-generated metadata
- No overwrite of non-empty customer-owned shared fields by default
- No silent auto-resolution of ambiguity with fuzzy or AI reasoning
- No promise of real-time in-org trigger behavior — scheduled continuous evaluation only
FAQ
How many L2A decision codes are there?
Eighteen decision codes in the core taxonomy: 5 safe-to-apply, 7 review-only, and 6 blocked. Three skip codes sit alongside (for why evaluation was intentionally skipped), and 10 apply result codes describe what happened when apply ran. Scout conclusions, skip reasons, and apply outcomes are intentionally separated.
Why are skip codes separate from decision codes?
Because SKIP_NO_RELEVANT_CHANGE, SKIP_RECENTLY_EVALUATED, and SKIP_OUTSIDE_SCOUT_FILTER describe why evaluation was intentionally not run — not a verdict about match quality. Mixing them into the decision taxonomy would pollute match analytics and confuse operator dashboards.
Why are apply result codes separate from decision codes?
Because plan-hash mismatch, stale-row failures, permission denials, and validation rule rejections are apply-time mechanics, not match conclusions. Keeping them separate keeps the decision taxonomy deterministic and lets operators audit the decision chain without apply-time noise.
Can Gremlin add custom decision codes for my org?
Not in v1. The code taxonomy is closed — extending it would break the determinism guarantee. Org-specific overrides belong in known_account_map (exact domains and explicitly approved suffix domains) rather than in new decision codes.
How do I look up the decision code for a specific lead?
Run g-gremlin salesforce l2a trace --email <email> or --lead-id <id>. The trace output includes the decision code, evidence chain, candidate accounts considered, and any existing-field comparison. It is the authoritative per-lead explanation.
Keep the conversation going
These pages are meant to help operators solve real problems. If you want the next guide, grab the low-friction option. If you need the implementation, not just the guide, book time.
Get the next guide when it ships
I publish architecture guides grounded in real implementations. No generic AI filler.
Use your work email so I can keep the list useful and relevant.
Need the implementation, not just the guide?
Book a 15-minute working session with Mike right on his calendar. Tooling, consulting, or a mix of both is fine.
Open Mike's calendarIf you want me to come in with context, leave your email and a short note before the call.