All skills

LinkedIn DM signal classifier

Use this skill to scan a team member's LinkedIn DMs daily and turn conversations into routed signals. It classifies each chat into a bucket (customer support, commercial, pipeline, re-engagement, prospect, partner, VC, community, and more), updates account memory and the CRM, and posts a digest to Slack. Detection and routing only — it never creates outreach.

SKILL.md
name:
linkedin-dm-signal-classifier
description:
Use this skill to scan a team member's LinkedIn DMs daily and turn conversations into routed signals. It classifies each chat into a bucket (customer support, commercial, pipeline, re-engagement, prospect, partner, VC, community, and more), updates account memory and the CRM, and posts a digest to Slack. Detection and routing only — it never creates outreach.

Template placeholders

Replace every {{...}} before enabling. See the setup checklist reference for the full setup list.

  • {{TRIGGER_OWNER}} — Team member whose LinkedIn DMs are scanned (one skill instance per person)
  • {{CRM}} — Your CRM (e.g. HubSpot, Attio)
  • {{SIGNALS_CHANNEL}} — Slack channel for all DM signal posts + daily digest
  • {{EXPANSION_CHANNEL}} — Slack channel for expansion/churn-risk cross-posts (optional)
  • {{PARTNERSHIPS_OWNER}} — Person to flag partner/agency signals to
  • {{FUNDRAISING_OWNER}} — Person to flag investor inquiries to (usually a founder)
  • {{LEAD_SCORING_SKILL}} — Sub-skill reference: your lead scoring & qualification methodology
  • {{HIGH_ACV_THRESHOLD}} — Company profile that suggests medium-to-high ACV (default: >30 employees, funded, or VP+/C-suite/Founder contact)

⚠️ EVAL MODE — CRM writes disabled (recommended for the first 1–2 weeks)

All {{CRM}} writes are temporarily disabled while you evaluate output quality. This means:

  • Do NOT create or update contacts in {{CRM}}
  • Do NOT write notes on contacts or deals in {{CRM}}
  • Do NOT associate anything in {{CRM}}

Instead, include the exact note text that WOULD have been written in the Slack post (see Step 6 for format). Workspace account-memory writes are still ON.

To go live: remove this section and the "{{CRM}} note (EVAL)" lines from Step 6.


Overview

Daily workflow for processing LinkedIn DM conversations. Runs once per person per day via a personal scheduled trigger. The trigger owner's LinkedIn account is used automatically.


Step 1 — Determine the Cursor

Check the trigger owner's user memory for a key like LI DM last run: [ISO timestamp].

  • If found: this is a subsequent run. You will skip chats with no activity since that timestamp.
  • If not found: this is the first run. Process chats up to the limit specified in the trigger instructions.

Do NOT write the cursor yet. The cursor is written at the END of the run after all chats have been processed successfully (Step 8). A mid-run failure must not advance the cursor — duplicate processing on retry is acceptable, silently skipped signals are not.


Step 2 — Fetch Chats (paginated)

Fetch the trigger owner's chat list with your LinkedIn tools, up to 250 chats per page.

Important: LinkedIn calls must be made ONE AT A TIME. Never parallelize them.

Pagination logic:

  • If the response indicates more pages AND the oldest chat returned has a last-message timestamp newer than the stored cursor (or this is the first run): fetch the next page using the returned pagination cursor.
  • Continue paginating until either: no more pages remain, OR the oldest chat in the page has a last-message timestamp older than or equal to the stored cursor.
  • On first run: respect the chat limit specified in the trigger instructions (default 50 if unspecified) — stop paginating once you've accumulated that many chats.

This guarantees no active chats are silently missed on a busy day.

After fetching all pages, filter the full list: keep only chats whose last message timestamp is newer than the stored cursor (on subsequent runs). On the first run, keep all fetched chats up to the trigger limit.


Step 3 — Read Messages

For each remaining chat:

  • Read the most recent messages in the chat (about 20 is enough context)

For each chat, note:

  • Whether the last message is inbound (sent by the other person) — this feeds the awaiting-reply flag in Step 5
  • The timestamp of the last inbound message, to compute days since last reply

Skip a chat if:

  • All messages are outbound only (sent by the trigger owner, never replied to)
  • The most recent inbound message is purely noise with no identifiable company context

Step 4 — Identify the Company

For each conversation with a meaningful inbound message:

  1. Look at the other party's profile name and headline in the chat metadata
  2. Identify their company from their LinkedIn headline (e.g., "VP Sales @ Acme Corp")
  3. Search your workspace for the company
  4. If not found, enrich via research tools to confirm domain and firmographics

