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

# Automations and operations

> Run agents on a schedule or an inbound event, receive webhooks, and cap spend.

Most agent work is not typed into a chat box. An **Automation** starts work when something happens: a clock ticks, a webhook arrives, or a subscribed conversation moves. What it starts depends on its target. An `agent_definition` target launches a Session; a `dream` target starts a memory curation job and no Session at all.

## Build visually or headlessly

The Checkfu dashboard's node editor has no private control path. It reads and
writes a shared, revisioned `AutomationGraph`, asks the server for the exact
plan, and applies that plan onto ordinary Automation and PermissionAssignment resources. Your
application can use the same public AutomationGraph operations, build a
simplified workflow UI, or embed the controlled React editor from
`@checkfu/ui/automation-builder`. The platform-free compiler in
`@checkfu/graph` defines the mapping used by server plan/apply; it is useful for
local previews, but a client preview never replaces the server plan.

The component owns no key, storage, router, catalog, or network connection. You
supply the current document and node positions, render any inspector you want,
and handle every emitted add/move/connect/remove intent. Semantic document
changes use `expected_revision`; layout changes use the independent
`expected_layout_version`, so dragging a node cannot stale an approved semantic
plan. AutomationGraph owns authored intent and reconciliation history only:
Automation remains the runtime authority consulted when work fires.

### Edit and apply from the CLI or embedded agent

The ergonomic CLI covers the complete shared lifecycle while still using the
generated public SDK:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
checkfu graph get agph_... --json
checkfu graph patch agph_... --input @graph-patch.json
checkfu graph plan agph_... --revision 2 --json
checkfu graph apply agph_... --revision 2 \
  --plan-fingerprint sha256:... --confirm
checkfu graph test agph_... --input @delivery.json
checkfu graph drift agph_...
```

`@delivery.json` must be a regular UTF-8 JSON file no larger than 1 MiB. The
CLI validates it through one owned file descriptor and forwards the admitted
bytes unchanged.

Use `checkfu graph create`, `list`, `layout`, `revisions`, `revision`,
`applications`, `application`, and `archive` for the rest of the lifecycle.
Apply and archive require `--confirm`. A new webhook graph can pause in
`waiting_for_input`; `checkfu graph complete <graph> <application> --confirm`
reads the ingest secret from protected terminal input and never accepts it in
arguments or prints it. The dashboard's embedded agent discovers these same
operation IDs, reads the current revision before a CAS edit, summarizes the
server plan before asking for apply approval, and sends webhook completion to
the human-only `/graphs` flow rather than a model transcript. `graph test`
resolves the graph's current managed Automation binding and runs the supplied
JSON through its real webhook AdmissionFilter without launching work; an
unapplied or archived graph fails closed.

Evaluation nodes share one palette but not one meaning. AdmissionFilter gates
verified webhook input before work, OutcomeSpec owns bounded revise-until-done,
FlowReview passively audits settled evidence, and DeliveryScreen remains a
property of the acting agent's published version. Dream Automations stay on
their dedicated memory-selector form because drawing them as message flow would
imply a Session they never create.

## Triggers

| Kind           | Configuration                                                                         | Input                           |
| -------------- | ------------------------------------------------------------------------------------- | ------------------------------- |
| `schedule`     | `cron` and `timezone`                                                                 | `snapshot`: a fixed `prompt`    |
| `webhook`      | `dedupe_key` and an optional `filter`                                                 | `template`: a `prompt_template` |
| `subscription` | A `source` (a conversation or an external resource) and a required `surface_scope_id` | `template`                      |

`dedupe_key` is an object, not a bare string: `{ "kind": "json_pointer", "pointer": "/id" }`.

The fields are mutually exclusive by schema rather than by convention: a schedule trigger cannot carry a `dedupe_key`, and a webhook trigger cannot carry a `cron`. An invalid combination is unrepresentable rather than ignored.

A template renders `{{event}}` as the canonical JSON of the inbound payload. Rendered prompts are capped at 64 KiB.

## Make the definition of done part of the trigger

Add `input.outcome` when recurring work should be judged against a definition of done instead of ending after one agent pass. The rendered `prompt` or `prompt_template` becomes the Outcome description; the spec supplies the rubric, iteration bound, and optional files to capture at settlement.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "input": {
    "kind": "snapshot",
    "prompt": "Publish the weekly reliability report",
    "outcome": {
      "rubric": "The report names every failed check and its owner.",
      "max_iterations": 4,
      "deliverables": ["/workspace/reliability.md"]
    }
  }
}
```

