Linear

Automate Linear issue comment threads with bot responses. Supports both standard comments mode and Linear's app-actor agent sessions.

Create a new bot

Scaffold a Next.js bot with the Linear adapter using create-chat-sdk:

pnpm create chat-sdk@latest my-bot --adapter linear memory -y

The generated app uses the Memory state adapter for local development. To add the adapter to an existing app, start at Install.

Install

pnpm add @chat-adapter/linear @chat-adapter/state-memory

Quick start

The adapter auto-detects credentials from LINEAR_API_KEY, LINEAR_ACCESS_TOKEN, LINEAR_CLIENT_CREDENTIALS_*, or LINEAR_CLIENT_ID/LINEAR_CLIENT_SECRET, plus LINEAR_WEBHOOK_SECRET and LINEAR_BOT_USERNAME.

For managed credentials and webhook verification, see Vercel Connect.

lib/bot.ts
import { Chat } from "chat";
import { createLinearAdapter } from "@chat-adapter/linear";
import { createMemoryState } from "@chat-adapter/state-memory";

export const bot = new Chat({
  userName: "my-bot",
  adapters: {
    linear: createLinearAdapter(),
  },
  state: createMemoryState(),
});

bot.onNewMention(async (thread, message) => {
  await thread.post("Hello from Linear!");
});

The memory state adapter keeps subscriptions and locks in process memory, which suits local development. Use Redis or PostgreSQL in production.

Then create the webhook route:

app/api/webhooks/linear/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function POST(request: Request): Promise<Response> {
  return bot.webhooks.linear(request, {
    waitUntil: (task) => after(() => task),
  });
}

The adapter defaults to mode: "comments" and handles Comment webhooks. For Vercel Connect and Linear app-actor installations, we recommend explicitly setting mode: "agent-sessions" to handle AgentSessionEvent webhooks, including mentions and replies within a session.

Authentication

Vercel Connect

Use Vercel Connect to source the Linear access token at runtime instead of storing a long-lived token or OAuth secret. The connectLinearAdapter() helper from @vercel/connect/chat wires an accessToken resolver and a webhookVerifier for Connect trigger-forwarded webhooks:

import { createLinearAdapter } from "@chat-adapter/linear";
import { connectLinearAdapter } from "@vercel/connect/chat";

createLinearAdapter({
  ...connectLinearAdapter("linear/acme-linear"),
  mode: "agent-sessions",
});

We recommend agent sessions for Vercel Connect bots. Enable Agent session events on the Linear app and use an app-actor installation. Omitting mode keeps the "comments" default; existing bots do not need to change their configuration. Changing the adapter mode does not change the app's webhook subscriptions or permissions.

accessToken accepts a string or () => string | Promise<string> resolver invoked per API call. When webhookVerifier is set it takes precedence over webhookSecret and LINEAR_WEBHOOK_SECRET.

Create the connector and attach it to your project with the Vercel CLI. --trigger-path registers the Linear webhook route as the trigger destination, and vercel env pull downloads a development OIDC token for local runs:

vercel link
vercel connect create linear --name acme-linear --triggers
vercel connect attach linear/acme-linear --triggers --trigger-path /api/webhooks/linear
vercel env pull

Without --trigger-branch, Connect forwards webhooks to your production deployment. See Vercel Connect for the full setup.

Personal API key

A personal API key suits personal projects and single-workspace bots. Actions are attributed to you as an individual.

  1. Go to Settings then Security & Access.
  2. Under Personal API keys, click Create key.
  3. Choose Only select permissions and enable Create issues and Create comments.
  4. Set LINEAR_API_KEY.
createLinearAdapter({ apiKey: process.env.LINEAR_API_KEY! });

OAuth access token

Use this when your app already manages the OAuth flow:

createLinearAdapter({ accessToken: process.env.LINEAR_ACCESS_TOKEN! });

Multi-tenant OAuth installs

