# ADR 0089: Async operations describe transport, not jobs

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0089-async-operations-describe-transport-not-jobs.md

---

# ADR 0089 — Async operations describe transport, not jobs

## Context

Long-running exports, imports and generation jobs repeat the same operation
surface, but Stitchkit must not become a queue, scheduler or persistence layer.

## Decision

`defineAsyncOperation({ mode:'runtime-only' })` generates ordinary pathless
runtime definitions for start, status and wait plus configured cancel, result
and artifacts capabilities. Missing optional capabilities are absent from the
inferred keys and discovery. `bindContractAsyncOperation({
mode:'contract-backed' })` instead binds literal methods from an existing
dedicated contract; it creates no router and requires start/status/wait to use
schema-compatible id/snapshot types. Typed callers therefore cannot select an
incompatible endpoint. Runtime identity checks remain defence-in-depth for
untyped callers and require the same Zod instances, so a dedicated contract
should still declare each id/snapshot schema once and reuse it.

Applications map inspected state to the canonical `pending | running |
succeeded | failed | cancelled` snapshot schema. Stitchkit validates each
snapshot but stores no history and enforces no monotonic transition. Every
follow-up first runs the mandatory resource `authorize` callback. Result and
artifacts receive the already-inspected state and only run for `succeeded`, so
there is no hidden second lookup. Cancel returns the validated `accepted |
already_terminal | rejected` envelope; aborting wait never calls cancel.
Every capability, including wait, uses its own `<action>.<capability>` identity
and optional per-capability scope override.

`pollUntil` owns one monotonic absolute deadline, caps each sleep to the
remaining budget and passes a linked caller/deadline signal to cooperative
polls. Non-cooperative callbacks cannot be force-stopped. HTTP, MCP and Agent
carry transport cancellation; CLI accepts an explicit signal and leaves OS
SIGINT binding to the application.

## Consequences

- Queue/storage, leases, retries, id uniqueness, domain states and authorization
  decisions remain application-owned.
- Failed snapshots contain only application-supplied caller-safe failure data;
  raw provider causes stay in existing internal observability.
- Progress is application-defined typed data. Realtime delivery remains an
  ordinary Socket.IO contract over the same status model.
