Guides

How Do I Tag Product-Qualified Site Visits in Linear?

Send product-qualified visitor signals from Leadpipe into Linear as tagged issues so product, growth, and CS know exactly who's evaluating which feature.

Elene Marjanidze Elene Marjanidze · · 10 min read
How Do I Tag Product-Qualified Site Visits in Linear?

Your product team works in Linear. Your growth team works in notebooks, Slack, and whatever dashboard got the most recent attention. The gap between them is where PLG signals go to die: someone visits your docs, clicks through three changelog pages, reads the pricing table for a feature they do not have yet, and nobody who cares knows it happened.

I am Elene, and this recipe is for teams running product-led motions who want the signal in Linear, where product and growth actually collaborate. Leadpipe identifies the visitor, a webhook fires, and Linear gets a scoped, labeled issue the right team can act on. Setup is around 25 minutes.


What you will build

A flow where identified visitors hitting your product, docs, or pricing pages create labeled Linear issues in the correct team, with the visitor’s company, title, pages viewed, intent topics, and a direct link to the Leadpipe record. Duplicates are deduped by account so your Linear board does not balloon.


Prerequisites

RequirementNotes
Leadpipe accountStart with 500 free identifications
Linear workspaceAny plan with API access (all paid plans)
Linear API keyFrom Settings, API, Personal API keys
Zapier or Make accountOptional for no-code
Time25 minutes

If your team does not live in Linear, check the Notion watchlist recipe or Airtable routing recipe for a similar signal-capture pattern in other tools.


When this makes sense

Not every identified visitor belongs in Linear. If they hit your homepage and bounce, that is sales’ problem, not product’s. The signal worth routing into Linear is narrower:

  • Visitor reads your docs (signal: active evaluation)
  • Visitor reads your changelog for a feature they do not have (signal: PLG upsell)
  • Visitor reads a specific feature page (signal: feature-gated interest)
  • Existing customer visits a feature or integration they are not using (signal: expansion)
  • Visitor spends over 3 minutes in product or API docs (signal: technical evaluator)

Everything else should route to sales (via CRM), alerts (via Slack or Gmail), or just the watchlist.


The data model

Linear team: "PQL signals"
  ├─ Labels:
  │    ├─ source:leadpipe
  │    ├─ type:docs, type:changelog, type:feature, type:expansion
  │    ├─ tier:enterprise, tier:mid, tier:smb
  │    └─ account:<domain>
  └─ Issues:
       Title: "[Company] visited [page], [title of visitor]"
       Description: full context (pages, duration, intent, LinkedIn)
       Project: optional, mapped from feature
       Assignee: auto-assigned by team

One issue per account per page-category per week. Sales and CS both get pulled into the issue when an account matches their list.


Step 1: Set up the Linear side

  1. Create a new Linear team called “PQL signals” (or use an existing growth/product team).
  2. Add these labels (Workspace or team-level): source:leadpipe, type:docs, type:changelog, type:feature, type:expansion, tier:enterprise, tier:mid, tier:smb.
  3. Optionally add one account:<domain> label per target account you want auto-tagged. This is what lets you filter the board to “Acme activity this month” with a click.
  4. Generate a Personal API Key: Settings, API, Create key. Copy it somewhere safe.
  5. Note the team ID from the URL when you are inside the team (the short code like PQL).

Step 2: Leadpipe webhook (targeted)

For Linear specifically, you do not want every identified visitor. You want the targeted subset: docs, changelog, pricing for features, expansion. Configure the webhook with a path filter.