Use top-level clientId / clientSecret for Slack-style multi-tenant installs. Each Linear workspace install is stored separately, webhook requests resolve the correct workspace token by organizationId, and withInstallation() lets you target a specific organization outside webhook handling.

  1. Go to Settings then API then Applications.
  2. Create the OAuth2 application.
  3. Note the Client ID and Client Secret.
const adapter = createLinearAdapter({
  clientId: process.env.LINEAR_CLIENT_ID!,
  clientSecret: process.env.LINEAR_CLIENT_SECRET!,
  mode: "agent-sessions",
});

await bot.initialize();
const { organizationId } = await adapter.handleOAuthCallback(request, {
  redirectUri: process.env.LINEAR_REDIRECT_URI!,
});

await adapter.withInstallation(organizationId, async () => {
  await adapter.postMessage("linear:issue-id", "Hello from a background job");
});

Single-tenant client credentials

Client credentials give the bot an app identity without multi-tenant installs. The adapter fetches and refreshes the token automatically.

createLinearAdapter({
  clientCredentials: {
    clientId: process.env.LINEAR_CLIENT_CREDENTIALS_CLIENT_ID!,
    clientSecret: process.env.LINEAR_CLIENT_CREDENTIALS_CLIENT_SECRET!,
    scopes: ["read", "write", "comments:create", "issues:create"],
  },
  mode: "agent-sessions",
});

Configuration

PropTypeDefaultDescription
apiKey?string
Personal API key. Auto-detected from `LINEAR_API_KEY`.
accessToken?string | (() => string | Promise<string>)
OAuth access token. Accepts a string, or (Vercel Connect) a resolver invoked per API call. Auto-detected from `LINEAR_ACCESS_TOKEN`.
clientId?string
Multi-tenant OAuth client ID. Auto-detected from `LINEAR_CLIENT_ID`.
clientSecret?string
Multi-tenant OAuth client secret. Auto-detected from `LINEAR_CLIENT_SECRET`.
encryptionKey?string
AES-256-GCM key for encrypting stored OAuth tokens. Auto-detected from `LINEAR_ENCRYPTION_KEY`.
clientCredentials?ClientCredentialsConfig
Single-tenant client credentials config (`clientId`, `clientSecret`, optional `scopes`).
mode?"comments" | "agent-sessions""comments"
Set agent-sessions for the recommended Vercel Connect setup with a Linear app-actor installation.
webhookSecret?string
Webhook signing secret. Auto-detected from `LINEAR_WEBHOOK_SECRET`.
webhookVerifier?(request, body) => unknown | Promise<unknown>
Custom verifier used in place of `webhookSecret` (e.g. Vercel Connect OIDC). Takes precedence over the secret; required in Connect mode.
userName?string"linear-bot"
Bot display name. Auto-detected from `LINEAR_BOT_USERNAME`.
apiUrl?string
Override the Linear GraphQL API base URL.

One of apiKey, accessToken (string or Vercel Connect resolver), top-level clientId/clientSecret, or clientCredentials is required, plus either webhookSecret or a webhookVerifier.

Environment variables

VariableRequiredDescription
LINEAR_API_KEYOne auth mode requiredPersonal API key.
LINEAR_ACCESS_TOKENOne auth mode requiredOAuth access token.
LINEAR_CLIENT_IDOne auth mode requiredMulti-tenant OAuth client ID. Use with LINEAR_CLIENT_SECRET.
LINEAR_CLIENT_SECRETOne auth mode requiredMulti-tenant OAuth client secret. Use with LINEAR_CLIENT_ID.
LINEAR_CLIENT_CREDENTIALS_CLIENT_IDOne auth mode requiredSingle-tenant client credentials client ID. Use with LINEAR_CLIENT_CREDENTIALS_CLIENT_SECRET.
LINEAR_CLIENT_CREDENTIALS_CLIENT_SECRETOne auth mode requiredSingle-tenant client credentials client secret.
LINEAR_CLIENT_CREDENTIALS_SCOPESNoComma-separated scopes for single-tenant client credentials.
LINEAR_ENCRYPTION_KEYNoBase64-encoded 32-byte key for encrypting stored OAuth tokens.
LINEAR_WEBHOOK_SECRETYes, unless using a webhookVerifierWebhook signing secret.
LINEAR_BOT_USERNAMENoBot display name. Defaults to linear-bot.

