🌍 Public

Early Notion Worker Lessons

What I've learned noodling with this powerful new functionality.
5-min read
SHARE
Early Notion Worker Lessons
Image Source: Google Gemini/Nano Banana via Claude Code

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.

πŸ€–
Disclosure: I used NotionAI to summarize the content below. The excessive em dashes make it obvious.

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:

  1. Add the property to the schema block:
schema: {
  properties: {
    Descr: Schema.title(),
    Code: Schema.richText(),
    NewProp: Schema.richText(), // add here
  },
},
  1. 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:

  1. The second Worker registers a webhook endpoint and gets a URL on deploy.
  2. A Notion database automation triggers on page changes and fires a POST to that URL.
  3. 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.

Before You Go…
If you'd like to support my writing efforts, I'd appreciate it.

TIP THE AUTHOR

Member discussion