Rules from JSON

Keep your thresholds in a file the business can edit, without a code change.

On this page

Rules from JSON

Credit-score cutoffs change. It is awkward to redeploy code every time. RuleBox can load the same rules from a JSON file instead.

Write the rules as data

Save this as config/rules/loan.json:

[
	{
		"name": "declineLowScores",
		"description": "Declines a score under 580",
		"priority": 20,
		"when": { "lt": [ "creditScore", 580 ] },
		"then": [ { "action": "decide", "params": { "decision": "DECLINED" } } ],
		"stop": true
	},
	{
		"name": "autoApprove",
		"description": "Approves a good score, unless the amount is very large",
		"when": { "gte": [ "creditScore", 680 ] },
		"except": { "gt": [ "requestedAmount", 500000 ] },
		"then": [ { "action": "decide", "params": { "decision": "APPROVED" } } ]
	}
]

It is the same two rules, using the same ideas: a name, a description, a priority, a when, an except, a then, and stop.

Conditions without code

A condition in a JSON file is a small structure, not code. That is on purpose: a file anyone can edit can never run code you did not write.

OperatorExampleMeaning
eq, neq{ "eq": [ "state", "CA" ] }Equal, not equal
lt, lte, gt, gte{ "lt": [ "creditScore", 580 ] }Less or greater than
in{ "in": [ "region", [ "US", "CA" ] ] }One of a list
and, or{ "and": [ cond, cond ] }Combine conditions
not{ "not": cond }Flip a condition

A condition compares a fact to a fixed value. That is why the except above uses a fixed 500000 instead of "five times income" like lesson 4.

Name your actions

A JSON file cannot hold code, so a then points at an action by name. You register the code behind that name, once:

var book = getInstance( "RuleBook@rulebox" )
	.registerAction( "decide", ( facts, result, params ) => {
		result.setValue( params.decision )
	} )
	.loadRules(
		new rulebox.models.JSONRuleSource( expandPath( "/config/rules/loan.json" ) )
	)
	.withDefaultResult( "MANUAL_REVIEW" )

var decision = book
	.run( { creditScore: 720, requestedAmount: 100000 } )
	.getResult()
	.getValue()

// "APPROVED"

params is whatever the JSON passed along, so one decide action serves every rule. Run it with a score of 540 and you get DECLINED. With 640 you get the default, MANUAL_REVIEW.

Everything from lesson 5 still works on a rulebook loaded from a file: the audit trail, dryRun() and metrics.

Describe the file and its facts

A rule file can also describe the rulebook and declare its facts, like defineFacts() in lesson 3. Wrap the rules in an object:

{
	"description": "Decides a loan application from the credit score",
	"facts": {
		"creditScore": { "type": "numeric", "required": true, "description": "300 to 850" },
		"requestedAmount": { "type": "numeric", "default": 0 }
	},
	"enforceFacts": true,
	"rules": [
		{
			"name": "declineLowScores",
			"description": "Declines a score under 580",
			"priority": 20,
			"when": { "lt": [ "creditScore", 580 ] },
			"then": [ { "action": "decide", "params": { "decision": "DECLINED" } } ],
			"stop": true
		},
		{
			"name": "autoApprove",
			"description": "Approves a good score, unless the amount is very large",
			"when": { "gte": [ "creditScore", 680 ] },
			"except": { "gt": [ "requestedAmount", 500000 ] },
			"then": [ { "action": "decide", "params": { "decision": "APPROVED" } } ]
		}
	]
}

rules holds the same array as before. enforceFacts checks the facts on every run, and strictFacts would also reject undeclared ones. A bare array still works, so add this only when you want it.

Other sources

JSONRuleSource is one of three. YAMLRuleSource reads a YAML file and DBRuleSource reads a database table. The Externalized Rule Definitions guide covers them.

Try it

Open loan.json and change 580 to 600. Run a score of 590. It was MANUAL_REVIEW before, and it is DECLINED now. You changed behavior without touching any BoxLang.

Next: use these rulebooks properly inside ColdBox.

Edit this page Download Markdown Last updated Oct 7, 2026, 10:59:26 AM
Lesson 6 of 10 - Tutorial Course