Step 5 — Classify the Signal

Classify each conversation into exactly one bucket. Use the priority order below — first matching bucket wins.


VENDOR_SPAM Vendors pitching products, recruiters, generic congratulations with no substantive follow-up, connection acceptances with no message, spam. → Skip entirely. No Slack post, no writes.


CUSTOMER_SUPPORT The contact's company is a current customer (closed-won) AND the message indicates: a product issue, bug, outage, credit or billing problem, a complaint, or any frustrated / at-risk tone. (Canonical example: an angry DM about billing or credits from a paying customer.) → Treat as urgent. See Step 6.


CUSTOMER_COMMERCIAL The contact's company is a current customer AND the message is about: expansion interest, renewal, plan upgrade/downgrade, pricing questions, or churn risk without a support/complaint dimension. → See Step 6.


ACTIVE_PIPELINE The contact's company has an open deal in your pipeline (any active sales stage between qualification and procurement). → See Step 6.


RE_ENGAGEMENT The contact's company is at a closed-lost, nurture, or self-serve stage — AND they are initiating contact after a prior sales interaction (demo completed, trial agreed, or previous outreach replied to). Self-serve accounts only qualify if there is evidence of prior sales contact in account memory or CRM history; a pure self-serve trial with no sales touch does not qualify. → See Step 6.


PROSPECT_HIGH_ACV A prospect (known or new) where the company profile meets {{HIGH_ACV_THRESHOLD}}. Includes known prospects in early funnel stages as well as previously unknown companies that clear the threshold. → See Step 6.


PROSPECT_LOW_ACV A prospect with low ACV potential: solo operators, fractional executives, independent consultants, tiny companies with no funding.

Also use this bucket when the contact has no identifiable company (no company name or domain in their headline or profile). Post with whatever profile info exists. Add the note: No company — enrichment skipped. Do NOT create a workspace company record or run scoring for these contacts. → See Step 6.


IMPLEMENTATION_PARTNER Agency, consultancy, done-for-you services firm in your space — whether already tracked as a partner or new. Note in the post whether this is ACTIVE (already tagged as a partner) or POTENTIAL (not yet tracked). → See Step 6.


COMMUNITY_SPEAKING Community demo requests, podcast invitations, event speaking invitations, newsletter or content distribution opportunities, co-marketing proposals with no service/revenue pitch. → See Step 6.


VC_INQUIRY An investor (VC, angel, family office) reaching out — about your company as an investment, a portfolio-company intro, or general relationship building. → See Step 6.


COLLABORATION Peer builders, friendly-competitor exchanges, or mutual-interest conversations with no clear revenue or partnership path. → See Step 6.


UNKNOWN Business-relevant message that does not fit any bucket above. Do not force a classification — post it and let the human decide. → See Step 6.


Awaiting-reply flag (applies to all buckets except VENDOR_SPAM): After classifying, check: is the last message in the chat inbound (the other person messaged last, and the trigger owner has not replied)?

  • If yes: compute N = days since that message. Add this line to the Slack post: ⏰ Awaiting reply (N days)
  • If no (the trigger owner sent the last message): do NOT include this line. The ⏰ flag is exclusively for threads where the other person spoke last. Outbound-last threads must never carry this line.

Step 5b — Deduplication Check

Before posting any signal in Step 6, check whether a signal for this same contact has already been posted within the last 7 days. Re-posting an unchanged thread creates noise and masks real new signals.

If the contact's company exists in the workspace (account memory available):

  • Scan the account memory for a DM signal entry matching this contact's name, dated within the last 7 days.
  • If one exists:
    • Post again only if there is a new inbound message since the last entry that adds materially new information: a new ask, a changed timeline, a new objection, or a clear intent shift.
    • Do not re-post because the trigger owner replied or there was only outbound activity since the last entry.
    • When posting an update: use a short update format, not a full new signal post. Open with: 🔄 DM Update — [contact name] @ [company] (follow-up to [earlier signal date]) and include only what is genuinely new.
    • If nothing new: skip the Slack post entirely. Still update account memory if there is meaningful new content to log.

If the contact has no company record (no account memory to check):

  • Read the trigger owner's user memory for a Recently Posted DM Signals section (format: - [contact name] — [ISO date]).
  • Apply the same 7-day rule.
  • After any new or update post for such a contact, add or update their entry in the trigger owner's user memory. If the section doesn't exist yet, create it before writing the first entry.

Step 6 — Route by Classification

VENDOR_SPAM

Skip. No writes, no actions.


