Externalizing Business Rules: Why Choose DecisionRules.io Over Custom Code

Written by | Nov 20, 2025

Businesses in financial services, insurance, and e-commerce all rely on complex decision logic to automate approvals, pricing, and compliance. Traditionally, these rules are hardcoded into applications, making every policy tweak or threshold update a developer-driven project. Externalizing business rules with platforms like DecisionRules.io brings agility to these processes by empowering business teams to control and update decision logic directly, eliminating deployment bottlenecks and reducing manual effort.

Any set of guidelines can benefit from separating decision logic from custom application code, be it pricing engines, eligibility checks, approval workflows, insurance underwriting, or compliance systems. DecisionRules.io makes this process much faster and simpler by enabling business users (not just developers) to update and deploy rules instantly via a user-friendly interface, instead of depending on code updates and deployments.​

At AgilityFeat, we recently built a small proof of concept for a money transfer fraud detection system that demonstrates the power of externalizing business rules. As an authorized DecisionRules.io development partner, we implement this custom rules engine solution for businesses worldwide.

Whether you’re experiencing deployment bottlenecks from hardcoded rules or looking to empower business teams with direct rule control and visibility, externalizing decision logic with DecisionRules.io avoids costly deployment delays, reduces risk of errors, and provides clear audit trails for compliance. 

This technical deep-dive shows exactly how rules engine integration works in production, giving you a practical way to eliminate code-based bottlenecks and let business users handle what they know best.

Challenges of Hardcoded Business Logic

Hardcoded business logic creates friction as applications scale. 

  • Deployment bottlenecks: adjusting a risk threshold from 20 to 25 points requires a full development cycle-code changes, testing, QA, deployment. 
  • Knowledge silos: risk analysts can’t tune fraud detection themselves; every change needs a developer. 
  • Testing overhead: threshold adjustments trigger regression testing across all code paths. 
  • Maintenance burden: logic scattered across files makes debugging decisions difficult.

Advantages of a Business Rules Engine Solution

Business rules engines externalize decision logic from application code. Instead of if/else statements scattered across your codebase, rules live in a dedicated system designed for managing complex conditional logic.

DecisionRules.io provides visual decision tables (spreadsheet-like interfaces), built-in version control, a test bench for validating rules before deployment, and a REST API for integration. Business users modify rules directly; developers integrate via API calls. This works for any domain with frequently changing logic-fraud detection, pricing, loan approval, eligibility checks.

DecisionRules.io Decision Table Configuration

For the proof of concept we built, the fraud detection is connected to a DecisionRules.io workflow that chains multiple decision tables. Here’s the configuration:

Risk Scoring Table

Decision table that evaluates transaction characteristics and accumulates risk points:

  • Table type: Standard
  • Evaluation mode: All matching rules (accumulates scores)
  • Inputs: TransactionValue (number), TransactionType (string), TransactionsInPastWeek (number), SenderOnWatchlist (string), HighRiskCountry (string), CustomerProfileMismatch (string)
  • Output: Score (number)
DecisionRules.io Thresholds Table

DecisionRules.io Risk Scoring Table

Thresholds Table

Determines final decision based on accumulated risk score:

  • Table type: Standard
  • Evaluation mode: First matching rule
  • Input: Score (number)
  • Output: Decision (string)
DecisionRules.io Workflow Assembly

DecisionRules.io Thresholds Table

Workflow Assembly

The workflow orchestrates these tables:

  1. Loop through RiskScoring table to accumulate points
  2. Pass score to Thresholds table for decision
  3. IF node checks Decision = “Escalate” -> route to email configuration
  4. Output assembly maps result to API response structure
DecisionRules.io Integration Architecture

DecisionRules.io Workflow Assembly

DecisionRules.io Integration Architecture

Three-tier pattern:

  • Frontend (React): Transaction submission form, decision display
  • Backend (Express): Orchestration—calls DecisionRules.io API, handles email notifications
  • External Services: DecisionRules.io for rule evaluation

Flow: User submits transfer -> Express sends data to DecisionRules.io -> receives decision -> triggers actions (emails, logging) -> returns result to frontend.

