Skip to main content
Stateful Linear GraphQL API emulation on Linear’s published schema, with organizations, users, teams, workflow states, issues, comments, labels, projects, cycles, OAuth apps, tokens, webhooks, and agent sessions. @linear/sdk works against it unchanged with apiUrl pointed at the emulator’s /graphql. Reads and writes use in-memory state.

At a glance

Start

Linear is a default provider and listens on http://localhost:4011 when all services start; started alone, it uses http://localhost:4000. Without a seed config there is a Baseline organization, an admin ([email protected]) and a developer ([email protected]), an ENG team with Bug and Feature labels, a project and a cycle, an OAuth app (lin_example_client_id), and the token lin_test_admin. Point @linear/sdk at the emulator’s /graphql with apiUrl:

URL mapping

Auth

GraphQL accepts Authorization: Bearer <token> or a bare personal API key value. The default seeded token is lin_test_admin. Seeded tokens map to users or app actors. Scope checks are relaxed by default. Set linear.strict_scopes: true to require supported operation scopes such as read, write, issues:create, comments:create, and admin.

OAuth

  • GET /oauth/authorize — Authorize: local user picker
  • POST /oauth/authorize/callback — User picker callback: local callback used by the user picker
  • POST /oauth/token — Exchange token: authorization code, refresh token, and client credentials grants
  • POST /oauth/revoke — Revoke token: access or refresh tokens
OAuth supports the authorization code grant with PKCE, refresh tokens, revoke, client credentials, and actor=app tokens for local app-actor tests. OAuth apps can use actor: user or actor: app. The configured actor is authoritative. User actor apps use authorization code flows. App actor apps use the app install flow and can request client credentials tokens.

GraphQL

  • POST /graphql — Run a GraphQL query or mutation
  • GET /graphql — Run a GraphQL query from the query string: for tooling. Queries only; a mutation over GET gets HTTP 405
The endpoint serves Linear’s published GraphQL schema, vendored from @linear/sdk, so queries are validated as Linear validates them: a field, argument, or input key Linear does not have fails with HTTP 400. Any GraphQL error answers HTTP 400. The endpoint runs on the shared GraphQL kit. Supported queries:
  • viewer
  • organization
  • users, user
  • teams, team, teamMembership
  • workflowStates, workflowState
  • issues, issue
  • comments, comment
  • issueLabels, issueLabel
  • projects, project
  • cycles, cycle
  • webhooks, webhook
  • agentSessions, agentSession
Supported mutations:
  • issueCreate, issueUpdate, issueDelete, issueArchive, issueUnarchive
  • commentCreate, commentUpdate, commentDelete
  • issueLabelCreate, issueLabelUpdate, issueLabelDelete (label names are unique, ignoring case, within a team and against workspace labels: Duplicate label name)
  • issueAddLabel, issueRemoveLabel
  • webhookCreate, webhookDelete
  • agentSessionCreateOnIssue, agentSessionCreateOnComment, agentSessionUpdate
  • agentActivityCreate
Any other root field Linear has (for example initiatives or documents) fails with <Type>.<field> is not emulated by baseline. Connections use Relay-style cursors with nodes, edges, and pageInfo.

Fields the emulator does not model

@linear/sdk selects every scalar field of a model, so the emulator answers the whole schema. A field it does not model reads as an empty workspace would have it: null when nullable, otherwise false, 0, an empty string, list, or JSON object, or an empty connection. A non-null field that has no empty value (an object, enum, or date) fails with <Type>.<field> is not emulated by baseline rather than inventing one. Issue selections expose both numeric priority and Linear’s derived priorityLabel values: No priority, Urgent, High, Medium, and Low. Issues and comments created with createAsUser return a botActor with that name and displayIconUrl. Teams expose viewerMembership, viewerCanJoin, and viewerCanLeave. Team membership is not modeled, so the viewer counts as a member of every team. Initiatives are not modeled either, so ledInitiativeCount is always 0 and initiativesEnabled is false.

Filters and ordering

Connections take Linear’s filter inputs (IssueFilter, TeamFilter, UserFilter, and so on) with their comparators: eq, neq, in, nin, null, lt, lte, gt, gte, the string comparators (contains, startsWith, containsIgnoreCase, and the rest), and and / or. Relations filter through the related entity (team: { key: { eq: "ENG" } }), and collections take some, every, and length (labels: { some: { name: { eq: "Bug" } } }). Date comparators take ISO dates or durations relative to now, such as -P2W. An issue id comparator matches the UUID or the identifier (ENG-12). A filter key the schema has but the emulator does not model fails with IssueFilter.<key> is not emulated by baseline rather than matching everything. orderBy takes createdAt (the default) or updatedAt. Issue lists leave out archived and trashed issues unless the query passes includeArchived: true, and user lists leave out deactivated users unless it passes includeDisabled: true.

Mutations

  • issueUpdate(id:, input:) accepts labelIds, or addedLabelIds and removedLabelIds to change labels in place.
  • issueDelete moves the issue to the trash (archived, with trashed: true), as Linear does. issueDelete(id:, permanentlyDelete: true) removes it with its comments and agent sessions, and needs an admin.
  • webhookCreate requires resourceTypes.
  • A single lookup by an unknown ID fails with Entity not found: <Type>.
  • An input key Linear accepts but the emulator does not model (for example IssueCreateInput.estimate) fails with <Input>.<key> is not emulated by baseline instead of being dropped.

Agent sessions

Agent sessions follow Linear’s agent API. A session belongs to an app user: the caller when it authenticates as an app (actor: app), otherwise the first seeded app user, otherwise the caller. Sessions start pending. Activities move them: agentActivityCreate takes agentSessionId and content, and validates content per type (an action needs action and parameter; the others need body). Only thought and action activities may be ephemeral. agentSessionUpdate sets plan, summary (one line, 1 to 255 characters), externalLink, and externalUrls, addedExternalUrls, or removedExternalUrls. Creating a session, delegating an issue to an app user, or mentioning an app user in a comment sends an AgentSessionEvent webhook with action: created. A user’s prompt activity sends action: prompted with the activity.

Errors

Every error carries Linear’s extensions.type, which @linear/sdk maps to its error classes: invalid input (InvalidInputLinearError) for bad input and unknown IDs, forbidden (ForbiddenLinearError) for a missing scope, graphql error (GraphqlLinearError) for a query the schema rejects, and feature not accessible (FeatureNotAccessibleLinearError) for a field the emulator does not model, which also keeps extensions.code: NOT_EMULATED.

Webhooks

Create local webhook subscriptions through webhookCreate or seed config. Supported writes dispatch Linear-shaped payloads with these headers:
  • Linear-Delivery
  • Linear-Event
  • Linear-Signature

Seed config

Inspector

  • GET / — Inspector: tabbed view of issues, teams, users, projects, agent sessions, OAuth apps, tokens, webhook subscriptions, and webhook deliveries

Limits

Resolvers for the root fields not listed above, exact production rate limiting, notification inbox behavior, rich document APIs, customer APIs, initiative APIs, exact search relevance, and full production agent behavior are not implemented.

Conformance

See CONFORMANCE.md for every provider.