# 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](quickstart.md) first if you have not: it explains
`CEDE_BASE_URL`, the `Authorization: Bearer` header and the job pattern. [A
technical price, from nothing](price.md) 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](feeds.md) 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.

<!-- cede:runnable -->

```bash
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](price.md) 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](feeds.md#the-measurement-variable-each-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](feeds.md) 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.value`
  must equal the lowest `payout_function.points[].level`. If they differ the
  price is refused rather than one of them being chosen.
* **The measurement and the feed.** `measurement.variable` must be one the
  named feed publishes, and `measurement.unit` must be the unit it publishes it
  in.
* **The trigger type and the model.** A `parametric_index` trigger 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_station` prices over rainfall or wind;
  `tropical_cyclone` prices 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](errors.md) 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](price.md) — the same trigger inside a full
  object, and every field of the price response.
* [The pinned data feeds](feeds.md) — every feed id, version, variable, unit
  and period of record, and what each one is and is not.
* [Backtest a structure](backtest.md) — the same trigger run year by year over
  the forty-year record.
* [Compose a model](model-builder.md) — 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.*
