The three outcomes of a declaration
- Identical declaration: nothing happens. This is the normal case. Your code sends the same declaration on every call, and Curious compares it to what is running.
- A changed declaration-class field: the running experiment is concluded with reason
supersededand a new one begins from the new declaration. These fields define what the variants were generated against, so changing one invalidates what has been learned. - A changed mutable field: the running experiment is updated in place. These guide future generation or serving policy without invalidating existing variants.
An omitted declaration-class field carries forward from the running experiment, so a call that sends only
element and user_id never supersedes anything. A control is required whenever a call creates an experiment and there is no running one to carry it from, which is the first call for an element; on a supersession the running control carries forward unless the change is the control itself. Unknown fields anywhere in a declaration, including inside nested objects, are a 400 naming the path, so a typo cannot silently become a no-op.
The control and the arms
The content Curious can serve for an element are its arms. The control is yours: the version you show today, bare content or{ "content": …, "hypothesis": … }. It is the counterfactual every variant is measured against, the content served while variants are being generated, the content held-out users always see, and the content served when nothing else can be. The control is never concluded by any path, which is why a response’s content is never null.
Variants are generated by Curious in rounds, each with a hypothesis about why it should win, and tested against the control and against each other. You may seed variants of your own with arms: one entry or a list, each bare content or {content, hypothesis}. Declared arms are additive and idempotent: an entry the experiment already has is skipped, and resending the list changes nothing. An entry equal to the control’s content, or more than 10 distinct entries in one call, is a 400. An entry that duplicates an earlier one in the same call, violates the format or the requirements, or would exceed the 50 active arms an experiment may hold is skipped with an arm_invalid warning in the response rather than failing the call.
Every decision response names the arm served, arm_id, and whether it was the control or a variant, arm_type.
Where a declaration lives
Send it inline on everydecide, or move it to a setup step with declare and let the serving code carry only element and user_id. Both paths run the same validation and the same transaction. Whichever you choose, keep the declaration in one place: a declaration-class field that differs between two call sites supersedes the experiment on every alternation. See The experiment restarted.