Skip to main content
The examples on this page use Northpine, a fictional outdoor-gear shop whose refund-exception decision grants, escalates or denies refund requests outside its standard return policy. How Moonbounce works introduces it.
Every node in a spec has a type. It says what the node is, either an indicator an engine detects (a feature) or a step Moonbounce computes from other nodes (an aggregator), and so what its value is and which other nodes can read it. Describe a decision builds Northpine’s spec with the three types that exist today. This page lists every type, including the ones that are designed but not built yet, and then shows patterns for combining them.

Available today

All three have a probability as their value, so any of them can be the spec’s output, a combine term or a calibrate input. That is what lets you stack them: a combine can read another combine, or a calibrated score.

Coming soon

The types below are designed but not available yet: a spec that uses one today is a 400. Names and fields may change before they ship, and this page will say when each one is live.

Features

choice asks the engine to choose one answer out of 2 to 26 options that don’t overlap. The engine returns a probability for each option, and they add up to 1. Describe every option or none: a single description pulls the answers toward that option.
scale asks for a rating on 2 to 10 ordered levels, lowest first. The engine returns a probability for each level, the most likely level, and the expected level: the probability-weighted average of the level positions, counted from 0 for the lowest.
extract pulls structured data out of the content, such as an order number, following a JSON schema. The root is an object, every property is listed in required (write an optional value as nullable, ["string", "null"]), and additionalProperties is false. Its value is the data itself, which your app reads from the response. It has no probability or label, so it can’t feed a score or a label test; a condition can only check whether it answered.
All three take the same prompt, guidance and inputs fields as a boolean.

Aggregators

max and min are a fuzzy OR and AND: the largest or smallest of their inputs’ probabilities. Each entry in of names a node, and negate: true reads it as 1 − p (“not”). They have no weights, so nothing is learned: use them where the logic is policy. all and any are true or false: whether all, or any, of their conditions hold. rules gives a label instead of a score. Its rules are tried in order and the first one whose condition holds sets the value; else names the label when none does. When output is a rules node, its label is the outcome, so the call sends no cut points and the spec has no target.

Conditions and thresholds

all, any and rules test conditions. A condition never holds a number: it names a threshold, and every call sends a value for each threshold its spec names, in a thresholds object next to outcomes. Like cut points, threshold values live in your code and are outside the hash, so tuning one never makes a new version. For a scale, the number in a comparison is its expected level, so { "node": "wear", "gte": "max_wear" } with max_wear set to 2 means “used or worse, on average”. A comparison on a node with no answer is neither true nor false but unknown. all is false if any part is false, any is true if any part is true, and a rules node that reaches an unknown rule before a true one has no value. Put an abstained rule first when you want a fallback label instead.

What can read what

A whole choice or scale has no single probability, so a term that names one must also name an option, as in { "node": "return_reason", "option": "shipping" }. Today the option field exists but any value is a 400, because no shipped node has options.

Patterns you can use today

Let one strong yes decide

A plain combine averages, so one confident answer moves the score by at most 1/n. When any single reason should be enough, set "prior": "any". From answers to an outcome works through the arithmetic with Northpine’s needs_person check.

Group features, then combine the groups

prior applies to a whole combine, but often only some of its features belong together. Northpine’s case is the carrier_delay prompt from Suggestions and previews. Added as a sixth term of the average, it does its job for the late tent, ticket C, which moves from 0.540 to grant at 0.612. But 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. 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 ways that can happen, and a ticket needs only one. So put the two in their own combine with prior: "any", put the two reasons for caution in another, and combine the groups:
With default weights and the same answers as on the other pages: C now clears the grant cut with room to spare, and A gets most of its margin back. A’s our_fault is 0.979 because of its defect, and C’s is 0.985 because of its carrier, so the “no” on the other prompt barely counts against either ticket. Each combine reports its own contributions, so the response explains a score one level at a time. C’s grant_score shows our_fault adding 0.246 and outside_window taking away 0.238, and its our_fault shows that carrier_delay (7.76) is the reason, not defective (0.40). Keep in mind:
  • With default weights, a group counts as one term of the combine that reads it. Here caution as a whole weighs as much as outside_window alone, so high_value now counts half as much as outside_window, where in the flat spec they counted the same. Group features because they share a reason, and check that the weights that follow still make sense.
  • A new_feature suggestion adds its feature to the combine that feeds the output, here grant_score. If it belongs in a group, move it there before you merge, and preview the result.
  • Feedback and review correct features, not groups: a feature claim names a boolean.
  • The spec still holds at most 64 nodes, and no node may read itself through others.

Try a feature before it counts

A boolean that no aggregator reads, and that isn’t the output, is observe-only: the engine answers it and the evaluation records it, but it never moves the score, and its failure never fails the call. Add a feature that way, watch its answers on real traffic, and add it as a term once it answers the way you expect. From answers to an outcome shows what the response looks like when one fails.

Patterns coming soon

These use the types above that are not available yet. Each says what to do today instead.

Fuzzy logic with min and max

Some rules are logic, not weighing. A review is spam if it promotes something else and isn’t about the reviewer’s own experience, or if it is junk text:
min and max nest like combines, and a combine can read them. Northpine’s tenure rule, “a repeat refunder is only a reason for caution with under five years’ tenure”, is min(repeat_refunder, not long_tenure), which then sits in caution where repeat_refunder is now. Today: use a combine with prior: "any" for an OR, as needs_person does, or fold the condition into one prompt (“Has the customer had two or more refunds in the last 12 months, and been a customer for under five years?”).

Ask once, use each answer

When the possible answers can’t all be true at once, one choice replaces several booleans, and a combine can read each option on its own. With return_reason from above, our_fault becomes:
Each option gets its own weight. Use separate booleans when answers can overlap: a choice’s probabilities add up to 1, so a ticket that is both defective and late splits its answer between the two options. Today: one boolean per option.

Route to a label with rules

When the result is a destination rather than a yes/no, make a rules node the output. A support inbox routes each ticket from a boolean, a choice and a scale:
Each call sends the three thresholds, such as "thresholds": { "refund_min": 0.5, "escalate_urgency": 2.5, "topic_confident": 0.7 }. The order is the policy: a billing refund goes to refunds even when it is urgent, and a ticket whose topic and urgency clear none of the thresholds falls through to human_triage. A feature with no answer is different: the first rule that reads it is unknown, so route has no value. To send those tickets to a person too, start with a rule such as { "when": { "any": [{ "node": "refund_request", "abstained": true }, { "node": "topic", "abstained": true }, { "node": "urgency", "abstained": true }] }, "then": "human_triage" }. Today: a decision’s output is a score, so evaluate one decision per yes/no and route in your own code.

Next

Engines, keys and environments covers which engine answers your features, which key can call which operation, and how test traffic stays apart from production.