create-ncblock 0.0.39 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/scripts/init.ts +39 -23
  3. package/scripts/utils/templates.ts +53 -8
  4. package/sdk-version.json +1 -1
  5. package/templates/worker/.agents/INSTRUCTIONS.md +593 -0
  6. package/templates/worker/.agents/skills/auth-guide/SKILL.md +227 -0
  7. package/templates/worker/.agents/skills/sync/SKILL.md +368 -0
  8. package/templates/worker/.agents/skills/sync-debug/SKILL.md +101 -0
  9. package/templates/worker/.agents/skills/sync-guide/SKILL.md +253 -0
  10. package/templates/worker/.agents/skills/sync-guide/api-pagination-patterns.md +661 -0
  11. package/templates/worker/.agents/skills/sync-guide/examples/incremental-basic.ts +103 -0
  12. package/templates/worker/.agents/skills/sync-guide/examples/incremental-bimodal.ts +207 -0
  13. package/templates/worker/.agents/skills/sync-guide/examples/incremental-events.ts +132 -0
  14. package/templates/worker/.agents/skills/sync-guide/examples/replace-paginated.ts +79 -0
  15. package/templates/worker/.agents/skills/sync-guide/examples/replace-simple.ts +57 -0
  16. package/templates/worker/.agents/skills/sync-validate/SKILL.md +60 -0
  17. package/templates/worker/.claudeignore +2 -0
  18. package/templates/worker/.codexignore +2 -0
  19. package/templates/worker/.examples/automation-example.ts +60 -0
  20. package/templates/worker/.examples/oauth-example.ts +79 -0
  21. package/templates/worker/.examples/sync-example.ts +184 -0
  22. package/templates/worker/.examples/tool-example.ts +37 -0
  23. package/templates/worker/.examples/webhook-example.ts +66 -0
  24. package/templates/worker/README.md +765 -0
  25. package/templates/worker/_gitignore +6 -0
  26. package/templates/worker/docs/custom-tool.png +0 -0
  27. package/templates/worker/notionhq-workers-0.4.0.tgz +0 -0
  28. package/templates/worker/package.json +25 -0
  29. package/templates/worker/src/index.ts +8 -0
  30. package/templates/worker/tsconfig.json +16 -0
  31. package/templates/worker/views/empty/AGENTS.md +71 -0
  32. package/templates/worker/views/empty/README.md +10 -0
  33. package/templates/worker/views/empty/_gitignore +2 -0
  34. package/templates/worker/views/empty/custom_blocks.json +4 -0
  35. package/templates/worker/views/empty/index.html +15 -0
  36. package/templates/worker/views/empty/package.json +23 -0
  37. package/templates/worker/views/empty/src/index.css +33 -0
  38. package/templates/worker/views/empty/src/index.tsx +20 -0
  39. package/templates/worker/views/empty/tsconfig.json +17 -0
  40. package/templates/worker/views/empty/vite.config.ts +7 -0
