This page as Markdown, byte for byte: /trigger.md
The trigger object#
A parametric structure pays on a measurement, and trigger is the block where that measurement is written down: which pinned feed is read, what is measured in it, over what window, and what fraction of the limit each level pays. It is the one block nothing else can supply — a schedule of buildings does not carry it, and Craton will not invent it — so POST /objects/{id}/price refuses an object without one.
This page is the field-by-field reference for that block and the smallest trigger of each shape that actually prices. Both are in a single example that is extracted from this page and executed against a live environment on every change to Craton, so the minimum documented here is the minimum the product enforces.
Read the quickstart first if you have not: it explains CEDE_BASE_URL, the Authorization: Bearer header and the job pattern. A technical price, from nothing is the fuller walkthrough — a complete object, a price, and every assumption behind the number. This page is narrower on purpose: the trigger, and nothing else.
The refusal that sends people here#
Ingest a location schedule, write a peril and a limit onto the object it produced, ask for a price, and the job ends failed:
422 unpriceable_object
trigger is required to price this structure and is absent
That is not a defect in your object. A location schedule carries buildings, coordinates and values; it does not carry the cover written over them, and Ingest normalises what a file says rather than deciding what it does not say. The peril, the limit, the attachment and the trigger are decisions you make after reading the file, and PATCH /objects/{id} is where they go — on the object the file became, keeping its exposure and its provenance.
What Price reads off an object#
| Block | What Price does with it |
|---|---|
trigger |
The whole subject of this page: the index, its feed, its window and its payout ladder. |
financial_structure.limit |
The maximum payout. Every number in the price is this multiplied by a ratio, so it must be positive and currency-tagged. |
financial_structure.attachment |
Where the structure attaches, as an index level for a parametric structure. It must agree with the first level of the payout ladder — see below. |
peril.code |
Which model is asked, and — with the feed — whether that model can measure this peril at all. |
peril.region.bounding_geometry |
Cat-in-a-box only: the region the catalogue is read inside. A single-ring, axis-aligned GeoJSON Polygon. |
period.inception, period.expiry |
The length of the cover period, which scales the expected loss. |
exposure is not read by either pricing model, and that is a property of parametric cover rather than an omission: the payout is the ladder times the limit, not a function of the insured values. The published schema still requires an exposure block on every risk object, so the example below carries a minimal one.
Two shapes, two models#
trigger.type chooses the shape. Each shape is priced by its own model — they read differently shaped bytes, and a model that quietly grew a second scope is a model whose refusals stop meaning anything.
trigger.type |
What it measures | peril.code it prices |
data_sources[0].kind |
Model |
|---|---|---|---|---|
parametric_cat_in_a_box |
The largest catalogue magnitude inside a region, per occurrence | earthquake |
not read (the feed's own kind must be quake_catalogue) |
cede/parametric-burn-eq-box |
parametric_index |
A station-measured daily series aggregated over a rolling window | weather_station, and tropical_cyclone over a wind feed |
station_network, required |
cede/parametric-burn-station-index |
wording |
Nothing — the indemnity-style branch, held and versioned but not priced by either model | — | — | none |
Which peril a parametric_index structure can price depends on which feed the trigger names, because the feed is what decides whether a level says anything about that peril: a daily wind maximum is a serviceable named-storm index, a daily rainfall total is not, and the refusal names the feed rather than your peril. The pinned data feeds is the list, and GET /primitives is the same list from the deployment you are pointed at.
The smallest trigger that prices#
Two objects, one of each parametric shape. Every member below is either required by the published schema or required by the model that prices it; nothing here is decoration, and taking any of it out produces a named refusal rather than a number.
Run this in an empty directory.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see the quickstart}"
# Read one field out of a JSON document on stdin: `field a.b.0.c`.
field() {
python3 -c 'import json, sys
document = json.load(sys.stdin)
for step in sys.argv[1].split("."):
document = document[int(step)] if step.isdigit() else document[step]
print(document)' "$1"
}
# Post an object, price it, wait for the job, print the number and the model.
price_it() {
local document="$1" object_id job_id state=unknown
object_id=$(curl -sS --fail-with-body -H "$auth" \
-H "Content-Type: application/json" \
--data-binary @"$document" "$CEDE_BASE_URL/objects" | field id)
job_id=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/objects/$object_id/price" | field id)
for _ in $(seq 1 300); do
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$job_id" > job.json
state=$(field status < job.json)
case "$state" in succeeded|failed) break ;; esac
sleep 0.2
done
if [ "$state" != succeeded ]; then
printf 'the price job did not succeed: %s\n' "$(cat job.json)" >&2
return 1
fi
python3 -c '
import json, sys
price = json.load(open("job.json"))["result"]["price"]
premium, burn = price["technical_price"], price["assumptions"]["burn"]
print(sys.argv[1] + ":", premium["amount"], premium["currency"],
"- rate on line", premium["rate_on_line"])
print(" priced by", price["model"]["name"], price["model"]["version"],
"-", burn["triggering_years"], "of", burn["years"],
"years of the record would have paid")
' "$1"
}
# 1 — An account of this block's own. No credential needed to ask for one.
CEDE_API_KEY=$(curl -sS --fail-with-body \
-H "Content-Type: application/json" \
-d '{"label": "trigger reference"}' \
"$CEDE_BASE_URL/signup" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'your key (shown once): %s\n' "$CEDE_API_KEY"
# 2 — Shape one: cat-in-a-box. The index reads the catalogue's preferred
# magnitude inside the region, clusters events 72 hours apart into one
# occurrence, and pays the whole limit at M6.0 and above. The data source
# needs an id and a version and nothing else; the feed's own manifest is
# what says it is a quake catalogue.
cat > cat-in-a-box.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "index",
"source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
},
"peril": {
"code": "earthquake",
"region": {
"bounding_geometry": {
"type": "Polygon",
"coordinates": [[[139.0, 34.9], [140.6, 34.9], [140.6, 36.2],
[139.0, 36.2], [139.0, 34.9]]]
}
}
},
"financial_structure": {
"limit": { "amount": 1000000000, "currency": "JPY" },
"attachment": { "value": 6.0, "unit": "M" }
},
"trigger": {
"type": "parametric_cat_in_a_box",
"index": {
"name": "kanto-eq-box-magnitude",
"version": "1.0.0",
"measurement": { "variable": "catalogue_preferred_magnitude", "unit": "M" },
"aggregation_window": { "duration": "PT72H" },
"thresholds": [ { "label": "attachment", "level": 6.0, "unit": "M" } ],
"payout_function": {
"type": "step",
"points": [ { "level": 6.0, "payout_ratio": 1 } ]
}
},
"data_sources": [ { "id": "usgs-eq-kanto", "version": "2026-08-10" } ]
},
"period": {
"inception": "2026-04-01T00:00:00+09:00",
"expiry": "2027-04-01T00:00:00+09:00",
"timezone": "Asia/Tokyo"
}
}
JSON
price_it cat-in-a-box.json
# 3 — Shape two: a station-measured index. Three-day rainfall totals, paying
# the whole limit at 120 mm. Two members this shape needs that the other
# does not: measurement.statistic, which names the transform, and
# data_sources[0].kind, which decides the reader before a byte is parsed.
cat > station-index.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "index",
"source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
},
"peril": { "code": "weather_station" },
"financial_structure": {
"limit": { "amount": 2000000, "currency": "USD" },
"attachment": { "value": 120, "unit": "mm" }
},
"trigger": {
"type": "parametric_index",
"index": {
"name": "bangkok-3-day-rainfall",
"version": "1.0.0",
"measurement": {
"variable": "daily_precipitation_mm", "unit": "mm", "statistic": "sum"
},
"aggregation_window": { "duration": "P3D" },
"thresholds": [ { "label": "attachment", "level": 120, "unit": "mm" } ],
"payout_function": {
"type": "step",
"points": [ { "level": 120, "payout_ratio": 1 } ]
}
},
"data_sources": [
{ "id": "era5-rain-bangkok", "kind": "station_network", "version": "2026-08-11" }
]
},
"period": {
"inception": "2026-06-01T00:00:00+07:00",
"expiry": "2027-06-01T00:00:00+07:00",
"timezone": "Asia/Bangkok"
}
}
JSON
price_it station-index.json
echo "done — two shapes, two models, two technical prices"
Both prices came back with their complete assumption set attached; a technical price, from nothing reads that response field by field. What the two objects above show is only where the floor is.
Field by field#
trigger#
| Member | Required | What it is |
|---|---|---|
type |
always | parametric_index, parametric_cat_in_a_box or wording. |
index |
on both parametric shapes | The index definition. Below. |
data_sources |
on both parametric shapes | The pinned feeds the index reads. Exactly one, for both pricing models. |
wording_ref |
on the wording shape only |
The indemnity-style branch. Exactly one of index+data_sources and wording_ref may be present; the schema refuses an object carrying both. |
trigger.index#
| Member | Required | What it is |
|---|---|---|
name |
schema | The index's name. Travels onto the price response so a number can be traced to the index that produced it. |
version |
schema | The index version, likewise. An index is versioned code: change what it measures and you have a new version, not an edit. |
description |
optional | Free text for humans. |
measurement.variable |
schema, and checked by the model | The measured quantity, named exactly as the feed publishes it: catalogue_preferred_magnitude, daily_precipitation_mm, daily_maximum_wind_speed_mph. Neither the publisher's own column name nor the English in the feed description. What each pinned feed publishes is the per-feed list. |
measurement.unit |
schema, and checked by the station-index model | The unit the level is written in. It must be the unit the feed publishes that variable in; a disagreement is refused rather than converted. |
measurement.statistic |
required by the station-index model, unused by the cat-in-a-box model | sum (rolling sum), peak (rolling maximum) or count (threshold day count). It names the transform applied to the daily series. |
aggregation_window.duration |
schema, both models | ISO 8601. The cat-in-a-box model reads any duration and uses it as the occurrence-clustering window, measured from the first event of a cluster — PT72H is the usual one. The station-index model reads whole days only (P1D, P3D, P30D), because a daily series has no finer resolution and it will not pretend otherwise. |
aggregation_window.alignment |
optional | event, rolling or calendar_day. The station-index model measures rolling windows, so it refuses any value other than rolling; leave it out and you get that behaviour. |
thresholds[] |
schema (at least one) | label, level, unit. The labels the models act on are attachment, max_focal_depth (cat-in-a-box: events deeper than this are excluded, and no threshold means no depth filter) and day_threshold (station index, count statistic only: the level a day has to reach to be counted). Any other label is documentation. |
payout_function.type |
schema | binary, linear, piecewise_linear or step in the schema; **both pricing models evaluate step only** and refuse the others by name. |
payout_function.points[] |
required by both models | The ladder: level and payout_ratio (0 to 1) per rung, in any order — they are sorted by level. The lowest level is where the structure attaches. A single rung is a valid ladder. |
payout_function.maximum_payout_ratio |
optional | A ceiling on the ratio, defaulting to 1. |
trigger.data_sources[]#
| Member | Required | What it is |
|---|---|---|
id |
schema, both models | The feed id, exactly as the pinned data feeds and GET /primitives spell it. |
version |
schema, both models | The pinned version. A price replays byte for byte, so the version has to pre-exist in the build; nothing is fetched at price time. |
kind |
required by the station-index model (station_network), not read by the cat-in-a-box model |
The data-source vocabulary term. It decides which reader parses the publisher's bytes, so it is checked against the feed's own manifest kind before a byte is read. |
name, vintage |
optional | Human labels. The price response carries the publisher's own name, licence, vintage and the sha256 of the snapshot regardless of what you write here. |
Where two declarations have to agree#
Most refusals are not a missing member — they are two members of your object saying different things. The models check each of these before reading a byte, because a plausible number from a model that does not fit the structure is worse than no number at all: nothing downstream can tell the difference.
- The attachment and the ladder.
financial_structure.attachment.valuemust equal the lowestpayout_function.points[].level. If they differ the price is refused rather than one of them being chosen. - The measurement and the feed.
measurement.variablemust be one the named feed publishes, andmeasurement.unitmust be the unit it publishes it in. - The trigger type and the model. A
parametric_indextrigger is never read by the cat-in-a-box model, and the other way round. The two shapes look alike in JSON, which is exactly why each model re-checks what it was handed. - The peril and the feed.
weather_stationprices over rainfall or wind;tropical_cycloneprices over a wind feed and is refused over a rainfall one, because this build cannot attribute rain to a named storm. - One feed, not several. Both models read exactly one pinned data source. Combining feeds is a later model, not an averaging either of these will do silently.
Three near misses, and what the refusal says about them#
The tables above are the shape. These are the three ways a reader who wrote the block from memory rather than copying it gets a 422 invalid_risk_object from POST /objects or PATCH /objects/{id} — filed by the MGA analyst persona as CEDE-230, all three at once:
| What was written | Why it is refused | What to write |
|---|---|---|
data_sources inside trigger.index |
trigger.index takes the members in the table above and no others. The feeds are a member of trigger itself. |
Move the array out one level, beside index. |
aggregation_window_days: 3 |
There is no such member. The window is an object, so that it can carry its alignment too. | "aggregation_window": { "duration": "P3D" } — ISO 8601, and alignment if you need it. |
payout_function as an array of rungs |
The ladder is one member of the payout function, which also names the function's type. | "payout_function": { "type": "step", "points": [ … ] }. |
None of the three is invented by this page to be knocked down: each is what the refusal now tells you by itself. It names the block that would not take the member, where the schema keeps that member instead, the real name when yours was close to one, and the kind a member takes when it arrived as another — then ends by pointing back here. If you are reading a refusal that does none of that, it came from an older build.
The wording branch#
trigger.type may also be wording, for indemnity-style structures: a wording_ref pointing at versioned text with clause-level metadata, and no index and no data sources. Craton stores and versions wordings; it never executes them as contracts, and neither pricing model reads them — a wording trigger sent to POST /objects/{id}/price is refused by name, saying which trigger types the model does read.
When it does not work#
Every one of these arrives as a failed job carrying 422 unpriceable_object, with the message shown. The error reference is the full list of refusal shapes.
| The message says | What to change |
|---|---|
trigger is required to price this structure and is absent |
The object has no trigger block. PATCH /objects/{id} one on. |
index is required to price this structure and is absent |
The trigger has a type but no index. Both parametric shapes need one. |
reads … triggers only; this structure's trigger is … |
The trigger type is not one this model prices — a wording trigger, or an empty type. |
Price v0 evaluates step payout functions only |
payout_function.type is binary, linear or piecewise_linear. Write the same shape as a step ladder. |
a step payout function needs at least one point |
payout_function.points is absent or empty. The schema allows it; the models need a ladder to read. |
the v0 parametric model measures catalogue_preferred_magnitude; this index measures … |
The cat-in-a-box model was asked to read a variable it does not compute. Check the variable against the feed. |
index.measurement.statistic is … ; this model computes count, peak, sum |
The station-index model needs a statistic it has a transform for. |
trigger.data_sources[0] declares kind …; the station-index model reads station_network sources |
Add "kind": "station_network" to the data source. |
no pinned snapshot |
The id or the version is not one this build pins. The message lists every pinned id@version. |
financial_structure.attachment is … but the payout function first pays at … |
The two disagree about where the structure attaches. |
a cat-in-a-box structure is priced from peril.region.bounding_geometry, which this object does not carry |
Add the region polygon — single ring, axis-aligned, closed. |
no year of the pinned record would have paid |
The ladder sits above everything the feed recorded in forty years. The message names the highest level the record reached and when; attach at or below it, or backtest the structure to see the same forty years with no payout in them. |
Where to go next#
- A technical price, from nothing — the same trigger inside a full object, and every field of the price response.
- The pinned data feeds — every feed id, version, variable, unit and period of record, and what each one is and is not.
- Backtest a structure — the same trigger run year by year over the forty-year record.
- Compose a model — the registry's no-code route to the same primitives: a feed, a measurement, a transform, a payout function.
The example on this page is extracted verbatim and executed against a live environment on every change to Craton. If the minimum documented here stopped being the minimum, the build would stop too.