Most AI assistants can only respond when you talk to them. But what if your AI could react to events happening in your project management tool — in real time?
In this guide, we’ll connect Linear (a popular project management tool) to OpenClaw using webhooks and a Cloudflare Tunnel. The result: when someone mentions your AI agent on a Linear issue, assigns it a task, or approves a piece of content, OpenClaw wakes up and takes action — automatically.
No polling. No delays. Instant reactions to real project events.
What We’re Building
By the end of this tutorial, you’ll have:
- A webhook server running on your OpenClaw VPS that receives Linear events
- A Cloudflare Tunnel exposing that server over HTTPS (Linear requires it)
- Both running as systemd services that survive reboots
- OpenClaw waking up automatically when mentioned on Linear issues
Prerequisites
- A running OpenClaw instance on a VPS (Ubuntu/Debian)
- A Linear account with API access
- A domain managed through Cloudflare (for the tunnel)
- Node.js installed on your server
Step 1: Get Your Linear API Key
Go to Linear Settings → API → Personal API Keys and create a new key. Save it — you’ll only see it once.
Create an environment file to store your credentials:
$ cat > ~/.openclaw/workspace/.env.linear << 'EOF'
LINEAR_API_KEY=lin_api_YOUR_KEY_HERE
LINEAR_API_URL=https://api.linear.app/graphql
EOF
Step 2: Test the Linear API Connection
Before building anything complex, let’s verify the API works. Create a simple test script:
$ cat > scripts/linear-test.js << 'SCRIPT'
#!/usr/bin/env node
require('dotenv').config({ path: '/root/.openclaw/workspace/.env.linear' });
async function testConnection() {
const response = await fetch(process.env.LINEAR_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': process.env.LINEAR_API_KEY
},
body: JSON.stringify({
query: `query Me {
viewer { id name email }
organization { id name }
}`
})
});
const data = await response.json();
if (data.errors) {
console.error('API Error:', data.errors);
process.exit(1);
}
console.log('Connected to Linear!');
console.log('Workspace:', data.data.organization.name);
console.log('User:', data.data.viewer.name);
console.log('User ID:', data.data.viewer.id);
console.log('Org ID:', data.data.organization.id);
}
testConnection();
SCRIPT
$ node scripts/linear-test.js
If you see your workspace name and user info, the API key is working.
Step 3: Build the Webhook Server
This is the core piece — a lightweight Node.js server that receives Linear webhook events and wakes up OpenClaw when relevant things happen.
$ cat > scripts/linear-webhook-server.js << 'SCRIPT'
#!/usr/bin/env node
require('dotenv').config({ path: '/root/.openclaw/workspace/.env.linear' });
const http = require('http');
const { execSync } = require('child_process');
const fs = require('fs');
const PORT = 3456;
const LOG_FILE = '/root/.openclaw/workspace/logs/linear-webhooks.log';
// Send a wake event to OpenClaw
function sendWakeEvent(text) {
try {
const safeText = text.replace(/"/g, '\\"').replace(/\n/g, ' ');
execSync(
`openclaw system event --text "${safeText}" --mode now --json`,
{ timeout: 15000, encoding: 'utf8' }
);
console.log('Wake event sent successfully');
} catch (err) {
console.error('Failed to send wake event:', err.message);
}
}
// Log events to file for debugging
function logEvent(event) {
fs.mkdirSync('/root/.openclaw/workspace/logs', { recursive: true });
const entry = `${new Date().toISOString()} - ${event.type} - ${event.action}\n`;
fs.appendFileSync(LOG_FILE, entry);
}
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/linear/webhook') {
let body = '';
req.on('data', chunk => { body += chunk.toString(); });
req.on('end', () => {
try {
const event = JSON.parse(body);
logEvent(event);
// COMMENT EVENTS: Check if your agent is mentioned
if (event.type === 'Comment') {
const comment = event.data;
// Change "rose" to your agent's name
if (comment.body && /@rose/i.test(comment.body)) {
const issueTitle = comment.issue?.title || 'Unknown';
const issueId = comment.issue?.identifier || '';
const commenter = comment.user?.name || 'Someone';
const text = comment.body.replace(/@rose/gi, '').trim();
sendWakeEvent(
`Linear mention from ${commenter} on ${issueId} ` +
`"${issueTitle}": ${text}`
);
}
}
// ISSUE EVENTS: Check if agent is assigned
if (event.type === 'Issue') {
const issue = event.data;
// Change "Rose" to your agent's display name in Linear
if (issue.assignee?.name === 'Rose') {
sendWakeEvent(
`Linear issue assigned to me: ${issue.identifier} ` +
`"${issue.title}" - ${(issue.description || '').substring(0, 300)}`
);
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
} catch (error) {
console.error('Parse error:', error.message);
res.writeHead(400);
res.end('Bad request');
}
});
} else {
res.writeHead(404);
res.end('Not found');
}
});
server.listen(PORT, () => {
console.log(`Linear webhook server listening on port ${PORT}`);
});
SCRIPT
The key concept here is sendWakeEvent(). This uses OpenClaw’s built-in system event command to inject a message into the agent’s session, waking it up immediately. Your agent receives the Linear context and can act on it — reply on the issue, create tasks, update statuses, whatever you’ve configured it to do.
Step 4: Set Up the Cloudflare Tunnel
Here’s the problem: Linear requires an HTTPS URL for webhooks. Your VPS is running a plain HTTP server on port 3456. You could configure Nginx + Let’s Encrypt, but there’s a much simpler solution: Cloudflare Tunnel.
A Cloudflare Tunnel creates a secure outbound connection from your server to Cloudflare’s edge network, then routes traffic to your local service. No open ports, no SSL certificates to manage, no firewall rules.
Install cloudflared
$ curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
$ echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
$ sudo apt update && sudo apt install cloudflared -y
Authenticate and Create the Tunnel
$ cloudflared tunnel login
$ cloudflared tunnel create linear-webhook
$ cloudflared tunnel route dns linear-webhook webhook.yourdomain.com
The tunnel login command opens a browser for Cloudflare authentication. The tunnel create command generates a credentials file. The route dns command creates a CNAME record pointing your subdomain to the tunnel.
Configure the Tunnel
$ cat > /etc/cloudflared/config.yml << EOF
tunnel: YOUR_TUNNEL_ID
credentials-file: /root/.cloudflared/YOUR_TUNNEL_ID.json
ingress:
- hostname: webhook.yourdomain.com
service: http://localhost:3456
- service: http_status:404
EOF
Replace YOUR_TUNNEL_ID with the UUID from the tunnel create step, and webhook.yourdomain.com with your actual subdomain.
Step 5: Make Everything Permanent with systemd
Background processes die when your SSH session ends. Systemd services survive reboots. Let’s create both.
Webhook Server Service
$ cat > /etc/systemd/system/linear-webhook.service << EOF
[Unit]
Description=Linear Webhook Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/root/.openclaw/workspace
ExecStart=/usr/bin/node /root/.openclaw/workspace/scripts/linear-webhook-server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl enable linear-webhook
$ sudo systemctl start linear-webhook
Cloudflare Tunnel Service
$ sudo cloudflared service install
$ sudo systemctl enable cloudflared
$ sudo systemctl start cloudflared
Both services now start automatically on boot and restart if they crash.
Step 6: Register the Webhook in Linear
Now tell Linear to send events to your webhook URL. We’ll do this via the API:
$ cat > scripts/linear-register-webhook.js << 'SCRIPT'
#!/usr/bin/env node
require('dotenv').config({ path: '/root/.openclaw/workspace/.env.linear' });
async function registerWebhook() {
// Change this to your tunnel URL
const webhookUrl = 'https://webhook.yourdomain.com/linear/webhook';
const response = await fetch(process.env.LINEAR_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': process.env.LINEAR_API_KEY
},
body: JSON.stringify({
query: `
mutation CreateWebhook($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
success
webhook { id url enabled }
}
}
`,
variables: {
input: {
url: webhookUrl,
resourceTypes: ["Issue", "Comment"],
label: "OpenClaw Integration"
}
}
})
});
const data = await response.json();
if (data.data.webhookCreate.success) {
const wh = data.data.webhookCreate.webhook;
console.log('Webhook registered!');
console.log('ID:', wh.id);
console.log('URL:', wh.url);
console.log('Enabled:', wh.enabled);
} else {
console.error('Failed:', JSON.stringify(data, null, 2));
}
}
registerWebhook();
SCRIPT
$ node scripts/linear-register-webhook.js
Save the webhook ID — you’ll need it if you want to update or delete the webhook later.
Step 7: Test It
Go to a Linear issue, write a comment mentioning your agent (e.g., @rose can you look into this?), and watch your webhook logs:
$ tail -f /root/.openclaw/workspace/logs/linear-webhooks.log
You should see the event arrive within seconds, and OpenClaw will wake up with the context of the mention.
Going Further: Custom Event Handlers
The webhook server we built handles mentions and assignments, but you can extend it for any workflow. Here are some ideas:
Auto-Schedule Content When Approved
If you use Linear to manage content (blog posts, social media threads), you can trigger actions when a specific label is added:
// Inside your Issue event handler:
const approvedLabelId = 'your-approved-label-id';
const labelIds = issue.labelIds || [];
const prevLabelIds = event.updatedFrom?.labelIds || [];
// Detect when "Approved" label was JUST added
if (labelIds.includes(approvedLabelId) &&
!prevLabelIds.includes(approvedLabelId)) {
sendWakeEvent(
`Content approved: ${issue.identifier} "${issue.title}". ` +
`Please schedule it for posting.`
);
}
Create Helper Scripts
With the API connection in place, you can also create issues programmatically from OpenClaw:
$ node scripts/linear-create-issue.js "Fix login page bug" "Users are seeing a 500 error on /login"
Created: ABU-12 - Fix login page bug
https://linear.app/abugosh/issue/ABU-12
This means your AI agent can both receive tasks from Linear and create new ones — closing the loop on project management automation.
Architecture Overview
Here’s what the complete flow looks like:
- Someone acts in Linear — creates an issue, leaves a comment, adds a label
- Linear sends a webhook to
https://webhook.yourdomain.com/linear/webhook - Cloudflare Tunnel routes the request to your local server on port 3456
- Webhook server parses the event and checks if it’s relevant (mention, assignment, label change)
- OpenClaw wakes up via
system eventwith the full context - Your agent acts — replies on the issue, creates tasks, updates statuses, whatever you need
The total latency from Linear action to agent response is typically under 3 seconds.
Monitoring and Debugging
Check if your services are running:
$ sudo systemctl status linear-webhook
$ sudo systemctl status cloudflared
$ tail -20 /root/.openclaw/workspace/logs/linear-webhooks.log
If the webhook server crashes, systemd restarts it within 5 seconds. If the tunnel drops, cloudflared reconnects automatically. This setup is surprisingly robust for how simple it is.
Wrapping Up
This integration turns your AI assistant from something you talk to into something that participates in your workflow. Instead of switching to a chat window to ask your agent to do something, you mention it where you’re already working — on a Linear issue — and it picks it up instantly.
The pattern applies beyond Linear too. Any service that supports webhooks (GitHub, Slack, Stripe, etc.) can use the same architecture: a lightweight Node.js server, a Cloudflare Tunnel for HTTPS, systemd for persistence, and OpenClaw’s system event for the wake-up call.
Your AI agent shouldn’t just wait for you to talk to it. It should be part of the team.