Early Notion Worker Lessons
At its recent developer conference, Notion announced a powerful new way to extend the application. Still in beta, Notion Workers let users do many interesting things. In the company's words:
Workers are isolated sandboxes managed by Notion, so the code behind your syncs, tools, and workflows runs on our infra(structure) instead of your servers.
Most intersting to me: the ability to sync data from third-party systems. For example, say that your organization runs Salesforce as its CRM. Workers make it relatively easy to bring that data into your Notion workspace for additional analysis and tracking.
I've spent a good deal of time noddling with the new sync capability. Claude Code helped me navigate the new Notion CLI and run some tests with a simple Google Sheet. In today's post, I'll share some of the things I've learned so far.
Long story short: Workers aren't perfect, but they're remarkable. I suspect that over the next year I'll be doing more with them for my Notion clients.
Table of Contents
Project & Worker Management
Useful CLI Reference Commands
| Command | Purpose |
|---|---|
ntn workers list |
List all Workers in the workspace |
ntn workers capabilities list |
List capabilities for the current project's Worker |
ntn workers sync trigger <key> |
Trigger a sync immediately |
ntn workers sync state reset <key> |
Clear sync state for a full re-fetch |
ntn workers sync state get <key> |
Inspect current sync state |
ntn workers runs list |
View recent run history and exit codes |
ntn workers runs get <run-id> |
Get details on a specific run |
ntn workers delete <worker-id> |
Delete a Worker by ID |
ntn workers deploy |
Deploy (create or update) a Worker from the current directory |
Directory Context Matters
The ntn CLI resolves the target Worker from the workers.json file in the current directory. Always cd into the correct project folder before running Worker commands. Running commands from the wrong directory targets the wrong Worker or fails entirely.
cd /Users/your_drectory
Finding Project Directories
To locate all Worker projects on your machine:
find ~ -name "workers.json" 2>/dev/null
Identifying a Worker's ID
To find the Worker ID tied to a specific project directory:
cat /path/to/project/workers.json
The workerId field is the canonical identifier for that Worker.
Renaming a Worker
The CLI lacks a dedicated rename command. The display name derives from the name field in package.json. Update that field and redeploy to rename the Worker. The sync capability key β which you define in src/index.ts β is independent of the display name and continues to work without modification.
Deleting a Duplicate Worker
With two Workers sharing the same display name, confirm which ID belongs to the unwanted one via workers.json, then delete it:
ntn workers delete <worker-id>
Code Reuse Across Workers
Once you build a working Worker, save it as a GitHub repo or template. The Worker structure, deploy flow, and Notion API calls transfer to future projects. Swap out the data source and schema β the scaffolding stays intact.
Syncing
Triggering a Manual Sync
To bypass the schedule and trigger a sync immediately, run the following from your Worker project directory:
ntn workers sync trigger <syncKey>
The sync key is the string passed to worker.sync("...") in src/index.ts β it may differ from the Worker's display name in the Notion UI.
Finding the Correct Sync Key
If you don't know the sync key, run this from the project directory:
ntn workers capabilities list
This returns the exact key and type for every registered capability.
Diagnosing a Sync That Runs But Writes Nothing
All runs showing exit code 0 with no database changes usually means an incremental sync has recorded state and sees no new data. Reset the state to force a full re-fetch:
ntn workers sync state reset <syncKey>
ntn workers sync trigger <syncKey>
Diagnosing a Runaway Sync Loop
If the runs list shows dozens of executions firing every few seconds, you've set the schedule option in src/index.ts too aggressively (such as "continuous" or "1m"). Change it to "1h" (or another appropriate interval) and redeploy:
ntn workers deploy
Left unchecked, a runaway sync burns through Worker credits rapidly.
Schema & Data
Adding a New Property to a Synced Database
Adding a column to the Google Sheet does not automatically add it to Notion. The Worker only syncs properties explicitly defined in src/index.ts. Two changes are required:
- Add the property to the schema block:
schema: {
properties: {
Descr: Schema.title(),
Code: Schema.richText(),
NewProp: Schema.richText(), // add here
},
},
- Add the property to the mapping inside
execute:
properties: {
Descr: Builder.title(row[1] ?? ""),
Code: Builder.richText(row[0]),
NewProp: Builder.richText(row[2] ?? ""), // add here
},
Then redeploy and resync:
ntn workers deploy
ntn workers sync state reset <syncKey>
ntn workers sync trigger <syncKey>
Changing a Property Type
Changing a property's type in src/index.ts (such as from Schema.richText() to Schema.number()) does not convert the existing property in Notion. Notion keeps the original type.
The fix: rename the property in src/index.ts to force Notion to create a fresh property with the correct type. The old property remains but sits empty β hide it from your views.
Replace Mode Only Manages Worker-Created Records
The Worker's replace mode mark-and-sweep only tracks and removes records it created. Records added manually in the Notion UI don't carry the Worker's internal tracking metadata, so the sync leaves them alone β even if they don't exist in the external source.
If orphaned manual records appear in a managed database, delete them directly in Notion. They won't be cleaned up automatically.
Identifying Read-Only vs. User-Defined Properties
Notion provides no visual indicator distinguishing synced (read-only) properties from user-defined ones. The reliable method: open a record and try to edit a field. Synced Worker properties are locked. User-defined properties accept edits.
From the code, read-only properties carry readOnly: true in the data source schema β these map directly to columns in the external source. Any property you add to the Notion database outside the Worker schema (such as a Status or ID field) remains fully editable and the sync won't touch it.
Two-Way Sync
Workers syncs are one-way by design: external source β Notion. To push Notion changes back to an external source, build a second Worker using worker.webhook(...). The flow:
- The second Worker registers a webhook endpoint and gets a URL on deploy.
- A Notion database automation triggers on page changes and fires a POST to that URL.
- The webhook handler reads the payload and writes the change to the external source (such as Google Sheets via its API).
The Notion-side plumbing transfers to any external source. The external API client and auth (such as OAuth for Google Sheets) require rewriting per source.
Deployment & Debugging
TypeScript Compilation Errors During Deploy
A failed deploy (exit code 2) means the TypeScript compiler rejected the code. The error appears directly in the Terminal output when you run ntn workers deploy. Always read that output before troubleshooting elsewhere.
Common example: Builder.number() does not accept null. Use a numeric fallback instead:
// Wrong
Num: Builder.number(row[2] ? parseFloat(row[2]) : null)
// Right
Num: Builder.number(parseFloat(row[2] ?? "0") || 0)
Debugging Failed Deploys
Use ntn workers runs list to check exit codes. A deploy with exit code 2 failed β any sync runs after it use the last successful build, not your latest code. The full error appears in the Terminal output of ntn workers deploy itself, not in the run logs.
Member discussion