Table of Contents

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.Outputs is the merged view: every fired rule's actions applied in turn. Status ends as approved because vip ran after provisional. Provisional is gone because vip removed it.
  • fired.Outputs is one rule's snapshot: the value at each target it touched, right after it ran. loyal-customer shows Score=55, the running total at that moment, not its own 30.
  • Snapshots are frozen. appendToOutput copies the list when it writes, so a later rule appending to Reasons doesn'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.