API Workflow for Automated Decisions

What we send to DecisionRules.io:

{
 "TransactionValue": 15000,
 "TransactionType": "Crypto",
 "TransactionsInPastWeek": 3,
 "SenderOnWatchlist": "John Doe",
 "ReceiverOnWatchlist": "Unknown",
 "HighRiskCountry": "Yes",
 "CustomerProfileMismatch": "No"
}

The API returns the decision, risk score, and escalation details when needed:

[{
 "Decision": "Escalate",
 "Score": 35,
 "Escalation": {
   "escalate": true,
   "message": {
     "address": "senior-compliance@company.com",
     "contents": "High-risk transaction detected: 
             $15,000 crypto to high-risk country"
   }
 }
}]

Backend Integration: DecisionRules.io API

Our Express backend orchestrates the workflow but delegates decision logic to DecisionRules.io.

Project Setup

# package.json
npm install express node-fetch dotenv nodemailer

// package.json
{
 "name": "fraud-detection-backend",
 "version": "1.0.0",
 "dependencies": {
   "express": "^4.18.2",
   "node-fetch": "^2.6.9",
   "dotenv": "^16.0.3",
   "nodemailer": "^6.9.1"
 }
}

Environment Configuration

# .env
DECISIONRULES_API_KEY=your_api_key_here
DECISIONRULES_RULE_ID=your_rule_id_here
SMTP_HOST=smtp.gmail.com
SMTP_USER=your_email@company.com
SMTP_PASS=your_app_password
PORT=3001

Variable explanations:

  • DECISIONRULES_API_KEY: API key from DecisionRules.io dashboard (Settings -> API Keys)
  • DECISIONRULES_RULE_ID: Workflow ID from DecisionRules.io (visible in workflow editor URL)
  • SMTP_HOST: Email server hostname for sending escalation notifications
  • SMTP_USER: Email account username for authentication
  • SMTP_PASS: Email account password or app-specific password (for Gmail, generate at account.google.com/apppasswords)
  • PORT: Backend server port (default 3001)

Email Service

/ services/emailService.js
const nodemailer = require('nodemailer');

// Configure SMTP transporter for sending escalation emails
const transporter = nodemailer.createTransport({
 host: process.env.SMTP_HOST,
 auth: {
   user: process.env.SMTP_USER,
   pass: process.env.SMTP_PASS
 }
});

// Send email notification when transaction requires escalation
async function sendEscalationEmail(address, contents) {
 await transporter.sendMail({
   from: process.env.SMTP_USER,
   to: address,
   subject: 'Transaction Escalation Required',
   text: contents
 });
}

module.exports = { sendEscalationEmail };

DecisionRules API Client

// services/decisionRulesClient.js
const fetch = require('node-fetch');

// Call DecisionRules.io API to evaluate transaction risk
async function evaluateTransaction(transactionData) {
 try {
   // POST transaction data to DecisionRules.io workflow
   const response = await fetch(
     `https://api.decisionrules.io/rule/solve/${process.env.DECISIONRULES_RULE_ID}`,
     {
       method: 'POST',
       headers: {
         'Authorization': `Bearer ${process.env.DECISIONRULES_API_KEY}`,
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(transactionData)
     }
   );

   if (!response.ok) {
     throw new Error(`DecisionRules API error: ${response.statusText}`);
   }

   // API returns array, extract first result
   const decision = await response.json();
   return decision[0];
 } catch (error) {
   // Fallback: default to manual review on API failure
   return { Decision: 'Manual Review', Score: 0, Error: error.message };
 }
}

module.exports = { evaluateTransaction };

Express.js Routing for Rules Engine Evaluation

// routes/transfers.js
const express = require('express');
const { evaluateTransaction } = require('../services/decisionRulesClient');
const { sendEscalationEmail } = require('../services/emailService');

const router = express.Router();

router.post('/api/transfers', async (req, res) => {
 // Map frontend data to DecisionRules.io input format
 const transferData = {
   TransactionValue: req.body.amount,
   TransactionType: req.body.type,
   TransactionsInPastWeek: req.body.pastWeekCount,
   SenderOnWatchlist: req.body.sender,
   ReceiverOnWatchlist: req.body.receiver || 'Unknown',
   HighRiskCountry: req.body.country,
   CustomerProfileMismatch: req.body.profileMismatch || 'No'
 };

 // Call DecisionRules.io to get risk decision
 const result = await evaluateTransaction(transferData);

 // Send email if transaction requires escalation
 if (result.Decision === 'Escalate' && result.Escalation?.escalate) {
   await sendEscalationEmail(
     result.Escalation.message.address,
     result.Escalation.message.contents
   );
 }

 res.json(result);
});

module.exports = router;

Server Setup for Rules Engine Implementation

// server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const transferRoutes = require('./routes/transfers');

const app = express();
app.use(cors()); // Enable CORS for frontend requests
app.use(express.json()); // Parse JSON request bodies
app.use(transferRoutes); // Mount transfer routes

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
 console.log(`Backend running on port ${PORT}`);
});

