Skip to main content

Crate linear_api

Crate linear_api 

Source
Expand description

§linear-api

CI crates.io docs.rs

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

cargo add linear-api

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

// Reads LINEAR_API_KEY from the environment:
let client = linear_api::LinearClient::from_env()?;

// Or configure explicitly:
let client = linear_api::LinearClient::builder()
    .api_key("lin_api_...")
    .timeout(std::time::Duration::from_secs(30))
    .build()?;

§Quickstart

use linear_api::issues::{IssueCreateInput, ListIssuesRequest};
use linear_api::{IssueFilter, LinearClient, StringComparator, TeamFilter};

let client = LinearClient::from_env()?;

// Who am I?
let viewer = client.viewer().await?;
println!("logged in as {} <{}>", viewer.display_name, viewer.email);

// Resolve the ENG team.
let team = client.teams().get(&linear_api::TeamId::new("ENG")).await?;

// List its open issues (one page; see `list_stream` for lazy full drains).
let page = client
    .issues()
    .list(
        ListIssuesRequest::builder()
            .filter(
                IssueFilter::builder()
                    .team(
                        TeamFilter::builder()
                            .key(StringComparator::builder().eq("ENG".to_string()).build())
                            .build(),
                    )
                    .build(),
            )
            .first(25)
            .build(),
    )
    .await?;
for issue in &page.nodes {
    println!("{} {} [{}]", issue.identifier, issue.title, issue.state.name);
}

// Create an issue.
let issue = client
    .issues()
    .create(
        IssueCreateInput::builder()
            .team_id(team.id.clone())
            .title("Fix the flux capacitor")
            .build(),
    )
    .await?;

// Comment on it, then link a blocker.
client.comments().create_on(&issue.id, "On it.").await?;
client
    .relations()
    .create_blocks(linear_api::IssueRef::identifier("ENG-1"), &issue.id)
    .await?;

Update mutations use Undefinable<T> for tri-state semantics — leave unchanged (default), clear (Undefinable::Null), or set:

use linear_api::Undefinable;
use linear_api::issues::IssueUpdateInput;

client
    .issues()
    .update(
        linear_api::IssueRef::identifier("ENG-123"),
        IssueUpdateInput::builder()
            .title("New title")            // set
            .assignee_id(Undefinable::Null) // clear (unassign)
            .build(),                       // everything else: unchanged
    )
    .await?;

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_... cargo run --example workspace_tour

§Coverage

