Reason Codes Are A Contract
If you want to operate an AI gate in production, your explanations must be more than human-readable prose—they must be machine-stable contracts. Reason codes are versioned, immutable identifiers that enable monitoring dashboards, incident alerts, and compliance workflows that natural language explanations cannot support.
The Problem with Dynamic Explanations
Many AI systems return explanations like this:
{
"verdict": "DENY",
"explanation": "Chargeback risk detected (confidence: 0.87). User flagged for suspicious activity."
}This seems helpful, but creates operational nightmares:
❌ Can't alert on it: Confidence changes every time (0.87 → 0.92 → 0.81)
❌ Can't trend it: Is "suspicious activity" the same as "unusual behavior"?
❌ Can't audit it: What exactly triggered the block—chargeback OR user flag OR both?
❌ Can't query it: No way to count "chargeback blocks" without regex hell
Why Reason Codes Fix This
Reason codes are stable machine strings with no dynamic content:
{
"verdict": "DENY",
"reason_codes": [
"CHARGEBACK_RISK_BLOCK",
"ACCOUNT_TAKEOVER_RISK_BLOCK"
]
}Now you can:
✅ Alert: Trigger PagerDuty when CHARGEBACK_RISK_BLOCK > 50/hour
✅ Trend: Graph % of denials by reason code over time
✅ Audit: Query: SELECT COUNT(*) WHERE reason_codes CONTAINS 'CHARGEBACK_RISK_BLOCK'
✅ Replay: Re-run the decision and get the same reason codes
Reason Codes as Public API
Lumyn treats reason codes as a versioned, backward-compatible API:
# src/lumyn/_data/schemas/reason_codes.v1.json
{
"schema_version": "reason_codes.v1",
"codes": [
{
"code": "CHARGEBACK_RISK_BLOCK",
"description": "Chargeback risk too high; refund blocked."
},
{
"code": "FAILURE_MEMORY_SIMILAR_BLOCK",
"description": "Similarity to a labeled failure is high; decision blocked."
}
]
}The Contract
- Stable names:
CHARGEBACK_RISK_BLOCKwill always mean chargeback risk - No dynamic content: Never
CHARGEBACK_RISK_BLOCK_87_PERCENT(that's a metric, not a code) - Versioned schema: v0 vs v1 reason codes are separate namespaces
- Documented semantics: Each code has a canonical description
This enables semantic interoperability: downstream systems (dashboards, tickets, audits) can depend on reason code meaning.
Lumyn's Production Reason Codes
Memory-Driven Codes (v1.3+)
When Lumyn's memory system detects similarity to labeled experiences:
| Code | Meaning | Verdict |
|---|---|---|
FAILURE_MEMORY_SIMILAR_BLOCK |
Similarity to a labeled failure is high | DENY |
FAILURE_MEMORY_SIMILAR_ESCALATE |
Similarity to a labeled failure is high | ESCALATE |
SUCCESS_MEMORY_SIMILAR_ALLOW |
Similarity to a labeled success is high | ALLOW |
Example:
{
"verdict": "DENY",
"reason_codes": ["FAILURE_MEMORY_SIMILAR_BLOCK"],
"risk_signals": {
"failure_similarity": {
"score": 0.94,
"top_k": [{
"memory_id": "01JBQW...",
"label": "failure",
"score": 0.94,
"summary": "Fraudulent refund request from compromised account"
}]
}
}
}Machine-stable code: FAILURE_MEMORY_SIMILAR_BLOCK
Dynamic evidence: risk_signals.failure_similarity.score = 0.94
Human context: summary = "Fraudulent refund..."
The code is what you alert on. The evidence is what you show in the dashboard. The summary is what operators read during incident response.
Policy-Driven Codes
From Lumyn's production schema:
| Code | Description |
|---|---|
REFUND_OVER_ESCALATION_LIMIT |
Refund amount over escalation threshold |
SPEND_OVER_APPROVAL_LIMIT |
Spend amount over approval threshold |
ACCOUNT_TAKEOVER_RISK_BLOCK |
Account takeover risk too high |
PAYMENT_INSTRUMENT_HIGH_RISK |
Payment instrument risk too high |
STORAGE_UNAVAILABLE |
Persistence unavailable; decision blocked |
MISSING_EVIDENCE_REFUND |
Missing required evidence for refund |
Complete list: reason_codes.v1.json
Anti-Patterns: What NOT to Do
❌ Dynamic Content in Codes
Bad:
{
"reason_codes": ["CHARGEBACK_RISK_87_PERCENT"]
}Why: Next request might be CHARGEBACK_RISK_92_PERCENT. Now you have hundreds of unique "reason codes" that are actually metrics.
Good:
{
"reason_codes": ["CHARGEBACK_RISK_BLOCK"],
"risk_signals": {
"chargeback_probability": 0.87
}
}❌ Natural Language as Codes
Bad:
{
"reason_codes": ["User might be engaged in suspicious activity based on recent patterns"]
}Why: Can't alert on it, can't query it, changes every time.
Good:
{
"reason_codes": ["PATTERN_ANOMALY_DETECTED"],
"explanation": {
"summary": "User might be engaged in suspicious activity based on recent patterns"
}
}❌ Mixing Codes and Metrics
Bad:
{
"reason_codes": ["HIGH_RISK", "MEDIUM_CONFIDENCE"]
}Why: "HIGH" is relative and changes. Is 0.7 high? Is 0.85 high?
Good:
{
"reason_codes": ["RISK_THRESHOLD_EXCEEDED"],
"risk_signals": {
"risk_score": 0.92,
"confidence": 0.76,
"threshold": 0.85
}
}What This Enables in Production
1. Reliable Dashboards
Query: Top deny reasons this week
SELECT
reason_code,
COUNT(*) as count,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () as percentage
FROM decision_records
WHERE verdict = 'DENY'
AND created_at > NOW() - INTERVAL '7 days'
GROUP BY reason_code
ORDER BY count DESC;Result:
reason_code | count | percentage
----------------------------------|-------|----------
CHARGEBACK_RISK_BLOCK | 1,247 | 42.3%
REFUND_OVER_ESCALATION_LIMIT | 891 | 30.2%
FAILURE_MEMORY_SIMILAR_BLOCK | 523 | 17.7%
ACCOUNT_TAKEOVER_RISK_BLOCK | 289 | 9.8%Impact: Product can prioritize fixing the top blocker (chargeback risk model).
2. Incident Alerts
Datadog Monitor:
alert: DENY rate for STORAGE_UNAVAILABLE > 10% of all decisions
query: sum(decisions.deny{reason_code:STORAGE_UNAVAILABLE}) / sum(decisions.total) > 0.1When the database goes down, Lumyn returns:
{
"verdict": "ABSTAIN",
"reason_codes": ["STORAGE_UNAVAILABLE"]
}The alert fires immediately because STORAGE_UNAVAILABLE is a stable code, not a dynamic error message.
3. Audit Workflows
Compliance requirement: "Prove how many high-value refunds were blocked due to fraud risk in Q4"
Query:
SELECT COUNT(*)
FROM decision_records
WHERE verdict = 'DENY'
AND action_type = 'refund'
AND amount_value > 1000
AND 'CHARGEBACK_RISK_BLOCK' = ANY(reason_codes)
AND created_at BETWEEN '2024-10-01' AND '2024-12-31';Result: 1,432 high-value refunds blocked for fraud risk
Proof: SQL query + Decision Record exports for auditors to replay
4. Incident Response: Replay with Same Codes
A customer escalates: "Why was my $500 refund blocked?"
Step 1: Find the Decision Record
$ lumyn show 01JBQX8P2M...
decision_id: 01JBQX8P2M...
verdict: DENY
reason_codes: CHARGEBACK_RISK_BLOCK, REFUND_OVER_ESCALATION_LIMITStep 2: Export and replay
$ lumyn export 01JBQX8P2M... --pack --out decision.zip
$ lumyn replay decision.zipReplay Output:
verdict: DENY
reason_codes: CHARGEBACK_RISK_BLOCK, REFUND_OVER_ESCALATION_LIMIT
matched_rules: R_FRAUD_BLOCK (HARD_BLOCKS)Result: Decision is deterministic. Same codes, same verdict, reproducible evidence for customer support.
Implementing Reason Codes in Your System
Define Your Code Schema
{
"schema_version": "reason_codes.v1",
"codes": [
{
"code": "APPROVAL_LIMIT_EXCEEDED",
"description": "Request amount over approval threshold",
"severity": "medium"
},
{
"code": "FRAUD_MODEL_HIGH_RISK",
"description": "Fraud detection model flagged high risk",
"severity": "high"
}
]
}Return Codes in Decisions
from lumyn import decide
record = decide(
request={
"action": {"type": "refund", "amount": {"value": 500, "currency": "USD"}},
# ...
},
config=LumynConfig(policy_path="policy.yml")
)
# Reason codes are guaranteed to be stable strings
assert isinstance(record["reason_codes"], list)
assert all(isinstance(code, str) for code in record["reason_codes"])
# Use codes for alerting/dashboards
for code in record["reason_codes"]:
metrics.increment(f"decisions.reason_code.{code}")Policy Rules Emit Codes
# policy.yml
rules:
- id: R_REFUND_LIMIT
stage: ESCALATIONS
if:
action_type: "refund"
amount_usd: { gt: 250 }
then:
verdict: ESCALATE
reason_codes:
- REFUND_OVER_ESCALATION_LIMITWhen this rule matches, the Decision Record includes REFUND_OVER_ESCALATION_LIMIT as a stable, machine-readable code.
Reason Codes + Human Explanations
Reason codes don't replace human-readable explanations—they complement them:
{
"verdict": "DENY",
"reason_codes": ["FAILURE_MEMORY_SIMILAR_BLOCK"],
"explanation": {
"summary": "This request is very similar to a previously labeled failed transaction that resulted in a chargeback.",
"details": "Similarity score: 0.94. Matched memory: 01JBQW..."
}
}- Reason codes: For machines (alerts, dashboards, queries)
- Explanation: For humans (customer support, incident investigation)
Both are essential. Reason codes enable operational workflows, explanations enable human understanding.
Frequently Asked Questions
Can reason codes change between versions?
Yes, but with semantic versioning. v0 reason codes are separate from v1 codes. Use schema_version: "reason_codes.v1" to lock your dependency. Lumyn guarantees v1 codes won't change meaning within the v1 namespace.
How do I handle deprecated reason codes?
Add new codes, but keep old ones for backward compatibility. Mark deprecated codes in documentation:
{
"code": "OLD_FRAUD_BLOCK",
"description": "Deprecated. Use CHARGEBACK_RISK_BLOCK instead.",
"deprecated": true,
"deprecated_since": "v1.2.0",
"replacement": "CHARGEBACK_RISK_BLOCK"
}Should I create a unique code for every rule?
No. Use semantic grouping. Multiple rules can emit the same reason code if they represent the same underlying policy concern.
Example:
- Rule R_HIGH_AMOUNT →
REFUND_OVER_ESCALATION_LIMIT - Rule R_REPEATED_REFUNDS →
REFUND_PATTERN_ANOMALY
Both are about refunds, but different policy concerns = different codes.
How granular should reason codes be?
Rule of thumb: If you would create a separate Datadog alert or dashboard chart for it, it deserves its own code.
Too granular: REFUND_500_USD, REFUND_501_USD (use metrics instead)
Too broad: POLICY_VIOLATION (which policy?)
Just right: REFUND_OVER_ESCALATION_LIMIT
Can one decision have multiple reason codes?
Yes! Lumyn returns all applicable codes:
{
"verdict": "DENY",
"reason_codes": [
"CHARGEBACK_RISK_BLOCK",
"ACCOUNT_TAKEOVER_RISK_BLOCK"
]
}This means: "Blocked due to BOTH chargeback risk AND account takeover signals."
Next Steps
- v1 Semantics - Understand how reason codes map to verdicts in Lumyn's evaluation engine
- Replay Guarantees - See how reason codes enable deterministic replay
- What is a Decision Record? - Learn about the full Decision Record structure
- Lumyn Memory - Explore memory-driven reason codes like
FAILURE_MEMORY_SIMILAR_BLOCK