---
title: "Record a new outcome measurement for an intent."
method: POST
path: "/api/v1/intents/{id}/outcomes"
tags: ["outcomes"]
---

# Record a new outcome measurement for an intent.

`POST /api/v1/intents/{id}/outcomes`

Accepts a pm_live_ Bearer key with write scope OR a session cookie. The two paths differ on purpose: a key authenticates through the API-key layer, reads with the admin client, and scopes the intent lookup to the key's workspace — RLS is bypassed there, so that filter is what prevents cross-workspace writes. A session keeps the original anon+RLS client and its owner|admin|editor role gate, and needs no workspace_id query param, so the in-app caller is unaffected. Body is zod-validated (lib/outcomeMeasurementRecord.ts). met is the SERVER's conclusion, not the caller's claim: when numericValue and measurement.expectation are both present the comparator is evaluated here, met_basis is 'computed', and a contradicting caller-supplied met is discarded rather than blended. Otherwise a caller-supplied met is stored with met_basis 'self_report', and with neither, met and met_basis are null. Prefer outcomeId: the outcome's text, index, and id are derived server-side from the intent row, so client input can never mislabel a measurement; the legacy outcomeIndex/outcomeText path remains for compatibility and still derives text/id when the index resolves. recorded_by is the authenticated user (the key's owner on the key path); source defaults to 'api' for key writes and 'manual' for session writes; evaluatedAt defaults to now when a measurement block omits it, since a measurement with no evaluation time cannot be placed against the change it measures. workspace_id derives from the intent row. No rate limiting on this endpoint.

## Path parameters

- `id` string, required

## Request body

- object
  - `outcomeId` string — Preferred: stable StructuredOutcome UUID of the outcome being measured. Must match an outcome on the intent (400 otherwise); outcome text and index are derived server-side from the intent row.
  - `outcomeIndex` number — Legacy positional path, used only when outcomeId is absent (one of the two is required; 0 is valid, must be a non-negative integer). When the index resolves against the intent's outcomes, text and id are derived server-side.
  - `outcomeText` string — Legacy outcome text snapshot. Only consulted when outcomeIndex does not resolve against the intent's current outcomes; required non-empty in that case.
  - `actualValue` string, required — Measured value. Required non-empty string; trimmed before insert.
  - `baselineValue` string — Baseline value before the change. Stored null if falsy.
  - `unit` string — Unit of measure. Stored null if falsy.
  - `numericValue` number — The measured value as a finite number, alongside the displayable actualValue. Required for the server to evaluate measurement.expectation — without it a verdict can only be recorded as a self-report.
  - `measurement` object — Provenance for the number: { provider, queryRef: { kind, id }, queryVersion?, window: { kind: rolling|fixed|since_ship, duration?, start?, end? }, evaluatedAt?, expectation?: { operator: gt|gte|lt|lte|eq, target, unit? }, rawResponse?, recordedByTool? }. queryRef.id must be a STABLE id (for PostHog, a saved insight id), never a metric name: names are mutable, so a renamed query would orphan the record. A rolling window requires duration; a fixed window requires start and end. rawResponse must be JSON-serialisable and under 20000 characters.
  - `met` boolean — Whether the outcome target was met. Stored null if not provided (uses ?? null).
  - `source` string — Provenance label. Defaults to 'manual' when falsy.

## Response `201`

Measurement recorded; returns the created measurement

- object

## Other responses

- `400` — Validation failure — neither outcomeId nor outcomeIndex given; outcomeId not on this intent; malformed field (actualValue empty/over 500 chars, met non-boolean, etc.)
- `401` — No valid Supabase session cookie (Unauthorized)
- `403` — Session user is not an owner/admin/editor of the intent's workspace
- `404` — Intent not found
- `500` — Failed to record measurement / internal server error

---

[API](https://skmtc.dev/pathmode/apis/pathmode-api.md) · [All operations](https://skmtc.dev/pathmode/apis/pathmode-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/pathmode/pathmode-api/revisions/e419abb0a327/schema)
