Source profileQuality 88/100

aden-hive/hive/core/framework/skills/_preset_skills/linkedin-automation/SKILL.md

hive.linkedin-automation

Read before automating LinkedIn with browser_* tools. LinkedIn combines shadow DOM (#interop-outlet), strict Trusted Types CSP that silently drops innerHTML, Lexical composer, native beforeunload dialogs that hang the bridge, and aggressive spam filters — each has bitten us at least once. Verified flows for profile messaging, connection-request acceptance, feed composition, and search. Requires hive.browser-automation. Verified against logged-in production 2026-04-11.

Source repository stars
10,849
Declared platforms
0
Static risk flags
1
Last source update
2026-05-29
Source checked
2026-08-04

Decision brief

What it does—and where it fits

LinkedIn is the hardest mainstream site to automate because it combines shadow DOM (interop-outlet for messaging), strict Trusted Types CSP (silently drops innerHTML), heavy React reconciliation (injected nodes get stripped on re-render), native beforeunload draft dialogs (hang…

Best for

    Not for

    • innerHTML injection is silently dropped — LinkedIn's Trusted Types CSP discards any innerHTML = "" from injected scripts, no console error. Always use createElement + appendChild + setAttribute for DOM injection. textCo…
    • Use browsertypefocused (not browsertype) on the message composer. The Lexical contenteditable lives inside the interop-outlet shadow root which document.querySelector (what browsertype's selector path uses under the hoo…

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/aden-hive/hive --skill "core/framework/skills/_preset_skills/linkedin-automation"
    Safe inspection promptEditorial

    Inspect the Agent Skill "hive.linkedin-automation" from https://github.com/aden-hive/hive/blob/3c390c9e6ca3a2b141567bea986079fedbcd7db1/core/framework/skills/_preset_skills/linkedin-automation/SKILL.md at commit 3c390c9e6ca3a2b141567bea986079fedbcd7db1. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      step 5 already focused Lexical, so insertText lands in the editor

      Review the “step 5 already focused Lexical, so insertText lands in the editor” section in the pinned source before continuing.

      Review and apply the “step 5 already focused Lexical, so insertText lands in the editor” source section.
    2. 02

      Process cards one at a time with human-like cadence

      for card in cards[:25]: browserclickcoordinate(card['cx'], card['cy']) click Accept sleep(2) After accepting, a "Message" button appears on the card — navigate to the profile and run the profile Message flow above, personalized by firstname. OR: if the "Message" button is inline…

      for card in cards[:25]: browserclickcoordinate(card['cx'], card['cy']) click Accept sleep(2) After accepting, a "Message" button appears on the card — navigate to the profile and run the profile Message flow above, pers…browsernavigate("https://www.linkedin.com/feed/", waituntil="load") sleep(4) browserevaluate("(function(){window.onbeforeunload=null;})()")
    3. 03

      Rule 0: screenshot + coordinates, not selectors

      LinkedIn changes class names aggressively and hides composers inside shadow roots AND iframes. Selectors break constantly. Your default strategy on every LinkedIn page should be:

      browserscreenshot() — see the page visuallyPick the target's position from the imagebrowsercoords(imagex, imagey) → get CSS pixels
    4. 04

      Invitation manager — inline message button path is BROKEN

      If the user asks to message a connection request from the invitation manager page without accepting first, the inline "Message" button opens a composer inside a nested iframe overlay (not a shadow root). The iframe's contentDocument is either cross-origin-blocked or not hydrated…

      If the user asks to message a connection request from the invitation manager page without accepting first, the inline "Message" button opens a composer inside a nested iframe overlay (not a shadow root). The iframe's co…Redirect: click the person's name/profile link on the card, go to the profile page, and use the standard Profile Message flow below. The profile flow is battle-tested; the inline-iframe flow isn't.If you end up writing document.activeElement.tagName === 'IFRAME' inside a browserevaluate, you've hit this trap. Stop and go to the profile page.
    5. 05

      Timing expectations

      browsernavigate(waituntil="load") — LinkedIn takes 4–5 seconds to load the feed cold.

      browsernavigate(waituntil="load") — LinkedIn takes 4–5 seconds to load the feed cold.After navigation, always sleep(3) to let React hydrate the profile/feed chrome before querying selectors. Without the sleep waitforselector will flake on elements that exist moments later.Composer modal slide-in takes 2 seconds after you click the Message button.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 55

    The documentation includes network, browsing, or remote request actions.

    browser_navigate("https://www.linkedin.com/in/<username>/", wait_until="load")

    Network access

    medium · line 185

    The documentation includes network, browsing, or remote request actions.

    browser_navigate("https://www.linkedin.com/mynetwork/invitation-manager/received/",

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score88/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars10,849SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    aden-hive/hive
    Skill path
    core/framework/skills/_preset_skills/linkedin-automation/SKILL.md
    Commit
    3c390c9e6ca3a2b141567bea986079fedbcd7db1
    License
    Apache-2.0
    Collected
    2026-08-04
    Default branch
    main
    View the original SKILL.md

    LinkedIn Automation

    LinkedIn is the hardest mainstream site to automate because it combines shadow DOM (#interop-outlet for messaging), strict Trusted Types CSP (silently drops innerHTML), heavy React reconciliation (injected nodes get stripped on re-render), native beforeunload draft dialogs (hang the bridge), and aggressive spam filters. Every one of those has bit us at least once. This skill documents what actually works.

    Always activate browser-automation first. This skill assumes you already know about CSS-px coordinates, browser_type/browser_type_focused, and browser_shadow_query. The guidance below is LinkedIn-specific; general browser rules are there.

    Rule #0: screenshot + coordinates, not selectors

    LinkedIn changes class names aggressively and hides composers inside shadow roots AND iframes. Selectors break constantly. Your default strategy on every LinkedIn page should be:

    1. browser_screenshot() — see the page visually
    2. Pick the target's position from the image
    3. browser_coords(image_x, image_y) → get CSS pixels
    4. browser_click_coordinate(css_x, css_y) — reaches shadow DOM, iframes, and React elements indifferently
    5. browser_type(use_insert_text=True, text=...) — types into whatever is focused, including Lexical composers

    If browser_evaluate(...querySelectorAll...) returns [] even once, do not try a different selector. Stop, screenshot, and click. The "what if I try .artdeco-list__item next" instinct has burned ~50 tool calls in real sessions before the agent pivoted. Don't fall into that loop.

    The selectors in the table below are only for when you already know the target is in the light DOM and you want a faster path than screenshot+coord. When in doubt, default to coordinates.

    Invitation manager — inline message button path is BROKEN

    If the user asks to message a connection request from the invitation manager page without accepting first, the inline "Message" button opens a composer inside a nested iframe overlay (not a shadow root). The iframe's contentDocument is either cross-origin-blocked or not hydrated at access time. This path is not reliably automatable today.

    Redirect: click the person's name/profile link on the card, go to the profile page, and use the standard Profile Message flow below. The profile flow is battle-tested; the inline-iframe flow isn't.

    If you end up writing document.activeElement.tagName === 'IFRAME' inside a browser_evaluate, you've hit this trap. Stop and go to the profile page.

    Timing expectations

    • browser_navigate(wait_until="load") — LinkedIn takes 4–5 seconds to load the feed cold.
    • After navigation, always sleep(3) to let React hydrate the profile/feed chrome before querying selectors. Without the sleep wait_for_selector will flake on elements that exist moments later.
    • Composer modal slide-in takes ~2 seconds after you click the Message button.

    Verified selectors

    TargetSelectorNotes
    Global search inputinput[data-testid='typeahead-input']Light DOM, straightforward
    Own profile linka[href*='linkedin.com/in/']Top nav; filter to the one near top-left
    Profile Message actiona[href*='/messaging/compose/'] filtered by NON_SELF_PROFILE_VIEW AND no body= param AND x < 700Is an <a>, not a <button>. Multiple match; filter carefully.
    Modal composer textareadiv.msg-form__contenteditable (inside #interop-outlet shadow)Multiple instances exist — pick largest-area in-viewport one.
    Modal Send buttonbutton.msg-form__send-button (inside #interop-outlet shadow)Same multi-instance trap — filter by y + height <= innerHeight.
    Invitation managernavigate to https://www.linkedin.com/mynetwork/invitation-manager/received/Direct URL is faster than nav-link clicking
    Pending connection card.invitation-card, .invitations-card, [data-test-incoming-invitation-card]Filter out "invited you to follow" / "subscribe" cards
    Accept buttonbutton[aria-label*="Accept"] within the card scopePer-card scoping is critical — there are many Accept buttons on the page

    LinkedIn changes class names aggressively. If a class-based selector breaks, fall back to browser_screenshot → visual identification → browser_click_coordinate with the pixel you read straight off the image (screenshots are CSS-sized, no conversion). The screenshot + coord path works regardless of class-name churn and regardless of shadow DOM.

    Profile Message flow (verified end-to-end 2026-04-11)

    # 1. Load the profile
    browser_navigate("https://www.linkedin.com/in/<username>/", wait_until="load")
    sleep(3)
    
    # 2. Strip onbeforeunload before any state-mutating work — prevents draft-dialog deadlock later
    browser_evaluate("""
      (function(){
        window.onbeforeunload = null;
        window.addEventListener('beforeunload', e => e.stopImmediatePropagation(), true);
      })();
    """)
    
    # 3. Find the profile Message link (NOT a button, and multiple exist)
    msg_btn = browser_evaluate("""
      (function(){
        const links = Array.from(document.querySelectorAll('a[href*="/messaging/compose/"]'));
        for (const a of links){
          const href = a.href || '';
          if (!href.includes('NON_SELF_PROFILE_VIEW')) continue;
          if (href.includes('body=')) continue;            // reject Premium upsell
          const r = a.getBoundingClientRect();
          if (r.width === 0 || r.x > 700) continue;        // reject sidebar / "More profiles for you"
          return {cx: r.x + r.width / 2, cy: r.y + r.height / 2};
        }
        return null;
      })();
    """)
    browser_click_coordinate(msg_btn['cx'], msg_btn['cy'])
    sleep(2.5)  # composer modal slide-in
    
    # 4. Find the modal composer textarea (pick biggest in-viewport; reject pinned chat bar)
    textarea = browser_evaluate("""
      (function(){
        const vh = window.innerHeight, vw = window.innerWidth;
        const candidates = [];
        function walk(root){
          const els = root.querySelectorAll ?
            root.querySelectorAll('div.msg-form__contenteditable') : [];
          for (const el of els){
            const r = el.getBoundingClientRect();
            if (r.width <= 0 || r.height <= 0) continue;
            if (r.y < 0 || r.y + r.height > vh) continue;  // reject pinned bar (below viewport)
            if (r.x < 0 || r.x + r.width > vw) continue;
            candidates.push({cx: r.x + r.width/2, cy: r.y + r.height/2, area: r.width * r.height});
          }
          const all = root.querySelectorAll ? root.querySelectorAll('*') : [];
          for (const host of all){ if (host.shadowRoot) walk(host.shadowRoot); }
        }
        walk(document);
        if (!candidates.length) return null;
        candidates.sort((a, b) => b.area - a.area);
        return candidates[0];
      })();
    """)
    
    # 5. Click to focus the modal composer (click-first is mandatory for Lexical)
    browser_click_coordinate(textarea['cx'], textarea['cy'])
    sleep(0.6)
    
    # 6. Insert text via browser_type_focused. This dispatches CDP
    #    Input.insertText to document.activeElement — the same underlying
    #    mechanism as execCommand('insertText') but with no JSON escaping,
    #    no browser_evaluate round trip, and built-in retry. The click in
    #    step 5 already focused Lexical, so insertText lands in the editor
    #    regardless of the shadow wrapping around #interop-outlet.
    #
    #    Use browser_type_focused (not browser_type) here — browser_type
    #    requires a selector, which cannot see past the #interop-outlet
    #    shadow root. browser_type_focused targets document.activeElement
    #    directly, sidestepping shadow boundaries entirely.
    browser_type_focused(text=message_text)
    sleep(1.0)   # let Lexical commit state + enable Send button
    
    # 7. Find the modal Send button (filter by in-viewport, reject pinned bar)
    send = browser_evaluate("""
      (function(){
        const vh = window.innerHeight;
        function walk(root){
          const btns = root.querySelectorAll ? root.querySelectorAll('button') : [];
          for (const b of btns){
            const cls = (b.className || '').toString();
            const txt = (b.textContent || '').trim();
            if (!cls.includes('send-button') && txt !== 'Send') continue;
            const r = b.getBoundingClientRect();
            if (r.width <= 0 || r.y + r.height > vh) continue;
            return {
              cx: r.x + r.width/2, cy: r.y + r.height/2,
              disabled: b.disabled || b.getAttribute('aria-disabled') === 'true',
            };
          }
          const all = root.querySelectorAll ? root.querySelectorAll('*') : [];
          for (const host of all){
            if (host.shadowRoot){
              const got = walk(host.shadowRoot);
              if (got) return got;
            }
          }
          return null;
        }
        return walk(document);
      })();
    """)
    
    # 8. ONLY click Send if it's enabled — if disabled, the insertText
    #    didn't land. DO NOT retry with a different tool; the fix is
    #    always: re-click the composer rect, re-run browser_type_focused(text=...),
    #    re-check. The Send button's `disabled` state IS the ground truth —
    #    if Lexical registered your text, it enables the button. If it's
    #    still disabled, your text did not reach the editor, regardless
    #    of what any tool call claims.
    if send['disabled']:
        # The editor didn't receive your text. Do NOT click Send. Do NOT
        # fall back to browser_type with a selector (see anti-pattern in
        # Common Pitfalls — selector-based type can't reach the shadow-DOM
        # composer). Instead: re-click the textarea rect from step 4, wait
        # a beat, re-run browser_type_focused(text=message_text) from
        # step 6. If that still fails after 2 retries, bail and surface —
        # the modal may have been reclaimed by a stale state or auth wall.
        raise Exception("Send button disabled after insertText — editor did not receive input")
    
    browser_click_coordinate(send['cx'], send['cy'])
    sleep(2.5)  # wait for send + bubble render
    

    Verify post-send: the composer textarea should now be empty (innerText === '') and .msg-s-event-listitem__message-bubble count should have grown by 1. Walk the shadow tree via browser_evaluate to check.

    Connection request acceptance flow

    Daily outbound pattern — accept pending connection requests and send a templated welcome message.

    browser_navigate("https://www.linkedin.com/mynetwork/invitation-manager/received/",
                     wait_until="load")
    sleep(4)
    browser_evaluate("(function(){window.onbeforeunload=null;})()")
    
    # Scan pending connection cards — FILTER OUT follow/subscribe invitations
    cards = browser_evaluate("""
      (function(){
        const out = [];
        const cards = document.querySelectorAll('[data-test-incoming-invitation-card], .invitation-card');
        for (const c of cards){
          const text = (c.textContent || '').toLowerCase();
          if (text.includes('invited you to follow')) continue;
          if (text.includes('invited you to subscribe')) continue;
          const nameEl = c.querySelector('a[href*="/in/"], strong');
          const name = nameEl ? nameEl.textContent.trim().split(/\\s+/)[0] : '';
          const accept = c.querySelector('button[aria-label*="Accept"]');
          if (!accept) continue;
          const r = accept.getBoundingClientRect();
          out.push({
            first_name: name,
            cx: r.x + r.width/2, cy: r.y + r.height/2,
          });
          if (out.length >= 25) break;   // strict daily cap — see rate limits below
        }
        return out;
      })();
    """)
    
    # Process cards one at a time with human-like cadence
    for card in cards[:25]:
        browser_click_coordinate(card['cx'], card['cy'])   # click Accept
        sleep(2)
        # After accepting, a "Message" button appears on the card — navigate to
        # the profile and run the profile Message flow above, personalized by first_name.
        # OR: if the "Message" button is inline on the card, click it directly and
        # use the shadow-root composer flow.
        sleep(random.uniform(5, 10))  # human-like delay BETWEEN targets
    

    Don't do 25 back-to-back sends with zero delay. LinkedIn's spam filter catches this. 5–10 second randomized sleeps between sends, hard cap at 25 per 24h window.

    Feed post composer flow

    browser_navigate("https://www.linkedin.com/feed/", wait_until="load")
    sleep(4)
    browser_evaluate("(function(){window.onbeforeunload=null;})()")
    
    # Click the "Start a post" trigger
    start_trigger = browser_get_rect("button.share-box-feed-entry__trigger, [aria-label*='Start a post']")
    browser_click_coordinate(start_trigger.cx, start_trigger.cy)
    sleep(1.5)  # modal slide-in
    
    # Find the post editor inside the modal (also contenteditable, may not be in shadow)
    editor = browser_get_rect("div[contenteditable=true][aria-placeholder*='talk about']")
    browser_click_coordinate(editor.cx, editor.cy)
    sleep(0.5)
    browser_type("div[contenteditable=true][aria-placeholder*='talk about']", post_text)
    sleep(1.0)
    
    # Verify Post button enabled before clicking
    state = browser_evaluate("""
      (function(){
        const btn = document.querySelector('button.share-actions__primary-action');
        if (!btn) return {found: false};
        return {
          found: true,
          disabled: btn.disabled || btn.getAttribute('aria-disabled') === 'true',
        };
      })();
    """)
    if state['found'] and not state['disabled']:
        browser_click("button.share-actions__primary-action")
    

    Posting WITH an image attached

    Do NOT click the "Add media" / image icon inside the feed post composer to pick a file. LinkedIn renders a styled button that opens Chrome's native OS file picker when clicked, and that dialog is unreachable via CDP — the automation will hang on an invisible modal. Use browser_upload directly against the hidden <input type='file'>:

    # After the post modal is open and the editor has text:
    # (A) First, click "Add media" to surface the file input
    #     (clicking THIS button reveals the input but does NOT itself open
    #     the OS picker on current LinkedIn — the picker only opens if
    #     you click the inner "Choose from your device" entry).
    media_btn = browser_get_rect("button[aria-label*='image'], button[aria-label*='photo']")
    browser_click_coordinate(media_btn.cx, media_btn.cy)
    sleep(0.8)
    
    # (B) Enumerate file inputs to find the right one
    inputs = browser_evaluate("""
      (function(){
        return Array.from(document.querySelectorAll('input[type="file"]'))
          .map((el, i) => ({
            idx: i,
            accept: el.accept || '',
            name: el.name || '',
          }));
      })();
    """)
    # Expect to see one with accept='image/*' or accept containing 'image/jpeg'
    
    # (C) Set the file programmatically — no dialog
    browser_upload(
        selector="input[type='file'][accept*='image']",
        file_paths=["/absolute/path/to/logo.png"],
    )
    sleep(3)  # LinkedIn shows an upload-progress bar + preview
    
    # (D) Verify the image preview rendered before clicking Post
    preview_ok = browser_evaluate("""
      (function(){
        // LinkedIn shows the preview as an <img> inside
        // .share-creation-state__image-preview or similar.
        return !!document.querySelector(
          '.share-creation-state__preview img, .image-preview-container img'
        );
      })();
    """)
    if not preview_ok:
        raise Exception("LinkedIn image upload did not render — do NOT click Post")
    
    # (E) Now click Post as usual
    browser_click("button.share-actions__primary-action")
    sleep(4)  # media post takes longer to commit than text-only
    

    If the image isn't already on disk, write it first with write_file(absolute_path, bytes). browser_upload only accepts absolute paths.

    Rate limits and safety

    LinkedIn's abuse detection is aggressive. Beware of the limits, let user know but dare to break them if user confirms.:

    ActionLimit
    Outbound messages to non-connectionsDo not attempt — will get you warned or restricted
    Outbound messages to new 1st-degree connections25/day max, 5–10s randomized delays
    Connection request sends100/week max, spread across days, warm intros preferred
    Profile viewsSeveral hundred/day is usually fine but varies by account age
    Post publications1–5/day, no URL-only posts

    Signals you're being throttled:

    • "Message failed to send" with no error detail
    • Redirect to https://www.linkedin.com/checkpoint/challenge/...
    • Profile views showing stale data
    • Connection requests auto-withdrawn after a few hours

    If any of those show up, stop the run, screenshot the state, and surface the issue to the human operator. Do not retry.

    Common pitfalls

    • innerHTML injection is silently dropped — LinkedIn's Trusted Types CSP discards any innerHTML = "<...>" from injected scripts, no console error. Always use createElement + appendChild + setAttribute for DOM injection. textContent, style.cssText, and .value assignments are fine.
    • Use browser_type_focused (not browser_type) on the message composer. The Lexical contenteditable lives inside the #interop-outlet shadow root which document.querySelector (what browser_type's selector path uses under the hood) cannot see. browser_type requires a selector and will fail with "Element not found". The reliable insert path is: (1) browser_click_coordinate on the composer rect — the response's focused_element confirms Lexical received focus → (2) browser_type_focused(text=message_text) — CDP Input.insertText dispatches to document.activeElement regardless of shadow wrapping.
    • Per-char keyDown on the message composer produces empty text — Lexical intercepts beforeinput and drops raw keys. Use browser_type_focused(text=..., use_insert_text=True) after click-coordinate focused the composer. The CDP Input.insertText method commits as if IME fired, which Lexical accepts cleanly.
    • Multiple Send buttons on the page — the pinned bottom-right messaging bar has its own msg-form__send-button that's usually below innerHeight. Filter by in-viewport before clicking.
    • window.onbeforeunload hangs navigation/close — after typing in a composer, any browser_navigate or close_tab can pop a native "unsent message, leave?" confirm dialog that deadlocks the bridge. Always strip onbeforeunload before any navigation, and wrap composer flows in a try/finally that runs the cleanup block:
    # Cleanup on exit — run even if the flow crashed mid-type.
    browser_evaluate("""
      (function(){
        window.onbeforeunload = null;
        const h = document.getElementById('__hive_hl');
        if (h) { try { h.__hiveStop && h.__hiveStop(); } catch(_){}; h.remove(); }
      })();
    """)
    
    • SPA reconciliation strips injected overlays — LinkedIn's React reconciler removes foreign children of documentElement on re-render. The framework highlight overlay survives (re-mount observer + bounded retries), but test overlays injected via raw browser_evaluate may not. If you need a stable test overlay, append it to document.documentElement AND wrap in a MutationObserver that re-appends on removal, capped at ~20 retries.
    • Profile page chrome is not in the AX snapshotbrowser_snapshot on a profile misses a lot of the structured layout. Use browser_screenshot to orient; use specific selectors or the shadow-walk pattern for actions.
    • Name parsing from a connection card is fragile — the card layout changes every few months. Prefer .textContent.split(/\s+/)[0] on the first link inside the card rather than relying on a class like .invitation-card-name.

    Auth wall detection

    If you see a "Log in" / "Join LinkedIn" prompt instead of the logged-in feed, stop immediately and surface the issue to user. Do NOT attempt to log in via automation — LinkedIn's bot detection will flag the account.

    Check via:

    is_logged_in = browser_evaluate("""
      (function(){
        return !!document.querySelector('nav.global-nav') ||
               !!document.querySelector('[data-test-global-nav-me]');
      })();
    """)
    

    Deduplication pattern

    Dedup is handled by the colony progress queue, not a separate JSON file. For any daily loop (connection acceptance, profile visits, DMs), the queen enqueues one row in the tasks table per (profile_url, action) pair; workers claim, act, and mark done. Already-done rows are skipped on the next claim — that's your crash-resume and cross-day dedup. See hive.colony-progress-tracker for the full claim/update protocol.

    If you need to check whether a given (profile_url, action) has already been handled in a prior run before enqueuing a new row, query the queue directly:

    sqlite3 "<db_path>" "SELECT status FROM tasks WHERE payload LIKE '%\"profile_url\":\"<url>\"%' AND payload LIKE '%\"action\":\"<action>\"%';"
    

    Empty → not yet enqueued, safe to add. Otherwise honor the existing row's status.

    See also

    • browser-automation skill — general CDP/coord/screenshot rules, the click-then-type pattern, shadow-DOM strategy
    • x-automation skill — X/Twitter equivalent

    Alternatives

    Compare before choosing

    Computed 10023,781

    alirezarezvani/claude-skills

    app-store-optimization

    App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist

    Computed 10014,225

    wanshuiyin/Auto-claude-code-research-in-sleep

    citation-audit

    Use it for operations and research tasks; the detail page covers purpose, installation, and practical steps.

    Computed 1004,922

    dotnet/skills

    migrate-vstest-to-mtp

    Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing

    Computed 1002,504

    aaron-he-zhu/aaron-marketing-skills

    social-selling-planner

    Use when the user asks to "set up my founder social-selling routine", "build a daily engagement block for target accounts", or "turn funding / hiring signals into selling plays"; produces the founder/seller daily operating block — a time-boxed engagement-block spec (substantive value-add comments on target-account posts, never a pitch), warm-touch-before-ask cadence rules, trigger-response plays consuming the social-pulse-monitor B2B trigger watchlist (funding / hiring / launch signals), and a q