In Leadpipe: Settings, Integrations, Webhooks, Add Webhook.

  • Destination URL: generated in Step 3.
  • Trigger: First Match.
  • Optional path filter (if your Leadpipe account supports it): include only URLs matching /docs/*, /changelog*, /product/*, or /api/*. If not available, you will filter in Zapier.

Full payload shape (from our webhook payload reference):

{
  "email": "tomoko.ito@fieldwise.io",
  "first_name": "Tomoko",
  "last_name": "Ito",
  "phone": "+1-415-555-0142",
  "company_name": "Fieldwise",
  "company_domain": "fieldwise.io",
  "company_industry": "Fintech",
  "company_employee_count": 240,
  "job_title": "Marketing Ops",
  "seniority": "Manager",
  "department": "Marketing",
  "linkedin_url": "linkedin.com/in/tomokoito",
  "city": "San Francisco",
  "state": "California",
  "country": "US",
  "page_url": "/docs/webhooks/retry-logic",
  "pages_viewed": ["/", "/docs/webhooks", "/docs/webhooks/retry-logic"],
  "visit_duration": 268,
  "intent_score": 68,
  "matched_topics": ["webhook integration", "developer tools"],
  "return_visit": true
}

GDPR reminder. For EU and UK visitors, Leadpipe defaults to company-level identification unless the visitor has given affirmative consent. Your Linear issue for those regions will list the company and page context without a person, which is often enough for the product team to file under “Acme is evaluating X” and follow up via the named CSM. See our GDPR compliance post for detail.


Step 3: Zapier path

  1. New Zap. Trigger: Webhooks by Zapier, Catch Hook. Copy the URL, paste into Leadpipe.

  2. Fire a test event. Zapier captures the fields.

  3. Filter step. Suggested logic:

    • page_url starts with /docs, /changelog, /product, /api, or /pricing
    • AND visit_duration greater than 45
    • AND email does not contain gmail.com, yahoo.com, outlook.com
  4. Formatter step: lowercase and strip www. from company_domain. This drives your dedup.

  5. (Optional) Filter or Path step to skip if an issue for this account+page was created within the past 7 days. If you want strict dedup, use the direct API path in Step 4 which handles this cleanly.

  6. Compute label set in a Formatter step:

    • If page_url starts with /docs or /apitype:docs
    • If page_url starts with /changelogtype:changelog
    • If page_url starts with /product or /features/*type:feature
    • If page_url starts with /pricing or /enterprisetype:feature
    • If Fieldwise is a known customer (you’ll need a lookup table or customer list) → type:expansion
    • Tier label from company_employee_count: under 50 → tier:smb, 50-500 → tier:mid, over 500 → tier:enterprise
  7. Action: Linear, Create Issue. Team: PQL signals. Title: [{{company_name}}] visited {{page_url}}, {{job_title}}. Description: see template below. Labels: the ones computed in Step 6 plus source:leadpipe and account:{{company_domain}} if it exists.

Description template

{{first_name}} {{last_name}} ({{job_title}}) at {{company_name}} viewed {{page_url}}.

Company
- Domain: {{company_domain}}
- Industry: {{company_industry}}
- Employees: {{company_employee_count}}
- Location: {{city}}, {{state}}, {{country}}

Session
- Pages viewed: {{pages_viewed}}
- Duration: {{visit_duration}}s
- Intent score: {{intent_score}}/100
- Matched topics: {{matched_topics}}
- Return visitor: {{return_visit}}

Contact
- Email: {{email}}
- Phone: {{phone}}
- LinkedIn: {{linkedin_url}}

Action hints
- If this is a customer on a plan that does not include this feature, pair with CS for an expansion conversation.
- If this is a prospect in docs, the evaluation is likely technical. Offer to jump on a call with the PM of the relevant area.

Zapier struggles with “do not create if an issue for this account and type exists within 7 days.” The direct path handles it in 20 lines.

// POST /leadpipe-webhook → Linear
// Env: LINEAR_API_KEY, LINEAR_TEAM_ID
import fetch from 'node-fetch';

const endpoint = 'https://api.linear.app/graphql';
const headers = {
  Authorization: process.env.LINEAR_API_KEY,
  'Content-Type': 'application/json',
};

function detectType(url) {
  if (url.startsWith('/docs') || url.startsWith('/api')) return 'type:docs';
  if (url.startsWith('/changelog')) return 'type:changelog';
  if (url.startsWith('/pricing') || url.startsWith('/enterprise')) return 'type:feature';
  if (url.startsWith('/product') || url.startsWith('/features')) return 'type:feature';
  return null;
}

function tierLabel(employees) {
  if (!employees) return 'tier:smb';
  if (employees >= 500) return 'tier:enterprise';
  if (employees >= 50) return 'tier:mid';
  return 'tier:smb';
}

export async function handler(req) {
  const v = req.body;
  const type = detectType(v.page_url || '');
  if (!type) return { statusCode: 200, body: 'not PQL' };
  if ((v.visit_duration || 0) < 45) return { statusCode: 200, body: 'short' };

  const domain = v.company_domain?.toLowerCase().replace(/^www\./, '');
  if (!domain) return { statusCode: 200, body: 'no domain' };

  // Dedup: is there a matching issue in the past 7 days?
  const sevenDaysAgo = new Date(Date.now() - 7 * 864e5).toISOString();
  const search = await fetch(endpoint, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      query: `query ($teamId: String!, $after: DateTime!) {
        issues(
          filter: {
            team: { id: { eq: $teamId } },
            createdAt: { gt: $after },
            labels: { name: { in: ["source:leadpipe", "account:${domain}", "${type}"] } }
          }
        ) { nodes { id title } }
      }`,
      variables: { teamId: process.env.LINEAR_TEAM_ID, after: sevenDaysAgo },
    }),
  });
  const { data } = await search.json();
  if (data?.issues?.nodes?.length) return { statusCode: 200, body: 'deduped' };

  // Create the issue
  await fetch(endpoint, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      query: `mutation ($input: IssueCreateInput!) {
        issueCreate(input: $input) { success issue { id identifier url } }
      }`,
      variables: {
        input: {
          teamId: process.env.LINEAR_TEAM_ID,
          title: `[${v.company_name}] visited ${v.page_url}, ${v.job_title || 'visitor'}`,
          description: buildDescription(v),
          labelIds: await resolveLabelIds([
            'source:leadpipe',
            type,
            tierLabel(v.company_employee_count),
            `account:${domain}`,
          ]),
        },
      },
    }),
  });

  return { statusCode: 200, body: 'ok' };
}

The label resolution helper (not shown) queries Linear’s issueLabels endpoint to turn label names into IDs. Cache the result per label since label IDs do not change.


What this looks like in practice

Tomoko, Marketing Ops Manager at Fieldwise (a 240-person fintech in San Francisco), reads three pages in your docs at 4:18 pm: /docs/webhooks, /docs/webhooks/retry-logic, /docs/webhooks/signatures. She spends four and a half minutes.

  1. Leadpipe identifies her. Webhook fires.
  2. Your middleware checks: Fieldwise has no open source:leadpipe + account:fieldwise.io + type:docs issue in the past 7 days. Proceed.
  3. A Linear issue is created in the PQL signals team:
    • Title: [Fieldwise] visited /docs/webhooks/retry-logic, Marketing Ops
    • Labels: source:leadpipe, type:docs, tier:mid, account:fieldwise.io
    • Description: full session context, contact info, LinkedIn
  4. The PQL signals team has a Linear automation that assigns type:docs issues to the developer relations lead, who spots the issue the next morning.
  5. Dev rel pings the CSM for Fieldwise (they are an existing customer on the starter plan). CSM schedules a technical call about webhook retry behavior. A month later Fieldwise upgrades to the plan that includes production-grade webhook retries.

None of that happens if the signal never leaves analytics. The point of this integration is to put the visit in front of the human who can actually act on it.


Troubleshooting and edge cases

Linear issue spam. If you skip the 7-day dedup, a single account browsing docs for an afternoon creates 10 issues. Use the direct API path for real dedup, or in Zapier use a Storage or Data Store step that remembers “this account + type in the last N days.”

Labels not applying. Linear’s API expects label IDs, not names, on creation. Pre-query the label IDs and cache them. If a label does not exist yet, create it via the API on first use.

Wrong team getting the issue. If your PQL team is not also the team that prioritizes features, add a second rule: when type:feature, route to the product team for that feature area. Linear projects or sub-teams make this clean.

Existing customers mixed with prospects. Your customers likely also hit docs and changelog. Use an “is customer” flag (either a Zapier lookup against a customer list, or check your billing system in middleware). If the account is a customer, apply type:expansion instead of type:docs so CS owns it.

GDPR EU visitors arriving with no person fields. Expected. The Linear issue title falls back to [Company] visited [page], visitor and the description shows company-level context only. Still actionable at an account level.

Signal fatigue. Product teams care about patterns, not individual hits. Pair this with a weekly Linear sub-issue or a Notion view that rolls up “docs visits this week by account,” so the team reviews in aggregate. See the Notion watchlist recipe for a companion roll-up.

Feature-specific signals. If you want the /product/orbit page specifically to land in your Orbit team in Linear, route by URL prefix to different team IDs. Our Orbit product page is a good example of a feature-specific landing worth routing this way.


Extending the recipe

  • Chain into Slack alerts so the relevant channel gets a ping when the Linear issue is created.
  • Use the Leadpipe MCP server to let Claude or ChatGPT query Linear and suggest the right outreach based on the pattern of PQL signals in the past month.
  • If your product team uses the Leadpipe intent API, cross-reference the account’s matched topics with the feature they are evaluating for a better handoff.
  • Pair with Airtable routing for target-account territory logic, and let Airtable be the source of truth on who owns the account while Linear captures the PQL signal for product.
  • Pipe the same stream into Clay enrichment first so the Linear issue description already has phone, revenue, tech stack for expansion conversations.

Why the product team wins here

Growth teams already have Slack, CRM, and dashboards. Product teams have Linear. Getting PQL signals into the tool where the product team already plans the next sprint is what makes the signal useful instead of another tab. When your CSM can file “expansion risk” under the feature the customer is evaluating, and your PM can see which docs are the most-visited by accounts on the wrong plan, the loop between usage and pipeline closes.

The best PQL signal is the one the person who can act on it actually sees.

Every plan ships with the same identity graph, 23 REST endpoints, webhooks, and a 27-tool MCP server. Start in 5 minutes →