Advanced — New in 2.0.0
XML RuleFlows
rulii-spring 2.0.0 adds a full <r:ruleflow> XML namespace that mirrors the RuleFlow builder DSL — script expressions substitute the builder's lambdas. Flows declared in XML are ordinary Spring beans: discoverable through the RuleRegistry, and composable with rules and rulesets declared anywhere else.
Prefer RuleFlows for orchestration
Just like in Java, a RuleFlow is the recommended way to run rules and rulesets in XML-driven configurations — it gives you conditionals, loops, async execution, and exception handling declaratively, instead of orchestrating individual run(...) calls in application code.
Namespace Declaration
RuleFlows use the same namespace as XML rule definitions. Load files via @RuleScan(xmlLocations = "classpath:rules/flows/"):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:r="http://www.rulii.org/schema/rulii"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.rulii.org/schema/rulii
https://www.rulii.org/spring/rulii-spring.xsd">
<r:scripting defaultLanguage="el"/>
<!-- flows, rules, rulesets go here -->
</beans>
Flow Skeleton — Element Order Matters
<r:ruleflow name="CheckoutFlow" description="...">
<r:context ref="..."/> <!-- optional, must be FIRST -->
<r:param .../> <!-- optional, repeatable -->
<!-- commands, in execution order -->
<r:on-exception .../> <!-- optional: flow-level (global) handler -->
<r:finalizer .../> <!-- optional: always runs, even on exit/error -->
<r:returning .../> <!-- optional: extracts the flow's return value -->
</r:ruleflow>
<r:param> uses the same grammar as rulesets: <r:param name="order" type="com.example.Order" required="false"/> with an optional <r:default-value> child.
finalizer and returning may also be declared as attributes on <r:ruleflow> itself (e.g. <r:ruleflow name="CheckoutFlow" returning="#ctx.total">) — see Inline Attribute Shorthand. The attribute form uses the default scripting language; declaring both forms is a parse error.
Command Reference
| Element | Purpose |
|---|---|
<r:bind> |
Bind a value: literal value=, Spring bean ref=, or a script expression (evaluated per execution) |
<r:run> |
Run a Rule / RuleSet / RuleFlow |
<r:async-run> |
Run one asynchronously; result bound as a CompletableFuture |
<r:apply> |
Evaluate a function expression, bind its result via as= |
<r:execute> |
Execute an action expression (side effect) |
<r:when> |
Conditional with <r:then> body and optional <r:otherwise> body |
<r:for-each> |
Iterate a source collection, binding each element |
<r:scope> |
Run a body inside a named binding scope |
<r:await> / <r:await-all> / <r:await-any> |
Block on async results |
<r:exit/> |
Stop the flow early (the finalizer still runs) |
<r:command> |
A user-supplied RuleFlowCommand bean |
Bodies (then, otherwise, for-each, scope, then-run, on-exception) contain the same command grammar — nesting is unlimited.
Run Targets — Exactly One of Three Forms
<r:run bean-ref="pricingRuleSet"/> <!-- Spring bean, resolved at WIRING time (startup) -->
<r:run name="AgeCheckRule"/> <!-- RuleRegistry lookup by name at EXECUTION time -->
<r:run class="com.example.TaxRule"/> <!-- RuleRegistry lookup by rule class at EXECUTION time -->
bean-reffails at startup if the bean is missing — safest for fixed wiring.name/classdefer to the registry on every execution — use when the rule set is dynamic.- Bind the result with
as="result"(optionallyscope="scopeName"). - Supply step-scoped literal parameters with
<r:with name="x" value="10"/>children. With-param values are constants — for computed values,bindbefore the run.
Conditionals, Loops & Scopes
<r:when condition="#ctx.total >= ${order.minTotal:100}">
<r:then>
<r:run name="ApplyDiscountRule"/>
</r:then>
<r:otherwise>
<r:exit/>
</r:otherwise>
</r:when>
<r:for-each item="n" source="{1, 2, 3}" stop-condition="#ctx.done == true">
<r:execute>#ctx.sum = #ctx.sum + #ctx.n</r:execute>
</r:for-each>
<r:scope name="tmp">
<r:bind name="scratch">#ctx.total * 0.1</r:bind>
</r:scope>
condition, source, and stop-condition may also be child elements (<r:condition>, <r:source>, <r:stop-condition>) — required when a per-expression language override is needed. Declaring both forms is a parse error.
Async Execution
<r:async-run bean-ref="ScoreFlow" as="f1"/>
<r:async-run bean-ref="RiskFlow" as="f2" context-mode="immutable">
<r:then-run result="risk">
<r:execute>#ctx.riskLevel = #ctx.risk</r:execute> <!-- continuation, non-blocking -->
</r:then-run>
</r:async-run>
<r:await name="f1" timeout="10" unit="seconds"/>
<r:await-all names="f1, f2" timeout="30" unit="seconds"/>
- Async steps run on the Spring-configured
rulii.executorServicepool. context-mode:shared(default) orimmutable(the async task gets an immutable snapshot of the bindings).awaitblocks but leaves the future bound — read the value afterwards with#ctx.f1.get().- Avoid launching multiple
shared-mode children that write bindings concurrently — serialize withawaitbetween launches, or useimmutable.
Exception Handling
<!-- Step-level: attached to run / apply / execute / async-run -->
<r:execute>
#ctx.risky.call()
<r:on-exception exception="org.rulii.model.UnrulyException">
<r:execute>#ctx.recovered = true</r:execute>
</r:on-exception>
</r:execute>
<!-- Flow-level (global), declared after the commands -->
<r:on-exception exception="java.lang.Exception">
<r:execute>#ctx.failed = true</r:execute>
</r:on-exception>
The caught exception is bound as ex inside the handler body (#ctx.ex.message). The step handler is checked first, then the global handler; a matched handler swallows the exception and execution continues with the next command. exception defaults to java.lang.Exception.
Custom Commands
A leaf command is a Spring bean implementing RuleFlowCommand; a container command extends ContainerCommand (retry, parallel, …) and receives the nested body:
<r:command ref="publishOrderCommand"/>
<r:command ref="retryCommand">
<r:run name="ChargeCardRule"/>
<r:execute>#ctx.attempts = #ctx.attempts + 1</r:execute>
</r:command>
Important
Container beans must be @Scope("prototype") when referenced from more than one <r:command> — setBody() mutates the bean, and reuse fails fast at startup. Keep per-execution state in the bindings, never in command fields (the instance is shared across executions and threads).
There is also <r:context ref="..."/> (first element only) — a Consumer<RuleContextBuilder> bean for programmatic context configuration. Java-only features with no XML form: inline context(...) lambdas and asyncRun(...).withContext(RuleContext).
Results — the Flow-Scope Rule
Flow-internal bindings live in the flow's own scope, which is removed after execution. bind results and as= bindings are invisible once run() returns. Two ways to get data out:
<!-- 1. returning — runs inside the scope, so it sees everything -->
<r:returning>#ctx.total</r:returning> <!-- or the attribute form: returning="#ctx.total" -->
// 2. Write-through to pre-bound bindings — #ctx.x = ... sets an EXISTING caller binding
RuleContext ctx = RuleContext.builder().with(ruleContextOptions).build();
ctx.getBindings().bind("approved", false); // pre-bind the observation slot
flow.run(ctx);
ctx.getBindings().getValue("approved"); // set by the flow via #ctx.approved = true
An empty <r:returning/> returns the RuleContext itself. The finalizer always runs — including on <r:exit/> and on errors.
Terse Attribute Forms
Single-expression slots can be attributes instead of child elements (file default language only; declaring both forms is a parse error):
<r:ruleflow name="SquareFlow" returning="#ctx.sq" finalizer="#ctx.done = true">
<r:param name="v" type="java.lang.Integer"/>
<r:apply as="sq">#ctx.v * #ctx.v</r:apply>
</r:ruleflow>
Available on <r:ruleflow>: finalizer, returning. On <r:when>: condition. On <r:for-each>: source, stop-condition.
Environment Placeholders
Any script expression may contain ${property:default} placeholders, resolved once at startup with @Value semantics (a missing key without a default fails startup; \${...} escapes):
<r:when condition="#ctx.total >= ${order.minTotal:100}">
Running a Flow from Java
@Autowired RuleRegistry ruleRegistry;
@Autowired RuleContextOptions options; // the auto-configured Spring options
RuleFlow<?> flow = ruleRegistry.getRuleFlow("CheckoutFlow");
RuleContext ctx = RuleContext.builder().with(options).build();
ctx.getBindings().bind("total", 150);
Object result = flow.run(ctx);
Why build the context with the Spring options?
Building the context .with(options) wires the Spring registry (for run name=/class= lookups), the Spring executor (for async-run), converters, and tracing into the flow. A plain RuleContext.builder().standard() has none of that.
Full Example
<r:scripting defaultLanguage="el"/>
<r:rule name="ValidateOrderRule" given="#ctx.order != null" then="#ctx.valid = true"/>
<r:ruleflow name="CheckoutFlow" description="Validates, prices, and finalizes an order"
finalizer="#ctx.audited = true" returning="#ctx.status">
<r:param name="order" type="com.example.Order"/>
<r:bind name="status" value="pending"/>
<r:run name="ValidateOrderRule"/>
<r:when condition="#ctx.valid == true">
<r:then>
<r:async-run bean-ref="pricingFlow" as="priceFuture"/>
<r:await name="priceFuture" timeout="10" unit="seconds"/>
<r:execute>#ctx.status = 'priced'</r:execute>
</r:then>
<r:otherwise>
<r:execute>#ctx.status = 'rejected'</r:execute>
<r:exit/>
</r:otherwise>
</r:when>
<r:command ref="publishOrderCommand"/>
<r:on-exception exception="java.lang.Exception">
<r:execute>#ctx.status = 'failed'</r:execute>
</r:on-exception>
</r:ruleflow>
Common Mistakes
| Mistake | Fix |
|---|---|
Asserting on bind/as= results after flow.run() |
The flow scope is removed after execution — use returning or write through to pre-bound bindings |
Two run targets on one element (bean-ref + name) |
Exactly one of bean-ref / name / class — parse error otherwise |
Reading an awaited result with #ctx.f1 |
await leaves the CompletableFuture bound — use #ctx.f1.get() |
<r:with> with a computed expression |
With-param values are literals — bind the computed value before the run instead |
A singleton ContainerCommand bean in two <r:command> bodies |
setBody() mutates the bean — declare it @Scope("prototype") |
<r:context> not first |
The schema requires it before params and commands |
Running with RuleContext.builder().standard() |
Registry lookups and async-run need the Spring wiring — build with .with(ruleContextOptions) |
Concurrent shared-mode async-runs writing bindings |
The scope stack is shared — serialize with await, or use context-mode="immutable" |