Checkfu starts the fresh Session with `user.define_outcome`, then uses the same platform grader, bounded revision loop, Usage ledger, Budget enforcement, and terminal Outcome results as an interactively defined Outcome. Omitting `outcome` preserves ordinary `user.message` behavior.

`rubric` is required and may contain up to 65,536 characters. `max_iterations` is optional (`1`–`20`, default `3`). `deliverables` is optional and accepts up to 32 absolute paths. Outcome descriptions are capped at 4,096 characters after template rendering.

<Note>
  Outcome-driven Automations currently require managed Runner execution. A create or patch that combines `input.outcome` with `connected_runtime` is rejected; Checkfu never drops the rubric and sends a plain connected message instead.
</Note>

## Review how the Turn was conducted

Add `input.flow_review` to an agent-target Automation for one passive, independent review after its originating Turn settles:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "flow_review": {
    "model_routing_profile_key": "managed-review-model",
    "tier": "planning",
    "criteria": [
      {
        "key": "evidence_grounded",
        "description": "Distinguish observed evidence from inference.",
        "severity": "high"
      }
    ]
  }
}
```

A FlowReview receives a bounded, provenance-preserving selection from the exact Turn in a fresh model context. It excludes model thinking, validates every cited event ID, records its own route and Usage, and projects only content-free counts and severity onto the firing. Read retained detail from `GET /v1/automation-firings/{id}/flow-review`.

FlowReview is deliberately not an Outcome, ActionPolicy, DeliveryScreen, generic judge hook, or workflow step. It cannot request a revision, release or block a message, pause an Automation, or alter an Outcome. That is why `Outcome satisfied` and `FlowReview concerns` can both be true. It is available only for managed, standard-retention, agent-target Automations; ZDR, connected-runtime, Dream, and deterministic `steps` targets reject it rather than degrading silently.

## Automation Blueprints and packaged guardrails

The console can start an Automation draft from one of three deliberately narrow **Automation Blueprints**:

| Automation Blueprint    | Trigger          | Declared prerequisite          | Packaged guardrail profile                                                                                    |
| ----------------------- | ---------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| Weekly release notes    | Weekday schedule | Repository target and access   | **Evidence required**: a bounded Outcome must produce a complete, factual report                              |
| Recent change review    | Weekday schedule | Repository target and access   | **Minimal change**: evidence-backed findings, proposed fixes, and a durable review report are graded together |
| Verified webhook triage | Signed webhook   | None beyond ordinary admission | **Fail-closed intake**: an admission Check drops ambiguous or unsafe input before an Agent Session exists     |

An Automation Blueprint is a creation aid, not a new API resource or runtime authority. Applying one writes ordinary trigger, input, Outcome, and filter fields into the draft. You can inspect and edit every field before review; the saved object is an ordinary Automation and has no dependency on the catalog afterward.

It is not an [AgentBlueprint](/concepts/agent-blueprints), which is an installable, versioned package that stands up a whole agent and its dependencies.

The creation flow also shows **Automation Blueprint requirements**. It compares structural requirements such as managed execution, bounded Outcome evidence, and fail-closed intake with the fields you actually edited. For context it identifies the target's AgentDeployment and its current revision; a target whose selection is `current` resolves a newer deployment revision on later firings.

Repository-oriented Automation Blueprints declare repository target and access as **unverified**. Automation Sessions do not currently mount a Project, and a Tool or Skill name is not proof of a usable repository or an authorized Connection. The preview never speculates about live authority: repository identity, PermissionAssignments, ActionPolicies, provider readiness, and Runner placement remain operator review or the API's admission decision when work fires.

The console also refuses to guess Workspace authority. The fail-closed Automation Blueprint supplies the Check tier, instructions, and failure disposition, but you must choose a current `model_routing_profile_key` from your Workspace before creation. Agent, person Principal, service identity, and execution authority are always selected or resolved from current Workspace data.

## Targets

An Automation aims at one of three closed targets, and the target changes what else is legal:

| Target             | Carries                                                                                                       | Input                                           |
| ------------------ | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `agent_definition` | `agent_deployment_id` and `agent_deployment_revision_selection`                                               | Required, per the table above                   |
| `steps`            | A bounded deterministic sequence from the closed steps contract                                               | Forbidden; the program is the whole instruction |
| `dream`            | `memory_store`, a `sessions` selector, and optional `model_routing_profile_key`, `instructions`, and `review` | **None**: `input` is forbidden                  |

`agent_deployment_revision_selection` lives on the target, not on the Automation itself. A `dream` target is schedule-only and takes no prompt at all, so the input column above applies only to agent targets. Its `sessions` selector is `{ "kind": "since_last" }` with an optional per-firing `limit` up to 100: each firing curates the Sessions the Workspace gained since the Automation's cursor, oldest first, and the cursor advances only after a Dream commits.

<Warning>
  Dream targets are `preview` / `rollout_fenced` in [capability status](/getting-started/status). They share versioned Memory's D7 rollout fence; a durable Postgres adapter is qualified, but hosted create, patch, resume, and fire operations remain fail closed until an operator-approved rollout enables `MEMORY_VERSIONED_SURFACE`. Scheduled firing is blocked at the shared Dream service boundary. Existing Automation metadata remains readable and pausable or deletable. Agent-definition targets have an independent support posture.
</Warning>

### Which step verbs execute

The `steps` action vocabulary is closed at five verbs — `run_agent`, `check`, `request_approval`, `wait`, and `call_connection` — and every one of them executes today. A `call_connection` step is platform egress under the Automation's own identity: it takes a durable claim, resolves the live Connection and tool inside it, runs the egress, binding, and policy ladder, and forwards the call exactly once. Because external egress has no shared idempotency authority, a crash between dispatch and its receipt settles the step as a verdict-free failure rather than re-issuing the call, and the recorded receipt carries only a bounded provider status, never a response body. Its `arguments` are static — fixed at review time, because an argument that varied per firing would make the frozen program fingerprint a lie about what the firing did — bounded at 8 KiB of UTF-8, and schema-checked against the tool's own `input_schema` both when the program is authored and again immediately before dispatch. Omitting `arguments` means `{}`, which only a tool whose schema accepts no required argument will bind.

The typed `steps_execution_unavailable` conflict survives only as the fail-closed answer to a verb nobody has classified: a half-durable program that started and then could not finish would be worse than the refusal, so a verb with no execution boundary yet is rejected before any claim exists. Classification is total and compile-enforced — a future sixth verb cannot fall through to "pending" silently, because placing it on one side of the boundary or the other is a requirement before the program compiles. That error therefore names an unclassified verb, never `call_connection`. If you see it, look for a verb your runtime does not yet reach, not for a working step to remove.

Every trigger a `steps` target can carry fires one. Manual and schedule payloads are content-free and rebuild from the claim row on any resume. A webhook delivery body is customer content, so it is taken into retention custody under the firing's own content id before its claim exists, proved by digest whenever it is read back, and shredded before the settling status flip or by the erasure that removes its parent. A previous-output handoff similarly needs retention custody across the child-Session park. Under ZDR there is nowhere to put either kind of resumable content, so a webhook program or any program containing `previous_step_output` is refused with `steps_payload_retention_unavailable` before a child launches.

A `run_agent` step admits one **fresh child Session** through the ordinary Automation admission path — the Automation's own creating person, service Principal, SurfaceScope, and Workspace, with only the step's AgentDeployment and revision selection substituted. It is not a Delegation: a Delegation admits under a parent Session's recorded context, and a steps Automation has no Session to inherit one from. Child Session and Run ids derive from the firing and the step, so a crash between admission and the park receipt recovers the same child rather than admitting a second, and two `run_agent` steps of one firing never collide on a Run id. The park is bounded by the child Run's own lease and turn deadlines, not by a step-level timeout.

A `run_agent` action has three mutually exclusive forms. The historical form
has a static `prompt`. The trigger form keeps `prompt` and adds
`"input": { "kind": "trigger_payload" }`; it renders `{{event}}` from the
firing's canonical payload just like an agent-target Automation template. The previous-output dataflow form has
`"input": { "kind": "previous_step_output" }` and is legal only immediately
after an unconditional `run_agent`. It uses that exact source Run's final
`agent.message` as the next Session's complete input only when the message
contains readable screened content. A
missing, withheld, incomplete, failed, or larger-than-64-KiB result fails closed;
Checkfu never substitutes a fallback prompt, wrapper, or summary. It retains the
resolved prompt before launching the successor so crash recovery uses the same
bytes, then shreds the firing's retained handoff content at settlement or
erasure.

In an AutomationGraph, `trigger fires A`, `A hands_to B` compiles a webhook's
entry Agent with `trigger_payload` (a schedule entry remains static) and every
later Agent with `previous_step_output`; longer unbranched Agent chains work the same way. Explicit
`starts` and `then` edges remain static step sequencing. Branches, joins,
cycles, disconnected chains, subscriptions, and mixing the handoff topology
with explicit steps or evaluator/authority edges are rejected rather than
assigned guessed semantics.

### What firing returns

`POST /v1/automations/{id}/fire` has four success shapes, and a client that assumes one will break on the others:

| Response                                                    | When                                                          |
| ----------------------------------------------------------- | ------------------------------------------------------------- |
| `Run`                                                       | An agent target launched a Session; this is its initial Run   |
| `Dream`                                                     | A dream target started a curation job                         |
| `{ "disposition": "skipped", "reason": "no_new_sessions" }` | A dream target found nothing new to curate                    |
| `AutomationFiring` with `status: "step_program"`            | A steps target ran its program; the receipt **is** the firing |

A steps target has no Session or Run to be `submitted` with, so its whole lifecycle and verdict live in the response's `program` object: a `state` of `pending`, `settled`, or `terminal`, a `result` that stays `null` while the program is walking or parked, the per-step outcome trace under the step ids you authored, and a `wake_at` set only while parked on a `wait`. Read them there and nowhere else. The step outcomes are content-free — closed outcome kinds against your own step ids, never a prompt, instruction, or payload.

For an agent target, the Session's public `principal` is **conditional**: it is the Automation's `created_by` when work runs on managed capacity, and the identity's `acted_as` when the Automation is pinned to a connected runtime. Attribute usage accordingly. Neither alone is correct.

<Note>
  `POST /v1/automations/{id}/fire` requires a **non-empty** `Idempotency-Key`. Pause and resume accept one but do not require it. That asymmetry is easy to miss and produces a confusing `400` on the endpoint most likely to be retried.
</Note>

## Inbound webhooks

`POST /v1/ingest/automations/{id}` is how an external system drives an Automation. It carries **no API key**: the signature is the entire authentication. It does still require the `Checkfu-Version` header, like every other versioned route.

<Note>
  Because it is authenticated by signature rather than by an API key, this route does not appear in the generated API reference, which renders only the customer-key surface. It is a real published endpoint: you will find it in the complete OpenAPI document the platform serves at `/api/openapi.complete.json`. This page is its reference.
</Note>

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Checkfu-Signature: t=1752686400,v1=<hex hmac-sha256>
```

