If you already live in Linear but your team keeps pinging you on Slack, WhatsApp, and Discord, wiring Linear into OpenClaw closes the loop. One set of slash commands in chat creates issues, moves them through cycles, and fires status digests—no tab-switching, no duplicate state. This post walks through a production-grade OpenClaw → Linear integration: API auth, command routing, cycle automation, and a cron job that drops a tidy report every morning.
Why bother wiring Linear into OpenClaw?
Linear nails product-engineering workflows, but it assumes you live in the Linear UI. Most startups don't—engineering lives in Slack, support is on WhatsApp, founders DM on Telegram. That context-switch shows up in lead time metrics. OpenClaw acts as the universal agent in those channels. By letting the agent talk to Linear over its GraphQL API, we get:
- Instant issue capture – pull a message thread into a bug with one command.
- Friction-free updates – change status or assignee without opening a browser tab.
- Automated cycle housekeeping – close stale tickets or bump carry-overs.
- Status broadcasts – daily or ad-hoc summary posts, readable from any messaging client.
Everything runs in Node 22+ because that’s what OpenClaw targets. Tested with Linear API v2024-06-15 and OpenClaw v3.7.2 (gateway) / v3.7.2 (daemon).
Prerequisites and TL;DR setup
Short version for the impatient:
- Generate a Linear API key with
readandwritescopes. - Install or upgrade OpenClaw:
npm install -g openclaw@3.7.2
- Enable the built-in Linear connector:
claw plugins install @openclaw/linear
claw plugins enable linear
- Insert the token into the gateway secrets store (Vault, dotenv, or ClawCloud UI).
- Reload the daemon and test with
/linear mein chat.
The rest of the article fills in every sharp edge those five lines hide.
Getting your Linear API token the right way
Linear makes it easy to mint personal API keys, but production bots shouldn’t ride on a human account—they die the second an employee off-boards. We’ll create a dedicated “OpenClaw Bot” user and a non-expiring key:
- In Linear, click your avatar → Workspace settings → Members → Invite. Use
bot+openclaw@your-domain.com. - Log in as that user once, accept the invite, and disable two-factor (bots with SMS is never fun).
- Head to Settings → API, hit New Token, name it
openclaw-daemon, scopesreadandwrite. Copy the value—Linear never shows it again.
Now drop it into your secret manager. Locally I use .env with direnv:
echo "LINEAR_API_KEY=lin_api_0123456789abcdef" >> .env
On ClawCloud the UI exposes a Secrets tab—same key, same var name.
Wiring the integration in OpenClaw (gateway & daemon)
OpenClaw extensions live in two places:
- The gateway handles HTTP/web UI and inbox-style memories.
- The daemon executes jobs, schedules, and long-running tasks.
The Linear plugin ships with both halves; you just need to enable and configure it.
Install the plugin
claw plugins install @openclaw/linear@1.1.0 # pin the version for reproducibility
The plugin declares a schema snippet for claw.config.js:
// claw.config.js
module.exports = {
integrations: {
linear: {
token: process.env.LINEAR_API_KEY,
defaultTeam: "ENG", // Linear team key, not display name
defaultCycle: "current" // or a specific cycle number
}
}
};
Hot-reload the gateway:
claw gateway restart
The daemon will pick up new jobs on its own, or force it:
claw daemon restart
Health check:
claw linear ping
Expect JSON with your workspace name.
Creating and updating issues directly from chat
Linear’s GraphQL schema is strict—missing the required teamId blows up with a 400. The plugin hides that. In a Slack channel where the OpenClaw bot is present:
/linear create "Payment webhook returns 500" --priority high --labels backend,urgent
The agent flows:
- Parses the command in OpenClaw’s command router.
- Looks up
ENGteam ID via cached config. - Calls
issueCreatemutation. - Replies with the new issue URL.
Updating works the same way:
/linear update LIN-231 --state done --comment "Fixed in commit 8ee3ad7"
The agent maps friendly args to GraphQL enums (completedStateId, commentCreate). For bulk edits we lean on piping:
/search "label:bug status:triage" | /linear bulk --state in-progress
The first command runs a vector search on OpenClaw’s memory of chat logs; the pipe passes the result (issue IDs) to the bulk updater. Feels magic, is just Unix philosophy in chat.
Automating cycle and status management
Nobody enjoys closing last cycle’s carry-overs on Monday. We’ll let the daemon do it.
Auto-close done tickets
// schedules/closeDone.js
module.exports = {
cron: "0 2 * * 1", // every Monday 02:00 UTC
job: async ({ linear }) => {
const doneStateId = await linear.stateId("done");
const lastCycle = await linear.currentCycle({ offset: -1 });
const issues = await linear.query({
teamId: linear.config.defaultTeamId,
cycleId: lastCycle.id,
stateId: doneStateId
});
await Promise.all(issues.map(i => linear.archive(i.id)));
return `Archived ${issues.length} issues from ${lastCycle.name}`;
}
};
Drop that file into openclaw/schedules, restart the daemon, done. The helper linear object comes from plugin dependency injection—no extra imports required.
Move carry-overs to the current cycle
If you prefer rolling carry-overs forward instead of archiving:
// schedules/moveCarry.js
module.exports = {
cron: "0 2 * * 1",
job: async ({ linear }) => {
const lastCycle = await linear.currentCycle({ offset: -1 });
const current = await linear.currentCycle();
const open = await linear.query({
teamId: linear.config.defaultTeamId,
cycleId: lastCycle.id,
state: "in-progress"
});
await Promise.all(open.map(i => linear.update(i.id, { cycleId: current.id })));
return `Moved ${open.length} carry-overs to ${current.name}`;
}
};
Choices, trade-offs—pick your poison.
Generating daily status reports automatically
Linear has Insights, but non-Linear users never open it. We’ll push a digest into chat at 09:00 local time.
// schedules/dailyReport.js
const tz = "Europe/Vienna"; // adjust
module.exports = {
cron: "0 7 * * *", // 07:00 UTC ~ 09:00 CET
job: async ({ linear, messenger }) => {
const team = linear.config.defaultTeamId;
const cycle = await linear.currentCycle();
const issues = await linear.query({ teamId: team, cycleId: cycle.id });
const counts = issues.reduce((acc, i) => {
acc[i.state.name] = (acc[i.state.name] || 0) + 1;
return acc;
}, {});
const msg = `📊 Linear summary for ${cycle.name}\n` +
Object.entries(counts).map(([k, v]) => `• ${k}: ${v}`).join("\n");
await messenger.post({ channel: "#engineering", text: msg });
}
};
The messenger helper abstracts Slack vs. Telegram vs. WhatsApp. It even falls back to email if the channel isn’t available—nice community patch by @agrawal-sid last month.
Managing the entire workflow end-to-end
With the pieces wired, a typical day looks like this:
- Support spots a bug in WhatsApp chat, types
/linear create. Issue goes to current cycle automatically. - Engineer picks it up with
/linear assign LIN-237 @alice. The bot updates the assignee and posts a green check. - Commits reference
LIN-237; the GitHub plugin moves the issue to In Review when the PR opens. - Merge triggers another webhook that sets state to Done.
- Monday 02:00 UTC, the daemon archives all Done tickets from last cycle.
- 09:00 local time, a digest hits
#eng-status—no manual screenshots of Linear dashboards.
The workflow is chat-first, Linear-native, and needs zero custom servers: OpenClaw handles auth refresh, rate-limit backoff, and secrets rotation under the hood.
Performance, rate limits, and gotchas
Linear caps API calls at 5000 per user per hour—generous until you bulk-update hundreds of tickets in a loop. The plugin batches writes in chunks of 100 with Promise.allSettled; still, large backfills should run off-peak.
- Error handling – Linear returns 502 for long queries; the SDK retries with exponential backoff up to 3 times.
- GraphQL cost – queries add up. Use the
query()helper that requests only the fields the plugin needs. - Link unfurling – Slack auto-fetches issue URLs which can double the call count. Disable unfurling in Slack if you’re tight on limits.
- Time zones –
currentCycle()relies on workspace default TZ. If your team is distributed, reference cycles by ID instead of date math.
Security notes
Linear tokens grant repo-level powers—don’t put them in plain text. Production checklist:
- Use ClawCloud’s KMS-backed secrets, not environment variables.
- Scope tokens to a bot user with the least required permissions.
- Rotate tokens quarterly; the plugin auto-reloads without downtime.
- Enable OpenClaw’s audit log (
claw audit tail) to track who executed which/linearcommand.
Next step: ship your own custom commands
Everything above is config, not code. When you outgrow the defaults—maybe you want a /linear bounty that sets a GitHub reward label—drop a TypeScript file into openclaw/commands:
// commands/bounty.ts
import { Command } from "@openclaw/core";
export default new Command({
name: "bounty",
description: "Tag a Linear issue as bounty and set $ value",
args: [{ name: "id" }, { name: "usd" }],
run: async ({ args, linear, github }) => {
const [id, usd] = args;
await linear.update(id, { labelIds: [await linear.labelId("bounty")] });
await github.comment(id, `Bounty: $${usd}`);
return `Issue ${id} marked as bounty for $${usd}`;
}
});
Recompile (npm run build) and the slash command appears instantly in Slack.
Practical takeaway
Hooking Linear into OpenClaw takes an afternoon. Once in place, every chat client becomes a project management console: capture bugs, update status, close cycles, broadcast progress—all without context-switching. Clone the recipe above, commit it to your openclaw repo, push to ClawCloud, and watch your Linear board update itself.