The examples on this page use one decision,
refund-exception, from an online store’s support app: should a refund request outside the store’s standard return policy be granted, escalated to a person, or denied? How Moonbounce works introduces it.Four kinds
Every type but the outcome ones produces a number, so they nest freely: a check can feed a probability, an average can feed a check, and any of them can be the decision’s
result. An outcome aggregation produces a name, so only rules and match can read it.
Shared rules
- Numeric and check aggregations list the features they read in
inputs: [{ "name": … }], with that type’s per-input fields next to the name.bandsandmatchread a single feature, named ininput;rulesread the features named in their conditions. - A reference is the feature’s path:
questions.<name>,attributes.<name>oraggregations.<name>, ininputs,input, rule conditions andresultalike. A bare name, or a path under the wrong root, is a 400 that says which path to write. invert: truereads an input as 1 − x, a “not”.logisticuses a negative weight instead.- The order of
inputsdoesn’t matter: reordering them changes neither the hash nor the value. Evaluation uses the same canonical input order as hashing, including for floating-point sums and products. - Non-finite numbers, question answers outside 0 to 1, and numeric overflow become unknown. A rule can handle an unknown input explicitly with
abstained: true. - An input that names a 0-to-1 type must be a 0-to-1 value: a question, a boolean attribute, a number attribute declared with
minimum: 0andmaximum: 1, or another number-valued aggregation.
Weighted averages
weighted_mean computes Σ wᵢ·xᵢ / Σ wᵢ. Use it for anything where you decide how much each feature counts: quality rubrics, ranking, risk indexes, rewards.
- Weights are relative. 0.5, 0.3, 0.2 means the same as 5, 3, 2. Give every input a weight, or none for a plain mean. The calculation scales the known inputs’ weights together to avoid overflow and tiny-weight underflow.
- Weights are positive. Direction comes from
invert. - It is not a probability. If
is_helpfulis 0.9 andis_conciseis 0.3, their average of 0.6 is not the chance that anything happens. That is what a rubric needs. - An average can’t veto. An unsafe answer with a small weight still rates high if everything else is good. Gate it with
min, as in A gate on an average.
Probabilities
These read 0-to-1 values and output the probability of a statement.Choosing min, max, product or noisy_or
product and noisy_or assume the inputs are independent; min and max assume they overlap as much as possible. The true answer always lies between the two.
Use
product and noisy_or for unrelated checks, such as spam and abuse. Use min and max for several questions about the same thing. With correlated inputs, product keeps dropping as checks are added, and noisy_or counts the same evidence more than once.
logistic
The place for fitted numbers: the coefficients and intercept from a logistic regression you trained on labeled data paste in directly.
- Weights are signed and absolute. A negative weight lowers the result, so there is no
invert. biasand every weight are required.biassets the result when every input is 0; without one, no evidence would always give 0.5.- Inputs are raw values, not log-odds, so
logisticalso reads number attributes outside 0 to 1, such as an account’s age in days.
Checks
all and any compare each input to a threshold and output 1 or 0. Because a check produces a number, it nests anywhere: inside a probability, inside weighted_mean as the share of checks passed, or as the result.
- The default threshold is ≥ 0.5. A bare entry means “at least 0.5”, which for a yes/no question reads as “more likely yes than no”. Writing
"gte": 0.5hashes the same as leaving it out. - Other thresholds use
gte(≥),gt(>),lte(≤) orlt(<), one per entry, within the feature’s range. A number outside 0 to 1 has no default, so it always names one. - Thresholds are part of the structure. Changing one is a new version, like changing a question.
Outcome aggregations
bands cuts one number into ranges, rules names an outcome from an ordered list of conditions, and match maps a label straight to an outcome. From answers to an outcome covers all three with the refund tickets, and what each puts in the response’s outcome, value and via.
match refuses the case key __proto__. To route that exact label, use rules with an is condition instead.
What can read what
Missing answers
A question with no answer, because the engine abstained, is unknown, and so is any aggregation that can’t be computed without it:weighted_meanleaves the input out, averages the rest and lists it inmissing. It is unknown only when nothing is left.logistic,min,max,productandnoisy_orare unknown when any input is.allis false as soon as a check fails, and unknown if none failed but an answer is missing.anyis true as soon as a check passes, and unknown if none passed and one is missing.bandsandmatchgive theirabstainoutcome, or are unknown.rulesstop at the first rule that is true or unknown; an unknown one makes the result unknown. A rule with{ "name": "…", "abstained": true }catches it first.
status abstained. Whatever the status, the evaluation’s top-level missing lists every question the result depends on that has no answer, so you can tell a decided result that used every answer from one that went without some. It is absent when nothing is missing.
Validation
The structure is checked before anything runs. The graph may not loop: an aggregation that reads itself, directly or through others, is a 400 whose message names the cycle, such asaggregations form a cycle: quality → reward → quality. The full list of rules is on Describe a decision.
Patterns
Group reasons, then weigh the groups
The refund decision’scarrier_delay question, “Did the carrier deliver the order late, or lose it?”, is the fix the loop finds for ticket C, the late tent. Added as a sixth input of the plain average, it lifts C from 0.540 to 0.612, still escalate under the 0.72 grant cut. And it says “no” on almost every other ticket, so it drags them all down by about a sixth: ticket A, the ripped zipper, falls from 0.766 to 0.642 and loses its grant.
The policy is really “any failure of ours is a reason to grant”, weighed against everything else. A defect and a late carrier are two independent ways that can happen, and a ticket needs only one. So group them with noisy_or, group the two reasons for caution with max, and weigh the groups:
A’s
our_fault is 0.961 because of its defect, and C’s is 0.972 because of its carrier, so the “no” on the other question barely counts against either ticket. The response shows every aggregation’s value, so it explains the outcome one level at a time.
Group features because they share a reason, and check that the weights that follow still make sense: caution as a whole now weighs as much as outside_window alone.
A gate on an average
An average can’t veto, so put amin on top. Here an RL reward is a weighted rubric, held down whenever the response is unsafe, however good it is otherwise:
computed and no outcome.
Let one strong yes decide
An average dilutes: in a three-input mean, one input moves the value by at most a third. When any single reason should be enough, usemax (the reasons overlap) or noisy_or (they are independent). A camp stove that caught fire, with legal_threat 0.02, safety_issue 0.97 and public_complaint 0.03:
noisy_or adds up weak evidence; max never does.
Rules that mix attributes and answers
An attribute your app supplies combines with a question in one condition. A stricter spam policy in the EU:Try a question before it counts
A question that no aggregation reads, and that isn’t theresult, is observe-only: the engine answers it and the evaluation records it, but it never moves the outcome, and its failure never fails the call. Add a question that way, watch its answers on real traffic, and wire it in once it answers the way you expect.
Next
From answers to an outcome follows the refund tickets throughbands, and covers rules, match and the status.