The HMAC is computed over `ASCII(t) || "." || raw_request_body`, keyed by the Automation's ingest secret. The body is **not normalized before verification**, so a caller must sign the exact byte sequence it sends. Re-serializing JSON between signing and sending breaks the signature.

Timestamps more than 300 seconds from now are rejected in **either** direction, so a fast-running clock ahead of the server fails just as a stale one does. Bodies are capped at 1 MiB and must decode as JSON no deeper than 64 levels and no larger than 100,000 nodes.

Ingest returns `201` with a `Run` when the delivery is admitted, or `200` with a `FilteredAutomationDelivery` when a filter stopped it. A `200` is not a failure: it means the delivery was understood and deliberately dropped.

<Tip>
  The header accepts **multiple** `v1` values and passes if any matches. Checkfu holds exactly one ingest secret at a time, so this does not create a server-side overlap window. What it buys you is the ability to sign with both the old and new secret across **your own** deploy, so no in-flight sender is rejected mid-rollout.
</Tip>

Unusually, the ingest secret is **supplied by you**, not issued by Checkfu. It is required when creating a webhook Automation, must match `whsec_` followed by 43 URL-safe characters, is encrypted at rest, and never appears on the Automation resource afterward. Rotate it by supplying a new one.

### Webhook sources: provider events without per-provider glue

