lineark is four crates that form a clean pipeline:
- lineark-codegen reads Linear API's GraphQL schema and generates typed Rust code into the SDK
- lineark-sdk combines a small hand-written core with the generated types, queries, and mutations into a cohesive and reusable client library
- lineark-derive provides
#[derive(GraphQLFields)]— a proc macro for custom lean types with zero overfetching (re-exported bylineark-sdk, no extra dependency needed) - lineark consumes the SDK as a normal library — zero GraphQL, typed method calls with custom lean types via
#[derive(GraphQLFields)]
graph TD
subgraph "Schema Sources"
Schema["schema.graphql<br/><i>Linear's GraphQL SDL</i>"]
Ops["operations.toml<br/><i>allowlist</i>"]
end
Codegen["lineark-codegen"]
Schema & Ops --> Codegen
Derive["lineark-derive<br/><i>#[derive(GraphQLFields)]</i>"]
subgraph SDK["lineark-sdk"]
Generated["generated/<br/><i>types, queries, mutations,<br/>client_impl</i>"]
FieldSel["field_selection.rs<br/><i>GraphQLFields trait</i>"]
end
Codegen -- "writes .rs files" --> Generated
Derive -- "proc macro" --> SDK
CLI["lineark CLI"]
CLI -- "client.issues().filter(f).send()" --> SDK
API["Linear GraphQL API"]
SDK -- "HTTP POST" --> API
The codegen crate parses Linear's GraphQL schema (vendored as schema.graphql) using apollo-parser, then emits seven Rust source files into the SDK:
graph TD
SDL["schema.graphql<br/><i>Linear's GraphQL SDL</i>"]
TOML["operations.toml<br/><i>allowlist</i>"]
SDL & TOML --> Parse["parser::parse()<br/><i>apollo-parser CST → simplified structs</i>"]
Parse --> Always & Gated
subgraph "Always generated (full schema)"
Always["emit_scalars · emit_enums<br/>emit_types · emit_inputs"]
end
subgraph "Gated by operations.toml"
Gated["emit_queries · emit_mutations"]
end
Always --> Out1["scalars.rs · enums.rs<br/>types.rs · inputs.rs"]
Gated --> Out2["queries.rs · mutations.rs<br/>client_impl.rs"]
Out1 & Out2 --> FMT["prettyplease + cargo fmt"]
FMT --> Dir["lineark-sdk/src/generated/"]
Types, enums, scalars, and inputs are always fully generated from the schema. Queries and mutations are gated by operations.toml — only explicitly listed operations get code emitted. This keeps the SDK surface incremental and intentional.
The SDK has a small hand-written core and a large generated layer:
graph TD
Consumer["Your code calls:<br/><b>client.issues().filter(f).first(50).send()</b>"]
Consumer --> Impl
subgraph lineark-sdk
Impl["client_impl.rs<br/><i>impl Client — thin delegation<br/>generated by codegen</i>"]
Impl --> QM
subgraph "Generated by codegen"
QM["queries.rs / mutations.rs<br/><i>generic fns + builders<br/>T: DeserializeOwned + GraphQLFields</i>"]
Types["types.rs / inputs.rs / enums.rs<br/><i>Issue, Team, IssueCreateInput,<br/>IssueFilter, IssuePriority, ...<br/>+ impl GraphQLFields for each type</i>"]
end
QM -- "uses types for<br/>args and return values" --> Types
QM --> Client
subgraph "Hand-written (~200 LOC)"
Client["client.rs — Client struct<br/><i>execute() · execute_connection()</i>"]
Support["auth.rs · error.rs · pagination.rs<br/>field_selection.rs"]
end
Client --> Support
end
Client -- "HTTP POST" --> API["Linear GraphQL API"]
The key trick: Client is defined in hand-written client.rs, but codegen adds methods to it via a separate impl Client block in client_impl.rs. Rust's open impl blocks make this seamless — consumers see one unified Client type with both hand-written and generated methods.
All query/mutation methods are generic over T: DeserializeOwned + GraphQLFields. The generated types implement GraphQLFields automatically (via codegen), and consumers can define custom lean structs with #[derive(GraphQLFields)] to fetch only the fields they need — the derive macro is re-exported by lineark-sdk, so use lineark_sdk::GraphQLFields gives you both the trait and the macro with no extra dependency. Custom types must include #[graphql(full_type = X)] pointing to the corresponding generated type — this is required for the query's type constraint and also validates fields at compile time.
The CLI is a pure consumer of the SDK. It has zero GraphQL strings and uses custom lean types with #[derive(GraphQLFields)] to fetch only the fields it needs:
sequenceDiagram
participant User
participant CLI as lineark CLI
participant SDK as lineark-sdk
participant API as Linear API
User->>CLI: lineark issues list --team ENG --mine
CLI->>CLI: Parse args (clap)
CLI->>SDK: resolve_team_id("ENG")
SDK->>API: teams(first: 250)
API-->>SDK: Connection<Team>
SDK-->>CLI: team UUID
CLI->>CLI: Build IssueFilter via serde_json
CLI->>SDK: client.issues::<IssueRow>().filter(f).first(50).send()
SDK->>SDK: Build GraphQL query + variables
SDK->>API: POST /graphql
API-->>SDK: JSON response
SDK-->>SDK: Deserialize into Connection<IssueRow>
SDK-->>CLI: Connection<IssueRow>
CLI->>CLI: Format as table or JSON
CLI-->>User: Output
Each CLI command module follows the same pattern:
- Parse command-line args
- Resolve human-friendly names to UUIDs (teams, users, labels, projects, cycles, issues)
- Call SDK builder methods
- Format output (tables for terminal, JSON when piped)