The pattern: receive data -> call DecisionRules.io API -> handle response -> trigger actions. This same structure works for pricing, eligibility, approvals.

React Frontend for Decision Automation

React frontend for submitting transfers and displaying decisions.

App Component

// src/App.js
import React, { useState } from 'react';
import TransferForm from './components/TransferForm';
import DecisionDisplay from './components/DecisionDisplay';

function App() {
 const [decision, setDecision] = useState(null);
 const [loading, setLoading] = useState(false);

 const handleSubmit = async (transferData) => {
   setLoading(true);
   // Send transfer data to backend for evaluation
   const response = await fetch('http://localhost:3001/api/transfers', {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(transferData)
   });
   const result = await response.json();
   setDecision(result);
   setLoading(false);
 };

 return (
   <div className="App">
     <header>
       <h1>Fraud Detection System</h1>
     </header>
     <main>
       <TransferForm onSubmit={handleSubmit} loading={loading} />
       {decision && <DecisionDisplay decision={decision} />}
     </main>
   </div>
 );
}

export default App;

Transfer Form Component

// src/components/TransferForm.js
import React, { useState } from 'react';

function TransferForm({ onSubmit, loading }) {
 // Track form state with all transaction fields
 const [formData, setFormData] = useState({
   amount: '',
   type: 'Wire transfer',
   sender: '',
   receiver: '',
   country: '',
   pastWeekCount: 0,
   profileMismatch: 'No'
 });

 // Update form state and convert numeric fields
 const handleChange = (e) => {
   const { name, value } = e.target;
   setFormData(prev => ({
     ...prev,
     [name]: name === 'amount' || name === 'pastWeekCount' ? Number(value) : value
   }));
 };

 return (
   <form onSubmit={(e) => { e.preventDefault(); onSubmit(formData); }}>
     <div className="form-group">
       <label>Transaction Amount ($)</label>
       <input
         type="number"
         name="amount"
         value={formData.amount}
         onChange={handleChange}
         required
       />
     </div>

     <div className="form-group">
       <label>Transaction Type</label>
       <select name="type" value={formData.type} onChange={handleChange}>
         <option>Wire transfer</option>
         <option>Crypto</option>
         <option>International transfer</option>
       </select>
     </div>

     {/* ... other form fields for sender, receiver, country, pastWeekCount, profileMismatch ... */}

     <button type="submit" disabled={loading}>
       {loading ? 'Evaluating...' : 'Submit Transfer'}
     </button>
   </form>
 );
}
<span style="font-weight: 400;">export</span> <span style="font-weight: 400;">default</span><span style="font-weight: 400;"> TransferForm;</span>

Decision Display Component

// src/components/DecisionDisplay.js
import React from 'react';