Most providers (GitHub, Linear, Sentry) sign webhooks with their own schemes and cannot produce a `Checkfu-Signature`. The provider-specific verifier is therefore an explicit trust owner. A deployment-supplied ingress aggregator can use a **webhook source** binding to one governed Connection. Without an aggregator, a customer-hosted receiver can use `@checkfu/integration-host` to verify the provider signature, preserve the exact body, and re-sign ordinary Automation ingest. Dedupe, admission filtering, filtered-delivery records, and firing semantics are unchanged.

<Warning>
  D215 removed the bundled ingress aggregator. A `WebhookSource` is active only when your deployment supplies that forwarder. The stock alternative is the customer-hosted verify-and-resign relay described in [Trigger agents from providers](/guides/trigger-agents-from-providers); Checkfu does not guess or bypass a provider's signature dialect.
</Warning>

* `POST /v1/automations/{id}/webhook-sources`: bind a Connection (optionally recording the provider resource it follows). The Automation must have a webhook or subscription trigger, its creator must own the Connection, and the Connection must be active; a subscription also requires the Connection provider to match its active ExternalInstallation. An Automation holds at most 16 sources, enforced atomically.
* `GET /v1/automations/{id}/webhook-sources`: list bindings.
* `DELETE /v1/automations/{id}/webhook-sources/{sourceId}`: remove one; forwarding for that binding stops immediately.