TierAreaOperationsStatus
P0Viewer / workspaceviewer, organization✅ shipped
P0Teamsteams().list / list_stream / get / states✅ shipped
P0Usersusers().list / list_stream / get✅ shipped
P0Workflow statesworkflow_states().list✅ shipped
P0Issuesget / list / list_stream / search / create / batch_create / update / archive / delete / add_label / remove_label✅ shipped
P0Projectslist / list_stream / get / create / update / archive / statuses✅ shipped
P0Project milestonesmilestones / create_milestone / update_milestone / delete_milestone✅ shipped
P0Labelslist / list_stream / create / update / delete / ensure (find-or-create)✅ shipped
P0Issue relationscreate / create_blocks / delete / of_issue (blocks / blocked-by views)✅ shipped
P0Commentslist_for_issue / list_for_issue_stream / create / create_on / update / delete✅ shipped
P0Typed filtersIssueFilter, ProjectFilter, TeamFilter, UserFilter, WorkflowStateFilter, IssueLabelFilter, CommentFilter, …✅ shipped
P0Escape hatchexecute_raw (arbitrary documents, same error taxonomy)✅ shipped
P1Cycleslist/get, issue scheduling🔲 planned
P1AttachmentsattachmentCreate, attachmentLinkURL🔲 planned
P1Project updatesstatus posts + health reporting🔲 planned
P1Batch issue updateissueBatchUpdate🔲 planned
P2Webhookspayload types + HMAC-SHA256 verification helper🔲 roadmap
P2Documents / initiatives / customer needs🔲 roadmap
P2OAuth🔲 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 via LinearClient::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 + RATELIMITED extension code) are retried after the header-indicated wait — these are rejected before execution, so retrying is always safe, mutations included. If the wait exceeds RetryConfig::max_rate_limit_wait (default 30s) the call fails fast with Error::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 issueCreate may have landed, and a blind retry double-creates. Opt in with RetryConfig::retry_mutations_on_transient if your caller dedupes.
  • GraphQL errors are fail-closed: if errors is non-empty the call returns Error::Api { .. } even when partial data was present (execute_raw is 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

FeatureDefaultDescription
rustls-tlsyesTLS via rustls with webpki roots
native-tlsnoTLS via the platform’s native stack
tracingnotracing 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.

Modules§

comments
Issue comment threads: read an issue’s comments and create, update, or delete comments (threading included).
issues
Issue CRUD, list/search, batch create, and label convenience operations — IssuesService, obtained via LinearClient::issues.
labels
Issue labels: listing, create/update/delete, and the LabelsService::ensure find-or-create convenience.
projects
Projects, project statuses, and project milestones.
relations
Issue relations — the blocks/blocked-by DAG edges between issues.
workspace
Teams, users, and workflow states — the workspace lookup surface every board and plan operation needs (resolving team keys, assignees, and state names to the IDs that issue and project mutations require).

Structs§

BooleanComparator
Comparator for boolean fields.
BooleanComparatorBuilder
Use builder syntax to set the inputs and finish with build().
CommentFilter
Filter for comments (CommentFilter input).
CommentFilterBuilder
Use builder syntax to set the inputs and finish with build().
CommentId
UUID of a Linear comment.
CycleId
UUID of a Linear cycle.
DateComparator
Comparator for date fields. Values are strings: ISO-8601 instants ("2026-07-01T00:00:00Z") or relative durations like "-P2W".
DateComparatorBuilder
Use builder syntax to set the inputs and finish with build().
ErrorExtensions
Linear’s extensions object on GraphQL errors.
GraphQlError
One GraphQL error object as returned by Linear.
IdComparator
Comparator for ID fields.
IdComparatorBuilder
Use builder syntax to set the inputs and finish with build().
IssueFilter
Filter for issues (IssueFilter input).
IssueFilterBuilder
Use builder syntax to set the inputs and finish with build().
IssueId
UUID of a Linear issue.
IssueLabelFilter
Filter for issue labels (IssueLabelFilter input).
IssueLabelFilterBuilder
Use builder syntax to set the inputs and finish with build().
IssueRelationId
UUID of a Linear issue relation.
IssueStub
Minimal reference to an issue.
LabelId
UUID of a Linear issue label.
LabelRef
Minimal reference to an issue label.
LinearClient
Async client for the Linear GraphQL API.
LinearClientBuilder
Builder for LinearClient. Obtain via LinearClient::builder.
MilestoneRef
Minimal reference to a project milestone.
NullableNumberComparator
Comparator for nullable number fields (e.g. priority, estimate).
NullableNumberComparatorBuilder
Use builder syntax to set the inputs and finish with build().
NumberComparator
Comparator for non-nullable number fields.
NumberComparatorBuilder
Use builder syntax to set the inputs and finish with build().
Organization
The workspace (organization) the API key belongs to.
OrganizationId
UUID of a Linear organization (workspace).
Page
One page of results plus its pagination metadata.
PageInfo
Relay-style page metadata returned by every Linear connection.
ProjectFilter
Filter for projects (ProjectFilter input).
ProjectFilterBuilder
Use builder syntax to set the inputs and finish with build().
ProjectId
UUID of a Linear project.
ProjectMilestoneFilter
Filter for project milestones (ProjectMilestoneFilter input).
ProjectMilestoneFilterBuilder
Use builder syntax to set the inputs and finish with build().
ProjectMilestoneId
UUID of a Linear project milestone.
ProjectRef
Minimal reference to a project.
ProjectStatusId
UUID of a Linear project status.
RateLimitInfo
Snapshot of Linear’s rate-limit budget headers, parsed from every response. The reset headers are UTC epoch milliseconds on the wire.
RelationExistsComparator
Comparator asserting the existence (or absence) of a relation.
RelationExistsComparatorBuilder
Use builder syntax to set the inputs and finish with build().
RetryConfig
Retry policy. Defaults are safe for non-idempotent mutations: Linear has no idempotency keys, so mutations are not retried on post-send transport errors or 5xx unless retry_mutations_on_transient is opted into. Rate-limit rejections happen before execution and are always retried (within max_rate_limit_wait).
StateRef
Minimal reference to a workflow state.
StringComparator
Comparator for String fields.
StringComparatorBuilder
Use builder syntax to set the inputs and finish with build().
TeamFilter
Filter for teams (TeamFilter input).
TeamFilterBuilder
Use builder syntax to set the inputs and finish with build().
TeamId
UUID of a Linear team.
TeamRef
Minimal reference to a team.
TemplateId
UUID of a Linear template.
TimelessDate
Linear’s TimelessDate scalar: a calendar date with no time component, serialized as "YYYY-MM-DD".
UserFilter
Filter for users (UserFilter input).
UserFilterBuilder
Use builder syntax to set the inputs and finish with build().
UserId
UUID of a Linear user.
UserRef
Minimal reference to a user.
Viewer
The user the API key authenticates as.
WorkflowStateFilter
Filter for workflow states (WorkflowStateFilter input).
WorkflowStateFilterBuilder
Use builder syntax to set the inputs and finish with build().
WorkflowStateId
UUID of a Linear workflow state.

Enums§

Error
Everything that can go wrong talking to the Linear API.
IssueRef
Reference to an issue where the API accepts a UUID or a human identifier such as "ENG-123": issue getters, issueUpdate’s id, and issueRelationCreate inputs.
IssueRelationType
The kind of an issue relation.
LinearErrorType
Typed classification of Linear API errors, mirroring @linear/sdk’s LinearErrorType. Wire strings vary in casing and separators (e.g. "authentication error"), so parsing is case-, space-, underscore-, and hyphen-insensitive; unmatched values are preserved in LinearErrorType::Unrecognized.
PaginationOrderBy
Sort order for paginated list queries.
Priority
Issue priority. On the wire this is an integer 0..=4: 0 = no priority, 1 = urgent, 2 = high, 3 = medium, 4 = low.
ProjectHealth
Project health as reported in project updates.
ProjectStatusType
The kind of a project status.
Undefinable
GraphQL nullable-input tri-state: absent (leave unchanged) / null (clear) / value (set).
WorkflowStateType
The kind of a workflow state (column category on a Linear board).

Functions§

collect_all
Drains paginate into a Vec, stopping after limit items when given.
nodes
Deserializes a GraphQL connection object {"nodes": [...]} directly into a Vec<T>.
paginate
Lazily streams every item across pages by repeatedly invoking fetch with the previous page’s end_cursor (None on the first call).

Type Aliases§

Result
Crate-wide result alias defaulting the error type to Error.