Medium syndication agent

The next Lab Notes post becomes a Medium story that points back at the original.

A post on a small company blog is read by the people who already found the blog. Medium has readers, search placement and distribution of its own, and a copy there with a canonical link back to the original earns that reach without competing with the original in search. Done by hand it means re-pasting every post, fixing the formatting Medium breaks, and setting a canonical link Medium no longer offers a switch for.

Running on our own company since Oct 11, 2026 · Medium · Risk: Low. Everything on this page is public; there is nothing to sign in for.

Goes inThe oldest post not yet on Medium: a Lab Notes post, or one opted in with publishToMedium in its frontmatter
Comes outOne story on Medium with a canonical link to the original, and one entry in the registry file this log is built from
Human stepSomeone keeps the Chrome signed in to Medium. A run that stops leaves its draft for a person; nothing is retried on its own
Runs onA machine you control, two mornings a week, through a Chrome profile already signed in to Medium

Tasks and outputs

What it takes on

Pick the post

Lists the Lab Notes posts and the opted-in posts, drops the retired, the opted-out and the already syndicated, and takes the oldest. One per run.

One post

Import, replace, publish

Imports the post’s URL for the canonical link, pastes the site’s own rendering over Medium’s, fills the preview title, subtitle and topics, publishes.

Live story

Verify and record

Reads the canonical back from the live story and writes the story into the registry; the push that follows deploys the site.

Registry entry

Who it is for: Teams who already keep a blog and want selected posts in front of Medium’s readers, while the original page stays the one search engines and AI engines treat as the source.

How we use it: We have run it on our own blog since October 2026, two mornings a week (Monday and Thursday) from the founder’s machine. It cross-posts the Lab Notes posts only: the site is already read as an AI news blog, and the Medium profile is meant to be about AI visibility, so the daily news posts are deliberately left out. The first story went out on October 11 with its canonical verified; every story since is on the public run log.

tools/medium/syndicate.mjsQueue and render rules of the script

What goes to Medium, and how

01Lab Notes posts go. A post of any category goes with publishToMedium: true; publishToMedium: false keeps it out. Daily AI-news posts are left out on purpose.

02Retired posts and posts already in the registry are skipped. Oldest first, one per run.

03The canonical link comes from Medium’s own importer. The script reads it back from the live story and records whether it is the original.

04The body is the site’s rendering: GFM markdown, tables as lists, site-relative links made absolute.

05First line: “Originally published at claw-world.app on <date>”. Last line: the same with the full URL.

06Preview title is the post title, preview subtitle the excerpt cut to Medium’s 140 characters, five topics. The script never retries a publish.

Read from the scriptDry run by default; --publish to go live

The rules the agent’s output is written to, from the file named above. This site prints no example output; what the agent has shipped is in its run log.

Workflow

How a run goes, step by step

Pick a step to see what goes in, what comes out, and where a person is involved.

Step 1 of 6

Pick the post

Reads every post’s frontmatter, keeps the Lab Notes posts and the ones opted in with publishToMedium, drops the retired and opted-out ones and whatever is already in the registry, and takes the oldest. If the queue is empty, the run stops before opening Chrome and says so.

Goes in

