Connecting OpenClaw to Linear: Real-Time Webhooks for AI-Powered Project Management

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:

  1. Someone acts in Linear — creates an issue, leaves a comment, adds a label
  2. Linear sends a webhook to https://webhook.yourdomain.com/linear/webhook
  3. Cloudflare Tunnel routes the request to your local server on port 3456
  4. Webhook server parses the event and checks if it’s relevant (mention, assignment, label change)
  5. OpenClaw wakes up via system event with the full context
  6. 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.

Posted in:

Want to learn more about OpenClaw? 🦞

Join our community to get access to free support and special programs!

🎉

Welcome to the OpenClaw Community!

Check your email for next steps.