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 { createRegistry } from "@earendil-works/pi-durable";
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",
registry: createRegistry(),
actions: {
// Schedules the answer and returns, so the route can respond in time.
receive: async (c, comment: LinearComment) => {
await c.schedule.after(0, "reply", comment);
},
reply: async (c, comment: LinearComment): Promise<void> => {
const self = c.client<typeof registry>().agent.getForId(c.actorId);
// A message sent during a run waits as a follow-up.
// The requestId stops a duplicate delivery from running twice.
const result = await self.prompt(comment.text, { requestId: comment.commentId });
await postReply(comment, result.status === "done" ? result.text : undefined);
},
},
});
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 = { commentId: string; 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({ commentId: comment.id, 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 conversation.
- 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 schedulesreply, which prompts the agent and posts the answer as a comment with the Linear SDK. The schedule is saved with the agent, so the reply still runs if the request that delivered the message goes away. - A message that arrives while the agent is answering waits as a follow-up, and gets its own reply when its run ends.
- The prompt’s
requestIdis the comment’s id, so a comment that Linear delivers twice runs the agent only once.
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. |