A read/write source over Linear's published GraphQL API.
Linear Issue maps to [Task], Project to [Project], Document to [Document],
IssueLabel and ProjectLabel to [Label], and WorkflowState.name is preserved
while its type (backlog, unstarted, started, completed, or canceled) maps to
the normalized status category. Issue relations/inverseRelations and
project relations provide native dependency traversal in both directions.
Label, workflow-state, project, and orphan filters are sent in the
issues(filter:)/projects(filter:) variables. Pagination uses Relay first and
after.
Every issue, project and document reports its own Linear web address as its
[Location], as a link rather than a path — the counterpart of a folder of Markdown
reporting the path of the file behind an item. It does not replace the url field
those types already carry; it is the same address said in the shape a reader can act on.
What this source declares, field by field
One verdict per field of [Capabilities]. A field is supported and proven when this
source applies it and a shared journey drives it against the real binary; the shared
table is crates/onetaskgraph/tests/e2e/fixtures.rs, the journeys are beside it, and
every_row_declares_exactly_what_its_plugin_reports is what keeps this list and
capabilities from parting.
| Field | Verdict |
|---|---|
projects |
Supported and proven. issues(filter:{project:{id:{eq:…}}}). |
documents |
Supported and proven. Linear's own first-class Document, read through documents(first:,after:,filter:) and document(id:), written through documentCreate/documentUpdate and taken back by documentDelete. See the ruling below on what a Linear document cannot hold. |
orphan_tasks |
Supported and proven. issues(filter:{project:{null:true}}). |
filter_by_label |
Supported and proven. labels:{some:{name:{inIgnoreCase:…}}} and {eqIgnoreCase:…} for what an item must carry, labels:{every:{name:{neqIgnoreCase:…}}} for what it must not. |
filter_by_status |
Supported and proven. state:{type:{in:[…]}}, over the WorkflowState.type vocabulary the category maps to. |
search_title |
Unsupported, and unimplemented rather than a limit of the API. See the ruling below. |
search_content |
Unsupported, and unimplemented rather than a limit of the API. See the ruling below. |
task_dependencies |
Supported and proven, in both directions: relations and inverseRelations. |
project_dependencies |
Supported and proven, in both directions, by the project relations of the same shape. |
max_page_size |
Supported and proven. 250, Linear's own connection maximum; every read pages with Relay first/after. |
Ruling: the two searches are unimplemented, not unsupportable
Linear's published API does offer issue search — searchIssues is a documented
operation of it — so there is no property of the remote service that makes a title-only
or a body-only match impossible here. What is true today is narrower and is recorded as
such: no production operation in this crate sends one, so declaring either predicate
Native would break capability rule 1, and Unsupported is the only honest
declaration for the code that exists.
The engine compensates correctly for both — it over-fetches and narrows, and the shared
journeys assert that this row returns the same rows every native row does with the plan
naming the engine — so the declaration is sound as well as honest. It is still a gap
rather than a limit, and reading it as a limit is what would leave it here forever.
Implementing it is tracked in docs/follow-ups.md.
Ruling: a Linear document carries no label, and that is Linear's
Unlike the two searches above, this one is a property of the remote service. The
types of Linear's published schema carrying a labels field are Issue, Project,
Team, Initiative and Organization; Document is not among them, re-observed
2026-09-01 and pinned in tests/fixtures/schema.graphql. So this source reports a
document's labels as none and refuses by name a document write carrying one, rather
than dropping it or standing a slot up beside a first-class type. The shared journey
table's row says so, and the shared document journeys drive that claim.
Two predicates therefore reach a fetched page rather than the documents(filter:)
variables, and both are still applied — which is what Native means here, and why
the declaration stays honest. Labels, for the reason above. And orphans, because
DocumentFilter.project is a ProjectFilter where IssueFilter.project is a
NullableProjectFilter: only the nullable one carries null:, so Linear cannot be
asked for the documents belonging to no project. The page-by-page walk asks for only
what is still owed, so neither predicate can make a read return more than the caller
asked for, and neither can drop a document the walk already fetched.
Caller metadata is canonical JSON in a trailing
<!-- onetaskgraph.metadata ... --> Markdown comment in the item's description. The
visible description is returned unchanged without that slot. Writes put the same
canonical encoding back beside the visible description, and use Linear issue/project
relations for same-source dependencies. Only cross-source far ends use the reserved
onetaskgraph.depends_on metadata key.
Fixture provenance is recorded in tests/fixtures/README.md. The ignored live lane
drives every field of the table above against Linear itself: it builds its own fixture
on the scratch team LINEAR_WRITE_TEAM names — two projects, one issue filed under
each, one filed under neither, two labels and two workflow states — because that shape
is what tells an honoured predicate from an ignored one, and a workspace where every
issue carries the label answers a filter the same way either way. The two searches are
asserted as what they are declared: the wider set, unnarrowed. Everything the lane
creates it deletes whether its assertions passed or failed, and it clears residue named
the way it names its own before it starts. A failed live cleanup is reported as a test
failure and may require manual deletion from that scratch team.