---
image: true
author: nathan-flurry
published: "2025-05-28"
category: guide
keywords: ["linear","agent","ai","starter"]
title: "Building Linear Agents in Node.js & Rivet: Full Walkthrough and Starter Kit"
description: "**In this guide, you'll learn how to build an AI-powered Linear agent that automates development tasks by generating code based on issue descriptions. You'll build a complete application that authenticates with Linear, receives webhooks, and responds to assignments + comments.**"
---

export const imgAuthLinear = { src: "https://assets.rivet.dev/website/blog/2025-05-28-building-linear-agents-in-node-js-and-rivet-full-walkthrough-and-starter-kit/auth-linear.png", width: 2908, height: 2106, format: "png" };
export const imgAppSetup = { src: "https://assets.rivet.dev/website/blog/2025-05-28-building-linear-agents-in-node-js-and-rivet-full-walkthrough-and-starter-kit/app-setup.png", width: 1202, height: 2378, format: "png" };

**In this guide, you'll learn how to build an AI-powered Linear agent that automates development tasks by generating code based on issue descriptions. You'll build a complete application that authenticates with Linear, receives webhooks, and responds to assignments + comments.**

This project is built with:

- Rivet's library to manage the agent & authentication state
- [Hono](https://hono.dev/) for the HTTP server
- [Linear SDK](https://developers.linear.app/docs/sdk/installation) for Linear API integration
- [AI SDK](https://ai-sdk.dev) for AI-powered responses

[View Source Code on GitHub →](https://github.com/rivet-dev/engine/tree/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter).

## TL;DR

The core functionality is in [`src/actors/issue-agent.ts`](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/issue-agent.ts), where the agent handles different types of Linear events and interfaces with the LLM.

## Walkthrough

The Linear integration works through the following steps:

- **Step 1: OAuth**: Users initiate the [OAuth flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow) to install your agent in their Linear workspace
- **Step 2: Webhooks**: Linear sends webhook events to your server when the agent is mentioned or assigned
- **Step 3: Agent processing**: Your server processes these events and routes them to the appropriate agent
- **Step 4: AI response**: The agent generates AI responses based on issue descriptions and comments

The system exposes three endpoints:

- `GET /connect-linear`: Initiates the Linear OAuth flow ([src/server/index.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/server/index.ts#L41))
- `GET /oauth/callback/linear`: OAuth callback endpoint ([src/server/index.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/server/index.ts#L82))
- `POST /webhook/linear`: Receives Linear webhook events ([src/server/index.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/server/index.ts#L116))

And maintains state using three main Rivet Actors:

1. **Issue Agent**: Handles Linear issue events and generates responses ([src/actors/issue-agent.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/issue-agent.ts#L11))
2. **Linear App User**: Manages authentication state for the application ([src/actors/linear-app-user.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/linear-app-user.ts#L6))
3. **OAuth Session**: Handles OAuth flow state ([src/actors/oauth-session.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/oauth-session.ts#L10))

<Tip>
	You can read more about actors [in the Rivet Actors documentation](https://rivet.dev/docs/actors).
</Tip>

We'll dive in to how these work together next.

### Authentication Flow

Before our agent can interact with a Linear workspace, we need to set up authentication using OAuth 2.0. This allows users to securely authorize our agent in their workspace without sharing their Linear credentials. Our app appears as a team member that can be mentioned and assigned to issues.

**Step 1: Initial OAuth Request**

The user is directed to `GET /connect-linear` ([src/server/index.ts](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/server/index.ts#L41)) to initiate the OAuth flow:
- We generate a secure session with a unique ID and nonce
- We store this state in an [`oauthSession`](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/oauth-session.ts#L10) Rivet Actor
- We redirect to Linear's authorization page with our OAuth parameters

```typescript src/server/index.ts
router.get("/connect-linear", async (c) => {
  // Setup session with a unique ID and nonce for security
  const sessionId = crypto.randomUUID();
  const nonce = openidClient.randomNonce();
  const oauthState = btoa(
    JSON.stringify({ sessionId, nonce } satisfies OAuthExpectedState),
  );

  // Create an actor to track this OAuth session
  await actorClient.oauthSession.create(sessionId, {
    input: { nonce, oauthState },
  });

  // Build redirect URL to authorize the agent with Linear
  const parameters: Record<string, string> = {
    redirect_uri: LINEAR_OAUTH_REDIRECT_URI,
    state: oauthState,
    scope: "read write app:assignable app:mentionable",
    actor: "app",  // This is a Linear "actor", not to be confused with Rivet Actor
  };
  const redirectTo: URL = openidClient.buildAuthorizationUrl(
    openidConfig,
    parameters,
  );

  return c.redirect(redirectTo.href);
});
```

For our OAuth scopes, we request:
- `read` - Read access to the workspace data
- `write` - Write access to modify issues and comments
- `app:assignable` - Allows the agent to be assigned to issues
- `app:mentionable` - Allows the agent to be mentioned in comments and issues

**Step 2: User Authentication**

The user is redirected to Linear and then:

- User authenticates the app with Linear
- Linear redirects back to our OAuth callback

<img src={imgAuthLinear.src} alt="Authorize with Linear" />

**Step 3: OAuth Callback**

When the user completes authentication, Linear redirects to our callback URL with an authorization code:

- We validate the state parameter for security
- We exchange the code for an access token
- We get the app user ID for the agent in this workspace (our unique identifier for this workspace)
- We store the access token in the [`linearAppUser`](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/linear-app-user.ts#L6) Rivet Actor

```typescript src/server/index.ts
router.get("/oauth/callback/linear", async (c) => {
  const stateRaw = c.req.query("state");
  const state = JSON.parse(atob(stateRaw!)) as OAuthExpectedState;

  // Validate state with our OAuth session actor
  const expectedState = await actorClient.oauthSession
    .get(state.sessionId)
    .getOAuthState();

  // Exchange code for access token
  const tokens: openidClient.TokenEndpointResponse =
    await openidClient.authorizationCodeGrant(
      openidConfig,
      new URL(c.req.url),
      { expectedState },
    );

  // Get the app user ID (unique ID for this agent in the workspace)
  const linearClient = new LinearClient({ accessToken: tokens.access_token });
  const viewer = await linearClient.viewer;
  const appUserId = viewer.id;

  // Save the access token in the linearAppUser actor
  await actorClient.linearAppUser
    .getOrCreate(appUserId)
    .setAccessToken(tokens.access_token);

  return c.text(`Successfully linked with app user ID ${appUserId}`);
});
```

After OAuth completes, our agent is fully integrated with the Linear workspace:
- Appears as a team member that can be assigned to issues
- Can be @mentioned in comments and issues  
- Receives webhook events when users interact with it

### Webhook Event Handling

Once a user has authorized our application in their Linear workspace, Linear sends webhook events to our endpoint whenever something happens that involves our agent. 

The server parses these events and routes them to the appropriate handlers in [`src/actors/issue-agent.ts`](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/issue-agent.ts#L11):

- **`issueMention`**: Triggered when the agent is mentioned in an issue description
- **`issueEmojiReaction`**: Triggered when someone reacts with an emoji to an issue
- **`issueCommentMention`**: Triggered when the agent is mentioned in a comment (our agent reacts with 👀 and generates a response)
- **`issueCommentReaction`**: Triggered when someone reacts to a comment where the agent is mentioned
- **`issueAssignedToYou`**: Triggered when an issue is assigned to the agent (updates status to "started" and generates code)
- **`issueUnassignedFromYou`**: Triggered when an issue is unassigned from the agent
- **`issueNewComment`**: Triggered when a new comment is added to an issue the agent is involved with
- **`issueStatusChanged`**: Triggered when the status of an issue changes

Here's how we handle webhooks in our server:

```typescript src/server/index.ts
router.post("/webhook/linear", async (c) => {
  const rawBody = await c.req.text();

  // Verify signature to validate this is sent from Linear
  const signature = c.req.header("linear-signature");
  const computedSignature = crypto
    .createHmac("sha256", LINEAR_WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");
  if (signature !== computedSignature) {
    throw new Error("Signature does not match");
  }

  // Send event to the agent
  const event: LinearWebhookEvent = JSON.parse(rawBody);
  if (event.type === "AppUserNotification") {
    const notification = event.notification;
    const issueId = event.notification.issueId;

    // Get or create agent for this issue
    const issueAgent = actorClient.issueAgent.getOrCreate(issueId, {
      createWithInput: { issueId },
    });

    // Forward event
    switch (notification.type) {
      case "issueMention":
        issueAgent.issueMention(event.appUserId, notification.issue);
        break;
      case "issueCommentMention":
        issueAgent.issueCommentMention(
          event.appUserId,
          notification.issue,
          notification.comment!,
        );
        break;
      case "issueAssignedToYou":
        issueAgent.issueAssignedToYou(
          event.appUserId,
          notification.issue,
        );
        break;
      // Additional cases handled here...
    }
  }

  return c.text("ok");
});
```

Now that we are handling webhook events correctly, we can implement the functionality of our agent.

### Agent Implementation

The issue agent is the brain of our application. It handles Linear events and generates AI responses. Importantly, it maintains conversation state by storing message history, allowing it to have context-aware conversations with users.

```typescript src/actors/issue-agent.ts
import { actor } from "actor-core";
import type { CoreMessage } from "ai";

interface IssueAgentState {
  messages: CoreMessage[];
}

export const issueAgent = actor({
  state: {
    messages: [],
  } as IssueAgentState,
  actions: {
    // Simple implementation of the issue assignment handler
    issueAssignedToYou: async (c, appUserId: string, issue: WebhookIssue) => {
      // Get the Linear client for this app user
      const linearClient = await buildLinearClient(appUserId);

      // Generate response based on the issue description
      const fetchedIssue = await linearClient.issue(issue.id);
      const response = await prompt(
        c,
        `I've been assigned to issue: "${issue.title}". The description is:\n${fetchedIssue.description}`
      );
      
      // Post the response as a comment
      await linearClient.createComment({
        issueId: issue.id,
        body: response,
      });
    },
    // Additional handlers for other events...
  },
});
```

<Tip>
	In your own implementation, you can add extra state to the agent, such as correlated GitHub issues, data from other providers, and more.
</Tip>

When generating responses, the agent calls the AI SDK with a history of all messages:

```typescript src/actors/issue-agent.ts
async function prompt(c: ActionContextOf<typeof issueAgent>, content: string) {
  // Add the user's message to the conversation history
  c.state.messages.push({ role: "user", content });

  // Generate a response using the full conversation history
  const { text, response } = await generateText({
    model: anthropic("claude-4-opus-20250514"),
    system: SYSTEM_PROMPT,
    messages: c.state.messages,
  });

  // Add the AI's response to the conversation history
  c.state.messages.push(...response.messages);

  return text;
}
```

### Demo

Now the authentication, webhooks, and agents are all wired up correctly, we can see everything working together in action:

<iframe className="mx-auto" width="560" height="560" 
src="https://www.youtube.com/embed/GNsKmW6_44M" 
frameborder="0" 
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
allowfullscreen>
</iframe>

## Building Your Own Agents

### Prerequisites

Before getting started with building your own Linear agent, make sure you have:

- Node.js (v18+)
- Linear account and API access
- Anthropic API key (can be swapped for any [AI SDK provider](https://ai-sdk.dev/docs/foundations/providers-and-models))
- [ngrok](https://ngrok.com/download) for exposing your local server to the internet

### Setup

1. Clone the repository and navigate to the example:
   ```bash
   git clone https://github.com/rivet-dev/engine.git
   cd rivet/examples/linear-agent-starter
   ```

2. Install dependencies:
   ```bash
   npm install
   ```
3. Set up ngrok for webhook and OAuth callback handling:

   ```bash
   # With a consistent URL (recommended)
   ngrok http 5050 --url=YOUR-NGROK-URL
   
   # Or without a consistent URL
   ngrok http 5050
   ```
   
4. Create a Linear OAuth application:
   1. Go to to [Linear's create application page](https://linear.app/settings/api/applications/new)
   2. Enter your _Application name_, _Developer name_, _Developer URL_, _Description_, and _GitHub username_ for your agent
   3. Set _Callback URL_ to `https://YOUR-NGROK-URL/oauth/callback/linear` (replace `YOUR-NGROK-URL` with your actual [ngrok URL](https://ngrok.com/docs/universal-gateway/domains/))
      * This URL is where Linear will redirect after OAuth authorization
   4. Enable webhooks
   5. Set _Webhook URL_ to `https://YOUR-NGROK-URL/webhook/linear` (use the same ngrok URL)
      * This URL is where Linear will send events when your agent is mentioned or assigned
   6. Enable _Inbox notifications_ webhook events
   7. Create the application to get your _Client ID_, _Client Secret_, and webhook _Signing secret_
   
   <img className="mx-auto w-auto h-[800px]" src={imgAppSetup.src} alt="Linear App Setup" />

5. Create a `.env.local` file with your credentials:
   ```text .env.local
   LINEAR_OAUTH_CLIENT_ID=<client id>
   LINEAR_OAUTH_CLIENT_AUTHENTICATION=<client secret>
   LINEAR_OAUTH_REDIRECT_URI=https://YOUR-NGROK-URL/oauth/callback/linear
   LINEAR_WEBHOOK_SECRET=<webhook signing secret>
   ANTHROPIC_API_KEY=<your_anthropic_api_key>
   ```
   
   Remember to replace `YOUR-NGROK-URL` with your actual ngrok URL (without the https:// prefix).

6. Run the development server:

   ```bash
   npm run dev
   ```

   The server will start on port 5050. Visit http://127.0.0.1:5050/connect-linear to add the agent to your workspace.

### Adding Your Own Functionality

The core of the agent is in [`src/actors/issue-agent.ts`](https://github.com/rivet-dev/engine/blob/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter/src/actors/issue-agent.ts#L11). You can customize:

1. **Event Handlers**: Modify actions for different Linear events:
   
   - `issueMention`: When the agent is mentioned in an issue description
   - `issueEmojiReaction`: When someone reacts with an emoji to an issue
   - `issueCommentMention`: When the agent is mentioned in a comment
   - `issueCommentReaction`: When someone reacts to a comment where the agent is mentioned
   - `issueAssignedToYou`: When an issue is assigned to the agent
   - `issueUnassignedFromYou`: When an issue is unassigned from the agent
   - `issueNewComment`: When a new comment is added to an issue the agent is involved with
   - `issueStatusChanged`: When the status of an issue changes

   For example, you could implement intelligent emoji reactions:

   ```typescript src/actors/issue-agent.ts
   async issueEmojiReaction(c, appUserId, issue, emoji) {
     const linearClient = await buildLinearClient(appUserId);
     
     // Respond with the same emoji as a form of acknowledgment
     await linearClient.createReaction({
       issueId: issue.id,
       emoji: emoji,
     });
     
     // Or conditionally respond based on specific emojis
     if (emoji === "🔥") {
       await linearClient.createComment({
         issueId: issue.id,
         body: "Thanks for the encouragement! I'll prioritize this task.",
       });
     }
   }
   ```

2. **AI Prompt**: Customize the system prompt to change the agent's behavior:
   ```typescript src/actors/issue-agent.ts
   const SYSTEM_PROMPT = `
   You are a code generation assistant for Linear. Your job is to:
   
   1. Read issue descriptions and generate appropriate code solutions
   2. Iterate on your code based on comments and feedback
   3. Provide brief explanations of your implementation
   
   When responding:
   - Always provide the full requested code
   - Do not exclude parts of the code, always include the full code
   - Focus on delivering working code that meets requirements
   - Keep explanations concise and relevant
   - If no language is specified, use TypeScript
   
   Your goal is to save developers time by providing ready-to-implement solutions.
   `;
   ```

3. **AI Model**: Change the model used for generating responses:
   ```typescript src/actors/issue-agent.ts
   const { text, response } = await generateText({
     model: anthropic("claude-4-opus-20250514"), // Change to your preferred model
     system: SYSTEM_PROMPT,
     messages: c.state.messages,
   });
   ```

### Recommendations for Agent Experiences 

When building a Linear agent, consider these practices for a better developer experience:

- **Immediate Acknowledgment**: Always acknowledge when mentioned or assigned to an issue with a comment or reaction
- **Status Updates**: Move unstarted issues to "started" status when beginning work
- **Clear Communication**: Respond when work is complete, leaving a comment to trigger notifications

## Full Source Code

You can find the complete source code for this Linear agent starter kit on GitHub:

[View Source Code on GitHub →](https://github.com/rivet-dev/engine/tree/e13e6e95c56ea63bc73312fa7d01a647412ac507/examples/linear-agent-starter)