A source carries no filter of its own. Relevance stays in the Automation's admission filter, which sees the provider's payload as `{{event}}`. Only a webhook trigger has one: a subscription trigger cannot carry a `filter` at all, so every forwarded occurrence it admits fires. A subscription adapter sends a normalized occurrence as the signed forwarded payload:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "kind": "checkfu.subscription_occurrence",
  "schema_version": 1,
  "external_event_id": "provider-event-123",
  "source": {
    "kind": "resource",
    "resource_type": "repository",
    "external_id": "checkfu/checkfu"
  },
  "payload": { "action": "opened" }
}
```

The source must exactly match the subscription trigger. Checkfu uses the external event ID in its existing durable firing claim, so an exact retry returns the same Run and a changed payload conflicts; it does not create a second occurrence ledger. A reserved occurrence envelope that is malformed or uses an unsupported version fails visibly and asks the aggregator to retry. An ordinary raw event routes only to webhook bindings; when a Connection has only subscription bindings, that unnormalized event also fails visibly. Every new occurrence atomically rechecks the live Connection, ExternalInstallation, SurfaceScope, and Automation versions while claiming the firing, so revoking authority ends new forwarding without touching the Automation. Once ingress resolves the live source, exact D51 retries recover their already-frozen claim before the firing port's mutable authority reads. Revoking the Connection or deleting the source still stops ingress before that boundary. Provider-side webhook registration and provider-specific normalization are currently out-of-band configuration in your provider or credential aggregator; no public Checkfu endpoint performs provider-specific registration.

### Filtering, and why silence is explainable

A webhook Automation can filter deliveries before spending anything. The ladder has two rungs:

1. **Predicates**: up to 32 checks of `exists`, `equals`, or `contains` against JSON Pointers. Zero token cost. A predicate that cannot be evaluated fails rather than erroring the request.

   `equals` and `contains` compare strings, so a missing pointer or a non-string value fails them. `exists` is looser and passes for numbers, objects, and arrays, but it fails on a JSON `null`. A key present with a null value does **not** exist for filtering purposes.
2. **Check**: an optional model-evaluated instruction, with an explicitly declared failure disposition.

When a filter stops a delivery, no Session is created. Without a record, that delivery would vanish, so Checkfu writes a `FilteredAutomationDelivery` instead, with a `reason` of `predicate_unmatched`, `check_drop`, or `check_failed`, plus a payload fingerprint and any model usage the check consumed. It records structural facts only, never payload content.

`GET /v1/automations/{id}/filtered-deliveries` is therefore the endpoint you reach for when someone asks "why didn't that fire?"

An editor can exercise this exact ladder without keeping an ingest secret or
launching work: authenticated `POST /v1/automations/{id}/test-delivery` accepts
the same raw JSON body and returns `admitted` or `filtered`, the reason, payload
fingerprint, and any Check usage. It writes no firing or filtered-delivery row,
so tests never inflate production proactivity. Each call can still consume
model tokens when a Check is configured.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const candidate = new TextEncoder().encode(
  JSON.stringify({ id: "candidate-1", action: "opened" }),
)
const result = await checkfu.automations.test(automationId, candidate)

if (result.disposition === "filtered") {
  console.log(result.reason) // predicate_unmatched | check_drop | check_failed
}
```

