Objectives
An Objective is a Service Level Objective: a target evaluated against an Indicator. It pairs a plain-English promise to your users (the description) with a machine-checkable target (the definition), and Firetiger evaluates it continuously, opening Investigations when the target is at risk.
Service: firetiger.observability.v1.ObjectivesService
Resource name pattern: objectives/{objective}
Access: Read-write
Methods
| RPC | HTTP |
|---|---|
CreateObjective |
POST /v2/objectives |
GetObjective |
GET /v2/{name=objectives/*} |
EvaluateObjective |
GET /v2/{name=objectives/*}:evaluate |
ListObjectives |
GET /v2/objectives |
UpdateObjective |
PATCH /v2/{objective.name=objectives/*} |
DeleteObjective |
DELETE /v2/{name=objectives/*} |
ActivateObjective |
POST /v2/{name=objectives/*}:activate |
DeactivateObjective |
POST /v2/{name=objectives/*}:deactivate |
ReactivateObjective |
POST /v2/{name=objectives/*}:reactivate |
AcceptObjective |
POST /v2/{name=objectives/*}:accept |
ArchiveObjective |
POST /v2/{name=objectives/*}:archive |
RestoreObjective |
POST /v2/{name=objectives/*}:restore |
UndeleteObjective |
POST /v2/{name=objectives/*}:undelete |
RecreateObjectiveExpertSession |
POST /v2/{name=objectives/*}:recreateExpertSession |
CreateRecommendedObjectiveChange |
POST /v2/{parent=objectives/*}/recommendedObjectiveChanges |
ListRecommendedObjectiveChanges |
GET /v2/{parent=objectives/*}/recommendedObjectiveChanges |
GetRecommendedObjectiveChange |
GET /v2/{name=objectives/*/recommendedObjectiveChanges/*} |
AcceptRecommendedObjectiveChange |
POST /v2/{name=objectives/*/recommendedObjectiveChanges/*}:accept |
RejectRecommendedObjectiveChange |
POST /v2/{name=objectives/*/recommendedObjectiveChanges/*}:reject |
Request/response shapes are in proto/firetiger/observability/v1/objectives.proto.
Objective resource
| Field | Type | Behavior | Description |
|---|---|---|---|
name |
string | Output only | Format: objectives/{objective} |
display_name |
string | Required | Short label shown in lists and headers |
description |
string | Required | One-sentence, plain-English promise to the user (avoid SRE jargon) |
indicator |
string | Required | The Indicator this Objective is computed against. Format: indicators-v2/{indicator} |
definition |
oneof gauge | ratio |
Conditionally required | Kind-specific SLO body; at most one variant, and it must match the Indicator’s kind. The target (GAUGE threshold, RATIO target_failure_rate) lives inside the variant. Optional while the Objective is RECOMMENDED or CALIBRATING (agent recommendations are concept-only and the working target is tuned during calibration); required to enter ACTIVE — activation is where the target is committed |
filter |
DimensionFilter[] | Optional | Narrows included rows to a subset of the Indicator’s declared dimension values. Filters do not define whether a dimension matters |
owner_resource |
string | Optional | Catalog resource this Objective belongs to (MVP: services/{service}) |
state |
ObjectiveState | Output only | Lifecycle state. CALIBRATING on create (unless calibration_config.skip); activated via ActivateObjective; paused/resumed via DeactivateObjective and ReactivateObjective. Agent-recommended Objectives persist in RECOMMENDED until accepted. See ObjectiveState |
calibration_config |
CalibrationConfig | Optional | Set skip = true to start ACTIVE with no calibration phase. A skipping create must carry a definition (rejected INVALID_ARGUMENT otherwise), and accepting a skip-flagged recommendation that has no definition lands CALIBRATING instead of ACTIVE |
calibration_observations |
CalibrationObservations | Output only | Rolling daily baseline snapshots (≤7, FIFO) gathered while CALIBRATING; frozen on activation |
trigger |
ObjectiveTrigger | Optional | What opens an Investigation against this Objective. A default RateMultiplierTrigger is applied on create when unset |
latest_evaluation |
LatestObjectiveEvaluation | Output only | Latest persisted health snapshot written by the periodic evaluator. Includes bounded per-Cell health rows, aggregate health counts, evaluation time, and any google.rpc.Status execution error |
trigger_state |
ObjectiveTriggerState | Output only | Bounded server-owned bookkeeping for the current unhealthy episode observed by trigger. Present only while the Objective is unresolved; cleared when all observed Cells are healthy. See ObjectiveTriggerState |
expert_session |
string | Output only | The per-Objective objective-expert agent session that owns this Objective’s Indicator lifecycle (review, re-validation, repair). Format: agents/{agent}/sessions/{session} |
indicator_unit |
string | Output only | Display unit of the backing Indicator (e.g. s, ms, %), resolved from the referenced Indicator on read by GetObjective/ListObjectives so threshold/target numbers render with their unit. Computed on read, never persisted; empty when the Indicator can’t be read |
recommendation_confidence |
ObjectiveRecommendationConfidence | Optional | Agent’s confidence in this recommendation. See ObjectiveRecommendationConfidence |
recommendation_evidence |
string[] | Optional | Supporting evidence the agent cited when recommending this Objective |
recommendation_reasoning |
string | Optional | Free-text rationale for why this Objective was recommended |
etag |
string | Output only | Optimistic-concurrency token; required on Update, stale etags fail with ABORTED |
create_time / update_time / delete_time |
timestamp | Output only | Standard AIP lifecycle timestamps |
UpdateObjective is field-masked: send the Objective with an update_mask of the paths to write. Output-only fields (state, calibration_observations, latest_evaluation, trigger_state, expert_session, indicator_unit, etag, timestamps) are server-managed; do not set them on Create/Update.
Calibration-phase revisions are agent-internal tooling, not a separate API: the per-Objective expert agent revises the working definition/details through a calibration-gated masked UpdateObjective (its tooling refuses any Objective that is not CALIBRATING, restricts the mask to {display_name, description, gauge, ratio, filter}, and echoes the read etag so a concurrent activation fails the write with ABORTED). Once ACTIVE, agent changes go through RecommendedObjectiveChange.
ObjectiveState
| Value | Description |
|---|---|
OBJECTIVE_STATE_UNSPECIFIED |
Default zero value; not a valid persisted state |
OBJECTIVE_STATE_CALIBRATING |
Set on create (unless calibration_config.skip); gathers baseline samples but does not alert. The working definition and details are revisable in place by the per-Objective expert agent while in this state. Transitions to ACTIVE only via ActivateObjective or an accepted KIND_ACTIVATE recommended change |
OBJECTIVE_STATE_ACTIVE |
Evaluating and alerting |
OBJECTIVE_STATE_INACTIVE |
Customer-paused via DeactivateObjective; not evaluating. Resumed with ReactivateObjective |
OBJECTIVE_STATE_RECOMMENDED |
Persisted agent recommendation. Visible in the Service UI but does not calibrate, evaluate, alert, or spin up an objective-expert session until accepted |
OBJECTIVE_STATE_ARCHIVED |
Dismissed recommendation. Hidden from active views and skipped by AcceptService bulk-activation |
Customer lifecycle
Customer-owned Objectives move through the normal operational lifecycle after creation:
ActivateObjectivetransitions aCALIBRATINGObjective toACTIVE, optionally applying athreshold_overridein the same transaction. It is idempotent if the Objective is alreadyACTIVE.DeactivateObjectivepauses anACTIVEObjective by moving it toINACTIVE. Paused Objectives are not evaluated and do not open Investigations. The call is idempotent for an already-INACTIVEObjective and rejectsCALIBRATING,RECOMMENDED, andARCHIVEDObjectives withFAILED_PRECONDITION.ReactivateObjectiveresumes anINACTIVEObjective by moving it back toACTIVE. The call is idempotent for an already-ACTIVEObjective and rejectsCALIBRATING,RECOMMENDED, andARCHIVEDObjectives withFAILED_PRECONDITION.DeleteObjectivesoft-deletes the Objective by settingdelete_time.UndeleteObjectiveclearsdelete_timefor non-ARCHIVEDObjectives, backing undo flows after user-initiated archive/delete actions.
ObjectiveRecommendationConfidence
| Value | Description |
|---|---|
OBJECTIVE_RECOMMENDATION_CONFIDENCE_UNSPECIFIED |
Confidence not set |
OBJECTIVE_RECOMMENDATION_CONFIDENCE_HIGH |
High confidence |
OBJECTIVE_RECOMMENDATION_CONFIDENCE_MEDIUM |
Medium confidence |
OBJECTIVE_RECOMMENDATION_CONFIDENCE_LOW |
Low confidence |
Recommendation lifecycle
Beyond the customer-authored Create → calibrate → activate path, Objectives can be proposed by a Service’s expert agent. A recommended Objective is persisted as a real objectives/{objective} resource in OBJECTIVE_STATE_RECOMMENDED: it shows up in the Service UI alongside active Objectives but does not calibrate, evaluate, alert, or create an objective-expert session.
AcceptObjectivemoves aRECOMMENDEDObjective into the normal lifecycle (enteringCALIBRATING, orACTIVEif calibration is skipped), spinning up its expert session.ArchiveObjectivedismisses a recommendation intoOBJECTIVE_STATE_ARCHIVED. Archived Objectives are hidden from active views and skipped byAcceptServicebulk-activation.RestoreObjectivebrings an archived Objective back toRECOMMENDED.
Recommendation metadata (recommendation_confidence, recommendation_evidence, recommendation_reasoning) is carried on the Objective resource itself.
Recommending changes to existing Objectives
A RecommendedObjectiveChange is a child of the Objective it advises (objectives/{objective}/recommendedObjectiveChanges/{recommended_objective_change}) and proposes an edit, removal, or activation of that live Objective, distinct from a brand-new-Objective recommendation (which is a RECOMMENDED-state Objective). The agent only ever creates one; it never mutates an ACTIVE Objective directly. The human accepts or rejects it. At most one change is pending per Objective: creating a new one supersedes (resolves) any still-pending proposal on the same target.
| Field | Type | Behavior | Description |
|---|---|---|---|
name |
string | Output only | Format: objectives/{objective}/recommendedObjectiveChanges/{recommended_objective_change} |
kind |
Kind | Required | KIND_UPDATE (edit), KIND_DELETE (removal), or KIND_ACTIVATE (lock in the calibrated target and activate) |
proposed |
Objective | Optional | UPDATE/ACTIVATE. Sparse Objective holding just the changed fields; applied under objective_update_mask. Not validated at propose time, the merged result is validated on accept |
objective_update_mask |
FieldMask | Optional | UPDATE/ACTIVATE. Paths to apply from proposed; for UPDATE restricted to display_name, description, gauge, ratio, filter (the SLO body is masked by its oneof variant gauge/ratio, not definition); for ACTIVATE restricted to at most one of gauge/ratio (the settled target), or empty when the working definition was already tuned in during calibration |
proposed_indicator |
Indicator | Optional | UPDATE only. Sparse query/dimension rewrite of the target Objective’s backing Indicator, applied under indicator_update_mask |
indicator_update_mask |
FieldMask | Optional | UPDATE only. Paths to apply from proposed_indicator |
reasoning / confidence |
— | Optional | Why the change is proposed and the agent’s confidence in it. reasoning is review-card copy, capped at 1200 characters: a longer value is rejected with INVALID_ARGUMENT at create time |
target_etag |
string | Output only | The target Objective’s etag captured at propose time. Advisory: the UI warns when it differs from the live Objective. Accept is last-write-wins, not gated on it |
source |
string | Optional | The agent session that authored the proposal (agents/{agent}/sessions/{session}). The agent tooling fills this from the running session, like Issue.source; a direct caller may set it or leave it blank |
diff |
FieldDiff[] | Output only | Server-rendered {field_path, before, after} for each masked path (objective fields, plus backing-indicator fields prefixed indicator.), computed at create time. Clients render these directly rather than reconstructing the diff from proposed + masks, so no change is hidden before accept. Empty for a DELETE |
create_time / update_time / delete_time |
timestamp | Output only | delete_time is stamped when the change is accepted or rejected (soft-delete as history) |
AcceptRecommendedObjectiveChangeapplies the change in one transaction: UPDATE →UpdateObjective+UpdateIndicator(the rewrite is always bound to the target Objective’s own backing Indicator, never an arbitrary one a proposal might name); DELETE →DeleteObjective, also resolving the Objective’s other pending changes so none dangle; ACTIVATE → applies the proposedgauge/ratio(if the card carries one) and transitionsCALIBRATING→ACTIVEatomically, with the same semantics asActivateObjective(idempotent if a manual activation raced the card). The accepted change is soft-deleted as history.RejectRecommendedObjectiveChangesoft-deletes the change without touching the Objective.UndeleteObjectiveclearsdelete_timeon a soft-deleted Objective, backing the undo after an accepted removal. Restricted to non-ARCHIVEDobjectives: a service-archived Objective is also soft-deleted but is restored with its Service (it carries thedelete_timemarker that service-restore keys on), so calling this on anARCHIVEDObjective returnsFAILED_PRECONDITION.
Errors / edge cases:
- Accept or reject of a change that is already resolved (its
delete_timeis set) returnsFAILED_PRECONDITION. - Creating a change supersedes any still-pending change on the same Objective (the superseded card is soft-deleted as resolved history), so at most one card is pending per Objective.
KIND_UPDATEandKIND_DELETErequire a live target (ACTIVE,INACTIVE, orCALIBRATING); aRECOMMENDEDorARCHIVEDtarget is rejected withFAILED_PRECONDITION— corrections to a pending recommendation are replacement proposals (create the corrected recommendation, archive the stale one), not edits layered on an unaccepted card.- An ACTIVATE may not carry indicator changes, and its
objective_update_maskadmits at most one ofgauge/ratio, whose variant must be present inproposed. An ACTIVATE with an empty mask against an Objective that has no definition is rejected withINVALID_ARGUMENTat create time — there would be no target to activate against. - An ACTIVATE may only be proposed against a
CALIBRATINGObjective; any other target state is rejected withFAILED_PRECONDITIONat create time (target edits on anACTIVEObjective useKIND_UPDATE). If the Objective was activated manually while the card was pending, accepting the card is idempotent: it resolves without re-applying the proposed target. - Accept of an UPDATE is last-write-wins: it applies the masked patch to the current live Objective/Indicator.
target_etagis advisory (the UI warns on drift) and is not a precondition, so an accept against a since-changed Objective succeeds and only overwrites the masked paths. objective_update_maskis restricted to{display_name, description, gauge, ratio, filter}andindicator_update_maskto{query[.confit_sql/.connections/.description], display_name, description, unit, dimensions}; any other path is rejected withINVALID_ARGUMENTat create time. The indicator rewrite is always bound to the target Objective’s own backing Indicator regardless of any name on the proposed patch.reasoninglonger than 1200 characters is rejected withINVALID_ARGUMENTat create time — it is the human-scanned review-card rationale, not a place for full evidence dumps.- Accept of an UPDATE re-runs the standard Objective (definition-vs-Indicator) and Indicator (ConfitSQL/placeholder/dimension) validation against the merged result; a now-invalid merge fails with
INVALID_ARGUMENTand the Objective is left unchanged. - Accept of a DELETE soft-deletes the Objective and resolves its other pending changes;
UndeleteObjectiverestores it.
How evaluation works
Firetiger re-checks every active Objective on a short cycle (about every 5 minutes). It does not wait for a long window to “fill up” — each check looks back over two rolling windows that both end at now:
- Short window (default 5 minutes) — “is it bad right now?”
- Long window (default 1 hour) — “is it bad looking back over the longer period?”
A Cell is Unhealthy only when both windows are over the line at the same time. Requiring both is deliberate: the short window catches a problem quickly, and the long window confirms it is real and not a one-off blip. (A window with too little traffic or no data is reported LOW_VOLUME / NO_DATA instead of being judged.)
“Over the line” means the Objective’s target adjusted by the trigger’s multiplier (1.0 = fire right at the target). For dimensional Objectives, every Cell is judged on its own and the Objective is Unhealthy if any Cell is.
How fast it reacts depends on what you measure:
- Latency-style Objectives (a GAUGE, e.g. p99 latency) react quickly. If responses are slow right now, that slowness shows up in both the 5-minute and the 1-hour view at the same time, so it is flagged on the next check. The long window mainly makes the breach “stick” a little longer.
- Error-rate Objectives (a RATIO) react more slowly, on purpose. The 1-hour view averages all traffic together, so a brief blip gets diluted and ignored. The Objective only turns Unhealthy once errors keep happening hard or long enough to move the whole hour’s average past the target — a genuinely sustained problem.
Once a Cell is Unhealthy, Firetiger opens at most one Investigation per cooldown window (default 1 hour), so a sustained problem does not re-page every cycle.
Trigger settings (RateMultiplierTrigger)
Every Objective carries a trigger.rate_multiplier; a default is applied on create, and each field can be tuned per Objective.
| Field | Default | Meaning |
|---|---|---|
multiplier |
1.0 (GAUGE), 14.4 (RATIO) |
How far past the target counts as a breach. 1.0 fires right at the target; a higher value adds margin so only a clear breach triggers |
long_window |
1h |
The longer rolling window — confirms the problem is sustained |
short_window |
5m |
The recent rolling window — catches the problem quickly and bounds how soon a recovered Cell clears |
cooldown |
1h |
Minimum gap between automated Investigations for the same ongoing episode |
min_volume |
per-kind default | Minimum events in the short window before a Cell is evaluated. See Minimum-volume guard |
Dimensional evaluation
Objective dimensionality comes from the backing Indicator:
- If
Indicator.dimensionsis empty, the Objective is scalar. - If
Indicator.dimensionscontains ServiceDimensions, Firetiger evaluates the Objective independently for each observed Cell. A Cell is the set of observed ServiceDimension values for that row, such asregion=us-west-2andtenant=acme-corp. Objective.filter[]narrows which rows are included before evaluation; it does not change which bound dimensions define the Cells.
Unhealthy dimensional Cells do not create one Investigation per Cell. Firetiger creates at most one Objective-level Investigation per cooldown window and includes the unhealthy Cells as evidence.
EvaluateObjective returns the current health on demand. The response contains health_status, summary, and cell_evaluations; scalar Objectives return one Cell with empty dimensions, while dimensional Objectives return one row per observed Cell with dimension values, health status, long/short observed values, target, row count, and ratio total events when applicable.
Minimum-volume guard
A Cell whose evaluation window holds too few events for its percentile or ratio to be statistically trustworthy is reported OBJECTIVE_HEALTH_STATUS_LOW_VOLUME instead of being evaluated, so a single outlier in a low-traffic Cell cannot trip a breach. Like NO_DATA, a LOW_VOLUME Cell never opens an Investigation and never clears the trigger episode; it is counted separately from no_data in summary.
The floor is trigger.rate_multiplier.min_volume, measured over the short window. When unset (0) the server applies a conservative per-kind default: a small event count for RATIO (from the Indicator’s total_events), and a small number of populated data points for GAUGE. Set min_volume explicitly to raise or lower the floor for a given Objective.
The periodic evaluator writes the same bounded health shape to Objective.latest_evaluation so readers can render current health without running a fresh Indicator query:
| Field | Type | Description |
|---|---|---|
health_status |
ObjectiveHealthStatus | Overall Objective health: OBJECTIVE_HEALTH_STATUS_HEALTHY, OBJECTIVE_HEALTH_STATUS_UNHEALTHY, OBJECTIVE_HEALTH_STATUS_LOW_VOLUME, or OBJECTIVE_HEALTH_STATUS_NO_DATA |
summary |
ObjectiveHealthSummary | Aggregate counts: unhealthy, healthy, low_volume, and no_data |
evaluation_time |
timestamp | Time the evaluator produced this snapshot |
cell_evaluations |
ObjectiveCellEvaluation[] | Bounded, priority-sorted per-Cell rows. Truncated snapshots set cell_evaluations_truncated = true |
total_cells |
int32 | Full number of Cells evaluated before truncation |
cell_evaluations_truncated |
bool | True when the server kept only the highest-priority Cell rows |
execution_status |
google.rpc.Status | Set when evaluation failed; nil or OK means the snapshot ran successfully |
ObjectiveTriggerState
trigger_state is not an alert history or a copy of Objective evaluations. It is bounded coordination state for the current unhealthy episode so the evaluator can dedupe automated Investigation check-ins without storing every evaluation row in Postgres. NO_DATA and LOW_VOLUME leave this state intact because an inconclusive evaluation does not prove recovery; only a fully healthy observed evaluation (every Cell HEALTHY) clears it.
| Field | Type | Description |
|---|---|---|
unhealthy_scope_fingerprint |
string | Stable hash of the Objective name plus the current set of unhealthy Cell identities. Metric values, severity, and timestamps are excluded so small fluctuations do not create new episodes |
first_unhealthy_time |
timestamp | First time this continuous unhealthy episode was observed |
last_investigation_time |
timestamp | Last automated Investigation check-in started for this episode |
last_investigation_session |
string | Last automated Investigation session created for this episode. Format: agents/{agent}/sessions/{session} |
investigation_count |
int32 | Number of automated Investigation check-ins started for the current unhealthy scope fingerprint. Used only to derive backoff |
next_investigation_time |
timestamp | Earliest time the evaluator may start another automated Investigation check-in for this episode |
ObjectiveHealthStatus
| Value | Description |
|---|---|
OBJECTIVE_HEALTH_STATUS_UNSPECIFIED |
Default zero value; not a persisted health state |
OBJECTIVE_HEALTH_STATUS_HEALTHY |
No evaluated Cell is unhealthy and at least one Cell has data |
OBJECTIVE_HEALTH_STATUS_UNHEALTHY |
At least one Cell crossed the Objective target in both long and short windows |
OBJECTIVE_HEALTH_STATUS_NO_DATA |
No usable evaluation data, or evaluation could not produce a health result |
OBJECTIVE_HEALTH_STATUS_LOW_VOLUME |
A Cell had data but too few samples in the evaluation window for its percentile or ratio to be trustworthy, so it is reported rather than evaluated. Inconclusive like NO_DATA — it never opens an Investigation and never clears trigger_state — but distinct so clients can surface “low volume” rather than “no data”. See Minimum-volume guard |
Calibration and activation
New Objectives start in CALIBRATING: the evaluator gathers up to seven daily baseline snapshots, but no Investigations are triggered yet. While calibrating, the working definition and details are revisable in place — agent-recommended Objectives arrive concept-only (no definition), and the per-Objective expert agent tunes the working target against the accruing observations through its calibration-gated tooling.
Activation (CALIBRATING → ACTIVE) is human-driven, by either path:
ActivateObjective— direct, optionally applying athreshold_overrideatomically. The override is required when the Objective has no definition yet; activation is where a target is first committed, so a target-less activate returnsINVALID_ARGUMENT.- An accepted
KIND_ACTIVATErecommended change — the expert-authored path: the card carries the settledgauge/ratioplus the reasoning grounded in the observed distribution, and accepting it applies the target and activates in one transaction.
Pass calibration_config.skip = true on create (with a definition) to start ACTIVE immediately.