CUSTOMER_SUPPORT

  1. Update account memory:
    [CUSTOMER SUPPORT DM] [trigger owner name] ↔ [contact name, title] — [date]
    Issue type: [bug / billing / credits / complaint / at-risk]
    Summary: [1 sentence]
    
  2. (EVAL MODE — skip actual write) Compose the {{CRM}} note:
    {{CRM}} note (EVAL — not written):
    DM via LinkedIn — [date]. [contact name] ([title]). Support signal.
    Issue: [type]. Summary: [1 sentence].
    
  3. Post to {{SIGNALS_CHANNEL}}:
    • 🚨 Customer Support DM
    • Company name + domain
    • Contact: name, title
    • Issue type: 🐛 bug / 💳 billing/credits / 😤 complaint / ⚠️ at-risk
    • What they said (verbatim or close paraphrase)
    • {{CRM}} note (EVAL) block
    • ⏰ Awaiting reply line (if applicable)
    • Conversation owner: [trigger owner name]
    • Note: Urgent — route to your support process

CUSTOMER_COMMERCIAL

  1. Update account memory:
    [CUSTOMER COMMERCIAL DM] [trigger owner name] ↔ [contact name, title] — [date]
    Signal: [expansion / renewal / pricing / churn risk]
    Summary: [1 sentence]
    
  2. (EVAL MODE — skip actual write) Compose the {{CRM}} note (same format as above, with the commercial sub-type).
  3. Post to {{SIGNALS_CHANNEL}}:
    • 📩 Customer Commercial DM
    • Company name + domain; contact name, title
    • Signal type: 📈 expansion / 🔄 renewal / 💬 pricing question / ⚠️ churn risk
    • 1-sentence summary; {{CRM}} note (EVAL) block; ⏰ line if applicable; conversation owner
  4. If signal is expansion or churn risk: also cross-post to {{EXPANSION_CHANNEL}}.

ACTIVE_PIPELINE

  1. Update account memory:
    [PIPELINE DM] [trigger owner name] ↔ [contact name, title] — [date]
    Deal stage: [current stage]
    Signal: [objection / timeline / pricing / champion re-engagement / positive]
    Summary: [1-2 sentences]
    
  2. (EVAL MODE — skip actual write) Compose the {{CRM}} note.
  3. Post to {{SIGNALS_CHANNEL}}:
    • 📩 Pipeline DM Signal
    • Company + current deal stage; contact name, title; signal summary; {{CRM}} note (EVAL) block; ⏰ line if applicable; conversation owner

RE_ENGAGEMENT

  1. Update account memory:
    [RE-ENGAGEMENT DM] [trigger owner name] ↔ [contact name, title] — [date]
    Prior stage: [closed-lost / nurture / self-serve]
    Summary: [1 sentence]
    
  2. Treat as first-party intent. Load and follow {{LEAD_SCORING_SKILL}}. No outreach.
  3. Post to {{SIGNALS_CHANNEL}}:
    • 🔁 Re-engagement DM
    • Company + prior stage; contact name, title; what they said; tier scored; ⏰ line if applicable; conversation owner

PROSPECT_HIGH_ACV

  1. If the company is not yet in the workspace: enrich via research tools and add it.
  2. Update account memory:
    [PROSPECT DM] [trigger owner name] ↔ [contact name, title] — [date]
    ACV tier: HIGH
    Summary: [1 sentence]
    
  3. Load and follow {{LEAD_SCORING_SKILL}}. DM = first-party intent. No outreach. Promotion rule: A substantive inbound DM (the contact initiated a real business conversation) is strong first-party intent. If the account's current tier is one below your alert tier, instruct the scoring skill that this direct DM interaction justifies promotion one tier up — unless there is contrary evidence in the account memory (explicit disqualification, stated "not now", churned for cause). Never lower an existing tier as a result of this DM run.
  4. (EVAL MODE — skip actual write) Compose the {{CRM}} contact that WOULD be created:
    {{CRM}} contact (EVAL — not created):
    Name: [name] | Title: [title] | LinkedIn: [URL] | Company: [company]
    
  5. Post to {{SIGNALS_CHANNEL}}:
    • 📩 High-ACV Prospect DM
    • Company + enriched summary (employees, funding, description)
    • Contact: name, title, what they said
    • {{CRM}} contact (EVAL) block; tier scored; ⏰ line if applicable
    • Close with a note that accurately reflects what actually happened:
      • Both ran: Note: Enriched + scored. No outreach queued — surface for review.
      • Enrichment ran, scoring skipped: Note: Enriched. Scoring skipped. No outreach queued — surface for review.
      • Enrichment skipped: Note: Enrichment skipped. [Reason]. No outreach queued — surface for review.

