draftaday docs draftaday.com Join the beta
On this page

Docs · page 02 of 09

The Chrome extension

Studio's LinkedIn token carries w_member_social plus the OIDC scopes and nothing else, so every read endpoint of the API answers 403 — there is no supported way to ask LinkedIn anything. What is left is the public web, and it is split in two. A published post's own page is readable without a login, and the server reads it: that is where engagement counts, a post's video, its images and its carousel PDF come from. Profile pages answer the VPS with HTTP 999, and nothing at all shows the server your logged-in view.

So anything that needs your signed-in browser happens in the extension: feed harvest, one-post captures with their comments, pulling a post or video into Compose, and turning a profile URL into a member URN. The server fills in the public-page half of a pull behind it.

Install and connect

The extension is MV3, named "LinkedIn Post Generator", currently 1.17.0, and is loaded unpacked from the repo.

To install it: open chrome://extensions, turn on Developer mode, click Load unpacked, and select the repo root. Pin it.

To connect it to Studio: right-click the extension icon → Options → the card headed n8n pipeline (the left-hand nav calls it Pipeline). Two fields matter:

  • Studio URL (Send to Studio) — empty on a fresh install, with https://studio.draftaday.com shown as the placeholder. Any base that is not https:// is refused: Studio URL must be https.
  • Studio token (x-studio-token) — must match STUDIO_TOKEN on the Studio server.

You can also skip typing them. A content script runs on the Studio origin and, on every page load, hands the extension whatever Studio token that page is holding — so the extension picks up its URL and token by itself, and a rotated token repairs itself on the next load.

That only works once a token is in the Studio page to adopt, and the token gets there one way: Studio → Settings → Studio connection → Studio access token. Signing in through the sign-in page with Google, Microsoft or Facebook does not put one there. If you signed in with an account and never pasted the token, every Studio call from the extension fails with:

Studio URL/token not set in the extension Options

Fix it by pasting the token once — into Studio's Settings (and the extension adopts it on the next load), or straight into the extension's Options.

Good to know — several error messages say "Options → Studio Bridge", but the Options page has no section by that name. The Studio URL and token live in the card headed n8n pipeline, first in the left-hand nav as Pipeline.

Why settings are mirrored server-side

chrome.storage is keyed to the extension ID, and an unpacked extension's ID is a hash of its absolute folder path. Move or re-pack the folder and the ID changes, the store is empty, and every Studio call dies with the same "not set in the extension Options" error.

So the rest of the settings sheet is kept on the server through GET /api/settings/extension and PUT /api/settings/extension. Options pushes a backup on save; the extension restores only the fields that are missing locally, so a value you just typed always wins. Secrets are never stored there — studioToken, webhookToken, anthropicKey and apiKey are stripped on write, and the whole sheet is capped at 64 KB.

What the permissions buy

HostWhat it enables
www.linkedin.comEvery content script: feed scan, capture, composer pre-fill, profile reads
*.licdn.comThe service worker downloads a pulled post's images and video directly, with no CORS bridge
studio.draftaday.com, studio.choicebyrezwana.comThe Studio bridge, and the token-guarded API calls the worker makes
youtube.com, *.googlevideo.comPlaying a video and downloading the signed media URLs it hands the decoder
www.tiktok.com, vm.tiktok.com, vt.tiktok.com, *.tiktokcdn.com, *.tiktokcdn-us.comThe same capture on TikTok, plus a direct file download when one is available

optional_host_permissions covers https://*/* and http://*/*. Nothing is granted up front: Chrome asks the first time you press Test webhook (for your webhook host) or publish media (for the file host serving the bytes).

Cross-origin calls go through the service worker, with the x-studio-token header, over HTTPS. There is one deliberate exception: at the end of a feed scan the content script POSTs its own scan diagnostics straight to /api/feed/scan-diag with the same header, because a service worker that has gone to sleep can swallow them silently and this is the one channel that needs neither the worker nor your DevTools.

Harvest

To scan the feed once: open linkedin.com/feed, open the popup, set Pages to Scroll and Posts to capture (both start at 30), and press Scan Feed & Send. The in-page panel shows progress with a Cancel. Options caps the fields under Capture limits (Max pages / Max posts, default 99, hard max 999).

To also ship those captures to the Studio feed, turn on Harvest scans to Studio feed and set Harvest min score (default 1000). Score is engagement, not impressions: likes ×1 + comments ×10 + reposts ×20.

To harvest passively while you browse: Options → Auto-harvest while browsing (off by default). As you scroll the active LinkedIn tab, qualifying posts buffer in the service worker and flush to the Studio feed. Tunables: Passive min score (engagement, NOT impressions) (default 100; 0 harvests everything), Send every N qualifying posts (default 5), Flush window (minutes) (default 5). Capture full post text expands each post's "…more" so the whole body is stored. Only the tab you are actually viewing is read.

To harvest your own posts: open your profile → Activity → Posts, then popup → Harvest my posts (this page). These go to a separate self-posts store with no min-score filter — a post with zero engagement is kept, because it is still evidence of your voice and it still feeds the traction loop.

You mostly will not need to do that by hand. Track my posts' performance daily, in the same Options card, is on by default: once a day it opens your own Activity page in a background tab, records how every one of your posts is doing, and closes it. That is why a LinkedIn tab sometimes appears and vanishes on its own. It needs My LinkedIn profile URL plus the Studio URL and token, and the toggle turns it off.

If My LinkedIn profile URL is set in Options, a guard stops you harvesting someone else's page as yours:

This looks like <slug>’s profile, not yours (<your-slug>). Open your own Activity page and retry.

One-post captures

Right-click on a LinkedIn post and pick Capture post + comments to Studio. The content script opens the thread, clicks "load more comments" up to ten times, and sends the post with up to 300 comments (90-second timeout). On the server, any commenter who is already a known contact gets an inbound interaction logged against them in the CRM, deduped per post and person, so a re-capture never duplicates it.

Two other items sit on the same menu: Send to Draft (improve in Studio) saves the post as a source to improve later, and Log a win from this post… records a real business outcome.

On a linkedin.com/analytics/* page, right-click → Capture post analytics to Studio reads impressions, members reached, reactions, comments and reposts and snapshots them against the matching Studio post. The menu only appears on analytics pages:

Right-click on a post analytics page (linkedin.com/analytics/…)
No matching Studio post for this URL yet — harvest your posts once (popup → Harvest MY posts), then retry.

Pull into Compose

Compose has one URL box — Pull a post or video by URL — with one Pull button. What happens next is decided by the parsed hostname, never a substring, because the URL is handed to something that opens it in your logged-in browser. A host matches only as an exact label or a subdomain of it, so notyoutube.com and linkedin.com.evil.test are rejected.

You pasteWhat you get
A LinkedIn postIts text into Seed the Idea, the author, its images and video into your media pool, a carousel fetched as its original PDF, and everyone it tags pre-filled as mention chips
A lnkd.in shortlinkExpanded first by the server (POST /api/media/expand-url, tracking query stripped), then pulled as a LinkedIn post
A YouTube video (youtube.com, youtu.be or youtube-nocookie.com)The video file, plus its title, description, hashtags and top comments as grounding
A TikTok videoThe video, captured by playing it. Caption only — "TikTok comments are not read — only the caption." Experimental

A LinkedIn pull is two halves working together. The extension opens the post in your browser and brings back the text, the author, the tags and whatever media it can reach; then Compose asks the server for the rest off the post's public page — always the carousel (POST /api/media/pull-linkedin-doc, which returns the original PDF untouched), and, when the extension came back without them, the images (/pull-linkedin-image) and the video (/pull-linkedin-video, because the logged-in player hides the MP4 behind MSE). The status line names each stage as it runs.

Before the extension is detected the status line reads "Checking for the DraftADay extension…", then either "Extension ready — paste a LinkedIn post, YouTube video or TikTok video URL and press Pull." or:

Install/enable the DraftADay extension (and reload this page) to pull by URL. You can still paste a post's text below.

An unsupported host is named rather than guessed at: <host> is not somewhere Studio can pull from yet. LinkedIn posts, YouTube videos and TikTok videos work today.

Good to know — TikTok publishes no API that returns a video file, so playing it and recording what the player decodes is the only route there is. A captcha or a region wall will stop it, and that is a TikTok answer, not a bug.

@mention resolution

A real tag needs urn:li:person:<id> or urn:li:organization:<id>, and reading it means reading a profile page — the one thing the server is shut out of (HTTP 999). So the extension opens the profile in a background tab, reads the id and the display name out of the served HTML, and posts both to POST /api/network/member-urn, where they are stored against the contact or the page. That is a one-time cost: the id is reused from then on, and a pull that already carries ids opens no tabs at all.

Resolution runs as one message for the whole list, capped at 10 entities per call ("Only the first 10 tags were resolved."). Each unresolved tag costs a real background tab, roughly 2–5 seconds, and they are resolved one at a time.

Good to know — the tag has to be anchored to text. LinkedIn only links a mention where the post already spells that name. And, measured: companies and pages publish as real clickable tags on every post type, but LinkedIn renders a person tag only for your own account — anyone else publishes as their name in the text. The mention chips show all three states (@ linked, soft, name not in the post).

The Network CRM panel

On a linkedin.com/in/* profile or an open /messaging/* thread, a floating 🔎 Who is this? button opens the read-only Network CRM panel. From it:

  • Guide me — per-contact engagement coaching over your history with them. Drafts only.
  • Compare — local overlap math against another open profile tab, or Myself if My LinkedIn profile URL is set in Options.
  • 📥 Save full profile to CRM (or ➕ Add this profile to CRM for someone new) — scrolls the open tab, expands "…see more", and reads the full profile into the CRM.
  • 🎓 Onboard as beta tester — operator-only, under 🎓 Beta onboarding (operator only).

The panel footer says what it is: "Read-only. Drafts only — you review and send everything." An ambiguous name shows a candidate list and never auto-picks. If the token is wrong you get:

Could not reach Studio. Check Options → Studio Bridge (URL + token).

Safety posture

The extension is read-only on LinkedIn. It never auto-posts, auto-connects or auto-comments. It reads one profile at a time, on demand — no connection-list scraping, no bulk export, no crawling.

A feed scan halts the moment LinkedIn shows a challenge:

Stopped: LinkedIn may be showing a security/verification prompt.

And the extension's publishing path stops one step short of publishing. The composer script pre-fills the text, hashtags and media into LinkedIn's "Start a post" composer and stops at the review state — it never clicks Post or Submit. You click Post. (Studio's own scheduled publishing is a separate path: it posts through the LinkedIn API on the w_member_social scope, not through your browser.)

Keeping it current

Reload the extension at chrome://extensions after every release; an unpacked extension does not update itself, and older builds simply never send the newer messages. Pulling from TikTok needs 1.17.0 or later.