@@ -0,0 +1,227 @@
1
+ ---
2
+ name: auth-guide
3
+ description: Guide to setting up third-party authentication for a Notion Worker. Covers external-service API keys / personal access tokens and OAuth. Use when the worker needs credentials for a non-Notion API, not for Notion API tokens or `ntn login`.
4
+ user-invocable: false
5
+ ---
6
+
7
+ ## What this guide is for
8
+
9
+ This guide is for authentication against the **third-party service** your worker integrates with — the place data is coming from or going to (GitHub, Stripe, Salesforce, Google, Slack, etc.).
10
+
11
+ Use it when the worker needs credentials for a non-Notion API. Do not use it for Notion API tokens, `ntn login`, or general Notion workspace setup.
12
+
13
+ Most workers will use one of two auth patterns from the upstream service:
14
+
15
+ - personal API key / personal access token
16
+ - OAuth
17
+
18
+ ## Decision framework
19
+
20
+ Before recommending anything, check the provider's current developer docs. Confirm whether it offers:
21
+
22
+ - personal API keys / personal access tokens
23
+ - OAuth
24
+ - neither
25
+
26
+ Always research the provider's current auth docs on the web before advising the user. Do not rely on memory for auth availability, setup steps, or settings locations.
27
+
28
+ Then choose mechanically:
29
+
30
+ 1. **Service offers personal API keys / PATs?** Recommend API key / PAT first. It is usually the simplest fit for an individual-scoped worker.
31
+ 2. **Service is OAuth-only?** Use OAuth.
32
+ 3. **Both exist, but the worker should not depend on one person's credential or should be easy to re-auth if that person leaves?** Recommend OAuth.
33
+ 4. **Service has neither?** See "When neither option is available" at the end of this guide.
34
+
35
+ State the recommendation in one sentence with the reason. Example: "Linear offers personal API keys, so use an API key; it's the simplest fit here."
36
+
37
+ ## Setup: API key
38
+
39
+ Pattern: store the credential in `.env` (or directly in the deployed worker's secrets), read it from `process.env` inside the capability's `execute`, push `.env` to the deployed worker before going live.
40
+
41
+ 1. Look up the provider's current docs for creating a token. Link the user to the exact settings page when possible.
42
+
43
+ 2. **Have the user add the token to `.env` themselves** (create the file if it doesn't exist), or tell them the equivalent `ntn workers env set` command. Tell them the variable name to use:
44
+
45
+ ```
46
+ GITHUB_API_TOKEN=<paste your token here>
47
+ ```
48
+
49
+ `.env` is automatically loaded for local execution (`--local`).
50
+
51
+ 3. Read the token inside `execute` (this is the part you write):
52
+
53
+ ```ts
54
+ const token = process.env.GITHUB_API_TOKEN ?? "";
55
+
56
+ const res = await fetch("https://api.github.com/user", {
57
+ headers: { Authorization: `Bearer ${token}` },
58
+ });
59
+ ```
60
+
61
+ 4. If auth seems broken, test the token outside the worker first against a simple authenticated endpoint from the provider docs. Example for GitHub:
62
+
63
+ ```shell
64
+ curl -H "Authorization: Bearer $GITHUB_API_TOKEN" https://api.github.com/user
65
+ ```
66
+
67
+ 5. Test locally with `ntn workers exec <capability> --local`. Confirm auth works before deploying.
68
+
69
+ 6. **Push the secret to the deployed worker.** Once the token is already in `.env`, run:
70
+
71
+ ```shell
72
+ ntn workers env push
73
+ ```
74
+
75
+ If the user prefers not to keep the token in `.env`, they can use the direct-set form instead:
76
+
77
+ ```shell
78
+ ntn workers env set GITHUB_API_TOKEN=<paste token>
79
+ ```
80
+
81
+ 7. Tell the user how to rotate: revoke the old token at the service, generate a new one, update `.env` (or `ntn workers env set` directly), and re-push if needed.
82
+
83
+ ## Setup: OAuth
84
+
85
+ `worker.oauth()` declares an OAuth capability. The runtime handles the authorization redirect, token exchange, and refresh — you call `accessToken()` inside `execute` to get a fresh token.
86
+
87
+ The user has to register an OAuth app with the provider, then plug the credentials in:
88
+
89
+ ```ts
90
+ const myAuth = worker.oauth("myAuth", {
91
+ name: "my-provider",
92
+ authorizationEndpoint: "https://provider.example.com/oauth/authorize",
93
+ tokenEndpoint: "https://provider.example.com/oauth/token",
94
+ scope: "read write",
95
+ clientId: process.env.MY_OAUTH_CLIENT_ID ?? "",
96
+ clientSecret: process.env.MY_OAUTH_CLIENT_SECRET ?? "",
97
+ // Optional: extra params the provider needs on the auth URL
98
+ authorizationParams: { ... },
99
+ });
100
+ ```
101
+
102
+ Setup steps:
103
+
104
+ 1. **Check the provider's current OAuth docs.** Confirm the authorization endpoint, token endpoint, scopes, any extra authorization params, and how the provider wants redirect URLs configured.
105
+
106
+ 2. **Register an OAuth app with the provider.** If the provider asks for a redirect URL up front and you do not have it yet, create the app shell first and come back to fill in the redirect URL after the first deploy.
107
+
108
+ 3. **Have the user add credentials to `.env` themselves**, or tell them the equivalent `ntn workers env set` commands. Tell them which variable names to use:
109
+
110
+ ```
111
+ MY_OAUTH_CLIENT_ID=<paste client id>
112
+ MY_OAUTH_CLIENT_SECRET=<paste client secret>
113
+ ```
114
+
115
+ 4. **Add the `worker.oauth()` declaration** to `src/index.ts`. Read `clientId`/`clientSecret` from `process.env`.
116
+
117
+ 5. **Create the worker (if not already created), push secrets, and deploy.** The deployed worker reads `clientSecret` from environment variables during capability registration, so the secret must be present remotely before `deploy`. Have the user run these themselves:
118
+
119
+ ```shell
120
+ ntn workers create --name <name> # if not already created
121
+ ntn workers env push # push .env to remote
122
+ # or, to set values directly without putting them in .env:
123
+ # ntn workers env set MY_OAUTH_CLIENT_SECRET=<paste secret>
124
+ ntn workers deploy
125
+ ```
126
+
127
+ **Important:** any time the client ID or client secret changes, you must redeploy (`ntn workers deploy`) — the OAuth capability binds these values at registration time, so updating env vars alone won't take effect.
128
+
129
+ 6. **Get the redirect URL and have the user add it to the provider's app settings.** The redirect URL comes from the deployed worker. Get it with:
130
+
131
+ ```shell
132
+ ntn workers oauth show-redirect-url
133
+ ```
134
+
135
+ The user must paste this exact value into their OAuth app's "redirect URI" (or "authorized redirect URL", or "callback URL") setting before starting the OAuth flow. **Always remind the user of this step — OAuth will fail with a redirect mismatch error if it's missing or wrong.**
136
+
137
+ 7. **Start the OAuth flow:**
138
+
139
+ ```shell
140
+ ntn workers oauth start <oauthCapabilityKey>
141
+ ```
142
+
143
+ This opens the user's browser, walks them through the provider's consent screen, and stores the resulting tokens.
144
+
145
+ 8. **Use the token inside `execute`:**
146
+
147
+ ```ts
148
+ const token = await myAuth.accessToken();
149
+ const res = await fetch("https://provider.example.com/v1/things", {
150
+ headers: { Authorization: `Bearer ${token}` },
151
+ });
152
+ ```
153
+
154
+ `accessToken()` returns a valid, refreshed access token. The runtime handles refresh automatically — you don't need to track expiry yourself.
155
+
156
+ ### Local testing with OAuth
157
+
158
+ OAuth capabilities can be tested locally, but only after a one-time bootstrap — the access token has to exist somewhere before `accessToken()` can read it. The flow:
159
+
160
+ 1. Deploy the worker, configure the redirect URL, and complete the OAuth flow once (steps 5–7 above).
161
+ 2. Pull the deployed worker's env vars (which now include the OAuth access token) into local `.env`:
162
+
163
+ ```shell
164
+ ntn workers env pull
165
+ ```
166
+
167
+ 3. Now `ntn workers exec <key> --local` works — `accessToken()` reads the token from local `.env`.
168
+
169
+ Caveats:
170
+
171
+ - Access tokens expire. The deployed runtime auto-refreshes; your local `.env` does not. When the local token goes stale, run `ntn workers env pull` again (or, if the refresh token has also expired, redo `ntn workers oauth start <key>` then `env pull`).
172
+ - Until that first deploy + OAuth completes, you can't `--local`. Run `npm run check` for type validation, or mock `accessToken()` in a test file if you need to exercise the rest of the logic.
173
+
174
+ ## Common pitfalls
175
+
176
+ 1. **Hardcoded credentials in source.** Tokens and secrets must come from `process.env` — never inline them in `src/index.ts`. Even in personal repos, committed secrets get scraped.
177
+
178
+ 2. **Forgetting `ntn workers env push`.** Local works, deploy fails with auth errors. Always push secrets after changing `.env`. The deployed worker doesn't see local `.env`.
179
+
180
+ 3. **Debugging worker code before testing the raw token.** If API key auth is failing, hit a simple authenticated endpoint with `curl` first so you can separate bad credentials from worker bugs.
181
+
182
+ 4. **Pushing secrets after `ntn workers deploy` for OAuth.** OAuth `clientId` is read from `process.env` during capability registration — push secrets *before* `deploy`, or use the `create` → `env push` → `deploy` sequence.
183
+
184
+ 5. **Wrong redirect URL for OAuth.** `redirect_uri_mismatch` is the #1 OAuth failure mode. Always run `ntn workers oauth show-redirect-url` and verify the user has set the exact URL at the provider.
185
+
186
+ 6. **Asking for too many OAuth scopes.** Request the narrowest set that works. Scope creep makes the consent screen scary and slows OAuth review for production apps.
187
+
188
+ 7. **Not telling the user about manual rotation.** API keys don't refresh themselves. Tell the user up front that they'll need to rotate, and how.
189
+
190
+ ## CLI reference
191
+
192
+ ```shell
193
+ # Push .env secrets to the deployed worker (run after any .env change)
194
+ ntn workers env push
195
+
196
+ # Pull remote env vars into local .env (useful for OAuth: brings access tokens
197
+ # down so `ntn workers exec --local` can read them)
198
+ ntn workers env pull
199
+
200
+ # List remote env vars (without values)
201
+ ntn workers env list
202
+
203
+ # Set a single env var
204
+ ntn workers env set KEY=value
205
+
206
+ # OAuth: get the redirect URL to configure at the provider
207
+ ntn workers oauth show-redirect-url
208
+
209
+ # OAuth: start the authorization flow (opens browser)
210
+ ntn workers oauth start <oauthCapabilityKey>
211
+
212
+ # OAuth: inspect token state
213
+ ntn workers oauth token <oauthCapabilityKey>
214
+ ```
215
+
216
+ ## When neither option is available
217
+
218
+ If the service offers neither an API key nor an OAuth flow, the honest first answer is often that the integration isn't viable on that service.
219
+
220
+ Before giving up, there are a few **indirect paths** worth considering.
221
+
222
+ - **OAuth into a related service that already has the data.** Sometimes the data flows downstream into a place you *can* reach with proper auth — a calendar provider, file storage, a shared workspace. Following the data to a sanctioned interface is preferable to forcing a connection at the original source.
223
+ - **Have the user export and upload.** If the service offers a manual data export (CSV/JSON), the user can drop files somewhere the worker can read (S3, Drive, etc.) and the worker syncs from there. Higher-friction but unambiguously sanctioned.
224
+ - **Pull data out of the user's own email.** If the service sends the user emails containing the data (digests, notifications, exports, receipts), OAuth into the user's own email account (Gmail, etc.) and parse those messages. The user owns the inbox, the service is sending them the data on purpose, and the email provider has a real OAuth API. Indirect but stable.
225
+ - **Use the service's own internal/frontend endpoints** (the JSON routes its web app calls). Sometimes the only thing the service exposes is the API its own UI talks to — you can authenticate as the logged-in user (session cookie, captured bearer token) and call those routes from the worker. Honest caveats: it's often flaky (the routes can change with any frontend release), it relies on credentials that probably weren't intended for programmatic use, and **the user needs to confirm this doesn't violate the service's terms of service** before doing it. Reasonable for a personal tool or hobby integration; not something to lean on for serious production use. Don't recommend it as a first choice — but if the user goes this way knowingly, help them do it carefully (sane pacers, descriptive `User-Agent`, manual credential rotation, no rate-limit evasion).
226
+
227
+ **Tip for discovery:** ask the user to export a `.har` file from their browser's devtools (Network tab → right-click → "Save all as HAR with content"). HAR files capture every request/response the page made — URLs, methods, headers, bodies — which lets you see the exact endpoint shape without the user having to describe it.
@@ -0,0 +1,368 @@
1
+ ---
2
+ name: sync
3
+ description: Scaffold a new sync capability with guided setup — asks about data source, mode, pagination, and cursor design, then generates working code
4
+ user-invocable: true
5
+ disable-model-invocation: true
6
+ allowed-tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep", "Agent"]
7
+ ---
8
+
9
+ ## Instructions
10
+
11
+ You are helping the user create a new sync capability for their Notion Worker. Walk through each step, asking questions and making recommendations. Generate working code at the end.
12
+
13
+ Before you begin, read these reference files to understand sync patterns:
14
+ - `.agents/skills/sync-guide/SKILL.md` — concepts, modes, patterns, common mistakes
15
+ - `.agents/skills/sync-guide/api-pagination-patterns.md` — real-world API strategies
16
+ - `.agents/skills/sync-guide/examples/` — working code templates
17
+
18
+ Also read the current `src/index.ts` to understand what already exists.
19
+
20
+ ### Step 1: Understand the Data Source
21
+
22
+ Ask the user:
23
+ - What data are you syncing? (e.g., "Jira issues", "Stripe customers", "ServiceNow tickets")
24
+
25
+ If they name a well-known API, look up its pagination mechanism and change-tracking capabilities (does it have `updated_at`? an events endpoint? cursor-based pagination?).
26
+
27
+ ### Step 2: Determine the Right Architecture
28
+
29
+ Based on what you know about the data source, recommend one of two architectures:
30
+
31
+ #### Simple replace sync
32
+
33
+ Use when: the source is small (<1k records) OR the API has no change tracking
34
+ (no `updated_at`, no event feed).
35
+
36
+ One sync, replace mode. Re-fetches everything each cycle. The runtime
37
+ auto-deletes records that disappear from the source. Simplest option.
38
+
39
+ #### Backfill + delta pair
40
+
41
+ Use when: the API supports change tracking (`updated_at`, events, changelog) —
42
+ this covers most enterprise APIs (Salesforce, Stripe, Linear, GitHub, HubSpot).
43
+
44
+ Two separate syncs writing to the **same database**:
45
+
46
+ - **Backfill sync** (replace mode, `schedule: "manual"`): Paginates the full
47
+ dataset. Triggered manually via CLI when a full re-import is needed. Replace
48
+ mode's mark-and-sweep automatically cleans up records deleted from the source.
49
+ - **Delta sync** (incremental mode, `schedule: "5m"` or similar): Fetches only
50
+ recently changed records. Runs on a timer for low-latency updates.
51
+
52
+ Advantages over a single bi-modal sync:
53
+ - No phase discrimination in state — each sync has simple, focused state
54
+ - No backfill-to-delta transition logic
55
+ - Backfill and delta run independently — re-backfill anytime without disrupting delta
56
+ - Easier to reason about and debug
57
+
58
+ **Change tracking drives the decision, not dataset size.** A Linear workspace
59
+ may only have a few thousand issues, but its API supports the queries needed
60
+ for delta sync, so backfill+delta is the right choice. Conversely, a website
61
+ listing local pickleball courts has no `updated_since` endpoint regardless of
62
+ how many records it has.
63
+
64
+ Recommend an architecture with a brief explanation. Let the user override if
65
+ they disagree.
66
+
67
+ ### Step 3: Design the Schema
68
+
69
+ Based on the API's response shape, propose a schema. Look up what fields the
70
+ API returns and map the most useful ones to Schema types. Don't ask the user
71
+ to enumerate fields — propose a sensible default and let them adjust.
72
+
73
+ For example, if syncing Jira issues, propose:
74
+ ```ts
75
+ const issuesDb = worker.database("issuesDb", {
76
+ type: "managed",
77
+ initialTitle: "Jira Issues",
78
+ primaryKeyProperty: "Issue Key",
79
+ schema: {
80
+ properties: {
81
+ "Issue Key": Schema.richText(), // primaryKeyProperty — the unique ID
82
+ "Summary": Schema.title(), // the main display field
83
+ "Status": Schema.select([...]), // mapped from Jira statuses
84
+ "Assignee": Schema.richText(), // or Schema.people() if email available
85
+ "Updated": Schema.date(),
86
+ },
87
+ },
88
+ });
89
+ ```
90
+
91
+ Guidelines:
92
+ - Declare the database with `worker.database()` and reference the handle in `worker.sync()`
93
+ - Every schema needs exactly one `Schema.title()` — pick the most descriptive field
94
+ - Use `Schema.richText()` for the primary key property (the unique ID)
95
+ - Use `Schema.url()`, `Schema.email()`, `Schema.date()`, `Schema.number()`,
96
+ `Schema.checkbox()`, `Schema.select()` where the data type fits
97
+ - Use `Schema.relation("otherDatabaseKey")` for relations to another managed database
98
+ - Start with 10-20 properties — be generous, include most useful fields from the API
99
+ - See the full type list in `.agents/skills/sync-guide/SKILL.md` under "Schema Reference"
100
+
101
+ Present the proposed schema to the user and ask if they want to add, remove,
102
+ or change any fields before generating code.
103
+
104
+ ### Step 4: Design the State Machine
105
+
106
+ Research the API to determine its pagination and change-tracking mechanisms.
107
+ Do NOT ask the user about pagination details — figure it out from the API docs,
108
+ your knowledge of the API, or by looking up the API. The user shouldn't need
109
+ to know whether their API uses opaque cursors vs page numbers.
110
+
111
+ You need to determine:
112
+ 1. **How the API paginates list results** (opaque cursor, page number, offset, keyset)
113
+ 2. **Whether the API has change tracking** (updated_at field, events endpoint, changelog)
114
+ 3. **Whether the API has deletion signals** (archived filter, audit log, delete events)
115
+
116
+ Then design the state accordingly:
117
+
118
+ **For simple replace syncs:** State is just within-cycle pagination.
119
+ - Opaque cursor: `{ cursor: string | null }`
120
+ - Page number: `{ page: number }`
121
+
122
+ **For backfill + delta pairs:** Each sync has its own simple state — no
123
+ bi-modal discriminated union needed.
124
+
125
+ - **Backfill state:** Just pagination cursor for walking the full dataset.
126
+ Depends on how the API paginates its list endpoint:
127
+ - Opaque cursor: `{ cursor: string | null }`
128
+ - Page number: `{ page: number }`
129
+ - Keyset: `{ cursorTimestamp: string | null; cursorId: string | null }`
130
+
131
+ - **Delta state:** Change-tracking cursor for fetching recent modifications.
132
+ Depends on how the API exposes changes:
133
+ - Opaque cursor (API sorted by updated_at): `{ cursor: string | null }`
134
+ - Timestamp keyset: `{ cursorTimestamp: string; cursorId: string }`
135
+ - Event ID: `{ eventCursor: string }`
136
+
137
+ **Consistency buffer (delta syncs only):** In incremental mode the cursor
138
+ never resets, so if you advance past a record that hasn't been indexed yet,
139
+ it's lost permanently. Apply a consistency buffer: never advance the cursor
140
+ closer than 10-15 seconds to "now". This is especially important for APIs
141
+ with eventual consistency (Stripe, Salesforce, etc.).
142
+
143
+ **Deletion handling:**
144
+ There are three cases to consider:
145
+
146
+ 1. **API supports delta deletes** (e.g., Stripe events with `*.deleted` types,
147
+ APIs that return `deleted: true` in change feeds): Emit
148
+ `{ type: "delete", key }` in the delta sync. This is the cleanest approach.
149
+ 2. **Deletes are rare or irrelevant** for the use case: No action needed.
150
+ Stale records remain in Notion but don't cause problems.
151
+ 3. **Deletes matter but the API has no delete signal**: The backfill sync's
152
+ replace mode handles this automatically — its mark-and-sweep deletes any
153
+ records not seen during the full cycle. Trigger a backfill periodically
154
+ to clean up stale records.
155
+
156
+ If the API has no change tracking at all, go back and recommend a simple
157
+ replace sync instead.
158
+
159
+ Present your state design to the user as a brief summary (e.g., "This API uses
160
+ cursor-based pagination and has an `updated_at` field, so I'll use a
161
+ backfill+delta pair with opaque cursor for backfill and timestamp keyset for
162
+ delta"). Let them confirm or adjust before generating code.
163
+
164
+ ### Step 5: Set Up Authentication
165
+
166
+ Before generating code, determine what auth the API needs and set it up so
167
+ you can test locally.
168
+
169
+ There are two patterns:
170
+
171
+ **Pattern A: Static API token/key**
172
+ For APIs where the user has a personal token or API key (e.g., Jira API token,
173
+ GitHub PAT, simple API keys).
174
+
175
+ Ask the user for their token and add it to `.env`:
176
+ ```
177
+ JIRA_API_TOKEN=...
178
+ JIRA_EMAIL=user@example.com
179
+ ```
180
+ If `.env` doesn't exist, create it. The `.env` file is automatically loaded
181
+ during local execution (`--local` flag).
182
+
183
+ **Pattern B: OAuth**
184
+ For APIs that require OAuth (e.g., Google, Salesforce, HubSpot). This has
185
+ two parts:
186
+
187
+ 1. **Client credentials** — the OAuth app's client ID and secret. These go in `.env`:
188
+ ```
189
+ MY_OAUTH_CLIENT_ID=...
190
+ MY_OAUTH_CLIENT_SECRET=...
191
+ ```
192
+
193
+ 2. **User token** — obtained through the OAuth flow *after* deploying. This is
194
+ handled by the runtime automatically via `worker.oauth()` and `.accessToken()`.
195
+
196
+ For OAuth syncs, you'll add a `worker.oauth()` call in the generated code.
197
+ Always use `UserManagedOAuthConfiguration` (the shape with explicit endpoints and
198
+ client credentials) rather than the `{ provider: "..." }` shorthand, as
199
+ Notion-managed OAuth is in alpha and the user likely does not have access.
200
+
201
+ ```ts
202
+ const myAuth = worker.oauth("myAuth", {
203
+ name: "my-provider",
204
+ authorizationEndpoint: "https://provider.example.com/oauth/authorize",
205
+ tokenEndpoint: "https://provider.example.com/oauth/token",
206
+ scope: "read write",
207
+ clientId: process.env.MY_OAUTH_CLIENT_ID ?? "",
208
+ clientSecret: process.env.MY_OAUTH_CLIENT_SECRET ?? "",
209
+ });
210
+ ```
211
+
212
+ Then use `await myAuth.accessToken()` in the execute function instead of
213
+ reading a static token from `process.env`.
214
+
215
+ Note: OAuth syncs can't be fully tested locally since the OAuth flow requires
216
+ a deployed worker. Local testing will fail at the `.accessToken()` call. This
217
+ is fine — proceed to deploy and test via preview (Step 8).
218
+
219
+ ### Step 6: Generate the Code
220
+
221
+ Write the sync into `src/index.ts`. Use the closest example from `.agents/skills/sync-guide/examples/` as a starting point:
222
+ - `replace-simple.ts` — static data, no API
223
+ - `replace-paginated.ts` — paginated replace mode (also used for backfill syncs)
224
+ - `incremental-basic.ts` — delta sync with opaque cursor
225
+ - `incremental-bimodal.ts` — full backfill + delta pair example
226
+ - `incremental-events.ts` — delta sync with event feed
227
+
228
+ Include in the generated code:
229
+ - Proper imports (`Worker`, `Builder`, `Schema`)
230
+ - Database declaration via `worker.database()` with schema and `primaryKeyProperty`
231
+ - A pacer for the upstream API via `worker.pacer()` — and `await pacer.wait()` before every API request
232
+ - The state type(s) — simple types, one per sync (no discriminated unions needed)
233
+ - The `worker.sync()` call(s) referencing the database handle
234
+ - For backfill+delta: two syncs targeting the same database, backfill with `schedule: "manual"`, delta with a timed schedule
235
+ - A consistency buffer for delta syncs (if the API is eventually consistent)
236
+ - Inline comments explaining *why* each design choice was made
237
+ - API calls using `fetch` with auth from `process.env`
238
+
239
+ **Code generation checklist:**
240
+ - [ ] Database declared with `worker.database()` and referenced by handle
241
+ - [ ] Pacer declared with `worker.pacer()` for the upstream API
242
+ - [ ] `await pacer.wait()` called before every `fetch` to the upstream API
243
+ - [ ] State types are simple (no bi-modal discriminated unions)
244
+ - [ ] Backfill sync uses `mode: "replace"` and `schedule: "manual"` (if applicable)
245
+ - [ ] Delta sync uses `mode: "incremental"` with a timed schedule (if applicable)
246
+ - [ ] Consistency buffer applied to delta cursor advancement (if applicable)
247
+ - [ ] Deletion handling matches one of the three cases from Step 4
248
+
249
+ ### Step 7: Test Locally
250
+
251
+ Test the sync before deploying. This catches bugs early without a deploy cycle.
252
+
253
+ **For syncs using static API tokens (Pattern A):**
254
+
255
+ 1. Run `npm run check` to verify TypeScript types compile. Fix any errors.
256
+
257
+ 2. Run `ntn workers exec <key> --local` to execute the sync locally.
258
+ This runs the execute function on your machine with `.env` loaded.
259
+ - Check: does it return data? Are properties populated correctly?
260
+ - Check: does `hasMore` look right? Does the cursor advance?
261
+
262
+ 3. If it returns `hasMore: true`, test the next page:
263
+ `ntn workers exec <key> --local -d '<nextState from previous output>'`
264
+
265
+ 4. If there are errors (auth failures, wrong field mappings, crashes):
266
+ fix the code and re-run — no deploy needed, iteration is fast.
267
+
268
+ 5. For backfill+delta pairs, test each sync independently:
269
+ - Test the backfill sync: `ntn workers exec <backfillKey> --local`
270
+ - Test the delta sync: `ntn workers exec <deltaKey> --local`
271
+ - Verify they both return well-formed data with the correct properties.
272
+
273
+ 6. Write a test file (`test.ts`) that exercises the sync. Import the worker
274
+ directly and call its `.run()` method.
275
+
276
+ If the user has API credentials in `.env`, write a test that hits the real
277
+ API — this is the most valuable test because it validates actual field
278
+ mappings, pagination behavior, and auth against the real service. If
279
+ credentials aren't available, stub the HTTP calls instead.
280
+
281
+ **Integration test (preferred when credentials are available):**
282
+ ```ts
283
+ import "dotenv/config"; // load .env
284
+ import worker from "./src/index.ts";
285
+ import assert from "node:assert";
286
+
287
+ async function test() {
288
+ // First page (backfill start, no prior state)
289
+ const page1 = await worker.run("mySync", undefined, { concreteOutput: true });
290
+ console.log(`Page 1: ${page1.changes.length} records, hasMore: ${page1.hasMore}`);
291
+ assert(page1.changes.length > 0, "Should return records");
292
+
293
+ // Verify fields are populated
294
+ const first = page1.changes[0];
295
+ assert(first.key, "Record should have a key");
296
+ console.log("Sample record:", JSON.stringify(first, null, 2));
297
+
298
+ // Test pagination
299
+ if (page1.hasMore) {
300
+ const page2 = await worker.run("mySync", page1.nextState, { concreteOutput: true });
301
+ console.log(`Page 2: ${page2.changes.length} records, hasMore: ${page2.hasMore}`);
302
+ assert(page2.changes.length > 0, "Second page should return records");
303
+ }
304
+
305
+ console.log("All tests passed!");
306
+ }
307
+
308
+ test().catch((err) => { console.error(err); process.exit(1); });
309
+ ```
310
+
311
+ Run with `npx tsx test.ts`. Adapt to the specific sync: use the actual
312
+ capability key, add assertions for specific field values, verify both
313
+ backfill and delta syncs for backfill+delta pairs, etc.
314
+
315
+ **For syncs using OAuth (Pattern B):**
316
+ Local execution won't work because `.accessToken()` requires a deployed worker
317
+ with a completed OAuth flow. Skip to Step 8 (deploy + preview) instead.
318
+ You can still run `npm run check` to verify types compile.
319
+
320
+ ### Step 8: Deploy and Validate with Preview
321
+
322
+ Once local testing passes (or immediately for OAuth syncs), deploy and test remotely.
323
+
324
+ If secrets need to be available at deploy time (e.g., OAuth `clientSecret` read
325
+ from `process.env` during capability registration), create the worker and push
326
+ secrets first:
327
+ 1. `ntn workers create --name <name>` — create the worker without deploying
328
+ 2. `ntn workers env push` — push `.env` secrets to remote
329
+ 3. `ntn workers deploy` — now deploy with secrets available
330
+
331
+ Otherwise, the simpler flow:
332
+ 1. `ntn workers deploy` — build and publish
333
+ 2. `ntn workers env push` — push `.env` secrets to remote
334
+
335
+ Then, if the sync uses OAuth, complete the OAuth flow before previewing.
336
+ **Important:** `env push` must happen before `oauth start` — the deployed worker needs the client secret to exchange the authorization code for tokens.
337
+ - `ntn workers oauth show-redirect-url` — get the redirect URL
338
+ - Tell the user to configure this URL in their OAuth provider's app settings
339
+ - `ntn workers oauth start <oauthKey>` — opens browser to complete the OAuth flow
340
+ 4. `ntn workers sync trigger <syncKey> --preview` — execute remotely without writing to Notion
341
+ - Inspect the output: record count, property values, hasMore status
342
+ - If `hasMore: true`, continue: `ntn workers sync trigger <syncKey> --preview --context '<nextState>'`
343
+ 5. If the preview shows issues, fix the code and redeploy (go back to step 1)
344
+
345
+ For backfill+delta pairs, preview both syncs:
346
+ - `ntn workers sync trigger <backfillKey> --preview`
347
+ - `ntn workers sync trigger <deltaKey> --preview`
348
+
349
+ ### Step 9: Go Live
350
+
351
+ When the preview looks good:
352
+
353
+ 1. `ntn workers sync trigger <key>` — trigger a real sync
354
+ 2. `ntn workers sync status` — check that the sync is running and progressing
355
+ 3. `ntn workers runs list` then `ntn workers runs logs <runId>` — check for errors
356
+ 4. Run `ntn workers sync status` again to confirm progress (record count increasing, no errors)
357
+
358
+ For backfill+delta pairs, trigger the backfill first to load all data, then
359
+ let the delta sync's schedule handle ongoing changes:
360
+ 1. `ntn workers sync trigger <backfillKey>` — start the full dataset load
361
+ 2. Monitor with `ntn workers sync status` until the backfill completes
362
+ 3. The delta sync will run automatically on its configured schedule
363
+
364
+ Tell the user: the first sync run is the backfill, which may take a while
365
+ depending on dataset size. They should periodically run `ntn workers sync status`
366
+ to monitor progress until the initial backfill completes. After that, the delta
367
+ sync runs automatically on its configured schedule. To re-backfill later:
368
+ `ntn workers sync state reset <backfillKey> && ntn workers sync trigger <backfillKey>`