Objectives #

An objectives file is the contract the gate enforces. It is a small YAML document, it lives in the repository next to the code it guards, and it is reviewed like code — because lowering a target is the easiest way to make a failing gate pass.

A complete file #

name: example-platform

objectives:
  - name: chat-availability
    kind: availability
    description: Chat calls answer. A safety refusal counts as an answer.
    target: 0.999
    period: 30d
    select:
      operation: chat

  - name: chat-latency
    kind: latency
    description: 99% of chat calls answer within five seconds.
    target: 0.99
    threshold_ms: 5000
    period: 30d
    select:
      operation: chat

  - name: monthly-spend
    kind: spend
    description: The platform costs no more than $80 a month.
    budget_usd: 80
    period: 30d
    rules:
      - {name: fast-burn, severity: page, long_window: 1d, short_window: 6h, burn_rate: 2}

examples/objectives.yaml is this file with the reasoning left in the comments. Each of the four shipped scenarios breaks exactly one of its objectives, which is the argument that they measure different things rather than three views of one signal.

The three kinds #

availability #

The fraction of in-scope calls that succeeded. target: 0.999 allows one call in a thousand to fail over the period.

A refusal counts as a success: the platform responded, and a policy declined the request. An objective that treats refusals as outages rewards a team for weakening its safety filter.

latency #

The fraction of in-scope calls faster than threshold_ms.

Phrased as "99% of calls under 5s" rather than "p99 under 5s" on purpose. The first has an error budget — the 1% that were not — and slots directly into the burn-rate machinery. The second needs a percentile estimator over a sliding window, which is a considerably worse thing to build a gate on: it is approximate, its error depends on the distribution, and two implementations of it disagree.

A failed call is not a slow call. is_bad counts a span only when it succeeded and exceeded the threshold. Counting failures as slow would mean an outage silently consumes the latency budget too, and the two objectives would stop being independent measurements of different things.

spend #

Dollars against budget_usd over period. Needs --pricebook.

The burn rate is spend measured against the window's pro-rata share of the period budget. Six hours of a 30-day $80 budget is $0.667, so spending $2 in those six hours is 3× — the same number, on the same scale, as three times the acceptable rate of failed calls. That is what lets one engine serve all three kinds (ADR-004).

A spend objective is the only one that sees a prompt change which triples output length: no errors, no latency signal, and an invoice that doubles.

Spend has a genuine limitation under diurnal traffic, and the shipped spend objective overrides the default rules because of it. The effect is measured and explained in burn rate.

Keys #

Key Applies to Notes
name all Required. Findings are reported by it
kind all availability, latency or spend
description all Shown in the Markdown summary. Worth writing
target availability, latency 0 < target < 1, exclusive
threshold_ms latency Required and positive
budget_usd spend Required and positive
period all 30d, 7d, 24h. Defaults to 30d
select all Exact-equality matches; omitted means all traffic
rules all Overrides the standard table
min_samples all Overrides the 30-sample floor

Unknown keys are refused, not ignored. A misspelt threshold_ms would otherwise leave the threshold at zero, which makes every call slow and every window an outage — a gate that fails loudly for a reason nobody can find.

target: 1.0 is refused with an explanation. A 100% objective has a zero error budget: every burn rate is a division by zero and the only honest report is that one failure consumed everything. Saying so at parse time is kinder than saying it during an incident. Use 0.9999.

Selectors #

select:
  operation: chat
  environment: prod

Every key must match, exactly. provider, model, operation and outcome are read from the span itself; anything else is looked up in the attribute bag.

There is no pattern syntax — no globs, no regular expressions, no negation. That is deliberate. A selector is the thing that decides which traffic an objective is about, and a selector nobody can evaluate by reading it is a selector that silently covers the wrong calls. Exact equality is checkable at a glance; if you need two shapes of traffic, write two objectives.

The report states each objective's scope in words — operation=chat, or all traffic — so a reader can see what was measured without re-deriving it.

Rules #

Omit rules and the objective gets the standard four-rule table. Supply them and you replace it entirely:

rules:
  - name: fast-burn
    severity: page          # page | ticket
    long_window: 6h
    short_window: 30m       # optional; defaults to long_window / 12
    burn_rate: 14.4
    min_samples: 30         # optional

Both windows must exceed burn_rate for the rule to fire. Overriding is the right move in exactly two situations, both shown in examples/objectives.yaml with the reasoning attached: a spend objective under diurnal traffic, and a low-volume service that cannot be watched as closely.

Reading the verdict #

BREACHED  3/4 objective(s) held
  PAGE  chat-availability: fast-burn: FIRING — burn 229.17x over 1h and 400.00x over 5m, threshold 14.4x
  note  chat-availability: slow-burn not covered — the rule looks back 1d and only 22.6667h of history precedes 2026-09-01T22:40:00+00:00

A note is not a pass. "We did not look" and "we looked and it was fine" are different facts, and every renderer keeps them in separate sections: JSON, JUnit (a skipped case, not a passing one) and Markdown alike.

The objectives digest #

Every report carries a content address over the objectives that produced it:

"objectives_digest": "sha256:..."

It covers each objective's name, kind, target, period, threshold, budget, selector and rules — that is, everything that changes a verdict, and nothing that does not. A report and the file that produced it can be matched up months later without trusting a commit message.

Limits #

Limit Value
File size 1 MB
Objectives per file 64

Parsing is yaml.safe_load. Nothing in an objectives file is evaluated, imported or executed.

See also #