Policy Evaluation: Multi-Engine Business Rules
Status: MVP (v1.0.0) | Maturity: Production-Ready | Tests: 12 (OPA: 7, DMN: 3, Integration: 2)
Performance: OPA <5ms, DMN 10-50ms | Engines: OPA (primary), DMN (secondary)
Cascade Platform evaluates business rules using multiple policy engines, enabling complex decisions without modifying application code.
What is Policy Evaluation?
Policy evaluation is the ability to encode business rules separately from application code and execute them in workflows. Instead of hardcoding approval thresholds, compliance checks, or business logic, you define policies once and reference them everywhere.
Key benefits:
- ✅ Rules defined in business-friendly languages
- ✅ No code changes to update rules
- ✅ Audit trail of policy evaluation
- ✅ A/B testing different policies
- ✅ Multi-engine support (OPA, DMN, WASM future)
Architecture: Decision Strategy
4-Tier Decision Strategy
Cascade implements a layered approach to decisions:
┌─────────────────────────────────────────┐
│ Tier 1: Fast-Path (Milliseconds) │
│ - Choice states (comparison) │
│ - Simple conditions (1-5 milliseconds) │
└─────────────────────────────────────────┘
│
├─→ Can decide? → Answer + log
│
└─→ Need more logic? ↓
┌─────────────────────────────────────────┐
│ Tier 2: OPA Policies (< 5 milliseconds) │
│ - Rego rules (most cases) │
│ - Medium complexity │
│ - JSON input/output │
└─────────────────────────────────────────┘
│
├─→ Can decide? → Answer + log
│
└─→ Need structured decisions? ↓
┌─────────────────────────────────────────┐
│ Tier 3: DMN (10-50 milliseconds) │
│ - Decision tables (complex logic) │
│ - Multiple outputs per decision │
│ - Auditable decision trace │
└─────────────────────────────────────────┘
│
├─→ Can decide? → Answer + trace
│
└─→ Can't decide? ↓
┌─────────────────────────────────────────┐
│ Tier 4: Human Review │
│ - Escalation to human │
│ - Manual judgment (HumanTask) │
│ - Business context │
└─────────────────────────────────────────┘Engine 1: OPA (Open Policy Agent)
What is OPA?
OPA uses Rego language to express policies as rules. Think of it as “business logic as code” but in a declarative language.
When to use: Most common policies, scoring, complex logic
Performance: <5ms typical, <20ms worst-case
Basic Rego Policy
Simple approval rule:
package loan_policies
# Allow approval if score >= 700 and amount <= 100k
allow_approval {
input.credit_score >= 700
input.loan_amount <= 100000
}
# Deny if any red flags
deny_approval {
input.previous_bankruptcies > 0
}
# Require manager review if score between 650-700
require_manager_review {
input.credit_score >= 650
input.credit_score < 700
}CDL Integration
- name: EvaluateLoanPolicy
type: EvaluatePolicy
resource: urn:cascade:policy:loan_approval
parameters:
credit_score: "{{ $.credit_score }}"
loan_amount: "{{ $.loan_amount }}"
previous_bankruptcies: "{{ $.bankruptcy_count }}"
result: $.policy_decision
next: ApplyDecisionPolicy evaluation returns:
{
"allow_approval": true,
"deny_approval": false,
"require_manager_review": false,
"reason": "Credit score acceptable, amount within limits"
}Advanced Rego: Scoring Logic
Calculate approval score:
package loan_policies
import data.scoring_rules
# Score calculation
approval_score = score {
score := scoring_rules.credit_score_weight * input.credit_score / 850 +
scoring_rules.income_weight * min(input.annual_income, 200000) / 200000 +
scoring_rules.employment_weight * (input.years_employed / 20)
}
# Approval decision based on score
allow_approval {
approval_score >= 0.75
}
# Strong approval
strong_approval {
approval_score >= 0.90
}
# Marginal - needs review
marginal_approval {
approval_score >= 0.60
approval_score < 0.75
}
# Recommendation
recommendation = rec {
strong_approval -> rec := "auto_approve"
marginal_approval -> rec := "manager_review"
_ -> rec := "deny"
}Usage in workflow:
- name: EvaluateScore
type: EvaluatePolicy
resource: urn:cascade:policy:loan_scoring
parameters:
credit_score: "{{ $.credit_score }}"
annual_income: "{{ $.income }}"
years_employed: "{{ $.employment_tenure }}"
result: $.score_result
next: RouteBasedOnScore
- name: RouteBasedOnScore
type: Choice
choices:
- condition: "{{ $.score_result.recommendation == 'auto_approve' }}"
next: ProcessApproval
- condition: "{{ $.score_result.recommendation == 'manager_review' }}"
next: ManagerReview
default: DenyAndNotifyAdvanced Rego: Compliance Checks
Multi-rule compliance policy:
package compliance_policies
# Sanctions check
pass_sanctions_check {
not data.sanctions_list[input.applicant_name]
}
# Age verification
passes_age_check {
input.age >= 18
}
# Income verification
passes_income_check {
input.annual_income >= 25000
}
# Combined compliance
all_checks_pass {
pass_sanctions_check
passes_age_check
passes_income_check
}
# Detailed compliance result
compliance_result = result {
result := {
"sanctions": pass_sanctions_check,
"age": passes_age_check,
"income": passes_income_check,
"overall": all_checks_pass
}
}Engine 2: DMN (Decision Model & Notation)
What is DMN?
DMN is a visual language for representing business decisions as decision tables. Easy for business analysts to understand and modify.
When to use: Complex multi-condition decisions, audit requirements, visual representation needed
Performance: 10-50ms typical (slower than OPA but more readable)
Simple Decision Table
Loan routing table:
| Credit Score | Loan Amount | Approval | Route | Notes |
|---|---|---|---|---|
>= 750 | <= 100K | Auto | Disburse | Excellent credit |
>= 750 | > 100K | Escalate | Manager | High amount |
| 700-749 | <= 50K | Auto | Disburse | Good credit, small |
| 700-749 | > 50K | Escalate | Manager | Needs review |
| 650-699 | Any | Review | Manager | Borderline |
< 650 | Any | Deny | Rejection | Poor credit |
CDL Integration
- name: RouteApplication
type: EvaluatePolicy
resource: urn:cascade:policy:loan_routing_dmn
parameters:
credit_score: "{{ $.credit_score }}"
loan_amount: "{{ $.loan_amount }}"
result: $.routing_decision
next: ApplyRoutingDMN Result:
{
"approval": "Auto",
"route": "Disburse",
"notes": "Excellent credit",
"decision_id": "dmn-123456"
}DMN XML Format
Behind the scenes, DMN is XML-based:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20191111/MODEL/">
<decision name="LoanApproval" id="d1">
<decisionTable id="dt1">
<input id="i1">
<inputExpression typeRef="number">
<text>credit_score</text>
</inputExpression>
</input>
<input id="i2">
<inputExpression typeRef="number">
<text>loan_amount</text>
</inputExpression>
</input>
<output id="o1" name="approval" typeRef="string">
<text></text>
</output>
<rule id="r1">
<inputEntry id="ie1">
<text>creditScoreCondition</text>
</inputEntry>
<inputEntry id="ie2">
<text>loanAmountCondition</text>
</inputEntry>
<outputEntry id="oe1">
<text>"auto_approve"</text>
</outputEntry>
</rule>
<!-- More rules -->
</decisionTable>
</decision>
</definitions>Real-World Example: Insurance Claims Policy
Multi-Engine Policy Chain
Complete claims evaluation workflow:
workflows:
- name: EvaluateClaim
start: FastPathCheck
states:
# Tier 1: Quick decision
- name: FastPathCheck
type: Choice
choices:
- condition: "{{ $.claim_amount <= 1000 && $.claim_age < 7 }}"
next: FastApprove
description: "Small, recent claim"
- condition: "{{ $.claim_amount > 50000 }}"
next: ComplexEvaluation
description: "Large claim needs evaluation"
default: StandardEvaluation
# Tier 1 result: Fast path
- name: FastApprove
type: Task
resource: urn:cascade:activity:auto_approve_claim
end: true
# Tier 2: OPA evaluation
- name: StandardEvaluation
type: EvaluatePolicy
resource: urn:cascade:policy:claim_evaluation_opa
parameters:
claim_amount: "{{ $.claim_amount }}"
claimant_history: "{{ $.history.claims_count }}"
claimant_rating: "{{ $.history.customer_rating }}"
damage_type: "{{ $.damage_assessment.type }}"
result: $.opa_result
next: CheckOPAResult
- name: CheckOPAResult
type: Choice
choices:
- condition: "{{ $.opa_result.auto_approve }}"
next: ProcessApproval
- condition: "{{ $.opa_result.needs_investigation }}"
next: InitiateInvestigation
default: DMNEvaluation
# Tier 3: DMN for complex cases
- name: DMNEvaluation
type: EvaluatePolicy
resource: urn:cascade:policy:claim_decision_dmn
parameters:
claim_value: "{{ $.claim_amount }}"
claimant_tenure: "{{ $.history.customer_since }}"
previous_fraud_indicators: "{{ $.fraud_score }}"
result: $.dmn_result
next: ApplyDMNDecision
- name: ApplyDMNDecision
type: Choice
choices:
- condition: "{{ $.dmn_result.approval == 'Approve' }}"
next: ProcessApproval
- condition: "{{ $.dmn_result.approval == 'Review' }}"
next: ManagerReview
default: DenyAndClose
- name: ManagerReview
type: HumanTask
ui:
schema: urn:cascade:schema:claim_review_form
target: appsmith
timeout: 3d
next: ProcessApproval
- name: ProcessApproval
type: Task
resource: urn:cascade:activity:approve_and_pay
end: true
- name: DenyAndClose
type: Task
resource: urn:cascade:activity:deny_claim
end: true
- name: InitiateInvestigation
type: Task
resource: urn:cascade:activity:start_investigation
next: WaitForInvestigation
- name: WaitForInvestigation
type: Wait
duration: 14d
next: ReviewInvestigationPolicy Definitions
OPA Policy (claims_evaluation):
package claims_policies
# Auto-approve low-risk claims
auto_approve {
input.claim_amount <= 5000
input.claimant_history.claims_count <= 2
input.claimant_history.customer_rating >= 4.5
}
# Flag suspicious patterns
needs_investigation {
input.claimant_history.claims_count > 5
}
needs_investigation {
input.fraud_score > 0.7
}
# Recommendation
recommendation = rec {
auto_approve -> rec := "approve"
needs_investigation -> rec := "investigate"
_ -> rec := "review"
}DMN Policy (claims_decision_dmn):
| Claim Value | Customer Tenure | Fraud Score | Approval | Route |
|---|---|---|---|---|
<= 5K | > 2 years | < 0.3 | Approve | Auto |
| 5K-25K | > 2 years | < 0.5 | Approve | Auto |
| 5K-25K | > 2 years | 0.5-0.7 | Review | Manager |
| 25K-100K | > 1 year | < 0.5 | Review | Manager |
> 100K | Any | Any | Review | Director |
| Any | < 1 year | > 0.5 | Investigate | Investigation |
Advanced Patterns
Pattern 1: A/B Testing Policies
Test two policies simultaneously:
- name: ABTestApprovalPolicy
type: Parallel
branches:
- name: EvaluateControlPolicy
type: EvaluatePolicy
resource: urn:cascade:policy:approval_control
parameters:
credit_score: "{{ $.credit_score }}"
amount: "{{ $.amount }}"
result: $.control_decision
- name: EvaluateTestPolicy
type: EvaluatePolicy
resource: urn:cascade:policy:approval_test
parameters:
credit_score: "{{ $.credit_score }}"
amount: "{{ $.amount }}"
result: $.test_decision
completion_strategy: ALL
next: LogABTest
- name: LogABTest
type: Task
resource: urn:cascade:activity:log_ab_test_results
parameters:
test_id: "approval_policy_v2"
control_result: "{{ $.control_decision }}"
test_result: "{{ $.test_decision }}"
approved: "{{ $.control_decision.approved }}" # Use control for actual approval
next: ProcessApprovalPattern 2: Policy Chain (Consensus)
Require agreement from multiple policies:
- name: CheckConsensus
type: Parallel
branches:
- name: PolicyA
type: EvaluatePolicy
resource: urn:cascade:policy:approval_policy_a
result: $.policy_a
- name: PolicyB
type: EvaluatePolicy
resource: urn:cascade:policy:approval_policy_b
result: $.policy_b
- name: PolicyC
type: EvaluatePolicy
resource: urn:cascade:policy:approval_policy_c
result: $.policy_c
completion_strategy: ALL
next: CheckConsensusResult
- name: CheckConsensusResult
type: Choice
choices:
- condition: "{{ $.policy_a.approved && $.policy_b.approved && $.policy_c.approved }}"
next: ProcessApproval
description: "All policies agree"
default: EscalateForReviewPattern 3: Policy Versioning
Test new policy versions safely:
- name: EvaluateWithVersion
type: EvaluatePolicy
resource: "urn:cascade:policy:approval_{{ $.test_version | default: 'v1' }}"
parameters:
credit_score: "{{ $.credit_score }}"
amount: "{{ $.amount }}"
result: $.policy_result
next: RouteByVersionUsage:
# Use v1 (default)
cascade process start \
--workflow EvaluateLoan \
--input '{"credit_score": 720, "amount": 50000}'
# Test v2
cascade process start \
--workflow EvaluateLoan \
--input '{"credit_score": 720, "amount": 50000, "test_version": "v2"}'Performance Characteristics
Latency Comparison
| Engine | Typical | P95 | P99 | Notes |
|---|---|---|---|---|
| Choice | <0.1ms | <1ms | <5ms | In-memory |
| OPA | 3ms | 5ms | 15ms | Cached policies |
| DMN | 20ms | 40ms | 50ms | Table evaluation |
| LLM (future) | 500ms | 2s | 5s | Network latency |
Throughput
| Engine | Ops/sec | Bottleneck |
|---|---|---|
| Choice | 100K+ | CPU |
| OPA | 20K+ | Rego compilation |
| DMN | 1-2K+ | Table I/O |
Monitoring & Observability
Policy Execution Metrics
# Prometheus metrics
cascade_policy_evaluation_duration_seconds # Policy latency
cascade_policy_decisions_total # Decision count
cascade_policy_errors_total # Evaluation failures
cascade_policy_approval_rate # % approvedAudit Trail
Every policy evaluation logged:
{
"timestamp": "2025-10-29T15:45:23Z",
"workflow_id": "wf-123",
"policy": "urn:cascade:policy:loan_approval",
"engine": "opa",
"input": {
"credit_score": 720,
"amount": 50000
},
"result": {
"allow_approval": true,
"reason": "Score acceptable"
},
"latency_ms": 3.2,
"decision_id": "dec-456"
}Query audit trail:
# Show all loan policy decisions today
cascade audit query \
--policy "loan_approval" \
--since "24h"
# Show decisions by result
cascade audit query \
--policy "approval" \
--result "approved" \
--count 100Troubleshooting
OPA Policy Not Found
Error: Policy urn:cascade:policy:my_policy not found
Solution:
# List available policies
cascade policy list
# Check policy details
cascade policy inspect urn:cascade:policy:my_policy
# Validate policy syntax
cascade policy validate policy.regoUnexpected Policy Decision
Symptom: Policy returns unexpected result
Diagnosis:
# Check policy with exact input
cascade policy test \
--policy urn:cascade:policy:loan_approval \
--input '{"credit_score": 720, "amount": 50000}'
# Show policy trace
cascade policy trace \
--policy urn:cascade:policy:loan_approval \
--input '{"credit_score": 720, "amount": 50000}'Performance Issues
Symptom: Policy evaluation slow (>50ms)
Solutions:
- Profile:
cascade perf profile policy - Check Rego complexity
- Consider caching results
- Move to DMN if complex
Best Practices
✅ DO:
- Use OPA for most policies (fast, flexible)
- Test policies thoroughly before deployment
- Version policies like code
- Log all decisions for audit
- Use meaningful policy names (URNs)
- Document policy logic with comments
❌ DON’T:
- Hardcode business logic in workflows
- Mix policy engines unnecessarily
- Skip policy testing
- Deploy untested policy changes
- Store sensitive data in policies
- Make policies overly complex
Next Steps
Ready to write policies? → Policy Development Guide
Need Rego help? → OPA/Rego Reference
Want to visualize decisions? → DMN Modeling
Production deployment? → Policy Deployment Guide
Updated: October 29, 2025
Version: 1.0
OPA Version: v0.50+
DMN Version: v1.3
Production-Ready: Yes