Skip to main content
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:
@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

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.
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 (120, default 3). deliverables is optional and accepts up to 32 absolute paths. Outcome descriptions are capped at 4,096 characters after template rendering.
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.

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:
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: 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, 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: 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.
Dream targets are preview / rollout_fenced in capability 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.

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: 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.
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.

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.
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.
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.
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.
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.
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; Checkfu does not guess or bypass a provider’s signature dialect.
  • 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:
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.
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: 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. 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

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. A complete create request is worked through in Attribute usage. 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.
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.

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: 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

Receive webhooks

Verify a signature, deduplicate, and survive retries.

Billing and platform supply

How entitlement differs from the Budgets above.