Webhooks

Webhook management requires workspace admin access. If you don't see the API settings page, ask a workspace admin.

  1. Go to Settings then API and click Create webhook.
  2. Set the URL to https://your-domain.com/api/webhooks/linear.
  3. Copy the Signing secret to LINEAR_WEBHOOK_SECRET.
  4. Under Data change events, select Comments (required for mode: "comments"), Agent session events (required for mode: "agent-sessions"), Issues, and optionally Emoji reactions.
  5. Choose a team selection and click Create webhook.

Making the bot @-mentionable

To make the bot appear in Linear's @-mention dropdown as an Agent:

  1. In your OAuth app settings, enable Agent session events under webhooks.
  2. Have a workspace admin install the app with actor=app and the app:mentionable scope:
https://linear.app/oauth/authorize?
  client_id=your_client_id&
  redirect_uri=https://your-domain.com/callback&
  response_type=code&
  scope=read,write,comments:create,issues:create,app:mentionable&
  actor=app

Once installed with actor=app, set mode: "agent-sessions" so the adapter treats AgentSessionEvent as the entrypoint:

  • onNewMention fires from session-created events.
  • thread.startTyping() sends an ephemeral Linear thought.
  • thread.post(stream) uses agent activities and session plan updates.
  • Session threads are append-only; sent.edit() / sent.delete() are not supported there.

See the Linear Agents docs for full details.

Token encryption

For multi-tenant OAuth installs, pass a base64-encoded 32-byte key as encryptionKey (or set LINEAR_ENCRYPTION_KEY) to encrypt stored access and refresh tokens at rest:

openssl rand -base64 32

When encryptionKey is set, setInstallation() encrypts tokens before writing to the configured state adapter. Existing plaintext records continue to work, so you can roll the key in without flushing installs.

Direct API client

Access the underlying LinearClient through .linearClient:

const linear = bot.getAdapter("linear").linearClient;
const issue = await linear.issue("ENG-123");

API key, access token, and single-tenant client-credentials modes return the same client anywhere. Multi-tenant OAuth requires webhook handler context.

The previous .client getter still works as a deprecated alias for .linearClient.

Message history

For agent-session threads, fetchMessages() throws a ValidationError if the session has no associated issue or its issue ID does not match the issue ID in the thread ID. The adapter checks this before fetching session comments or activities.

Comment mutations

Comment edits, deletions, and reactions retrieve the target comment before writing. The adapter throws a ValidationError if the comment doesn't exist, has no issue, belongs to another issue, or is outside the requested comment thread. The connection needs read access to verify ownership, in addition to the permissions required for the write.

Thread IDs

Linear has four thread variants:

TypeDescriptionThread ID format
Issue-levelTop-level comments on an issuelinear:{issueId}
Comment threadReplies nested under a commentlinear:{issueId}:c:{commentId}
Agent session on issueApp-actor session on an issuelinear:{issueId}:s:{agentSessionId}
Agent session on comment threadApp-actor session on a comment threadlinear:{issueId}:c:{commentId}:s:{agentSessionId}

When a user writes a comment, the bot replies within the same comment thread.

Feature support

Messaging

FeatureSupported
Post message
Message replies
Edit messagePartial
Delete messagePartial
File uploads
StreamingAgent sessions / Post+Edit
Scheduled messages

Rich content

FeatureSupported
Card formatMarkdown
Buttons
Link buttons
Select menus
TablesGFM
Charts
Fields
Images in cards
Modals

Conversations

FeatureSupported
Slash commands
Mentions
Add reactions
Remove reactionsPartial
Typing indicatorAgent sessions
Mark as read
Message edit events
Message delete events
DMs
Ephemeral messages
User lookup
Parent subject
Native client
Custom API endpoint

Message history

FeatureSupported
Fetch messages
Fetch single message
Fetch thread info
Fetch channel messages
List threads
Fetch channel info
Post channel message

More adapters