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.refund-exception spec.
A decision is an id and a spec
A decision is adecision_id you choose plus a spec. The id names the call site in your app, and it stays the same while the spec changes underneath it: every version of the spec is recorded under the same id.
A decision_id starts with a lowercase letter, followed by up to 127 lowercase letters, digits, _, . or -. refund-exception and quickstart.refund-request are both valid; Refund Exception is not. Ids belong to your team, so another team’s refund-exception is a different decision.
The spec is a JSON document you keep in your repository. Northpine keeps it in moonbounce/refund-exception.json, and its support app sends it inline with every evaluation, so whatever is merged is what runs.
The smallest useful spec
Start with one feature. A spec with a single yes/no feature is already a decision:formatis always"moonbounce.spec/0".nodesis a map of named nodes. A node is either a feature (an indicator an engine detects in the content) or an aggregator (a node that computes a value from other nodes). Here there is one feature, of typeboolean: a yes/no prompt. Node types and patterns lists every type, including the ones coming soon.outputnames the node whose value is the decision’s score.
defective might come back as 0.96, and that is the score.
A node name is a lowercase slug: a lowercase letter, then up to 63 lowercase letters, digits or underscores. constructor and prototype are reserved. Names are the keys you see in every response, so pick ones you are happy to read in logs.
This spec needs no target. The score already has a meaning: it is the probability that the answer to defective’s prompt is yes. You will need a target as soon as the output is something you compute from several answers.
A feature in full
prompt is the only text a boolean feature requires. Three optional fields help when a prompt needs more:
guidanceis background for this prompt only.criteria.trueandcriteria.falsesay what counts as a yes and what counts as a no. Either may be left out.inputslists thesubjectparts this prompt reads, by theirname. Without it, the prompt reads every subject part.contextparts are always read. Every name must match a subject part of the call, or the evaluation is a 400CONTENT_INVALID.
customer_message and the order record as a context part named order, so the defective above reads both. Each text field holds 1 to 4,000 characters after surrounding whitespace is trimmed. Northpine’s real spec uses only prompt: add the other fields when answers come back wrong, not up front.
Writing good features
- One fact per prompt. “Is the item defective and was the request on time?” mixes two facts, and a single probability can’t say which one it is unsure about. Ask two features.
- Answerable from the content you send. “Has the customer had two or more refunds in the last 12 months?” works for Northpine because the
orderpart carriesrefunds_last_12_months. If no part of the request holds the fact, the engine can only guess. - Unambiguous yes or no. “Is this a big order?” invites a judgment call; “Was the order total above $250?” does not. Concrete thresholds give the engine, and later your reviewers, the same line to stand on.
- A fact, not a verdict. Ask about the situation, and leave what it means for the decision to the next step.
high_valueasks whether the total is above $250, not whether the order is risky. Whether a yes argues for or against granting is declared once, in the combine below.
Combine several features
One feature rarely decides a refund. Add whether the request is late and whether the item was used, then acombine node that turns the three answers into one score:
effect on the score: raises (the default) or lowers. A yes to defective pushes the grant score up; a yes to outside_window or item_used pushes it down. A combine holds 1 to 64 terms, each node at most once, and their order doesn’t matter. A term usually names a feature, but any node with a probability works, including another combine: Group features, then combine the groups shows when that helps. Terms also accept an option field, which is reserved for feature types that are not available yet: any value is a 400.
Direction lives in your code because it is policy: “a defect is a reason to grant” is a statement your team owns and reviews like any other rule. How much each term moves the score is a number, and numbers are Moonbounce’s side: they live in a separate weights document. Weights may change how much a term counts, never which way it points: weights whose sign contradicts a term’s effect are refused with 409 WEIGHTS_INCOMPATIBLE.
Before any weights are learned, a combine uses default weights: the plain average of its answers, with each lowers answer flipped. From answers to an outcome works through the arithmetic. A combine also takes prior and fixed, which change those starting numbers; that page covers both.
The target and calibrate
The output is now a computed score, so the spec has to say what the score is a probability of. That is the target, a yes/no statement about the item:output names a combine or calibrate node; without it the spec is a 400. It is what learned weights and calibration aim at, independent of your cut points, and it is what a target feedback claim answers (“yes, this refund should have been granted”).
A calibrate node reads one input and maps it onto the probability that the target is true. Its numbers, like a combine’s, are learned from feedback. Today no calibration has been fitted, so grant passes grant_score through unchanged and the response marks it "calibrated": false. Adding the node now means the output becomes a calibrated probability once fitting is available, without a change to your spec.
Guidance every prompt sees
Prompts are short. The policy behind them goes in the spec-levelguidance, up to 20,000 characters that every feature sees:
Meta: for people only
meta describes the decision for the people and tools reading it. It never affects answers or scores, and it is outside the hash, so editing it never creates a new version:
title (up to 200 characters), description (4,000), owner (200), tags (up to 32, each up to 64) and display are all optional. display maps an outcome name to its label: text of 1 to 40 characters and an optional help of up to 200. Moonbounce stores meta with the structure and returns it when you read the structure back. Registering the same spec again through POST /v1/decisions/{decision_id}/structures with different meta updates it in place.
The full Northpine spec
Two more features,repeat_refunder and high_value, both arguing for caution, complete the spec. Here is Northpine’s decision file: the spec, next to the weights it uses and its cut points, which From answers to an outcome explains.
moonbounce/refund-exception.json
Validation
The API parses a spec strictly wherever you send one:POST /v1/evaluate, POST /v1/decisions/{decision_id}/structures and POST /v1/specs/hash. The last needs no key and stores nothing, so it is a cheap check to run in CI. A spec that breaks a rule is a 400 with type invalid_request, a message, a param pointing at the field, and every problem listed in details.issues. Here is POST /v1/specs/hash answering a spec whose grant_score lists a carrier_delay term before that node exists:
The body itself must be strict JSON. A key repeated in the same object (two
"output" keys, say) is refused before the spec is read, as a 400 with reason CONTENT_INVALID and details.code DUPLICATE_KEY, because different JSON parsers would keep different copies.
Keep the spec static
Any change to a prompt, its guidance, criteria or inputs, the spec-level guidance, the target or the node graph gives the spec a new structure hash, and the API records each new structure hash as a new version. Formatting, key order, term order andmeta don’t count.
So the spec should be the same on every call. Data about the item goes in the request’s subject and context parts, never into prompt or guidance text. If Northpine wrote “This order totals $129” into its guidance, every ticket would mint its own version. Instead it sends the order as a part:
Next
From answers to an outcome follows three Northpine tickets from their answers to a score and on togrant, escalate or deny.