PROSPECT_LOW_ACV

  1. Do NOT create a workspace company record. Do NOT run scoring.
  2. Post to {{SIGNALS_CHANNEL}}:
    • 📩 Low-ACV Prospect DM
    • Contact: name, title, company (if any); what they said; ⏰ line if applicable; conversation owner
    • No company — enrichment skipped (only if no company was identified)
    • Note: Low ACV signal — no workspace record created.

IMPLEMENTATION_PARTNER

  1. Update account memory (if company record exists):
    [PARTNER DM] [trigger owner name] ↔ [contact name, title] — [date]
    Status: ACTIVE / POTENTIAL
    Summary: [1 sentence on what was discussed]
    
  2. Post to {{SIGNALS_CHANNEL}}:
    • 🤝 Implementation Partner DM
    • Company name, domain, what they do; contact name, title
    • Status: ACTIVE or POTENTIAL
    • Summary of what was discussed; ⏰ line if applicable; conversation owner
    • Note: Flagging for {{PARTNERSHIPS_OWNER}}'s awareness

COMMUNITY_SPEAKING

  1. Post to {{SIGNALS_CHANNEL}}:
    • 🎤 Community / Speaking Opportunity
    • Contact: name, title, company (if any)
    • Opportunity type: podcast / event / community demo / distribution / co-marketing
    • What they proposed; ⏰ line if applicable; conversation owner
    • Note: Surface for review — no action taken.

VC_INQUIRY

  1. Post to {{SIGNALS_CHANNEL}}:
    • 💼 VC Inquiry
    • Contact: name, title, fund name; what they said; ⏰ line if applicable; conversation owner
    • Note: Surface for {{FUNDRAISING_OWNER}}'s awareness.

COLLABORATION

  1. Post to {{SIGNALS_CHANNEL}}:
    • 🤜 Collaboration / Peer Exchange
    • Contact: name, title, company; brief summary (1 sentence); ⏰ line if applicable; conversation owner

UNKNOWN

  1. Post to {{SIGNALS_CHANNEL}}:
    • ❓ Unclassified DM
    • Contact: name, title, company (if any); what they said (verbatim or close paraphrase); ⏰ line if applicable; conversation owner
    • Note: Could not classify — needs human review.

Step 7 — End of Run Summary

After processing all chats, always post a summary to {{SIGNALS_CHANNEL}}. This post is mandatory on every run — including runs where zero signals were found. Silence must mean a failure, never "nothing found."

If at least one non-noise signal was found:

📋 LinkedIn DM Scan — [trigger owner name] — [date]
Chats fetched: [total fetched] | Processed: [N after filter]
Signals found: [N] — support: X, commercial: X, pipeline: X, re-engagement: X,
high-ACV prospect: X, low-ACV prospect: X, partner: X, community/speaking: X,
VC: X, collab: X, unknown: X
Vendor spam / noise skipped: [N] ([breakdown: recruiters, vendors, outbound-only, etc.])
⚠️ Memory-full warnings: [N accounts] (list company names if any)

Omit the memory-full line if count is 0.

If zero non-noise signals were found:

📋 LinkedIn DM Scan — [trigger owner name] — [date]. Chats checked: [N]. No new signals.

Step 8 — Write the Cursor

Only after all chats have been processed successfully, write the cursor to the trigger owner's user memory: LI DM last run: [ISO timestamp of when this run started]

Use the timestamp from when the run started (captured at Step 1), not the end time — this ensures the next run re-checks any chats that became active mid-run.


Important Constraints

  • LinkedIn calls are sequential — never parallel
  • Never surface noise signals — filter aggressively
  • No outreach created in this workflow under any circumstances
  • If a company is tagged as competitor or vendor: skip, log in memory only, no Slack post
  • Memory-full handling: If an account memory write fails due to capacity, do NOT delete or trim existing entries to make room. Skip the memory write entirely. Add ⚠️ account memory full — signal not persisted to that signal's Slack post, and count it in the end-of-run digest.

What good looks like

A great run puts every business-relevant DM in exactly one bucket with the right routing: urgent customer-support signals surface same-day with the verbatim complaint, the 7-day dedup keeps the channel quiet (updates use the short 🔄 format), the end-of-run digest posts even on zero-signal days — silence must mean failure, never "nothing found" — and the cursor only advances after a fully successful run.

Mediocre looks like: vendor spam surfacing as signals, unchanged threads re-posted, a missing digest, awaiting-reply flags on threads where the owner spoke last, or any outreach creeping into what is strictly a detection-and-routing skill.