linear-api
Unofficial, typed async Rust client for the Linear GraphQL API — not affiliated with Linear Orbit, Inc.
Curated, handwritten operations (no codegen) with every GraphQL document schema-validated in CI against Linear's committed SDL snapshot. Built for programmatic board automation: honest rate-limit handling, mutation-safe retries, tri-state update inputs, and typed IDs.
Installation
Authentication
API keys only (no OAuth). Keys look like lin_api_… and are created under
Linear → Settings → Security & access → Personal API keys. Linear expects the raw key in the
Authorization header — no Bearer prefix — and this crate sends it exactly that way,
stored as a secrecy secret and marked sensitive on the wire.
#
Quickstart
use ;
use ;
# async
Update mutations use Undefinable<T> for tri-state semantics — leave unchanged (default),
clear (Undefinable::Null), or set:
use Undefinable;
use IssueUpdateInput;
# async
Runnable examples:
examples/viewer.rs
(auth + rate-limit snapshot) and
examples/workspace_tour.rs
(a read-only tour of the typed surface). Both need LINEAR_API_KEY:
LINEAR_API_KEY=lin_api_...
Coverage
| Tier | Area | Operations | Status |
|---|---|---|---|
| P0 | Viewer / workspace | viewer, organization |
✅ shipped |
| P0 | Teams | teams().list / list_stream / get / states |
✅ shipped |
| P0 | Users | users().list / list_stream / get |
✅ shipped |
| P0 | Workflow states | workflow_states().list |
✅ shipped |
| P0 | Issues | get / list / list_stream / search / create / batch_create / update / archive / delete / add_label / remove_label |
✅ shipped |
| P0 | Projects | list / list_stream / get / create / update / archive / statuses |
✅ shipped |
| P0 | Project milestones | milestones / create_milestone / update_milestone / delete_milestone |
✅ shipped |
| P0 | Labels | list / list_stream / create / update / delete / ensure (find-or-create) |
✅ shipped |
| P0 | Issue relations | create / create_blocks / delete / of_issue (blocks / blocked-by views) |
✅ shipped |
| P0 | Comments | list_for_issue / list_for_issue_stream / create / create_on / update / delete |
✅ shipped |
| P0 | Typed filters | IssueFilter, ProjectFilter, TeamFilter, UserFilter, WorkflowStateFilter, IssueLabelFilter, CommentFilter, … |
✅ shipped |
| P0 | Escape hatch | execute_raw (arbitrary documents, same error taxonomy) |
✅ shipped |
| P1 | Cycles | list/get, issue scheduling | 🔲 planned |
| P1 | Attachments | attachmentCreate, attachmentLinkURL |
🔲 planned |
| P1 | Project updates | status posts + health reporting | 🔲 planned |
| P1 | Batch issue update | issueBatchUpdate |
🔲 planned |
| P2 | Webhooks | payload types + HMAC-SHA256 verification helper | 🔲 roadmap |
| P2 | Documents / initiatives / customer needs | — | 🔲 roadmap |
| P2 | OAuth | — | 🔲 roadmap |
Deprecated operations (e.g. issueSearch) are never exposed; searchIssues is used instead.
Until a tier lands, execute_raw covers the long tail.
Rate limits & retries
The client is header-aware and mutation-safe:
- Every response's budget headers (
X-RateLimit-Requests-*,X-Complexity,X-RateLimit-Complexity-*) are parsed into a snapshot readable viaLinearClient::last_rate_limit(). Budgets are never hardcoded — Linear's documented numbers are inconsistent; the headers are the source of truth. - Rate-limit rejections (HTTP 429, or Linear's HTTP 400 +
RATELIMITEDextension code) are retried after the header-indicated wait — these are rejected before execution, so retrying is always safe, mutations included. If the wait exceedsRetryConfig::max_rate_limit_wait(default 30s) the call fails fast withError::RateLimited { retry_after, .. }instead of parking your task. - Queries retry on transient failures (connect errors, post-send transport errors, 5xx) with jittered exponential backoff (default 3 attempts, 250ms base, 8s cap).
- Mutations retry only on connect errors (nothing was sent). Linear has no idempotency
keys — a timed-out
issueCreatemay have landed, and a blind retry double-creates. Opt in withRetryConfig::retry_mutations_on_transientif your caller dedupes. - GraphQL errors are fail-closed: if
errorsis non-empty the call returnsError::Api { .. }even when partialdatawas present (execute_rawis the lenient escape hatch).
Errors
One Error enum with honest variants: Config, Transport, Http, Api (typed via
LinearErrorType, e.g. AuthenticationError / Forbidden / InvalidInput), RateLimited
(with the server-indicated wait), Decode (the runtime schema-drift tripwire), MissingData,
and MutationFailed (success: false without errors). Helpers: is_rate_limited(),
is_authentication(), error_types().
Schema drift defense
Every GraphQL document ships in a compile-time registry and is validated against the committed
SDL snapshot (schema/linear.graphql) by tests/schema_validation.rs using apollo-compiler —
a required CI check. A nightly job diffs the live SDL against the snapshot, and env-gated live
smoke tests (cargo test -- --ignored with LINEAR_API_KEY; read-only) exercise the real API.
Features
| Feature | Default | Description |
|---|---|---|
rustls-tls |
yes | TLS via rustls with webpki roots |
native-tls |
no | TLS via the platform's native stack |
tracing |
no | tracing events: debug! per request, warn! per retry |
MSRV
Rust 1.85 (edition 2024). Checked in CI.
License
Licensed under either of MIT or Apache-2.0, at your option.