Skip to Content
GuidesPolicy Development

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: low

Unit 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

OperationTimeTips
Simple rule<1msCache results
Complex scoring3-10msPre-compute parts
External data10-50msUse 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