Externalized Rule Definitions

Load rules from JSON, YAML, or a database instead of hand-writing DSL code.

On this page

Externalized Rule Definitions

Every rule you've seen so far is written in BoxLang, using the RuleBox DSL. Sometimes that's the wrong place for a rule to live - business users want to tweak a threshold without a deploy, or the rules genuinely belong in a database table next to the data they govern. RuleBook.loadRules() loads rule definitions from an external source - JSON, YAML, or a database - and turns each one into a real Rule, wired into the same priority chain, audit trail, and dryRun() support as any other rule.

The rule-definition schema

A rule source is any object with a load() method that returns an array of rule-definition structs. Each struct can contain:

KeyRequiredMeaning
nameNoThe rule's name, used for auditing. Defaults like any other rule if omitted
priorityNoSee Rule Priority. Defaults to 0
whenNoA condition node or a predicate reference. Defaults to always-true
exceptNoSame shape as when, negated
thenNoAn array of action references. Defaults to none
usingNoAn array of fact names to restrict every then action to, applied the same way to each one - see The DSL
stopNotrue to stop the chain after this rule fires

A JSON example, mirroring the "credit approval" walkthrough used throughout these guides:

[
	{
		"name": "highRisk",
		"priority": 10,
		"when": { "lt": [ "creditScore", 600 ] },
		"then": [ { "action": "flagHighRisk" } ],
		"stop": true
	},
	{
		"name": "approve",
		"when": { "gte": [ "creditScore", 600 ] },
		"then": [ { "action": "approveApplicant", "params": { "reason": "good credit" } } ]
	}
]

The condition tree grammar

when/except accept a small, safe, declarative grammar instead of arbitrary code - there's no eval, so a rule definition loaded from a file or a database table can never execute code you didn't write yourself. A condition node is a struct with exactly one operator key:

OperatorShapeMeaning
eq / neq[ "factPath", value ]Equals / not equals
lt / lte / gt / gte[ "factPath", value ]Numeric/date comparison
in[ "factPath", [ values ] ]Fact value is one of the given values
and / or[ node, node, ... ]All / any of the child nodes
notnodeNegates the child node

factPath supports dot-notation into nested facts ("applicant.address.state"). A missing path resolves to null rather than throwing. Nodes nest freely:

{
	"and": [
		{ "eq": [ "state", "CA" ] },
		{ "or": [
			{ "lt": [ "creditScore", 600 ] },
			{ "not": { "eq": [ "flagged", true ] } }
		] }
	]
}

The predicate and action registries

The condition grammar covers comparisons, but not arbitrary logic, and a rule always needs to do something. For both cases, a rule definition references code by name, and that name must be registered on the RuleBook first via registerPredicate()/registerAction() - a rule definition can never carry inline code, so untrusted rule sources stay safe to load.

ruleBook
	.registerPredicate( "isEligible", ( facts, params ) => facts.creditScore >= params.threshold )
	.registerAction( "approveApplicant", ( facts, result, params ) => result.setValue( params.reason ) )

Referenced from a rule definition as:

{
	"when": { "predicate": "isEligible", "params": { "threshold": 600 } },
	"then": [ { "action": "approveApplicant", "params": { "reason": "good credit" } } ]
}

Both registerAction() and registerPredicate() accept three forms:

  • A closure/lambda - ( facts, result, params ) => { ... } for actions, ( facts, params ) => boolean for predicates
  • An object instance - duck-typed on an execute( facts, result, params ) or test( facts, params ) method. RuleAction/RulePredicate document the optional contract
  • A WireBox mapping ID string - e.g. "CreditService@myModule", resolved via getInstance() immediately when you call registerAction()/registerPredicate(), not deferred to rule execution
ruleBook.registerAction( "approveApplicant", "ApprovalService@myModule" )

An action/predicate name referenced by a rule definition but never registered throws RuleBox.UnregisteredActionException / RuleBox.UnregisteredPredicateException from loadRules(), rather than failing silently or at run time.

JSONRuleSource

ruleBook.loadRules( new rulebox.models.JSONRuleSource( "/path/to/rules.json" ) )

The file is a JSON array of rule-definition structs, as shown above.

YAMLRuleSource

ruleBook.loadRules( new rulebox.models.YAMLRuleSource( "/path/to/rules.yaml" ) )

Same schema, as YAML:

- name: highRisk
  priority: 10
  when:
    lt: [ creditScore, 600 ]
  then:
    - action: flagHighRisk
  stop: true

YAML support depends on the official BoxLang YAML module, which RuleBox does not install for you - install it yourself if you use YAMLRuleSource:

box install boxlang-yaml

DBRuleSource

Point it at a datasource and a SQL statement:

ruleBook.loadRules( new rulebox.models.DBRuleSource(
	datasource = "myApp",
	sql        = "SELECT * FROM rules WHERE ruleset = 'credit'"
) )

...or hand it a query you already have, which is also the easiest way to test code that uses DBRuleSource:

ruleBook.loadRules( new rulebox.models.DBRuleSource( query = myQuery ) )

Expected columns: name, priority, when_json, except_json (optional), then_json, stop, using_facts (optional, a comma-delimited list of fact names). when_json/except_json/then_json hold the same condition-tree/action JSON used by JSONRuleSource, stored as text:

ColumnMaps to
namename
prioritypriority
when_jsonwhen (deserialized)
except_jsonexcept (deserialized, optional)
then_jsonthen (deserialized)
stopstop
using_factsusing (comma-delimited list)

Writing your own source

Any object with a load() method returning an array of rule-definition structs works with loadRules() - a REST call, a config service, a cache, whatever fits. There's no interface to implement.

Edit this page Download Markdown Last updated Sep 17, 2026, 8:37:07 PM