web/content/blog/*.md and the registry

Comes out

One slug

Where a person steps in

Nobody, unless the queue is empty: then someone writes a Lab Notes post or opts one in.

Step 2 of 6

Import for the canonical

Opens Medium’s Import a story in the signed-in Chrome and gives it the post’s URL. Medium fetches the page and creates a draft that carries the canonical link and the original date; its formatting is not kept.

Goes in

The post’s public URL, server-rendered

Comes out

A Medium draft with a canonical link

Where a person steps in

When Chrome is unreachable or Medium is not signed in: the run stops with exit code 2 and a person signs in.

Step 3 of 6

Replace the body

Renders the markdown itself (GFM, tables as lists, absolute links, the lead line and the footer), copies it as rich text and pastes it over the imported body. Then checks that the first block is the lead line, that the footer is there and that Medium’s own import footer is gone; if not, the run stops.

Goes in

The post file

Comes out

The draft with the site’s rendering

Where a person steps in

When the check fails: a person deletes the draft (--delete) and runs again with the Chrome window left alone.

Step 4 of 6

Fill the dialog and publish

Sets the preview title, the preview subtitle from the excerpt and the five topics, then clicks Publish. In a dry run it stops before the click and leaves the tab open for a person to read.

Goes in

Title, excerpt and topics

Comes out

A live story

Where a person steps in

In a dry run, a person reads the draft before the real run. A renamed Medium button stops the run here with a screenshot.

Step 5 of 6

Verify the canonical

Opens the live story in the same Chrome and reads its canonical link. The registry records verified when it is the blog URL and unverified otherwise; the story is live either way.

Goes in

The story URL

Comes out

verified or unverified

Where a person steps in

When unverified: a person opens the story, checks its head, and runs --verify.

Step 6 of 6

Record and deploy

Writes the story URL, id, date, time, canonical state and topics into the registry under the post’s slug. The scheduled run then commits and pushes that one file and triggers the deploy, which builds this log from the registry.

Goes in

The run’s result

Comes out

One entry in web/content/medium-syndication.json

Where a person steps in

When the push fails: a person pushes. The entry is already written locally.

Before you start

What you’ll need to bring

Company material

Nothing beyond the blog itself: the lead line, the footer and the canonical all point at your own site.

Source material

A blog with server-rendered post pages and frontmatter the script can read: a category, a title, an excerpt and a date. Ours is the Lab Notes category on this site.

Platform account

A Medium account signed in once in a dedicated Chrome profile. No API token (Medium issues none), no password handed over.

Run environment

A machine that can keep that Chrome open during runs, Node.js for the publishing script, and an agent runner that follows a written workflow. We use Claude Code.

Guides and files

Set it up yourself

The setup guide, and the files the agent is built from. You run it in your own environment, with your own accounts.

Open · no account neededTwo Node scripts: the syndicator (a dry run by default, --publish for real, --verify to re-read a canonical, --delete to remove a dry-run draft) and an inspector for when Medium changes its editor, plus the wrapper the scheduler runs. Both scripts attach to an already signed-in Chrome over the DevTools protocol. Not packaged for public download yet — the setup guide below describes what they do; the managed tier runs it for you today.
Setup guide
  1. 01

    Runtime

    • Node.js 20 or later, with the dependencies installed in the scripts’ folder (a markdown renderer, a frontmatter parser and the browser driver).
    • A scheduler that starts the wrapper two mornings a week, or an agent runner that follows a written workflow — ours is a scheduled task on the founder’s machine; the steps are deterministic, so no model is in the loop.
  2. 02

    Browser setup

    • A Chrome started with remote debugging on a fixed port (we use 9222) and a dedicated profile, signed in to Medium once by a person. The LinkedIn, Reddit and Bing agents share the same window.
    • The script opens its own tabs and closes them; it never touches another tab. Keep the keyboard off that window while a run is pasting: keystrokes typed while it has focus land in the editor.
  3. 03

    Account setup

    • A Medium account, signed in in that Chrome. No API token (Medium stopped issuing them) and the script never sees the password.
    • A blog whose post pages are server-rendered. Medium’s importer fetches the page itself, so a page that is empty without JavaScript imports as nothing.
  4. 04

    Configuration

    • The rule for which posts go: a list of categories (ours is Lab Notes) plus the per-post opt-in and opt-out field in the frontmatter.
    • The five default topics, and the site address the canonical, the lead line and the footer point at.
    • The registry file the publish step writes, in the repository the site builds from, so the run log can be generated from it.
  5. 05

    First run

    • Run the script on one post without --publish: it imports, replaces the body and fills the publish dialog, then leaves the tab open. Read the draft on Medium, then delete it with --delete.
    • Publish one post for real, open the live story and check its canonical link by hand once; the script records what it read back.
    • Commit the registry and confirm the run appears on the log after the deploy.
Files · 4

syndicate.mjs

Script · Node. The queue, the import, the render and paste, the publish dialog, the canonical check and the registry write.

Script

inspect.mjs

Script · Node. Prints a Medium page’s text and controls through the same Chrome, for when the editor changes.

Script

run-scheduled.ps1

Scheduler · PowerShell. The Monday and Thursday run: takes the next post, publishes, commits and pushes the registry, triggers the deploy.

Scheduler

medium-syndication.json

Registry · JSON. One entry per story published, keyed by post slug. The file this log is built from.

Registry

Not packaged for public download yet. The guide on the left describes what each one does, step by step, so you can rebuild the setup; the Hosted and Fully managed tiers run them for you today.

Rather have us run it?

Hosted: your accounts in a cloud browser we run, and you review what goes out. Fully managed: we set the strategy and run the agents.

Good to know

Where it stops and asks for you

  • Medium is not signed in, or Chrome is unreachable. Exit code 2 before anything is imported. A person opens Medium in that Chrome and signs in.
  • The first body block is not the lead line. Something else landed in the editor, usually keystrokes while the window had focus. The run stops, the draft stays for a person to delete, nothing is published.
  • The canonical cannot be read back. The story is live; the record is marked unverified and waits for a person to check the head and run --verify.
  • The queue is empty. The run stops before opening Chrome and says so.
  • Medium changed its editor. A locator stops matching and the run stops with a screenshot. The inspector script prints the page’s controls so the right one can be picked.

Risk notice

What to watch for

  • Medium’s terms govern automation on the platform. The script works through a browser a person signed in, publishes at most two stories a week, and never retries a publish.
  • A canonical copy earns no search ranking of its own; that stays with the original, which is the point. What it earns is reach inside Medium, and only Medium’s own stats show that.
  • A rewrite mode, with original text on Medium instead of a copy, is deliberately not built until measurement shows the Medium copies being cited by AI engines.

FAQ

Questions about this agent

The script exits with code 2.

Chrome is not reachable on its debugging port, or Medium is not signed in in that Chrome. Start the debug Chrome, open medium.com in it and sign in by hand; nothing else changes.

The run hangs before it opens anything.

An idle new-tab page left in the debug Chrome has been seen to stop the DevTools connection from answering. The script closes those first; if the attach still hangs, close the tab through the debugging port’s /json/close/<id> endpoint and run again.

The run failed saying the first body block is not the lead line.

Something else landed in the editor, usually because the Chrome window had focus while the script was pasting. The run stops on purpose rather than publish a body it did not write; delete the draft and run again with the window left alone.

The import came back with an old version of the post.

Medium caches a URL it has fetched before and returns that copy on a second import. The body is replaced with the current rendering anyway, so the text is right; the import only has to supply the canonical link.

Would you rather we ran this agent for you? There is a managed service for that.

Talk to us