Actions and outputs
When a rule fires, its actions write into one set of outputs shared by the whole evaluation. Four action types decide how a value combines with what earlier rules wrote, so several rules can build a total, collect reasons, or override each other.
The four action types
{ "type": "setOutput", "target": "Tier", "value": "gold" }
{ "type": "addToOutput", "target": "Score", "value": 25 }
{ "type": "appendToOutput", "target": "Reasons", "value": "loyal customer" }
{ "type": "removeOutput", "target": "Provisional" }
| Type | Effect on target |
|---|---|
setOutput |
Replaces whatever is there. |
addToOutput |
Adds to a running numeric total across every fired rule. |
appendToOutput |
Appends to a List<object?> collected across every fired rule. |
removeOutput |
Deletes the target, undoing an earlier rule's write. Takes no value. |
A value is a constant or a computed expression.
Scoring an order
Four rules, run in priority order (highest first; ties keep document order):
{
"name": "Order review",
"rules": [
{
"id": "provisional",
"priority": 30,
"condition": { "field": "Order.Total", "operator": "GreaterThan", "value": 0 },
"actions": [
{ "type": "setOutput", "target": "Status", "value": "review" },
{ "type": "setOutput", "target": "Provisional", "value": true }
]
},
{
"id": "big-order",
"priority": 20,
"condition": { "field": "Order.Total", "operator": "GreaterThanOrEqual", "value": 200 },
"actions": [
{ "type": "addToOutput", "target": "Score", "value": 25 },
{ "type": "appendToOutput", "target": "Reasons", "value": "large order" }
]
},
{
"id": "loyal-customer",
"priority": 20,
"condition": { "field": "Customer.LoyaltyYears", "operator": "GreaterThanOrEqual", "value": 5 },
"actions": [
{ "type": "addToOutput", "target": "Score", "value": 30 },
{ "type": "appendToOutput", "target": "Reasons", "value": "loyal customer" }
]
},
{
"id": "vip",
"priority": 10,
"condition": { "field": "Customer.IsVip", "operator": "Equals", "value": true },
"actions": [
{ "type": "addToOutput", "target": "Score", "value": 20 },
{ "type": "setOutput", "target": "Status", "value": "approved" },
{ "type": "removeOutput", "target": "Provisional" }
]
}
]
}
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;
LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("actions.json"));
RuleEvaluationResult result = engine.Evaluate(rules, Facts.Vip());
// The merged view: every fired rule's actions, applied in priority order.
Console.WriteLine($"Status = {result.Outputs["Status"]}");
Console.WriteLine($"Score = {result.Outputs["Score"]}");
Console.WriteLine($"Reasons = {string.Join(", ", (List<object?>)result.Outputs["Reasons"]!)}");
Console.WriteLine($"Provisional present: {result.Outputs.ContainsKey("Provisional")}");
Console.WriteLine();
// Each rule's own snapshot: the value at each target it touched, right after it ran.
foreach (FiredRule fired in result.FiredRules)
{
IEnumerable<string> wrote = fired.Outputs.Select(o =>
$"{o.Key}={(o.Value is List<object?> list ? "[" + string.Join(", ", list) + "]" : o.Value)}");
Console.WriteLine($"{fired.RuleId,-15} {string.Join(" ", wrote)}");
}
Output
Status = approved
Score = 75
Reasons = large order, loyal customer
Provisional present: False
provisional Status=review Provisional=True
big-order Score=25 Reasons=[large order]
loyal-customer Score=55 Reasons=[large order, loyal customer]
vip Score=75 Status=approved
Reading the result
result.Outputsis the merged view: every fired rule's actions applied in turn.Statusends asapprovedbecausevipran afterprovisional.Provisionalis gone becausevipremoved it.fired.Outputsis one rule's snapshot: the value at each target it touched, right after it ran.loyal-customershowsScore=55, the running total at that moment, not its own30.- Snapshots are frozen.
appendToOutputcopies the list when it writes, so a later rule appending toReasonsdoesn't change an earlier rule's snapshot.
Important
When two rules write the same target with setOutput, the one that runs last wins, and that is the one with the lowest priority. Rule-set priority orders evaluation. It does not rank whose value is kept. To let the highest-priority match decide on its own, stop after the first match.
Types in the outputs
| You wrote | You read |
|---|---|
10 |
long |
4.95 |
decimal |
a number that isn't exactly representable as decimal |
double |
"text", true, null |
string, bool, null |
addToOutput |
decimal, or double if a contribution was floating point |
appendToOutput |
List<object?> |
The accumulators tolerate bad input: a contribution that is null, or not a number for addToOutput, is skipped rather than wiping the running result.
Next
Computed values: outputs computed from the fact.