> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sempera.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Dispositions

> Values produced from every finished phone call, priced, published and tested, with versioned extractors.

A disposition is a set of values you want Sempera to produce from every finished phone call of an agent: "did the caller book", "which service", "the callback number". You define the values, price the definition, publish it, and test it against real calls on your own schedule. From then on every new completed phone call runs the published version, and the result arrives on the `call.outcomes` webhook event ([Webhooks](https://docs.sempera.org/webhooks.md)).

## Conventions

These are `/v2` compatibility routes ([API overview](https://docs.sempera.org/api-overview.md)) under `/v2/agents/{agent_id}/dispositions`, where `{agent_id}` is the Sempera agent id. An agent the credential cannot read answers `404` like one that never existed.

| Work | Scope | Role |
| - | - | - |
| Reading | `agents:read` | any |
| Creating and editing drafts, estimating, extractors, corrections, align | `agents:write` | `operator` or `owner` |
| Starting a test run | `agents:write` | `operator` or `owner` |
| Publishing, disabling, deleting; publishing an extractor | `agents:write` | `owner` |

Answers use the compatibility envelope `{"data": ..., "errors": null}`. A refusal these routes make is `{"data": null, "errors": [{"error", "message"}]}` with `error` one of `BAD_REQUEST` (400), `NOT_FOUND` (404), `CONFLICT` (409), `TOO_MANY_REQUESTS` (429) or `SERVICE_UNAVAILABLE` (503). A missing credential, scope or role gets the standard error envelope ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)). Every body field is checked; an unknown field is refused. `GET /v1/dispositions/` is a placeholder that always answers `403`; use the per-agent routes.

## Routes

| Route | Notes |
| - | - |
| `GET .../dispositions` | `{"dispositions": [...]}`, the live ones, oldest first |
| `POST .../dispositions` | `{"key", "definition"}`. Starts `disabled` with its draft at revision 1. `201` |
| `GET .../dispositions/{id}` | One disposition (a deleted one still reads) |
| `DELETE .../dispositions/{id}` | Retire it. Idempotent; reads still answer, writes do not |
| `GET .../dispositions/{id}/draft` | `{"dispositionId", "revision", "baseVersionId", "definition"}` |
| `PATCH .../dispositions/{id}/draft` | `{"expectedDraftRevision", "definition"}` replaces the draft whole. `409` when someone saved first |
| `POST .../dispositions/{id}/estimates` | Price the current draft per call. Stores nothing |
| `POST .../dispositions/{id}/publish` | `{"expectedDraftRevision", "estimateId", "acknowledgeEstimate"}`. Freezes the draft into a numbered version and enables it |
| `POST .../dispositions/{id}/disable` | Stop it. Draft, versions and runs stay. No body |
| `GET .../dispositions/{id}/versions` | `{"versions", "nextCursor"}`, newest first, 25 a page; pass `beforeVersion` to page back |
| `GET .../dispositions/{id}/versions/{version_id}` | One published version |
| `GET .../dispositions/variable-catalog` | The agent's variables and published Outcomes fields a value may read |

A disposition reads `id`, `agentId`, `key`, `state` (`enabled` or `disabled`), `publishedVersionId`, `publishedVersionNumber`, `draftRevision`, `draftValueCount`, `hasUnpublishedChanges`, `name`, `description`, `createdAt`, `updatedAt` and `deletedAt`. `key` is 1 to 64 lowercase letters, digits and underscores, starting with a letter, and unique among the agent's live dispositions.

## The definition

```json theme={null}
{"name": "Appointment outcome", "description": "Did the caller book?", "timing": "immediate",
 "values": [{"id": "44444444-4444-4444-8444-444444444444", "key": "booked", "label": "Booked",
             "schema": {"kind": "boolean"},
             "source": {"kind": "judge", "judgeId": "<eval criterion id>"}}],
 "transformation": {"version": 1, "root": {"kind": "object", "fields": {
     "booked": {"kind": "value", "valueId": "44444444-4444-4444-8444-444444444444"}}}},
 "postCallWebhook": {"mode": "emit_followup"}}
```

| Field | Rule |
| - | - |
| `name`, `description` | Up to 120 and 2000 characters |
| `timing` | `immediate` or `within_24h` |
| `values` | Up to 50. Each is `{"id", "key", "label", "schema", "source"}`: `id` is a UUID you choose and keep, `key` follows the disposition key rule and is unique in the definition, `label` is up to 120 characters |
| `transformation` | `{"version": 1, "root": <node>}` assembles the delivered payload. A node is `{"kind": "value", "valueId"}`, `{"kind": "literal", "value"}`, `{"kind": "object", "fields": {...}}` or `{"kind": "array", "items": [...]}` (up to 50 items) |
| `postCallWebhook` | `{"mode": "emit_followup"}`: the result is sent as a follow-up event. `hold` is refused; the call's own webhook is never held back |

A value `schema` has a `kind`: `boolean`, `string` (optional `format: "date-time"`), `number`, `enum` (`options`: 1 to 24 of `{"key", "label"}`), `array` (`items`, optional `minItems` and `maxItems` up to 1000) or `object` (`properties`, up to 64, and `required`). Any schema may carry a `description`. At most 8 levels, 200 nodes and 64 KB.

A value `source` says how the value is produced:

| `kind` | Shape | Rule |
| - | - | - |
| `judge` | `{"judgeId"}` | An eval criterion of this workspace (`GET /v1/evals`) that applies to this agent. The value's schema is `boolean`, or an `enum` whose keys are exactly `pass` and `fail` |
| `variable_extraction` | `{"variableKey"}` | One of the agent's call variables, or a published Outcomes field named `outcomes.<key>`, whose value on a live call is the call's own captured outcome. The schema must match what the variable holds; the variable catalog lists each one's `outputSchema` |
| `structured_extraction` | `{"extractorId", "extractorVersionId"}` | Reads through a versioned extractor (below). The schema must equal the pinned version's `outputSchema` exactly. A draft may leave the version out; a test run needs one; publishing needs a published one |
| `custom_code` | | Refused: customer code does not run here yet |

A source may add `"execution": {"reasoningEffort": "auto", "inputSourceIds": ["transcript"]}`. Sempera reads the call transcript with one setting, so any other effort or input source is refused.

## Estimate and publish

`POST .../estimates` answers a quote: `id`, `draftRevision`, `perValue` (each value's `executionPath`, token volumes and `usdPerCall`), `totalUsdPerCall` (`min`, `expected`, `max`) and the `assumptions` behind it. The `id` is a digest of exactly what would run and how it is priced. Publish with that `id` and the draft revision you estimated; if anything changed since, publish answers `409` and you estimate again. `acknowledgeEstimate: true` is required when the expected cost per call is above zero. Publishing needs at least one value, freezes a copy of every pinned extractor version and judge wording into the version (a later edit never changes a published version), enables the disposition, and answers the version: `id`, `versionNumber`, `definition`, `publishedAt`.

## On live calls

Every enabled disposition with a published version runs on every new completed phone call of the agent, after the call's outcomes are captured. Previews, typed tests and simulations never run it, and a version published after a call ended never runs on that call. The `call.outcomes` event then carries `dispositions`: one entry per disposition with its `status`, `values` and the `payload` the transformation assembled. When no model is reachable a value is stored `not_evaluated` rather than guessed; Sempera reads it again when a model is back, within a day of the call, and sends the event again.

## Test runs and corrections

| Route | Notes |
| - | - |
| `POST .../dispositions/{id}/runs` | Start a run in the background. `201`; poll the run |
| `GET .../dispositions/{id}/runs` | `{"runs"}`, newest first; `limit` 1 to 100 (default 25) |
| `GET .../dispositions/{id}/runs/{run_id}` | Status, sizes, summary counts and the frozen definition |
| `GET .../dispositions/{id}/runs/{run_id}/rows` | One row per call: `status`, `results`, the latest `annotations` and, when complete, `payload`. `limit` 1 to 250 (default 50) and `offset` |
| `GET .../dispositions/{id}/runs/{run_id}/export-csv` | The whole run as CSV |
| `PUT .../runs/{run_id}/cases/{call_id}/annotations/{value_id}` | Correct one value on one call. `201` |
| `GET .../runs/{run_id}/cases/{call_id}/annotations/{value_id}` | That value's corrections, newest first (up to 50, `truncated` says if there are more) |
| `POST .../dispositions/{id}/runs/{run_id}/align` | Proposed rewrites of each corrected value's instructions. Writes nothing |

A run body is `{"target": "draft" | "published", "population": {...}, "executionSla": "realtime" | "queued"}`; `executionSla` defaults to `realtime` for a disposition whose timing is `immediate` and `queued` for `within_24h`. `population` may carry `environment`, which can only be `production`, and is `{"mode": "recent", "requestedSize": 1 to 250}` (default 20 of the agent's most recent finished calls with a transcript) or `{"mode": "calls", "callIds": [...]}` (1 to 250 ids). `{"replayRunId"}` instead replays a finished run over the same frozen definition and calls. At most 3 runs of one agent may be queued or running (`429`). A run's `status` is `queued`, `running`, `complete`, `partial` or `failed`; with no model reachable it fails and says so. Paging numbers on runs and rows are pulled into range rather than refused.

A correction is `{"state": "produced", "value": ..., "explanation": "..."}` or `{"state": "no_value", "explanation": "..."}`. The explanation is 40 to 4000 characters, the value must fit the value's frozen schema, and the run must be finished (`409` otherwise). Corrections are append-only revisions; the engine's own result is never changed.

Align takes an optional `{"valueIds": [...], "instructions": "..."}` (1 to 50 ids, up to 2000 characters) and answers `proposals` (each value's `changes`: `field`, `before`, `after`, `rationale`), `skipped` with a reason, and `truncatedCorrections`. It needs a finished run with at least one correction (`409 DISPOSITION_ALIGN_NOT_READY`), a reachable model (`503`), and allows 30 requests per workspace per hour (`429`). You apply a proposal yourself, or not.

## Extractors

An extractor is a reusable, versioned reader a `structured_extraction` value pins: a task prompt, an optional system prompt, the output schema and how it is read. It belongs to the workspace and is either workspace-wide or scoped to one agent; its `key` is unique in the workspace.

| Route | Notes |
| - | - |
| `GET .../dispositions/extractors` | Every extractor this agent can use, most recently updated first |
| `POST .../dispositions/extractors` | `{"key", "name", "promptMd", "outputSchema", "inferenceConfig"}`, optional `description`, `systemPromptMd` and `scopedToAgent`. `201` with `{"extractor", "currentVersion"}` |
| `GET .../dispositions/extractors/{extractor_id}` | `{"extractor", "currentVersion", "activeVersion"}` |
| `GET .../dispositions/extractors/{extractor_id}/versions/{version_id}` | One version |
| `PATCH .../dispositions/extractors/{extractor_id}/draft` | `{"expectedVersionId", "expectedDraftRevision", ...}` with any of `name`, `description`, `systemPromptMd`, `promptMd`, `outputSchema` and `inferenceConfig`; the fields you leave out keep their values. `key` and `scopedToAgent` are set at create and refused here as unknown fields. `409` when the draft moved |
| `POST .../dispositions/extractors/{extractor_id}/publish` | `{"expectedVersionId", "expectedDraftRevision"}`. Answers `publishedVersion` and a fresh editable `currentVersion` |
| `GET .../dispositions/extractor-catalog` | Extractors with a published version, with the `extractorVersionId` a value pins |
| `GET .../dispositions/model-profile-catalog` | The reader profiles: one, `balanced` |

`promptMd` is required and up to 8000 characters and 8000 UTF-8 bytes, so text with many multi-byte characters is refused sooner; `systemPromptMd` has the same cap, and the pair is at most 16000 bytes. `outputSchema` follows the value schema rules. `inferenceConfig` is `{"contextSources": ["transcript"], "modelProfileKey": "balanced", "temperature": 0 to 2, "maxOutputTokens": 1 to 64000}`; only `contextSources` is required, and any other source, profile or `thinking` setting is refused. An extractor always has one editable version; publishing freezes it as the active version (its `state` reads `archived`, meaning frozen) and opens the next number for editing. A disposition that pinned an earlier version keeps it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.