The RuleBook DSL

given / when / except / then / using / stop, explained.

On this page

The RuleBook Domain Specific Language

The RuleBox DSL uses the Given-When-Then format, popularized by Behavior Driven Development (BDD) and testing frameworks (TestBox, Cucumber, Spock), and heavily inspired by RuleBox's Java counterpart, RuleBook. Sentences should describe rules, and rules should be defined using a ubiquitous language that translates directly into the codebase.

Given-When-Then

  • Given - some Fact(s)
  • When - a condition evaluates to true
  • Except - a condition that evaluates to false
  • Then - an action is triggered

when() conditions can grow unwieldy, so RuleBox adds except() to the language: when().except().then(). Even when the when() condition evaluates to true, chaining an except() onto it that evaluates to true cancels the rule for that run.

given() / givenAll()

given()/givenAll() accept one or more facts, in various forms, as a collection of information provided to a single Rule. When grouping Rules into a RuleBook, facts are supplied when the RuleBook is run, so the "Given" step is inferred:

var homeLoans = getInstance( "tests.resources.HomeLoanRateRuleBook" )
	.withDefaultResult( 4.5 )
	.given( "creditScore", 650 )
	.given( "cashOnHand", 20000 )
	.given( "firstTimeHomeBuyer", false )
	.run()

var homeLoans = getInstance( "tests.resources.HomeLoanRateRuleBook" )
	.withDefaultResult( 4.5 )
	.givenAll( {
		creditScore: 650,
		cashOnHand: 20000,
		firstTimeHomeBuyer: false
	} )
	.run()

givenAll() accepts a second overwrite argument (default true) that controls whether facts already present are replaced.

when()

when() accepts a Predicate closure/lambda that evaluates a condition based on the facts provided. Only one when() can be specified per rule, and it must return boolean:

.when( ( facts ) => {
	// determine if we continue or not
	return boolean
} )

except()

except() negates the when() operation if it also passes. In other words: when the balance is greater than 100, except when the account is disabled, then dispense some money:

except( ( facts ) => facts.accountDisabled )

then()

then() accepts a Consumer closure/lambda describing the action to run if when() evaluates to true. A Rule can specify multiple then() calls, invoked in the order they're declared. If a then() returns true, no further consumers in that rule execute - the chain breaks. Returning void or false continues the chain:

.then( ( facts, result ) => {
	// do stuff

	// break the next then()
	return true
} )
.then( ( facts, result ) => {
	// This never fires
} )

using()

using() reduces the set of facts available to the next then() call. Multiple using() calls can be chained together - the aggregate of every fact name across them is what's made available to the immediately following then():

addRule(
	newRule()
		.when( ( facts ) => facts.keyExists( "hello" ) && facts.keyExists( "world" ) )
		using( "hello" ).then( ( facts ) => println( facts.hello ) )
		using( "world" ).then( ( facts ) => println( facts.world ) )
)

withPriority()

By default, rules execute strictly in the order they're added via addRule(). withPriority() lets a rule jump the queue - a higher priority runs earlier. Rules that share the same priority (the default is 0) keep insertion order relative to each other:

addRule(
	newRule( "checkBlocklist" )
		.withPriority( 10 )
		.when( ( facts ) => facts.applicant.isBlocklisted() )
		.then( ( facts, result ) => result.setValue( 0 ) )
		.stop()
)

addRule(
	newRule( "creditScoreAdjustment" )
		// no withPriority() - defaults to 0, runs after the rule above
		.when( ( facts ) => facts.applicant.getCreditScore() < 600 )
		.then( ( facts, result ) => result.setValue( result.getValue() * 4 ) )
)

withPriority() can be called before or after a rule is added to a RuleBook - the RuleBook re-derives its entire execution chain, sorted by priority (ties broken by insertion order), every time addRule() is called and every time withPriority() changes the priority of a rule that is already added, so a later, higher-priority rule (or a rule whose priority was raised after it was added) correctly slots ahead of rules already registered.

withDescription()

Describe what a rule does. The description is shown on the rule in the Rule Visualizer, returned by getDescription(), and included in each rule's dryRun() report. It has no effect on how the rule runs:

addRule(
	newRule( "checkBlocklist" )
		.withDescription( "Declines any applicant on the blocklist" )
		.when( ( facts ) => facts.applicant.isBlocklisted() )
		.then( ( facts, result ) => result.setValue( 0 ) )
		.stop()
)

A RuleBook can be described as well; see Describing a RuleBook.

stop()

stop() breaks the rule chain. If specified on a rule whose when() evaluates to true, then once that rule's then() action(s) complete, no further rules in the chain are evaluated.

addRule(
	newRule()
		.when( ( facts ) => facts.applicant.getCreditScore() < 600 )
		.then( ( facts, result ) => result.setValue( result.getValue() * 4 ) )
		.stop()
)

active()

active() restricts a rule to a time window. Outside of it, the rule is skipped exactly like a failed when() - no then() consumers run, and it shows as SKIPPED in the audit trail. Either bound can be omitted to leave that side open-ended:

addRule(
	newRule( "blackFridayPromo" )
		.active( from: "2025-11-28", until: "2025-12-01" )
		.then( ( facts, result ) => result.setValue( result.getValue() * 0.8 ) )
)

addRule(
	newRule( "legacyDiscount" )
		// no "from" - already active; expires at the given date
		.active( until: "2025-01-01" )
		.then( ( facts, result ) => result.setValue( result.getValue() * 0.9 ) )
)

active() can be called any time before run()/dryRun(); it's checked fresh on every evaluation, not just once.

Edit this page Download Markdown Last updated Oct 13, 2026, 8:42:39 PM