The SDK sends those bytes as `application/json` without serializing the
`Uint8Array` or wrapping a JSON string around it.

### Firing records

An `AutomationFiring` is `pending`, `submitted`, `failed`, or `step_program`, and the correlation fields are pinned to the status rather than left as loose optionals:

| Status         | `session_id` | `run_id` | `error` | `outcome`                  | `flow_review`                              |
| -------------- | ------------ | -------- | ------- | -------------------------- | ------------------------------------------ |
| `pending`      | `null`       | `null`   | `null`  | `null`                     | not yet frozen                             |
| `submitted`    | present      | present  | `null`  | terminal summary or `null` | pending, completed, unavailable, or `null` |
| `failed`       | `null`       | `null`   | present | `null`                     | not applicable                             |
| `step_program` | `null`       | `null`   | `null`  | `null`                     | not applicable — read `program` instead    |

A pending firing exposes no speculative Session identity, so you can never read an ID that turns out not to exist.

`step_program` names the **target**, not a state, because a step program has no Session or Run to be `submitted` with. Its lifecycle and verdict are in `program`, described under [What firing returns](#what-firing-returns).

`GET /v1/automation-firings` pages agent and step-program receipts together
under one environment-monotonic cursor, so one Automation's history never splits
across two read models. Scope it with `automation_id`; retrieve an individual
receipt by id when following the response from `fire`.

### Outcome analytics and firing history

For an Outcome-driven Automation, the console inspector projects the latest firing's definition-of-done evidence from its Session event log: current or terminal verdict, evaluation count, requested revisions, grader tokens, expected deliverables, and the grader's latest explanation. The Session link opens the underlying evidence.

The same inspector lists content-free Outcome status across firing receipts. Each submitted receipt's nullable `outcome` contains `outcome_id`, terminal `result`, `evaluation_count`, `revision_count`, and, when a typed Report was declared, only its accepted/rejected status and output counts. It deliberately excludes Report values, the task, rubric, explanation, deliverable paths, model route, digest, and any guessed settlement timestamp; open, legacy, or unavailable evidence stays `null`, never failure. Use `GET /v1/automation-firings?automation_id=auto_…&order=desc` for a newest-first history window.

Detailed typed Report values are resolved from the retention-governed Session log rather than copied into the firing store. Each result includes provenance linking it to the exact evaluation event, frozen-input digest, and grader route. Read one with `GET /v1/automation-firings/{id}/report`, page the history with `GET /v1/automations/{id}/reports`, or download the self-describing JSON document from `GET /v1/automations/{id}/reports/export`. History uses immutable Report-availability order rather than firing creation order. Ascending traversal continuously discovers late-settling Reports; descending traversal is a newest-first snapshot and discovers later arrivals when you refresh its first page. Expired or erased detail disappears honestly while its content-free firing summary remains usable for operational counts.

Checkfu does not turn an incomplete Session log or a capped receipt walk into an aggregate success rate. The console reports terminal, satisfied, and unknown/open counts separately, labels capped counts as incomplete, and keeps the latest Session link as the underlying evidence.

### One causal evidence view

`GET /v1/automation-firings/{id}/evidence` joins one submitted firing receipt with content-free facts from its exact Session Turn: admission, Run count, DeliveryScreen counts, Outcome summary, FlowReview summary, and human feedback. It is a read model, not a second authority. Its completeness marker distinguishes complete, open, truncated, expired, erased, and ZDR evidence; it never copies prompts, messages, rubrics, explanations, criteria, tool payloads, or model output.

An authorized Principal can record one content-free usefulness label per firing at `POST /v1/automation-firings/{id}/feedback`, list it, and change it with optimistic concurrency. The five values are `useful`, `noisy`, `incorrect`, `too_late`, and `already_handled`. Feedback is human opinion: it never trains, edits, or changes a live Automation implicitly.

`GET /v1/automations/{id}/proactivity` exposes separate completeness-bearing counts for admitted/failed firings, filtered deliveries, Outcomes, FlowReviews, and feedback. It never returns one quality score or success rate. `candidate_recall` is always the literal `unknown`, because verified ingress cannot reveal important events that never arrived.

## Outbound webhooks

A webhook endpoint subscribes to event types: the whole catalog, or an explicit list. To subscribe to everything, **omit `event_types` entirely on create**; `null` is accepted when patching an existing endpoint but rejected at creation, and an empty array is rejected in both. The catalog covers Session status events plus `run.created`, `run.started`, `run.start_timed_out`, `run.requires_action`, `run.action_authorized`, `run.resumed`, `run.completed`, `run.failed`, `run.canceled`, `action_approval.pending`, `action_approval.resolved`, `outcome.evaluation_started`, and `outcome.evaluation_completed`.

Organization-originated `user.*` events are deliberately excluded: you already know what you sent.

### The delivery body is thin on purpose

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "event",
  "id": "evt_0123456789abcdef0123456789abcdef",
  "seq": 12,
  "created_at": "2026-07-16T18:00:03.000Z",
  "data": {
    "type": "run.requires_action",
    "session_id": "sess_0123456789abcdef0123456789abcdef",
    "workspace_id": "wrkspc_0123456789abcdef0123456789abcdef"
  }
}
```

No payload content crosses the wire. Verify the signature, deduplicate on `id`, then fetch the Session or Run for detail. A webhook is a notification, not a data channel.

`data.session_id` is nullable. It is `null` on test deliveries, which also carry `seq: 0`. A consumer that dereferences it unconditionally will crash the first time someone presses the test button.

Signing is byte-identical to inbound ingest: `Checkfu-Signature` with `t=` and `v1=`, HMAC-SHA-256 over `t.body`. The key is the entire `whsec_…` string as UTF-8 bytes, not the decoded suffix. Using the decoded suffix is a common implementation error.

### Delivery guarantees

Delivery is at-least-once, with a 10-second timeout and retries at 30 seconds, 2 minutes, 10 minutes, 1 hour, 4 hours, and 12 hours. That is seven attempts in total. After 20 consecutive terminal failures the endpoint disables itself with a reason of `delivery_failures`. Re-enable it by patching `status` back to `active`, which also resets the failure count.

Failed delivery rows remain inspectable through
`GET /v1/webhook-endpoints/{id}/deliveries`. Once the receiver is repaired,
`POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/redeliver` queues one
new attempt on an active endpoint. It requires an idempotency key and preserves
the original delivery and event identity; it never creates a replacement event
or resets the attempt history. Arrival order is therefore not event order.

`POST /v1/webhook-endpoints/{id}/test` sends a synthetic delivery with the event type `webhook.test`, which never appears in a real event log, and reports whether it was delivered and with what status code. Send `{ "expected_version": <current version> }` in the request body. Test, secret rotation, and deletion reject a stale version so an action reviewed against one endpoint configuration cannot silently run against a newer one.

The signing secret is returned exactly once: at creation and at rotation.

## Budgets

A **Budget** is the customer's own ceiling, distinct from commercial entitlement. The current contract budgets provider-normalized tokens with `model_tokens` or platform-catalog integer micro-USD with `model_spend`. Sandbox compute and snapshot storage are metered in Usage but not yet budgetable.

| Field            | Values                                                                                                                                                                                                                                                                               |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `scope`          | One object variant: `{ "kind": "environment" }`, `{ "kind": "agent_definition", "agent_id": "agent_…" }`, `{ "kind": "principal", "principal_id": "prin_…" }`, `{ "kind": "surface_scope", "surface_scope_id": "surf_…" }`, or `{ "kind": "automation", "automation_id": "auto_…" }` |
| `metric`         | `model_tokens` or `model_spend`; spend thresholds are integer micro-USD                                                                                                                                                                                                              |
| `window`         | `{ "kind": "rolling", "duration_seconds": … }` or `{ "kind": "calendar_month" }`                                                                                                                                                                                                     |
| `soft_threshold` | A `limit` with action `notify`, or `null`                                                                                                                                                                                                                                            |
| `hard_threshold` | A `limit` with `breach_action` of `block_admission` or `interrupt_turn`                                                                                                                                                                                                              |
| `state`          | `active` or `paused`, set by patching, not at creation                                                                                                                                                                                                                               |

A complete create request is worked through in [Attribute usage](/guides/attribute-usage#cap-spend-per-user).

The soft limit must be strictly below the hard limit, and that invariant is enforced on create and on every patch. A soft crossing notifies once per crossing, not once per entry.

Budget create and patch accept an optional `Idempotency-Key`. A keyed patch replays the exact
thresholds, state, version, and timestamp produced by its first execution even if a later patch
changes the Budget. Reusing the key with a different request conflicts. Principal deletion wins
over both recovery and replay: compact receipts retain no Budget scope or Principal identifier,
and a deleted Budget returns not found.

<Warning>
  `breach_action` is not a choice between blocking and interrupting. **Any** breached hard threshold blocks new admission regardless of its `breach_action`. The setting decides only whether a turn already in flight is *additionally* interrupted. Choosing `block_admission` does not let a running turn finish more gracefully than `interrupt_turn` would. It means the in-flight turn is left alone.
</Warning>

## Usage

`GET /v1/usage` is the raw ledger read: one row per immutable billing fact, with no time-range parameter. Filter with `principal_id`, `agent_id`, `session_id`, or `run`. Note that last one is `run`, not `run_id`. `GET /v1/usage/series` is the complementary admin-only Workspace/day aggregate for bounded UTC ranges, including exact known platform spend and explicit completeness.

Each row has exactly one measurement dimension:

| `measurement.kind`   | Measures                                            |
| -------------------- | --------------------------------------------------- |
| `model`              | Token counts, including cached and reasoning tokens |
| `sandbox_compute`    | Active sandbox milliseconds                         |
| `snapshot_retention` | Retained snapshot storage over time                 |

`total_tokens` is authoritative and is never rebuilt from text or traces. Exactly one of `principal_id` or `anonymous_subject_ref` is present on every row. After a Principal erasure the accounting fact survives without the identity.

## Next steps

<CardGroup cols={2}>
  <Card title="Receive webhooks" icon="webhook" href="/guides/receive-webhooks">
    Verify a signature, deduplicate, and survive retries.
  </Card>

  <Card title="Billing and platform supply" icon="credit-card" href="/concepts/billing-and-platform-supply">
    How entitlement differs from the Budgets above.
  </Card>
</CardGroup>
