create-ncblock 0.0.39 → 0.0.40

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 (39) hide show
  1. package/package.json +1 -1
  2. package/scripts/utils/templates.ts +37 -7
  3. package/sdk-version.json +1 -1
  4. package/templates/worker/.agents/INSTRUCTIONS.md +536 -0
  5. package/templates/worker/.agents/skills/auth-guide/SKILL.md +227 -0
  6. package/templates/worker/.agents/skills/sync/SKILL.md +368 -0
  7. package/templates/worker/.agents/skills/sync-debug/SKILL.md +101 -0
  8. package/templates/worker/.agents/skills/sync-guide/SKILL.md +253 -0
  9. package/templates/worker/.agents/skills/sync-guide/api-pagination-patterns.md +661 -0
  10. package/templates/worker/.agents/skills/sync-guide/examples/incremental-basic.ts +103 -0
  11. package/templates/worker/.agents/skills/sync-guide/examples/incremental-bimodal.ts +207 -0
  12. package/templates/worker/.agents/skills/sync-guide/examples/incremental-events.ts +132 -0
  13. package/templates/worker/.agents/skills/sync-guide/examples/replace-paginated.ts +79 -0
  14. package/templates/worker/.agents/skills/sync-guide/examples/replace-simple.ts +57 -0
  15. package/templates/worker/.agents/skills/sync-validate/SKILL.md +60 -0
  16. package/templates/worker/.claudeignore +2 -0
  17. package/templates/worker/.codexignore +2 -0
  18. package/templates/worker/.examples/automation-example.ts +60 -0
  19. package/templates/worker/.examples/oauth-example.ts +79 -0
  20. package/templates/worker/.examples/sync-example.ts +184 -0
  21. package/templates/worker/.examples/tool-example.ts +37 -0
  22. package/templates/worker/.examples/webhook-example.ts +66 -0
  23. package/templates/worker/README.md +765 -0
  24. package/templates/worker/_gitignore +6 -0
  25. package/templates/worker/docs/custom-tool.png +0 -0
  26. package/templates/worker/notionhq-workers-0.4.0.tgz +0 -0
  27. package/templates/worker/package.json +25 -0
  28. package/templates/worker/src/index.ts +8 -0
  29. package/templates/worker/tsconfig.json +16 -0
  30. package/templates/worker/views/empty/AGENTS.md +67 -0
  31. package/templates/worker/views/empty/README.md +10 -0
  32. package/templates/worker/views/empty/_gitignore +2 -0
  33. package/templates/worker/views/empty/custom_blocks.json +4 -0
  34. package/templates/worker/views/empty/index.html +15 -0
  35. package/templates/worker/views/empty/package.json +23 -0
  36. package/templates/worker/views/empty/src/index.css +33 -0
  37. package/templates/worker/views/empty/src/index.tsx +20 -0
  38. package/templates/worker/views/empty/tsconfig.json +17 -0
  39. package/templates/worker/views/empty/vite.config.ts +7 -0
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: sync-debug
3
+ description: Diagnose a failing or misbehaving sync — fetch run logs, identify errors, cross-reference with code, and suggest fixes
4
+ user-invocable: true
5
+ disable-model-invocation: true
6
+ allowed-tools: ["Read", "Bash", "Glob", "Grep", "Edit", "Write"]
7
+ ---
8
+
9
+ ## Instructions
10
+
11
+ Help the user figure out why their sync is failing or producing wrong results. Work through the steps below systematically.
12
+
13
+ ### Step 1: Get Current State
14
+
15
+ Run these commands to understand the situation:
16
+
17
+ ```shell
18
+ ntn workers sync status
19
+ ```
20
+
21
+ Note which syncs are listed, their status, last run time, and next scheduled run. Look for syncs that are stuck, failing, or haven't run recently.
22
+
23
+ ### Step 2: Fetch Recent Runs
24
+
25
+ ```shell
26
+ ntn workers runs list
27
+ ```
28
+
29
+ Look for runs with non-zero exit codes (shown in red in table output). Note the run IDs for failed runs.
30
+
31
+ ### Step 3: Get Logs
32
+
33
+ For the most recent run (any capability):
34
+ ```shell
35
+ ntn workers runs list --plain | head -n1 | cut -f1 | xargs -I{} ntn workers runs logs {}
36
+ ```
37
+
38
+ For the most recent run of a specific sync:
39
+ ```shell
40
+ ntn workers runs list --plain | grep <syncKey> | head -n1 | cut -f1 | xargs -I{} ntn workers runs logs {}
41
+ ```
42
+
43
+ Read the full log output. Look for error messages, stack traces, and any `console.log` output from the sync code.
44
+
45
+ ### Step 4: Read the Sync Code
46
+
47
+ Read `src/index.ts` (and any imported modules) to understand the sync's logic. Cross-reference the error from the logs with the code.
48
+
49
+ ### Step 5: Diagnose
50
+
51
+ Common failure patterns and their fixes:
52
+
53
+ **API Authentication Errors (401/403)**
54
+ - Check if OAuth is configured: `ntn workers oauth token <oauthKey>`
55
+ - Check environment variables: `ntn workers env list`
56
+ - If env vars are missing remotely: `ntn workers env push`
57
+ - If OAuth token expired: `ntn workers oauth start <key>` to re-authenticate
58
+
59
+ **Rate Limiting (429)**
60
+ - Reduce batch size in the sync code
61
+ - Add delays between API calls if needed
62
+ - Check if the API has documented rate limits
63
+
64
+ **Timeout / Long Execution**
65
+ - The execute function is taking too long per call
66
+ - Reduce batch size (fewer records per page)
67
+ - Simplify per-record processing (defer heavy transforms)
68
+
69
+ **Cursor / State Errors**
70
+ - TypeError on state access: probably a first-run issue (state is undefined)
71
+ - State shape changed after a code update: the persisted state from the previous run has the old shape
72
+ - Fix: `ntn workers sync state reset <key>` to clear state and re-backfill from scratch
73
+
74
+ **Schema Mismatch**
75
+ - Properties in `changes` don't match the `schema.properties` definition
76
+ - Check that every key in the `properties` object of each change matches a key in the schema
77
+ - Check that `Builder.title()` is used for `Schema.title()` properties, `Builder.richText()` for `Schema.richText()`, etc.
78
+
79
+ **Infinite Loop (sync never completes)**
80
+ - `hasMore` is always `true` — the cursor isn't advancing
81
+ - Check that `nextState` changes between iterations
82
+ - Check the termination condition: is it reachable?
83
+
84
+ **Empty Results**
85
+ - API returning no data: test the API call directly (curl or local exec)
86
+ - Wrong endpoint or query parameters
87
+ - Auth working but insufficient permissions/scopes
88
+
89
+ **Network / Transient Errors**
90
+ - Single occurrence: may be transient — check if subsequent runs succeeded
91
+ - Repeated: check the API endpoint URL, DNS, connectivity
92
+ - Force a retry: `ntn workers sync trigger <key>`
93
+
94
+ ### Step 6: Fix and Verify
95
+
96
+ After identifying the issue:
97
+ 1. Apply the fix to the code
98
+ 2. Run `npm run check` to verify types
99
+ 3. If the state shape changed, warn the user they may need `ntn workers sync state reset <key>` (this triggers a full re-backfill)
100
+ 4. Deploy and preview to verify: suggest `/sync-preview` or run `ntn workers deploy && ntn workers sync trigger <key> --preview`
101
+ 5. When the fix is verified, `ntn workers sync trigger <key>` to resume
@@ -0,0 +1,253 @@
1
+ ---
2
+ name: sync-guide
3
+ description: Comprehensive guide to building Notion Workers syncs — covers the two-sync architecture (backfill+delta), replace mode, pagination, consistency buffers, pacers, deletion strategies, and common pitfalls. Auto-loads when sync-related work is detected.
4
+ user-invocable: false
5
+ ---
6
+
7
+ ## What is a Sync?
8
+
9
+ A sync is a recurring `execute` function that returns data changes to populate a Notion database. The runtime calls `execute` in a loop:
10
+
11
+ ```ts
12
+ const db = worker.database("myDb", {
13
+ type: "managed",
14
+ initialTitle: "My Data",
15
+ primaryKeyProperty: "ID",
16
+ schema: {
17
+ properties: {
18
+ Name: Schema.title(),
19
+ ID: Schema.richText(),
20
+ },
21
+ },
22
+ });
23
+
24
+ worker.sync("mySync", {
25
+ database: db,
26
+ execute: async (state, { notion }) => ({
27
+ changes: [
28
+ { type: "upsert", key: "1", properties: { Name: Builder.title("Item 1"), ID: Builder.richText("1") } },
29
+ ],
30
+ hasMore: false,
31
+ nextState: undefined,
32
+ }),
33
+ });
34
+ ```
35
+
36
+ Each call returns `{ changes, hasMore, nextState }`. If `hasMore` is `true`, the runtime calls `execute` again with `nextState`. This continues until `hasMore` is `false`, completing a **cycle**. The next cycle begins at the scheduled interval with the state from the end of the previous cycle.
37
+
38
+ **Imports:**
39
+ ```ts
40
+ import { Worker } from "@notionhq/workers";
41
+ import * as Builder from "@notionhq/workers/builder";
42
+ import * as Schema from "@notionhq/workers/schema";
43
+ ```
44
+
45
+ ## Decision Framework
46
+
47
+ ### Step 1: Choose an Architecture
48
+
49
+ The deciding factor is **API capability and dataset size**. Two tiers:
50
+
51
+ | Condition | Architecture |
52
+ |---|---|
53
+ | Small source (<1k records) or API with no change tracking | **Simple replace sync** — one sync, `mode: "replace"` |
54
+ | Everything else (API supports `updated_at`, change feeds, events) | **Backfill + delta pair** — two syncs writing to the same database |
55
+
56
+ **Simple replace sync**: One sync returns the full dataset each cycle. After the final `hasMore: false`, any records not seen are deleted automatically. Use when the dataset is small enough to re-fetch entirely.
57
+
58
+ **Backfill + delta pair**: Two syncs share a single database. The **backfill sync** (`mode: "replace"`, `schedule: "manual"`) re-fetches everything when triggered. The **delta sync** (`mode: "incremental"`, frequent schedule) fetches only changes since the last run. This separates concerns cleanly — no bi-modal state machine, no backfill-to-delta transition bugs.
59
+
60
+ ### Step 2: Understand Your API's Pagination
61
+
62
+ Most APIs require paginating through results. Return batches of ~100 changes. Returning too many changes in one `execute` call will fail.
63
+
64
+ **Backfill pagination** (full dataset load):
65
+ 1. **Opaque cursor token** — GraphQL `endCursor`, Stripe `starting_after`
66
+ 2. **Page number / offset** — `?page=N&limit=100`
67
+ 3. **Keyset (timestamp + id)** — `WHERE created_at > X OR (created_at = X AND id > Y)` — the gold standard for timestamp-sorted mutable data
68
+
69
+ **Delta pagination** (change-only loads, incremental mode):
70
+ 1. **Timestamp cursor** — `?updated_since=<cursor>` with consistency buffer
71
+ 2. **Keyset on updated_at + id** — same keyset pattern on the modification timestamp
72
+ 3. **Event/changelog feed** — `GET /events?after=<eventId>`
73
+ 4. **Same opaque cursor** — when the API sorts by `updated_at`, the backfill cursor works for delta too
74
+
75
+ ### Step 3: Consistency Buffer (Delta Syncs)
76
+
77
+ APIs tend to be eventually consistent. A record that was just written or updated may not appear in query results immediately. Since the cursor never resets in incremental mode, if it advances past a record that hasn't been indexed yet, that record is skipped permanently. Lag the cursor 10-60 seconds behind "now":
78
+
79
+ ```ts
80
+ const bufferMs = 15_000;
81
+ const maxCursor = new Date(Date.now() - bufferMs).toISOString();
82
+ const nextCursor = records.length > 0
83
+ ? min(lastRecord.updatedAt, maxCursor)
84
+ : maxCursor;
85
+ ```
86
+
87
+ ### Step 4: Deletion Strategies
88
+
89
+ 1. **Backfill sync (replace mode)**: free — unseen records are auto-deleted each cycle. This is the primary mechanism for handling deletes when the API has no delete signal.
90
+ 2. **Delta sync with delete API**: emit `{ type: "delete", key }` markers. If the delete signal comes from a separate endpoint (audit log, archived filter), use the **flip-flop pattern**: run the main delta stream until caught up (`hasMore: false`), then switch to the delete stream for a cycle, then back. Both cursors persist in state independently.
91
+ 3. **No delete API, large dataset**: rely on the backfill sync's replace-mode mark-and-sweep. Trigger the backfill manually or on a slow schedule to clean up stale records.
92
+
93
+ ## Replace Mode
94
+
95
+ Simple: fetch everything, return it all, let the runtime handle deletes. Use as a standalone sync for small sources, or as the backfill half of a backfill+delta pair.
96
+
97
+ ```ts
98
+ const db = worker.database("records", {
99
+ type: "managed",
100
+ initialTitle: "Records",
101
+ primaryKeyProperty: "ID",
102
+ schema: {
103
+ properties: { Name: Schema.title(), ID: Schema.richText() },
104
+ },
105
+ });
106
+
107
+ const apiPacer = worker.pacer("myApi", {
108
+ allowedRequests: 10,
109
+ intervalMs: 1000,
110
+ });
111
+
112
+ worker.sync("recordsBackfill", {
113
+ database: db,
114
+ mode: "replace",
115
+ schedule: "manual", // trigger manually or on a slow schedule
116
+ execute: async (state) => {
117
+ const page = state?.page ?? 1;
118
+ await apiPacer.wait();
119
+ const { items, totalPages } = await fetchPage(page, 100);
120
+ const hasMore = page < totalPages;
121
+ return {
122
+ changes: items.map((item) => ({
123
+ type: "upsert" as const,
124
+ key: item.id,
125
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
126
+ })),
127
+ hasMore,
128
+ nextState: hasMore ? { page: page + 1 } : undefined,
129
+ };
130
+ },
131
+ });
132
+ ```
133
+
134
+ See `examples/replace-simple.ts` and `examples/replace-paginated.ts` for complete working examples.
135
+
136
+ ## Incremental Mode (Delta Sync)
137
+
138
+ The delta sync fetches only changes since the last run. When paired with a replace-mode backfill sync on the same database, this replaces the old bi-modal single-sync pattern.
139
+
140
+ ```ts
141
+ // Reuses the same `db` and `apiPacer` from above
142
+
143
+ worker.sync("recordsDelta", {
144
+ database: db,
145
+ mode: "incremental",
146
+ schedule: "5m",
147
+ execute: async (state: { cursor: string } | undefined) => {
148
+ const cursor = state?.cursor ?? new Date(0).toISOString();
149
+ const bufferTs = new Date(Date.now() - 15_000).toISOString();
150
+
151
+ await apiPacer.wait();
152
+ const { items, nextCursor } = await fetchChanges(cursor);
153
+ const done = !nextCursor;
154
+
155
+ return {
156
+ changes: items.map(toUpsert),
157
+ hasMore: !done,
158
+ nextState: {
159
+ cursor: done ? min(nextCursor ?? cursor, bufferTs) : nextCursor,
160
+ },
161
+ };
162
+ },
163
+ });
164
+ ```
165
+
166
+ **Key points:**
167
+ - The delta sync's state is simple — just a cursor. No phase discrimination needed.
168
+ - The backfill sync (replace mode) handles the initial full load and periodic cleanup of deleted records.
169
+ - Both syncs write to the same database via the shared `db` handle.
170
+ - The pacer is shared between syncs — the server apportions the budget evenly.
171
+
172
+ See `examples/incremental-basic.ts`, `examples/incremental-bimodal.ts`, and `examples/incremental-events.ts` for complete patterns.
173
+
174
+ ## Schema Reference
175
+
176
+ Define the Notion database shape with `Schema` types and build values with `Builder`:
177
+
178
+ | Schema type | Builder value | Notes |
179
+ |---|---|---|
180
+ | `Schema.title()` | `Builder.title("text")` | Primary display field. Every schema needs exactly one. |
181
+ | `Schema.richText()` | `Builder.richText("text")` | Text content, IDs |
182
+ | `Schema.url()` | `Builder.url("https://...")` | URL field |
183
+ | `Schema.email()` | `Builder.email("a@b.com")` | Email field |
184
+ | `Schema.phoneNumber()` | `Builder.phoneNumber("+1...")` | Phone field |
185
+ | `Schema.checkbox()` | `Builder.checkbox(true)` | Boolean |
186
+ | `Schema.file()` | `Builder.file("https://...", "name")` | File URL + optional display name |
187
+ | `Schema.number()` | `Builder.number(42)` | Number. Optional format: `Schema.number("percent")` |
188
+ | `Schema.date()` | `Builder.date("2024-01-15")` | Date (YYYY-MM-DD). Also: `Builder.dateTime("2024-01-15T10:30:00Z")`, `Builder.dateRange(start, end)` |
189
+ | `Schema.select([...])` | `Builder.select("Option A")` | Single select. Define options: `Schema.select([{ name: "A" }, { name: "B" }])`. **Options must have non-empty `name` values** — `Schema.select([])` and `{ name: "" }` are not supported. |
190
+ | `Schema.multiSelect([...])` | `Builder.multiSelect("A", "B")` | Multi select |
191
+ | `Schema.status(...)` | `Builder.status("Done")` | Status with groups |
192
+ | `Schema.people()` | `Builder.people("email@co.com")` | People by email |
193
+ | `Schema.place()` | `Builder.place({ latitude, longitude })` | Geographic location |
194
+ | `Schema.relation("databaseKey")` | `[Builder.relation("pk")]` | Relation to another managed database. Value is an **array**. |
195
+
196
+ Relations use the related database key. Two-way relations are configured the same way:
197
+ ```ts
198
+ Schema.relation("otherDatabase", { twoWay: true, relatedPropertyName: "Back Link" })
199
+ ```
200
+
201
+ Row-level icons and page content:
202
+ ```ts
203
+ changes: [{
204
+ type: "upsert", key: "1",
205
+ properties: { ... },
206
+ icon: Builder.emojiIcon("🎯"), // or Builder.notionIcon("rocket", "blue")
207
+ pageContentMarkdown: "## Details\nSome text", // Markdown body for the page
208
+ }]
209
+ ```
210
+
211
+ ## Common Mistakes
212
+
213
+ 1. **Not using a pacer** — every API call inside `execute` should be preceded by `await apiPacer.wait()`. Without it, syncs will hit rate limits and fail.
214
+ 2. **Missing consistency buffer on delta syncs** — the cursor will permanently skip records not yet indexed in eventually consistent APIs.
215
+ 3. **Not paginating** — returning too many changes at once. Start with batches of ~100.
216
+ 4. **Using replace mode for large datasets** — if the API supports change tracking, pair a replace-mode backfill sync with an incremental delta sync instead of re-fetching everything each cycle.
217
+ 5. **Cursor that doesn't advance** — infinite loop. Ensure `nextState` changes between iterations.
218
+ 6. **Forgetting first-run handling** — `state` is `undefined` on first call. Use `state?.cursor ?? null`.
219
+ 7. **Forgetting that backfill + delta share a database** — both syncs must use the same `worker.database()` handle and the same key/properties shape.
220
+ 8. **Not triggering the backfill sync** — the backfill sync with `schedule: "manual"` won't run automatically. Trigger it on deploy or periodically to clean up deleted records.
221
+ 9. **Empty select values** — `Schema.select()` requires at least one option with a non-empty `name`. `Schema.select([])` and `{ name: "" }` are not supported.
222
+
223
+ ## CLI Commands for Sync Development
224
+
225
+ ```shell
226
+ # Deploy
227
+ ntn workers deploy
228
+
229
+ # Preview (test without writing)
230
+ ntn workers sync trigger <key> --preview
231
+ ntn workers sync trigger <key> --preview --context '<json>' # continue pagination
232
+
233
+ # Trigger a sync run
234
+ ntn workers sync trigger <key>
235
+
236
+ # Check sync status
237
+ ntn workers sync status
238
+
239
+ # View run logs
240
+ ntn workers runs list
241
+ ntn workers runs list --plain | head -n1 | cut -f1 | xargs -I{} ntn workers runs logs {}
242
+
243
+ # Reset state (full re-backfill)
244
+ ntn workers sync state reset <key>
245
+
246
+ # Manage secrets
247
+ ntn workers env set KEY=value
248
+ ntn workers env push
249
+ ```
250
+
251
+ ## API Patterns Reference
252
+
253
+ See [api-pagination-patterns.md](./api-pagination-patterns.md) for detailed strategies drawn from production syncs with Salesforce, Stripe, HubSpot, GitHub, and ServiceNow.