


I tested Notion Workers for the first time : here's what I built and what I learned
I'd been curious about Notion Workers for a while but never had a good excuse to try it.
So I decided to just dive in and find a simple enough use case to learn the basics without getting lost. I landed on this: a gallery of all my Spotify playlists inside Notion, with a one-click button to open each one directly in Spotify. Simple enough to be achievable, but it involved sync and OAuth which I wanted to understand.
Here's how it went.
---
What is Notion Workers?
It's a feature from Notion that lets you write TypeScript syncs and tools that run on Notion's own infrastructure. You write the logic, deploy it with a CLI, and Notion handles the scheduling, retries, and writes to your databases. No server to maintain.
---
Setting up
The CLI is called ntn. You scaffold a project, write your sync in src/index.ts, and deploy. The entry point is just:
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
Everything (syncs, databases, OAuth) gets registered on that worker object.
---
The OAuth part (Spotify)
Spotify requires OAuth to access user data like playlists. I was a bit worried this would be the hard part, but Workers handles it cleanly. You declare the OAuth capability with your app credentials:
const spotifyAuth = worker.oauth("spotifyAuth", {
name: "spotify",
authorizationEndpoint: "https://accounts.spotify.com/authorize",
tokenEndpoint: "https://accounts.spotify.com/api/token",
scope: "playlist-read-private playlist-read-collaborative",
clientId: process.env.SPOTIFY_CLIENT_ID ?? "",
clientSecret: process.env.SPOTIFY_CLIENT_SECRET ?? "",
});
Credentials go in a .env file. Then the deploy flow:
ntn workers create --name spotify
ntn workers env push # push secrets first — important!
ntn workers deploy
ntn workers oauth show-redirect-url # copy this into your Spotify app settings
ntn workers oauth start spotifyAuth # opens browser to authorize
Lesson learned: env push must happen before oauth start. The deployed worker needs the client secret to complete the token exchange. I got a confusing error by doing it in the wrong order.
After that, inside the sync I just call await spotifyAuth.accessToken() the runtime handles refresh automatically.
---
The sync itself
Spotify's playlist endpoint has no updated_since filter, so there's no way to fetch only what changed. The right approach here is a replace sync: re-fetch everything each cycle, and the runtime automatically removes playlists that disappear. Simple.
The Notion database schema:
const playlistsDb = worker.database("playlistsDb", {
type: "managed",
initialTitle: "Spotify Playlists",
primaryKeyProperty: "Playlist ID",
schema: {
properties: {
Name: Schema.title(),
"Playlist ID": Schema.richText(),
Description: Schema.richText(),
Owner: Schema.richText(),
Tracks: Schema.number(),
Public: Schema.checkbox(),
URL: Schema.url(), / ← one-click link to open in Spotify
"Snapshot ID": Schema.richText(),
},
},
});
The URL property is the key one for my use case, it shows up as a clickable link on every gallery card.
---
Getting cover images to display in gallery view
I stored the playlist cover as a Files & media property and set the gallery card preview to use it. The data was there — but no images appeared in the gallery.
Turns out Notion doesn't render external URLs from file properties as gallery previews. It only works with files actually hosted on Notion.
The workaround: embed the image in the page body via pageContentMarkdown, then switch the gallery card preview to "Page content" mode:
pageContentMarkdown: playlist.images?.[0]?.url
? ``
: "",
Works great. Each card shows the cover art, the playlist name, and a direct link to Spotify.
---
Overall impression
Notion Workers is genuinely pleasant to work with for this kind of thing. The OAuth flow was easier than I expected, the --preview command makes iteration fast, and the deploy cycle is quick. For anyone wanting to pull data from an external API into Notion without managing infrastructure, it's worth a look.
Happy to answer questions if you want to try something similar!