Connectors
Linear
Mention an agent in a Linear issue comment.
Quickstart
Create an API key
Create a Linear user for the agent, or use your own, and a personal API key for it with read and create-comments access.
Add the route
npm add @rivet-dev/pi rivetkit hono @linear/sdk
import { pi } from "@rivet-dev/pi";
import { Hono } from "hono";
import { setup } from "rivetkit";
import { type LinearComment, linearRoutes, postReply } from "./linear";
const agent = pi({
model: "anthropic/claude-opus-5-5",
state: { pending: [] as LinearComment[] },
vars: { replying: false },
actions: {
receive: async (c, comment: LinearComment) => {
c.state.pending.push(comment);
await c.schedule.after(0, "drain");
},
// Answers the oldest message. Messages that arrive mid-run wait their turn.
drain: async (c): Promise<void> => {
const comment = c.state.pending[0];
if (!comment || c.vars.replying) return;
c.vars.replying = true;
try {
const self = c.client<typeof registry>().agent.getForId(c.actorId);
await self.prompt(comment.text);
await postReply(comment, await self.getLastAssistantText());
} catch (error) {
c.log.error({ msg: "reply failed", error });
} finally {
c.state.pending.shift();
c.vars.replying = false;
}
if (c.state.pending.length > 0) await c.schedule.after(0, "drain");
},
},
});
export const registry = setup({ use: { agent } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.route("/linear", linearRoutes);
export default app;
import { createHmac, timingSafeEqual } from "node:crypto";
import { LinearClient } from "@linear/sdk";
import { Hono } from "hono";
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
export type LinearComment = { issueId: string; text: string };
const trigger = process.env.LINEAR_TRIGGER ?? "@agent";
const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });
const agentUserId = linear.viewer.then((viewer) => viewer.id);
const client = createClient<typeof registry>();
export const linearRoutes = new Hono();
linearRoutes.post("/webhook", async (c) => {
const body = await c.req.text();
if (!verifyLinearSignature(body, c.req.header("linear-signature") ?? "")) {
return c.text("invalid signature", 401);
}
const payload = JSON.parse(body);
if (Math.abs(Date.now() - payload.webhookTimestamp) > 60 * 1000) return c.text("stale webhook", 401);
const comment = payload.data;
if (
payload.type === "Comment" &&
payload.action === "create" &&
comment.issueId &&
comment.body.includes(trigger) &&
comment.userId !== (await agentUserId)
) {
const agent = client.agent.getOrCreate(["linear", comment.issueId]);
await agent.receive({ issueId: comment.issueId, text: comment.body });
}
return c.body(null, 200);
});
export async function postReply(comment: LinearComment, reply: string | undefined) {
await linear.createComment({ issueId: comment.issueId, body: reply || "I finished without a reply." });
}
function verifyLinearSignature(body: string, signature: string): boolean {
const secret = process.env.LINEAR_WEBHOOK_SECRET ?? "";
const expected = createHmac("sha256", secret).update(body).digest("hex");
return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Run the server with any runtime that serves a fetch handler, for example npx srvx --import tsx server.ts. See HTTP Server.
Set the environment
LINEAR_WEBHOOK_SECRET=lin_wh_...
LINEAR_API_KEY=lin_api_...
LINEAR_TRIGGER=@agent
ANTHROPIC_API_KEY=sk-ant-...
Add the webhook
In Linear’s Settings > Administration > API, which needs admin rights, create a webhook with the URL https://<your-worker>/linear/webhook and the Comments resource type. Copy its signing secret. Comment @agent on an issue.
How it works
- The route verifies the
Linear-SignatureHMAC against the raw body and rejects deliveries older than a minute. - Each Linear issue gets its own agent. Every comment with the trigger continues the same session.
- The route never answers the agent’s own comments, so replies do not loop.
- The route only hands the comment to the agent’s
receiveaction and returns. Thereceiveaction adds the message to the agent’s queue and schedulesdrain, which prompts the agent with the oldest message and posts the answer as a comment with the Linear SDK. A message that arrives while the agent is answering waits in the queue. The queue is saved with the agent, so replies still run if the request that delivered a message goes away.
Configure
| Variable | Purpose |
|---|---|
LINEAR_WEBHOOK_SECRET | Required. Verifies that deliveries come from Linear. |
LINEAR_API_KEY | Required. Posts replies as the agent’s Linear user. |
LINEAR_TRIGGER | Text that makes a comment start a reply. Defaults to @agent. |
ANTHROPIC_API_KEY | Required. The agent’s model key. For another provider, change model and set its key instead, such as OPENAI_API_KEY or OPENROUTER_API_KEY. See LLM API Keys. |
Supported events
| Linear event | Webhook path | What the route does |
|---|---|---|
Comment, create | /linear/webhook | Hands the comment to the issue’s agent, which replies with a comment. |