
Sync your team's Notion skills to Claude Code, Cursor, and Codex
Point the Agent Skills API at your team's Notion skills database and let a scheduled job mirror it into a plugin marketplace, so Claude Code, Cursor, and Codex run the same reviewed instructions as Notion Agent.
Notion 3.7, released on September 15, 2026, turned any page into a skill: a set of reusable instructions that Notion Agent follows for one kind of work. The same release added a way to send a skill to a local coding agent as a
SKILL.md file, badged in Notion when the original changes. 12That per-skill download is the right tool for one person. Ask a five-person team to keep their Claude Code, Cursor, and Codex copies current, and the same procedure quietly becomes five versions, each with its own idea of what is current.

SKILL.md file for one agent on one machine at a time. Image: Notion 3.7 release notes, September 15, 2026.The Agent Skills API is the other half of the loop. It exposes each skill the workspace shares with a connection as a plugin directory, and it gives every plugin a version identifier you can diff. 3 Run the listing on a schedule, download only what changed, commit the result into a repository, and that repository becomes the distribution channel: each coding agent registers it as a plugin marketplace and picks the skills up from there. Notion stays the place where the instructions are written and reviewed.

How one reviewed skill in Notion reaches three agent marketplaces, and which catalog file in the repository each client reads. Diagram by the author.
Notion publishes a runnable version of this loop at
makenotion/notion-skills-github-sync. The steps below follow what its sync does, so a team can fork it or write its own. 4Before you build it
- A Notion connection or personal access token carrying the Read content capability. A token without it gets
403 restricted_resourcefrom the listing endpoint. 5 - Access to the skill databases you expect to see. The listing returns the plugins shared with the connection, so a database nobody shared is simply absent from the results. 35
- The
Notion-Version: 2026-03-11header on every request. 3 - A repository to hold the marketplace, and a token that can push to it. The default
GITHUB_TOKENis scoped to the repository running the workflow, so writing into a second repository takes a fine-grained token withcontents: write. 6 - Administrator help with the credentials on both sides. The setup guide for Notion's sample sync,
notion-skills-github-sync, notes that creating the GitHub token and the Notion connection may need admin approval. 4
Step 1: Let Tags decide your plugin boundaries
The unit the API works in is a plugin. Every unique value in a skills database's Tags property becomes one plugin, and a skill with no tags becomes a plugin containing only itself. 5
That mapping is the design decision in this build. Tag the skills a team installs together —
engineering, product, gtm — and each group arrives as one directory that an agent can install as a unit.Two properties do the rest of the work.
Description is what an agent reads to decide whether a skill is relevant, and Files carries the checklists, templates, and scripts that travel with the instructions. 2One limit shapes how you tag. A plugin directory holds at most 100 skills, and a tag group that grows past that returns only its most recently updated members. 7
Step 2: List the plugins and compare version_id
curl -X GET "https://api.notion.com/v1/ai/plugins?page_size=100" \
-H "Authorization: Bearer $NOTION_API_KEY" \
-H "Notion-Version: 2026-03-11"Every entry carries four fields:
id, name, description, and version_id. 5 Keep id as the handle for the download call, and keep version_id to compare against the value you stored last time. Equal values mean the plugin has not changed, and the run can skip it. 3That comparison sets the cost of the sync. Downloads follow the number of edits rather than the size of the workspace, so a workspace with hundreds of plugins spends most hours doing one listing and no fetching at all.
version_id is an opaque string, so the whole comparison is equality. Any order or timestamp you read out of the string is an accident of the current implementation. 5Keep the pagination honest.
page_size stops at 100 per request, and next_cursor goes back as start_cursor until has_more reads false. 5Step 3: Download only what moved
For each plugin whose
version_id changed, ask for its directory:curl -X GET "https://api.notion.com/v1/ai/plugins/$PLUGIN_ID" \
-H "Authorization: Bearer $NOTION_API_KEY" \
-H "Notion-Version: 2026-03-11"The response holds a temporary signed URL to a gzipped tar archive. 7 Extracted, the archive follows the Agent Plugins layout: a
plugin.json beside a skills/ folder, with one directory per skill holding its SKILL.md and anything attached through the skill's Files property. 3Download the archive in the run that asked for the link. A signed URL is temporary by design, so a stored link is a broken link. 7
A single skill has its own route.
GET /v1/ai/skills/{id} takes the skill page's ID and returns the same kind of signed URL, this time for the bare skill directory with the skill's files at the top level. 8The catalog each client reads
Three clients read the same plugin directories and disagree about the file that points at them. Notion publishes a sample sync,
notion-skills-github-sync, that keeps the disagreement in one place, in src/sync/clients.ts. 49| Client | Catalog file it reads, and the entry it expects |
|---|---|
| Claude Code | .claude-plugin/marketplace.json, where an entry carries a plugin name, a source path relative to the repository root, and a description. 9 |
| Cursor | .cursor-plugin/marketplace.json, with the same plugin name, source, and description fields. 9 |
| Codex | .agents/plugins/marketplace.json, where an entry carries a structured local source, an install policy, and a category. 9 |
Everything outside the
plugins array in a catalog file is the repository's own identity — its name, owner, and description — and Notion supplies none of it. Replace the plugin list and leave the rest of the file alone. 9Per plugin, one more file carries the rest of the standard. A
plugin.json at the plugin's root is what every client reads, and Claude Code also reads a derived copy at .claude-plugin/plugin.json inside the plugin. 9Step 4: Land it as one commit, on a schedule
The trigger lives outside Notion. Notion's sample runs the sync from a GitHub Actions workflow on an hourly cron, with a manual dispatch button and a concurrency group set not to cancel a run already in progress. 6
Configuration splits in two. Non-secret settings live in repository variables, so the only committed material is the code, and the two credentials live in repository secrets: the Notion token and a GitHub token with write access to the target repository. 6
Name those settings with the platform's constraint in mind. GitHub rejects variable and secret names beginning with
GITHUB_, which is why the sample stores its target repository as SKILLS_GITHUB_REPO and maps it back to GITHUB_REPO inside the job. 6The write is one commit per run. The sample reads the target branch's tree, plans the files to write and delete against it, and commits the result through GitHub's Git Data API; text travels inline in the tree request, and binary files take their own upload. 10
The first run is the long one, because it downloads one archive per plugin before it commits anything. The sample sets a 45-minute timeout for that reason, and a run killed partway leaves no commit at all; the empty repository waits for the next hour. 6
The loop underneath is three HTTP calls and a commit, so the trigger is replaceable. A team whose repository does not run Actions can put the same sequence behind a scheduled workflow in n8n or Make, or a cron job on a machine that holds the two secrets.
Step 5: Register the marketplace where the agents are
claude plugin marketplace add your-org/your-marketplace
claude plugin install engineering@skillsThe install ID is the entry's
name, an @, and the marketplace's name. Inside a session, /plugin marketplace add and /plugin install do the same two steps. 11Validate a hand-edited catalog before anyone installs from it.
claude plugin validate ./your-marketplace reads the marketplace directory and reports JSON syntax errors, missing required fields, a source path containing .., and problems inside each plugin's own manifest. 11Keep an entry's
name identical to the name inside that plugin's plugin.json. When the two differ, an install reported by the manifest name fails with a not-found error while the plugin sits in the repository. 11What the team sees after it runs
Someone edits the skill page in Notion. The next scheduled run writes the changed plugin directory and makes one commit, and the commit message names the plugins it wrote and the ones it pruned. 12
A teammate who has registered the marketplace installs the plugin, and Claude Code, Cursor, and Codex run the same instruction Notion Agent runs. 911
The Notion pages stay the editable copy. The download menu on a skill page remains useful for a laptop that nobody automated. 2
Test it before you trust it
| Scenario | Setup | Expected result |
|---|---|---|
| Dry run | Run the sync with writes disabled | The plan lists the plugins to write, the files to change, and the plugins to prune, and the repository goes untouched. 12 |
| First sync into an empty repository | Point the run at a repository with no marketplace files | Every plugin directory is written, the catalog files are seeded, and the run makes one commit. 12 |
| Nothing edited since the last run | Run the sync again with no changes in Notion | Every comparison matches, nothing is fetched, and the run reports that it is up to date. 12 |
| One skill edited | Change a line on a single skill page | Only that plugin's version_id moves, only its directory is downloaded, and the commit names it. 3 |
| A skill deleted | Move a skill page to the trash | Its directory disappears from the repository and the commit lists it as pruned, provided the listing completed. 3 |
| Token without Read content | Call the listing endpoint with a token missing that capability | 403 restricted_resource, and the run stops before it writes anything. 5 |
| Listing fails partway | Interrupt the run during the listing | The previous commit stands and no directory is removed. 3 |
Gotchas
An incomplete listing must never prune. Remove local directories for plugin IDs missing from the list only after every page has been fetched successfully, and let a failed listing leave the local plugins where they are. 3
A 404 on the archive means retry, and the copy stays. The listing and the download route can disagree in one known condition: the archive answers
404 directory_not_found while the plugin is still listed. Notion's own sync keeps the existing copy and tries again on the next run, and stops the run on every other error. 712A tag group over 100 skills is trimmed in silence. Only the most recently updated skills come back, so a group that outgrows the cap loses its oldest members from the marketplace. 7
A name that slugifies to nothing needs a fallback keyed on identity. The sample found 26 of 420 plugins in its development workspace whose names flatten to an empty string once non-ASCII characters are stripped. It falls back to the last 12 characters of the plugin ID rather than a position-based suffix, because a position-based suffix shifts every later directory whenever one plugin is deleted, rewriting subtrees that never changed. 12
Refuse to overwrite a catalog you cannot parse. The sample stops with an error when an existing marketplace file is not valid JSON, which matters in a repository people also edit by hand. 12
Skip archive entries that escape the directory. The sample logs each unsafe path it skipped and carries on with the rest of the archive. 12
Start with a read-only run: list the plugins, diff the version IDs, print the plan, and write nothing. Once the plan matches what you expect, add the repository token and let it commit.
References
- 1
- 2Skills for Notion Agent | Notion Help
notion.com
- 3Agent Skills API – Notion Docs
developers.notion.com
- 4notion-skills-github-sync
github.com
- 5List plugins – Notion Docs
developers.notion.com
- 6sync.yml – notion-skills-github-sync
github.com
- 7Get a plugin directory – Notion Docs
developers.notion.com
- 8Get a skill directory – Notion Docs
developers.notion.com
- 9clients.ts – notion-skills-github-sync
github.com
- 10github.ts – notion-skills-github-sync
github.com
- 11Create a marketplace – Claude Code Docs
code.claude.com
- 12engine.ts – notion-skills-github-sync
github.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.