function DecisionDisplay({ decision }) {
 // Map decision types to color codes
 const getStatusColor = () => {
   const colors = {
     'Auto Approve': '#28a745',
     'Manual Review': '#ffc107',
     'Escalate': '#dc3545'
   };
   return colors[decision.Decision] || '#6c757d';
 };

 return (
   <div className="decision-result">
     {/* Display decision with color-coded badge */}
     <div
       className="status-badge"
       style={{ backgroundColor: getStatusColor(), color: 'white' }}
     >
       {decision.Decision}
     </div>

     <div className="decision-details">
       <p>Risk Score: {decision.Score}</p>

       {/* Show escalation details if applicable */}
       {decision.Escalation?.escalate && (
         <div className="escalation-notice">
           <p>Email sent to: {decision.Escalation.message.address}</p>
           <p>{decision.Escalation.message.contents}</p>
         </div>
       )}
     </div>
   </div>
 );
}

export default DecisionDisplay;

Deploying the DecisionRules.io Fraud Detection POC

Start Backend

cd server
npm install
node index.js
# Output: Fraud detection backend running on port 3001

Start Frontend

npm install
npm start
# Output: React app running on http://localhost:3000

Testing Rules Engine Scenarios

Test the system with these scenarios to see how different transaction characteristics affect risk scoring and decision outcomes:

Scenario 1: Low Risk (Auto Approve):

  • Amount: $500
  • Type: Internal transfer
  • Sender: Jane Smith
  • Receiver: John Smith
  • Country: USA
  • Past Week: 0
  • Expected: Decision = “Auto Approve”, Score < 10

POC system page with the scenario sent and the auto approved message appearing.

 

Scenario 2: Medium Risk (Manual Review):

  • Amount: $8,000
  • Type: International transfer
  • Sender: Bob
  • Receiver: John Smith
  • Country: Canada
  • Past Week: 0
  • Expected: Decision = “Manual Review”, Score 10-19

 

POC system page with the scenario sent and the flag for review message appearing.

 

Scenario 3: High Risk (Escalate):

  • Amount: $15,000
  • Type: Crypto
  • Sender: Alice (watchlist)
  • Receiver: Bob (watchlist)
  • Country: Japan (high risk)
  • Past Week: 0
  • Expected: Decision = “Escalate”, Score ≥ 20, email sent

POC system page with the scenario sent and the escalate to compliance message appearing.

The Business Value of Rules Engine Integration

Externalizing rules shifted decision logic from code to configuration in this implementation. The backend orchestrates workflow—receiving transaction data, calling the DecisionRules.io API, triggering notifications—without implementing rule evaluation itself. When compliance needs to adjust risk thresholds, they modify decision tables directly. When Product wants to experiment with new scoring factors, they update rules in the test bench. No code changes, no deployments, no developer bottlenecks.

This pattern extends across industries:

  • Financial services: Loan approvals, fraud detection, credit scoring, KYC/AML compliance
  • Insurance: Policy pricing, claims processing, underwriting decisions, risk assessment
  • E-commerce: Dynamic pricing, discount eligibility, inventory allocation, shipping rules
  • Healthcare: Patient eligibility, treatment protocols, billing rules, claims adjudication
  • SaaS platforms: Feature access, usage limits, billing tiers, approval workflows

The implementation architecture stays consistent: frontend collects data, backend calls the rules API, application reacts to decisions. Only the specific business rules differ.

AgilityFeat: DecisionRules.io Implementation Partner

As an authorized DecisionRules.io development partner, AgilityFeat specializes in designing and implementing this custom rules engine solution for businesses worldwide. We handle the complete technical integration from the API architecture, workflow design, frontend development to testing and deploying infrastructure, so your business teams can focus on rule logic, not deployment cycles.

If your organization struggles with hardcoded business logic, lengthy deployment cycles for simple rule changes, or knowledge silos between business and development teams, contact AgilityFeat to discuss how a rules engine implementation could streamline your decision systems.

About the author

About the author

Alice Habitzreuter

Alice Habitzreuter is a versatile Full Stack Developer from Brazil who has been part of our nearshore software development team since 2022. Alice has a proven track record building robust applications with ReactJS, Ruby on Rails, and PostgreSQL and is particularly adept at enhancing application performance and integrating APIs. Alice is currently working on a Master of Computer Science at Universidade do Vale do Itajaí.

Recent Blog Posts