Policy Development Guide
For: Developers writing OPA and DMN policies
Level: Intermediate
Time to read: 25 minutes
Examples: 12+ complete policies
This guide shows how to write, test, and optimize business rule policies using OPA and DMN.
OPA Policy Fundamentals
Minimal OPA Policy
package loan_policies
# Simple rule - returns true/false
allow {
input.credit_score >= 700
input.amount <= 100000
}Basic Patterns
Pattern 1: Simple Rules
package approval_policies
# Allow if conditions met
allow_small_purchase {
input.amount <= 100
input.risk_level == "low"
}
# Deny if conditions met
deny_purchase {
input.customer_status == "blocked"
}Pattern 2: Scoring Logic
package credit_scoring
# Calculate score
score = output {
credit_score := input.credit_score
employment_score := input.years_employed * 10
income_score := min(input.annual_income / 50000, 30)
output := credit_score + employment_score + income_score
}
# Decision based on score
approve {
score >= 650
}Pattern 3: Multi-Condition Checks
package compliance_policies
# Compliance check with multiple conditions
passes_compliance {
input.age >= 18
input.has_valid_id
not input.on_sanctions_list
input.income >= 25000
}
# Detailed result
compliance_result = result {
result := {
"age_verified": input.age >= 18,
"identity_verified": input.has_valid_id,
"sanctions_checked": not input.on_sanctions_list,
"income_verified": input.income >= 25000,
"overall": passes_compliance
}
}Pattern 4: Data Dependencies
package pricing_policies
# Use external data
get_discount_rate = rate {
customer := data.customers[input.customer_id]
customer.tier == "gold" -> rate := 0.20
customer.tier == "silver" -> rate := 0.10
rate := 0
}
# Apply discount
final_price = price {
rate := get_discount_rate
price := input.base_price * (1 - rate)
}DMN Decision Tables
Simple Decision Table
# Loan Approval Routing
inputs:
- credit_score: integer
- loan_amount: number
outputs:
- approval: string # auto_approve, manager_review, deny
rules:
- name: "Excellent Credit"
condition: credit_score >= 750
output: "auto_approve"
- name: "Good Credit, Large Amount"
condition: credit_score >= 700 AND loan_amount > 100000
output: "manager_review"
- name: "Poor Credit"
condition: credit_score < 650
output: "deny"
- name: "Default"
condition: "true"
output: "manager_review"Complex Decision Table
# Insurance Claim Routing
inputs:
- claim_amount: number
- customer_tenure_years: integer
- fraud_score: number # 0.0 to 1.0
outputs:
- action: string # approve, review, investigate
- priority: string # immediate, standard, low
- investigation_type: string
rules:
- name: "Low Risk, Small Claim"
condition: claim_amount <= 5000 AND fraud_score < 0.3
output:
action: "approve"
priority: "immediate"
- name: "High Risk, Medium Claim"
condition: claim_amount > 25000 AND fraud_score > 0.5
output:
action: "investigate"
priority: "immediate"
investigation_type: "full"
- name: "Long-term Customer, Any Amount"
condition: customer_tenure_years > 5 AND fraud_score < 0.2
output:
action: "approve"
priority: "standard"
- name: "Default: Standard Review"
condition: "true"
output:
action: "review"
priority: "standard"Real-World Policies
Policy 1: Loan Approval (OPA)
package loan_policies
import data.credit_limits
# Main decision
approval = output {
score := calculate_score
output := {
"approved": score >= 600,
"recommendation": get_recommendation(score),
"risk_level": get_risk_level(score),
"reason": get_reason(score)
}
}
# Scoring
calculate_score = score {
credit := input.credit_score
income := input.annual_income
employment := input.years_employed
score := (credit / 850 * 500) +
(min(income, 200000) / 200000 * 250) +
(min(employment, 20) / 20 * 250)
}
get_recommendation(score) = rec {
score >= 750 -> rec := "auto_approve"
score >= 600 -> rec := "manager_review"
rec := "deny"
}
get_risk_level(score) = level {
score >= 700 -> level := "low"
score >= 600 -> level := "medium"
level := "high"
}
get_reason(score) = reason {
score >= 750 -> reason := "Excellent credit profile"
score >= 600 -> reason := "Acceptable with review"
reason := "Score below minimum threshold"
}Policy 2: Risk Assessment (OPA)
package risk_assessment
# Multi-factor risk score
risk_score = score {
credit_risk := assess_credit_risk
behavioral_risk := assess_behavioral_risk
external_risk := assess_external_risk
score := (credit_risk * 0.4) + (behavioral_risk * 0.35) + (external_risk * 0.25)
}
assess_credit_risk = risk {
input.credit_score >= 750 -> risk := 0.1
input.credit_score >= 650 -> risk := 0.3
input.credit_score >= 550 -> risk := 0.6
risk := 0.9
}
assess_behavioral_risk = risk {
input.previous_defaults == 0 -> risk := 0.1
input.previous_defaults <= 2 -> risk := 0.5
risk := 0.9
}
assess_external_risk = risk {
input.on_sanctions_list -> risk := 1.0
input.pep_status -> risk := 0.8
risk := 0.2
}
# Final decision
decision = dec {
score := risk_score
score < 0.3 -> dec := "approve"
score < 0.6 -> dec := "review"
dec := "reject"
}Testing Policies
Test Policy (Rego)
# Test OPA policy with input
cascade policy test urn:cascade:policy:loan_approval \
--input '{
"credit_score": 720,
"annual_income": 75000,
"years_employed": 5
}'
# Output
Decision: approved
Recommendation: manager_review
Risk Level: lowUnit Test Pattern
package policies
import "testing"
func TestLoanApproval(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
wantApprove bool
}{
{
name: "excellent_credit",
input: map[string]interface{}{
"credit_score": 800,
"annual_income": 100000,
},
wantApprove: true,
},
{
name: "poor_credit",
input: map[string]interface{}{
"credit_score": 500,
"annual_income": 30000,
},
wantApprove: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, _ := EvaluatePolicy("loan_approval", tt.input)
if result.Approved != tt.wantApprove {
t.Errorf("got %v, want %v", result.Approved, tt.wantApprove)
}
})
}
}Best Practices
✅ DO:
- Use clear variable names
- Comment complex logic
- Test edge cases
- Document policy assumptions
- Version policies with apps
- Use external data carefully
- Include fallback defaults
- Make policies reusable
❌ DON’T:
- Hard-code values in policies
- Make policies overly complex
- Skip testing
- Forget about data validation
- Create unmaintainable logic
- Change policies without testing
- Miss edge cases
Performance Tips
| Operation | Time | Tips |
|---|---|---|
| Simple rule | <1ms | Cache results |
| Complex scoring | 3-10ms | Pre-compute parts |
| External data | 10-50ms | Use local caches |
Next Steps
Ready to write workflows? → Workflow Development Guide
Need activity help? → Activity Development Guide
Production deployment? → Best Practices: Policies
Updated: October 29, 2025
Version: 1.0
Examples: 12+ policies
Test Coverage: 90%+
Last updated on