Skip to main content

Crate onetaskgraph_linear

Crate onetaskgraph_linear 

Source
Expand description

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.

FieldVerdict
projectsSupported and proven. issues(filter:{project:{id:{eq:…}}}).
documentsSupported 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.
commentsSupported and proven, as the issue’s own comments: read oldest first through issue(id:){comments(last:,before:)}, added with commentCreate, edited with commentUpdate and removed with commentDelete — each of the last two only once comment(id:) has placed the comment on that very issue. See the ruling below on the order and on the author.
orphan_tasksSupported and proven. issues(filter:{project:{null:true}}).
filter_by_labelSupported and proven. labels:{some:{name:{eqIgnoreCase:…}}} for what an item must carry — one per label, gathered under or: where any one of them will do — and labels:{every:{name:{neqIgnoreCase:…}}} for what it must not. Linear’s StringComparator has no case-insensitive list operator; see the note beside filter.
filter_by_statusSupported and proven, and spelled twice. An issue narrows with state:{type:{in:[…]}} over WorkflowState.type; a project narrows with status:{type:{in:[…]}} over ProjectStatusType, a different member of a different filter over a different vocabulary. See the ruling below.
search_titleUnsupported, and unimplemented rather than a limit of the API. See the ruling below.
search_contentUnsupported, and unimplemented rather than a limit of the API. See the ruling below.
task_dependenciesSupported and proven, in both directions: relations and inverseRelations.
project_dependenciesSupported and proven, in both directions, by the project relations of the same shape. Linear types every one of them dependency; see the ruling below on the edge that has no spelling here.
max_page_sizeSupported and proven. 100; every read pages with Relay first/after. Linear’s connection maximum is 250 and its complexity budget is the tighter bound — see MAX_PAGE_SIZE.

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

§Ruling: a comment is read backwards, and its author is Linear’s to record

The order. The contract owes a task’s comments oldest first, across pages, and Linear’s Issue.comments takes no sort direction — only orderBy, whose members are createdAt (the default) and updatedAt. Linear’s pagination documentation says results are “ordered by createdAt” and that “to get most recently updated resources, you can alternatively order by updatedAt”, which reads that ordering as newest first. So this source walks the connection from its far end: last with before, each page reversed, the next page’s cursor being startCursor while hasPreviousPage holds. Reversing within a page and walking backwards across them is what makes the whole walk oldest first rather than each page alone. That direction is inferred from the documentation’s wording rather than observed against the real API, which is the one reading here a live run has not yet confirmed; if Linear is found to list oldest first, the correction is this walk’s direction and nothing else.

The author. Linear records the user whose credential made the request as a comment’s author, and this source authenticates with an API key. CommentCreateInput.createAsUser exists but is, in Linear’s own words, “only available to OAuth applications creating comments in actor=app mode”, which a key is not. So a comment carrying an author is refused before any request is sent, naming why and what to do instead, rather than posted under a name other than the one it was given. An author read back is the user’s displayName, which Linear keeps unique within a workspace, and is absent when Linear names no user — a comment an integration or a bot wrote.

What “no such comment” means. An edit or a removal first asks comment(id:) which issue the comment is on, and answers “no such comment” — no mutation sent — unless it is the task’s own issue: a comment on another issue, on no issue at all, or trashed, is not a comment this task has. The body is Linear’s body, which its schema describes as markdown derived from a rich-text document, so what an add or an edit answers with is what Linear now holds rather than an echo of what was sent.

§Ruling: a project’s filter is not an issue’s, and neither is its status

Linear’s IssueFilter and ProjectFilter read as one filter over two kinds of row. They are two input types, and this source built one object for both until 2026-09-04, which put two members into projects(filter:) that Linear does not have there. It refused the first outright — Field "team" is not defined by type "ProjectFilter". Did you mean "lead"? — and would have refused the second next.

A project has no team; it has the teams it is accessible from, so the configured team reaches accessibleTeams:{some:{key:{eqIgnoreCase:…}}}. And a project’s status is not an issue’s state: the counterpart of IssueFilter.state is ProjectFilter.status, while ProjectFilter.state exists and is a bare StringComparator over something else. The two do not even share a vocabulary — ProjectStatus.type is the ProjectStatusType enum, backlog, planned, started, paused, completed, canceled, where a workflow state is backlog, unstarted, started, completed, canceled, triage. So planned is where unstarted would be, paused reads as in progress and has no issue counterpart, and a filter spelled in the other level’s words matches nothing while being refused by nothing.

Neither of those could be caught by reading a document, and that is the general lesson. A filter is built at runtime and handed over as $filter, so it appears in no operation this crate declares, and the two pinned-schema checks that parse those operations could not see it — Linear was the only reader, one refusal per round trip. every_variables_object_this_source_sends_conforms_to_the_pinned_schema closes that: it drives this source’s whole surface, records what really went out, and walks every variables object against the pinned type of the argument it stands at.

§Ruling: a Linear project relation is always an ordering

This one is Linear’s too, and the validator says so in as many words. Asked on 2026-09-04 for a project relation typed related — and separately blocks and dependsOn — the real API refused each with Argument Validation Error and constraints: {"isEnum": "type must be one of the following values: dependency"}. That enumeration has one member and it is a timeline dependency, which is why the input carries an anchor at each end at all.

So a project edge carrying no ordering has nowhere here to land, and this source refuses it by name before the write rather than sending a value Linear will reject or quietly promoting it to a dependency it does not mean. DependencyKind::Related keeps its issue-level spelling, related, because IssueRelationCreateInput really does take it: the two relations are different relations with different vocabularies, and each level’s read accepts only its own.

Which end of a project relation waits is carried by the two anchors and not by the two id slots — measured, not reasoned, from Linear’s own ProjectFilter.hasBlockedByRelations against relations written both ways round. tests/fixtures/README.md records the whole probe, and write_relations records why the pair this source sends is the oriented one.

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.

§Ruling: a task’s status is set by category, and delivery is not carried

set_task_status refuses draft, queued and unknown before any request, because no Linear workflow state is any of them, and an issue already in the category asked for is answered with its own state and nothing written. Otherwise it resolves the configured team’s first workflow state of that category’s type and sends issueUpdate with that stateId alone.

delivers and delivered_by are read out of the metadata slot when something put them there, and taken out of the caller’s metadata as they are. They are never written: Linear has no field for either, so a write carrying either list or either reserved key, and every set_delivered_by, is refused by name before any request.

Fixture provenance is recorded in tests/fixtures/README.md. The live journey in tests/live.rs 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.

Modules§

graphql
Exact GraphQL query documents issued by this plugin.

Structs§

LinearConfig
Configuration contains only the credential variable’s name, never its value.
Plugin
The Linear plugin factory.

Constants§

KIND
The plugin kind a linear source’s plugin: field names.
MAX_PAGE_SIZE
The largest page this source will ask Linear for, and the capability it declares.