@project-ajax/create 0.0.30 → 0.0.32

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.
package/README.md CHANGED
@@ -25,6 +25,4 @@ npm init @project-ajax -- --directory my-worker --project my-worker
25
25
 
26
26
  ### Scheduling
27
27
 
28
- Generated sync workers default to `continuous`, meaning the sync runs continuously/as fast as possible. To slow cadence, set `schedule` in `src/index.ts` to an interval like `30m`, `1h`, or `1d` (min `1m`, max `7d`), which runs the sync once per interval.
29
-
30
- This can be useful for minding rate limits.
28
+ Sync workers default to running every 30 minutes. To change the cadence, set `schedule` in `src/index.ts` to an interval like `15m`, `1h`, or `1d` (min `1m`, max `7d`), or `continuous` to run as fast as possible.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/create",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "Initialize a new Notion Project Ajax extensions repo.",
5
5
  "bin": {
6
6
  "create-ajax": "dist/index.js"
@@ -1,14 +1,8 @@
1
- import { Client } from "@notionhq/client";
2
1
  import { Worker } from "@project-ajax/sdk";
3
2
 
4
3
  const worker = new Worker();
5
4
  export default worker;
6
5
 
7
- // Initialize the Notion client with OAuth token from environment
8
- const notion = new Client({
9
- auth: process.env.NOTION_API_TOKEN,
10
- });
11
-
12
6
  type RichTextProperty = {
13
7
  type: "rich_text";
14
8
  rich_text: Array<{ plain_text: string }>;
@@ -26,7 +20,8 @@ worker.automation("questionAnswerAutomation", {
26
20
  title: "Question Answer Automation",
27
21
  description:
28
22
  "Reads questions from database pages and updates them with answers",
29
- execute: async ({ pageId, pageData }) => {
23
+ execute: async (event, { notion }) => {
24
+ const { pageId, pageData } = event;
30
25
  // Extract email from the page dat
31
26
  const emailProperty = pageData?.properties?.Email as
32
27
  | RichTextProperty
@@ -54,7 +54,7 @@ worker.sync("mySync", {
54
54
  Project: Schema.relation("projectsSync"),
55
55
  },
56
56
  },
57
- execute: async () => {
57
+ execute: async (_state, { notion: _notion }) => {
58
58
  // Fetch and return data
59
59
  return {
60
60
  changes: [
@@ -41,7 +41,7 @@ worker.tool<
41
41
  additionalProperties: false,
42
42
  },
43
43
  // The function that executes when the tool is called
44
- execute: async (input) => {
44
+ execute: async (input, { notion: _notion }) => {
45
45
  // Destructure input with default values
46
46
  const { query: _query, limit: _limit = 10 } = input;
47
47
 
@@ -20,7 +20,7 @@ export default worker;
20
20
  worker.sync("tasksSync", {
21
21
  primaryKeyProperty: "ID",
22
22
  schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
23
- execute: async () => ({
23
+ execute: async (_state, { notion }) => ({
24
24
  changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
25
  hasMore: false,
26
26
  }),
@@ -30,37 +30,40 @@ worker.tool("sayHello", {
30
30
  title: "Say Hello",
31
31
  description: "Return a greeting",
32
32
  schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
- execute: ({ name }) => `Hello, ${name}`,
33
+ execute: ({ name }, { notion }) => `Hello, ${name}`,
34
34
  });
35
35
 
36
36
  worker.automation("sendWelcomeEmail", {
37
37
  title: "Send Welcome Email",
38
38
  description: "Runs from a database automation",
39
- execute: async () => {},
39
+ execute: async (event, { notion }) => {},
40
40
  });
41
41
 
42
42
  worker.oauth("googleAuth", { name: "my-google-auth", provider: "google" });
43
43
  ```
44
44
 
45
+ - All `execute` handlers receive a Notion SDK client in the second argument as `context.notion`.
46
+
45
47
  - For user-managed OAuth, supply `name`, `authorizationEndpoint`, `tokenEndpoint`, `clientId`, `clientSecret`, and `scope` (optional: `authorizationParams`, `callbackUrl`, `accessTokenExpireMs`).
46
48
 
47
49
  ### Sync
48
50
  #### Strategy and Pagination
49
51
 
50
- Syncs run in a "sync cycle": a back-to-back chain of `execute` calls that starts at a scheduled trigger and ends when an execution returns `hasMore: false`.
52
+ Syncs run in a "sync cycle": a back-to-back chain of `execute` calls that starts at a scheduled trigger and ends when an execution returns `hasMore: false`. By default, syncs run every 30 minutes. Set `schedule` to an interval like `"15m"`, `"1h"`, `"1d"` (min `"1m"`, max `"7d"`), or `"continuous"` to run as fast as possible.
51
53
 
52
54
  - Always use pagination, when available. Returning too many changes in one execution will fail. Start with batch sizes of ~100 changes.
53
55
  - `mode=replace` is simpler, and fine for smaller syncs (<10k)
54
56
  - Use `mode=incremental` when the sync could return a lot of data (>10k), eg for SaaS tools like Salesforce or Stripe
55
57
  - When using `mode=incremental`, emit delete markers as needed if easy to do (below)
58
+ - Use `Pacer.wait` to respect upstream rate limits during `execute` calls; pick a stable key per API or endpoint.
56
59
 
57
60
  **Sync strategy (`mode`):**
58
61
  - `replace`: each sync cycle must return the full dataset. After the final `hasMore: false`, any records not seen during that cycle are deleted.
59
62
  - `incremental`: each sync cycle returns a subset of the full dataset (usually the changes since the last run). Deletions must be explicit via `{ type: "delete", key: "..." }`. Records not mentioned are left unchanged.
60
63
 
61
64
  **How pagination works:**
62
- 1. Return a batch of changes with `hasMore: true` and a `nextContext` value
63
- 2. The runtime calls `execute` again with that context
65
+ 1. Return a batch of changes with `hasMore: true` and a `nextState` value
66
+ 2. The runtime calls `execute` again with that state
64
67
  3. Continue until you return `hasMore: false`
65
68
 
66
69
  **Example replace sync:**
@@ -70,8 +73,8 @@ worker.sync("paginatedSync", {
70
73
  mode: "replace",
71
74
  primaryKeyProperty: "ID",
72
75
  schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
73
- execute: async (context?: { page: number }) => {
74
- const page = context?.page ?? 1;
76
+ execute: async (state, { notion }) => {
77
+ const page = state?.page ?? 1;
75
78
  const pageSize = 100;
76
79
  const { items, hasMore } = await fetchPage(page, pageSize);
77
80
  return {
@@ -81,13 +84,13 @@ worker.sync("paginatedSync", {
81
84
  properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
82
85
  })),
83
86
  hasMore,
84
- nextContext: hasMore ? { page: page + 1 } : undefined,
87
+ nextState: hasMore ? { page: page + 1 } : undefined,
85
88
  };
86
89
  },
87
90
  });
88
91
  ```
89
92
 
90
- **Context types:** The `nextContext` can be any serializable value—a cursor string, page number, timestamp, or complex object. Type your execute function's context parameter to match.
93
+ **State types:** The `nextState` can be any serializable value—a cursor string, page number, timestamp, or complex object. Type your execute function's `state` to match.
91
94
 
92
95
  **Incremental example (changes only, with deletes):**
93
96
  ```ts
@@ -95,8 +98,8 @@ worker.sync("incrementalSync", {
95
98
  primaryKeyProperty: "ID",
96
99
  mode: "incremental",
97
100
  schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
98
- execute: async (context?: { cursor?: string }) => {
99
- const { upserts, deletes, nextCursor } = await fetchChanges(context?.cursor);
101
+ execute: async (state, { notion }) => {
102
+ const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
100
103
  return {
101
104
  changes: [
102
105
  ...upserts.map((item) => ({
@@ -107,7 +110,7 @@ worker.sync("incrementalSync", {
107
110
  ...deletes.map((id) => ({ type: "delete", key: id })),
108
111
  ],
109
112
  hasMore: Boolean(nextCursor),
110
- nextContext: nextCursor ? { cursor: nextCursor } : undefined,
113
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
111
114
  };
112
115
  },
113
116
  });
@@ -0,0 +1,179 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+ - `src/index.ts` defines the worker and capabilities.
5
+ - `.examples/` has focused samples (sync, tool, automation, OAuth).
6
+ - Generated: `dist/` build output, `workers.json` CLI config.
7
+
8
+ ## Worker & Capability API (SDK)
9
+ - `@project-ajax/sdk` provides `Worker`, schema helpers, and builders; `@project-ajax/cli` powers `npx workers ...`.
10
+ - Capability keys are unique strings used by the CLI (e.g., `npx workers exec tasksSync`).
11
+
12
+ ```ts
13
+ import { Worker } from "@project-ajax/sdk";
14
+ import * as Builder from "@project-ajax/sdk/builder";
15
+ import * as Schema from "@project-ajax/sdk/schema";
16
+
17
+ const worker = new Worker();
18
+ export default worker;
19
+
20
+ worker.sync("tasksSync", {
21
+ primaryKeyProperty: "ID",
22
+ schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
23
+ execute: async (_state, { notion }) => ({
24
+ changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
+ hasMore: false,
26
+ }),
27
+ });
28
+
29
+ worker.tool("sayHello", {
30
+ title: "Say Hello",
31
+ description: "Return a greeting",
32
+ schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
+ execute: ({ name }, { notion }) => `Hello, ${name}`,
34
+ });
35
+
36
+ worker.automation("sendWelcomeEmail", {
37
+ title: "Send Welcome Email",
38
+ description: "Runs from a database automation",
39
+ execute: async (event, { notion }) => {},
40
+ });
41
+
42
+ worker.oauth("googleAuth", { name: "my-google-auth", provider: "google" });
43
+ ```
44
+
45
+ - All `execute` handlers receive a Notion SDK client in the second argument as `context.notion`.
46
+
47
+ - For user-managed OAuth, supply `name`, `authorizationEndpoint`, `tokenEndpoint`, `clientId`, `clientSecret`, and `scope` (optional: `authorizationParams`, `callbackUrl`, `accessTokenExpireMs`).
48
+
49
+ ### Sync
50
+ #### Strategy and Pagination
51
+
52
+ Syncs run in a "sync cycle": a back-to-back chain of `execute` calls that starts at a scheduled trigger and ends when an execution returns `hasMore: false`. By default, syncs run every 30 minutes. Set `schedule` to an interval like `"15m"`, `"1h"`, `"1d"` (min `"1m"`, max `"7d"`), or `"continuous"` to run as fast as possible.
53
+
54
+ - Always use pagination, when available. Returning too many changes in one execution will fail. Start with batch sizes of ~100 changes.
55
+ - `mode=replace` is simpler, and fine for smaller syncs (<10k)
56
+ - Use `mode=incremental` when the sync could return a lot of data (>10k), eg for SaaS tools like Salesforce or Stripe
57
+ - When using `mode=incremental`, emit delete markers as needed if easy to do (below)
58
+ - Use `Pacer.wait` to respect upstream rate limits during `execute` calls; pick a stable key per API or endpoint.
59
+
60
+ **Sync strategy (`mode`):**
61
+ - `replace`: each sync cycle must return the full dataset. After the final `hasMore: false`, any records not seen during that cycle are deleted.
62
+ - `incremental`: each sync cycle returns a subset of the full dataset (usually the changes since the last run). Deletions must be explicit via `{ type: "delete", key: "..." }`. Records not mentioned are left unchanged.
63
+
64
+ **How pagination works:**
65
+ 1. Return a batch of changes with `hasMore: true` and a `nextState` value
66
+ 2. The runtime calls `execute` again with that state
67
+ 3. Continue until you return `hasMore: false`
68
+
69
+ **Example replace sync:**
70
+
71
+ ```ts
72
+ worker.sync("paginatedSync", {
73
+ mode: "replace",
74
+ primaryKeyProperty: "ID",
75
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
76
+ execute: async (state, { notion }) => {
77
+ const page = state?.page ?? 1;
78
+ const pageSize = 100;
79
+ const { items, hasMore } = await fetchPage(page, pageSize);
80
+ return {
81
+ changes: items.map((item) => ({
82
+ type: "upsert",
83
+ key: item.id,
84
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
85
+ })),
86
+ hasMore,
87
+ nextState: hasMore ? { page: page + 1 } : undefined,
88
+ };
89
+ },
90
+ });
91
+ ```
92
+
93
+ **State types:** The `nextState` can be any serializable value—a cursor string, page number, timestamp, or complex object. Type your execute function's `state` to match.
94
+
95
+ **Incremental example (changes only, with deletes):**
96
+ ```ts
97
+ worker.sync("incrementalSync", {
98
+ primaryKeyProperty: "ID",
99
+ mode: "incremental",
100
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
101
+ execute: async (state, { notion }) => {
102
+ const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
103
+ return {
104
+ changes: [
105
+ ...upserts.map((item) => ({
106
+ type: "upsert",
107
+ key: item.id,
108
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
109
+ })),
110
+ ...deletes.map((id) => ({ type: "delete", key: id })),
111
+ ],
112
+ hasMore: Boolean(nextCursor),
113
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
114
+ };
115
+ },
116
+ });
117
+ ```
118
+
119
+ #### Relations
120
+
121
+ Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and `Builder.relation(primaryKey)` entries inside an array.
122
+
123
+ ```ts
124
+ worker.sync("projectsSync", {
125
+ primaryKeyProperty: "Project ID",
126
+ ...
127
+ });
128
+
129
+ // Example sync worker that syncs sample tasks to a database
130
+ worker.sync("tasksSync", {
131
+ primaryKeyProperty: "Task ID",
132
+ ...
133
+ schema: {
134
+ ...
135
+ properties: {
136
+ ...
137
+ Project: Schema.relation("projectsSync"),
138
+ },
139
+ },
140
+
141
+ execute: async () => {
142
+ // Return sample tasks as database entries
143
+ const tasks = fetchTasks()
144
+ const changes = tasks.map((task) => ({
145
+ type: "upsert" as const,
146
+ key: task.id,
147
+ properties: {
148
+ ...
149
+ Project: [Builder.relation(task.projectId)],
150
+ },
151
+ }));
152
+
153
+ return {
154
+ changes,
155
+ hasMore: false,
156
+ };
157
+ },
158
+ });
159
+ ```
160
+
161
+ ## Build, Test, and Development Commands
162
+ - Node >= 22 and npm >= 10.9.2 (see `package.json` engines).
163
+ - `npm run dev`: run `src/index.ts` with live reload.
164
+ - `npm run build`: compile TypeScript to `dist/`.
165
+ - `npm run check`: type-check only (no emit).
166
+ - `npx workers auth login [--env=dev]`: connect to a Notion workspace.
167
+ - `npx workers deploy`: build and publish capabilities.
168
+ - `npx workers exec <capability>`: run a sync or tool.
169
+
170
+ ## Coding Style & Naming Conventions
171
+ - TypeScript with `strict` enabled; keep types explicit when shaping I/O.
172
+ - Use tabs for indentation; capability keys in lowerCamelCase.
173
+
174
+ ## Testing Guidelines
175
+ - No test runner configured; validate with `npm run check` and a deploy/exec loop.
176
+
177
+ ## Commit & Pull Request Guidelines
178
+ - Messages typically use `feat(scope): ...`, `TASK-123: ...`, or version bumps.
179
+ - PRs should describe changes, list commands run, and update examples if behavior changes.
@@ -13,7 +13,7 @@ single `Worker` instance.
13
13
  ## Quickstart
14
14
 
15
15
  ```shell
16
- npm init @project-ajax
16
+ npm init @project-ajax@latest
17
17
  # choose a folder, then:
18
18
  cd my-worker
19
19
  npm install
@@ -37,7 +37,7 @@ npx workers exec tasksSync
37
37
 
38
38
  - Worker: The Node/TypeScript program you deploy, defined in `src/index.ts`.
39
39
  - Capability: A named sync, tool, automation, or OAuth definition registered on a worker.
40
- - Secret: A key/value stored with `npx workers secrets`, exposed as environment variables (for example, `process.env.SECRET_NAME`).
40
+ - Secret: A key/value stored with `npx workers secrets`, exposed as environment variables (for example, `process.env.SECRET_NAME`). Use `KEY=VALUE` pairs when setting secrets.
41
41
 
42
42
  ## Build a Worker
43
43
 
@@ -73,7 +73,8 @@ worker.sync("tasksSync", {
73
73
  ID: Schema.richText(),
74
74
  },
75
75
  },
76
- execute: async () => ({
76
+ execute: async (_state, { notion }) => ({
77
+ // `notion` is the Notion API SDK client.
77
78
  changes: [
78
79
  {
79
80
  type: "upsert",
@@ -95,15 +96,15 @@ Notion will delete stale rows after each run. A stale row is a row that was in t
95
96
 
96
97
  When your sync is pulling in many rows of data (>1k), you'll want to use pagination. Breaking down pages to ~100 is a good starting point.
97
98
 
98
- You can use **context** to persist things like pagination tokens between `execute` runs. Notion passes `context` as the first argument to `execute`. Return `nextContext` to set the `context` for the next run:
99
+ You can use `state` to persist things like pagination tokens between `execute` runs. Notion passes `state` as the first argument to `execute`, plus a `context` object that includes the Notion client at `context.notion`. Return `nextState` to set the `state` for the next run:
99
100
 
100
101
  ```ts
101
102
  worker.sync("fullSync", {
102
103
  primaryKeyProperty: "ID",
103
104
  mode: "replace",
104
105
  schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
105
- execute: async (context?: { page: number }) => {
106
- const { items , nextCursor } = await fetchPage(context?.page);
106
+ execute: async (state, { notion }) => {
107
+ const { items , nextCursor } = await fetchPage(state?.page);
107
108
  return {
108
109
  changes: items.map((item) => ({
109
110
  type: "upsert",
@@ -111,13 +112,13 @@ worker.sync("fullSync", {
111
112
  properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
112
113
  })),
113
114
  hasMore: nextCursor ? true : false,
114
- nextCursor
115
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
115
116
  };
116
117
  },
117
118
  });
118
119
  ```
119
120
 
120
- Return `hasMore=false` for each run until you reach the end. On the last run, return `hasMore=true`. At the start of the next cycle, Notion will start anew and call `execute` with `context=null`.
121
+ Return `hasMore=false` for each run until you reach the end. On the last run, return `hasMore=true`. At the start of the next cycle, Notion will start anew and call `execute` with `state` undefined.
121
122
 
122
123
  #### Write a sync that syncs incrementally
123
124
 
@@ -130,8 +131,8 @@ worker.sync("incrementalSync", {
130
131
  primaryKeyProperty: "ID",
131
132
  mode: "incremental",
132
133
  schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
133
- execute: async (context?: { cursor?: string }) => {
134
- const { upserts, deletes, nextCursor } = await fetchChanges(context?.cursor);
134
+ execute: async (state, { notion }) => {
135
+ const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
135
136
  return {
136
137
  changes: [
137
138
  ...upserts.map((item) => ({
@@ -142,13 +143,13 @@ worker.sync("incrementalSync", {
142
143
  ...deletes.map((id) => ({ type: "delete", key: id })),
143
144
  ],
144
145
  hasMore: Boolean(nextCursor),
145
- nextContext: nextCursor ? { cursor: nextCursor } : undefined,
146
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
146
147
  };
147
148
  },
148
149
  });
149
150
  ```
150
151
 
151
- Unlike the `replace` sync mode, Notion will not drop "stale" rows and `context` will persist between sync cycles.
152
+ Unlike the `replace` sync mode, Notion will not drop "stale" rows and `state` will persist between sync cycles.
152
153
 
153
154
  **Deletes**
154
155
 
@@ -170,6 +171,33 @@ changes: [
170
171
  ]
171
172
  ```
172
173
 
174
+ #### Respect rate limits
175
+
176
+
177
+ If the source API has a rate limit, call `Pacer.wait` inside `execute` to pace requests (for example, 10 requests per minute):
178
+
179
+ ```ts
180
+ import { Pacer } from "@project-ajax/sdk/pacer";
181
+
182
+ worker.sync("tasksSync", {
183
+ primaryKeyProperty: "ID",
184
+ schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
185
+ execute: async () => {
186
+ const oneMinute = 60 * 1000;
187
+ await Pacer.wait("tasks-api", { requests: 10, intervalMs: oneMinute });
188
+ const items = await fetchTasks();
189
+ return {
190
+ changes: items.map((item) => ({
191
+ type: "upsert",
192
+ key: item.id,
193
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
194
+ })),
195
+ hasMore: false,
196
+ };
197
+ },
198
+ });
199
+ ```
200
+
173
201
  ### Tool
174
202
 
175
203
  Tools are callable by Notion custom agents.
@@ -184,7 +212,7 @@ worker.tool("sayHello", {
184
212
  required: ["name"],
185
213
  additionalProperties: false,
186
214
  },
187
- execute: ({ name }) => `Hello, ${name}`,
215
+ execute: ({ name }, { notion }) => `Hello, ${name}`,
188
216
  });
189
217
  ```
190
218
 
@@ -196,7 +224,8 @@ Automations run from Notion database buttons or automations.
196
224
  worker.automation("sendWelcomeEmail", {
197
225
  title: "Send Welcome Email",
198
226
  description: "Runs from a database automation",
199
- execute: async ({ pageId }) => {
227
+ execute: async (event, { notion }) => {
228
+ const { pageId } = event;
200
229
  console.log("Triggered for page", pageId);
201
230
  },
202
231
  });
@@ -297,6 +326,12 @@ Store secrets for runtime access:
297
326
  npx workers secrets set API_KEY=my-secret
298
327
  ```
299
328
 
329
+ You can pass multiple secrets and quote values that contain spaces or `=`:
330
+
331
+ ```shell
332
+ npx workers secrets set FOO=bar BAZ="qux" ASDF="x=y" GREETING="hello world"
333
+ ```
334
+
300
335
  ### `npx workers secrets list`
301
336
  List secret keys:
302
337
 
@@ -93,7 +93,7 @@ worker.sync("tasksSync", {
93
93
  },
94
94
  },
95
95
 
96
- execute: async () => {
96
+ execute: async (_state, { notion: _notion }) => {
97
97
  const emojiForStatus = (status: string) => {
98
98
  switch (status) {
99
99
  case "Completed":
@@ -124,17 +124,33 @@ worker.sync("tasksSync", {
124
124
  return {
125
125
  // List of changes to apply to the Notion database.
126
126
  changes,
127
- // Indicates whether there is more data to fetch this sync cycle. If true, the runtime will call `execute` again with the nextContext.
127
+ // Indicates whether there is more data to fetch this sync cycle. If true, the runtime will call `execute` again with the nextState.
128
128
  hasMore: false,
129
- // Optional context data Notion will pass back in the next execution.
129
+ // Optional state data Notion will pass back as `state`
130
+ // in the next execution.
130
131
  // This can be any type of data (cursor, page number, timestamp, etc.).
131
- nextContext: undefined,
132
+ nextState: undefined,
132
133
  };
133
134
  },
134
135
  });
135
136
 
137
+ type TaskSearchInput = {
138
+ taskId?: string | null;
139
+ query?: string | null;
140
+ };
141
+
142
+ type TaskSearchOutput = {
143
+ count: number;
144
+ tasks: {
145
+ id: string;
146
+ title: string;
147
+ status: string;
148
+ description: string;
149
+ }[];
150
+ };
151
+
136
152
  // Example agent tool for retrieving task information
137
- worker.tool("taskSearchTool", {
153
+ worker.tool<TaskSearchInput, TaskSearchOutput>("taskSearchTool", {
138
154
  title: "Task Search",
139
155
  description:
140
156
  "Look up sample tasks by ID or keyword. Helpful for demonstrating agent tool calls.",
@@ -156,7 +172,7 @@ worker.tool("taskSearchTool", {
156
172
  required: [],
157
173
  additionalProperties: false,
158
174
  },
159
- execute: async (input: { taskId?: string | null; query?: string | null }) => {
175
+ execute: async (input: TaskSearchInput, { notion: _notion }) => {
160
176
  const { taskId, query } = input;
161
177
 
162
178
  let matchingTasks = sampleTasks;
@@ -198,7 +214,7 @@ worker.tool("taskSearchTool", {
198
214
  status: task.status,
199
215
  description: task.description,
200
216
  })),
201
- };
217
+ } satisfies TaskSearchOutput;
202
218
  },
203
219
  });
204
220
 
@@ -206,7 +222,8 @@ worker.tool("taskSearchTool", {
206
222
  worker.automation("completeTaskAutomation", {
207
223
  title: "Mark Task Complete",
208
224
  description: "Automatically marks a task as complete when triggered",
209
- execute: async ({ pageId, actionType, pageData }) => {
225
+ execute: async (event, { notion: _notion }) => {
226
+ const { pageId, actionType, pageData } = event;
210
227
  // The pageData parameter contains the full page object from Notion's Public API
211
228
  // with all the database properties already encoded and ready to use.
212
229