@project-ajax/create 0.0.39 → 0.0.40-alpha.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/create",
3
- "version": "0.0.39",
3
+ "version": "0.0.40-alpha.0",
4
4
  "description": "Initialize a new Notion Workers extensions repo.",
5
5
  "bin": {
6
6
  "create-ajax": "dist/index.js"
@@ -31,10 +31,10 @@
31
31
  "template/.gitignore"
32
32
  ],
33
33
  "dependencies": {
34
- "@inquirer/prompts": "^8.0.1",
35
- "@project-ajax/shared": "*"
34
+ "@inquirer/prompts": "^8.0.1"
36
35
  },
37
36
  "devDependencies": {
37
+ "@project-ajax/shared": "*",
38
38
  "@types/node": "^22.19.0",
39
39
  "tsup": "^8.5.0",
40
40
  "typescript": "^5.9.3",
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Automations are only available in a private alpha.
3
+ */
4
+
1
5
  import { Worker } from "@project-ajax/sdk";
2
6
 
3
7
  const worker = new Worker();
@@ -33,8 +37,8 @@ worker.automation("questionAnswerAutomation", {
33
37
  emailValue = emailProperty.rich_text.map((rt) => rt.plain_text).join("");
34
38
  }
35
39
 
36
- // Handle empty email
37
- if (!emailValue) {
40
+ // Handle empty email or pageId
41
+ if (!emailValue || !pageId) {
38
42
  return;
39
43
  }
40
44
 
@@ -17,6 +17,7 @@ export default worker;
17
17
 
18
18
  // Option 1: Notion-managed provider (recommended when available).
19
19
  // Notion owns the OAuth app credentials and the backend has pre-configured provider settings.
20
+ // Notion-managed providers are only available in a private alpha.
20
21
  const googleAuth = worker.oauth("googleAuth", {
21
22
  name: "google-calendar",
22
23
  provider: "google",
@@ -24,6 +25,7 @@ const googleAuth = worker.oauth("googleAuth", {
24
25
 
25
26
  // Option 2: User-managed provider (you own the OAuth app credentials).
26
27
  // Keep client credentials in worker secrets and read them from `process.env`.
28
+ // Generally available.
27
29
  const myCustomAuth = worker.oauth("myCustomAuth", {
28
30
  name: "my-custom-provider",
29
31
  authorizationEndpoint: "https://provider.example.com/oauth/authorize",
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Syncs are only available in a private alpha.
3
+ */
4
+
1
5
  import { Worker } from "@project-ajax/sdk";
2
6
  import * as Builder from "@project-ajax/sdk/builder";
3
7
  import * as Schema from "@project-ajax/sdk/schema";
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Tools are generally available.
3
+ */
4
+
1
5
  import { Worker } from "@project-ajax/sdk";
2
6
 
3
7
  const worker = new Worker();
@@ -0,0 +1,209 @@
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
+
59
+ **Sync strategy (`mode`):**
60
+ - `replace`: each sync cycle must return the full dataset. After the final `hasMore: false`, any records not seen during that cycle are deleted.
61
+ - `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.
62
+
63
+ **How pagination works:**
64
+ 1. Return a batch of changes with `hasMore: true` and a `nextState` value
65
+ 2. The runtime calls `execute` again with that state
66
+ 3. Continue until you return `hasMore: false`
67
+
68
+ **Example replace sync:**
69
+
70
+ ```ts
71
+ worker.sync("paginatedSync", {
72
+ mode: "replace",
73
+ primaryKeyProperty: "ID",
74
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
+ execute: async (state, { notion }) => {
76
+ const page = state?.page ?? 1;
77
+ const pageSize = 100;
78
+ const { items, hasMore } = await fetchPage(page, pageSize);
79
+ return {
80
+ changes: items.map((item) => ({
81
+ type: "upsert",
82
+ key: item.id,
83
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
+ })),
85
+ hasMore,
86
+ nextState: hasMore ? { page: page + 1 } : undefined,
87
+ };
88
+ },
89
+ });
90
+ ```
91
+
92
+ **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.
93
+
94
+ **Incremental example (changes only, with deletes):**
95
+ ```ts
96
+ worker.sync("incrementalSync", {
97
+ primaryKeyProperty: "ID",
98
+ mode: "incremental",
99
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
+ execute: async (state, { notion }) => {
101
+ const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
+ return {
103
+ changes: [
104
+ ...upserts.map((item) => ({
105
+ type: "upsert",
106
+ key: item.id,
107
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
+ })),
109
+ ...deletes.map((id) => ({ type: "delete", key: id })),
110
+ ],
111
+ hasMore: Boolean(nextCursor),
112
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
+ };
114
+ },
115
+ });
116
+ ```
117
+
118
+ #### Relations
119
+
120
+ Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and `Builder.relation(primaryKey)` entries inside an array.
121
+
122
+ ```ts
123
+ worker.sync("projectsSync", {
124
+ primaryKeyProperty: "Project ID",
125
+ ...
126
+ });
127
+
128
+ // Example sync worker that syncs sample tasks to a database
129
+ worker.sync("tasksSync", {
130
+ primaryKeyProperty: "Task ID",
131
+ ...
132
+ schema: {
133
+ ...
134
+ properties: {
135
+ ...
136
+ Project: Schema.relation("projectsSync", {
137
+ // Optionally configure a two-way relation. This will automatically create the
138
+ // "Tasks" property on the project synced database: there is no need
139
+ // to configure "Tasks" on the projectSync capability.
140
+ twoWay: true, relatedPropertyName: "Tasks"
141
+ }),
142
+ },
143
+ },
144
+
145
+ execute: async () => {
146
+ // Return sample tasks as database entries
147
+ const tasks = fetchTasks()
148
+ const changes = tasks.map((task) => ({
149
+ type: "upsert" as const,
150
+ key: task.id,
151
+ properties: {
152
+ ...
153
+ Project: [Builder.relation(task.projectId)],
154
+ },
155
+ }));
156
+
157
+ return {
158
+ changes,
159
+ hasMore: false,
160
+ };
161
+ },
162
+ });
163
+ ```
164
+
165
+ ## Build, Test, and Development Commands
166
+ - Node >= 22 and npm >= 10.9.2 (see `package.json` engines).
167
+ - `npm run dev`: run `src/index.ts` with live reload.
168
+ - `npm run build`: compile TypeScript to `dist/`.
169
+ - `npm run check`: type-check only (no emit).
170
+ - `npx workers auth login [--env=dev]`: connect to a Notion workspace.
171
+ - `npx workers deploy`: build and publish capabilities.
172
+ - `npx workers exec <capability>`: run a sync or tool.
173
+ - `npx workers pack`: create a tarball; uses Git when available (respects `.gitignore`), always skips `node_modules`, `dist`, `workers.json`, `workers.*.json`, `.env`, and `.env.*`, and works in non-git repos.
174
+
175
+ ## Debugging & Monitoring Runs
176
+ Use `npx workers runs` to inspect run history and logs.
177
+
178
+ **List recent runs:**
179
+ ```shell
180
+ npx workers runs list
181
+ ```
182
+
183
+ **Get logs for a specific run:**
184
+ ```shell
185
+ npx workers runs logs <runId>
186
+ ```
187
+
188
+ **Get logs for the latest run (any capability):**
189
+ ```shell
190
+ npx workers runs list --plain | head -n1 | cut -f1 | xargs npx workers runs logs
191
+ ```
192
+
193
+ **Get logs for the latest run of a specific capability:**
194
+ ```shell
195
+ npx workers runs list --plain | grep tasksSync | head -n1 | cut -f1 | xargs npx workers runs logs
196
+ ```
197
+
198
+ The `--plain` flag outputs tab-separated values without formatting, making it easy to pipe to other commands.
199
+
200
+ ## Coding Style & Naming Conventions
201
+ - TypeScript with `strict` enabled; keep types explicit when shaping I/O.
202
+ - Use tabs for indentation; capability keys in lowerCamelCase.
203
+
204
+ ## Testing Guidelines
205
+ - No test runner configured; validate with `npm run check` and a deploy/exec loop.
206
+
207
+ ## Commit & Pull Request Guidelines
208
+ - Messages typically use `feat(scope): ...`, `TASK-123: ...`, or version bumps.
209
+ - PRs should describe changes, list commands run, and update examples if behavior changes.
@@ -6,161 +6,58 @@
6
6
  - Generated: `dist/` build output, `workers.json` CLI config.
7
7
 
8
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`).
9
+ `@project-ajax/sdk` provides `Worker`, schema helpers, and builders; `@project-ajax/cli` powers `npx workers ...`.
10
+
11
+ ### Agent tool calls
11
12
 
12
13
  ```ts
13
14
  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
15
 
17
16
  const worker = new Worker();
18
17
  export default worker;
19
18
 
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
19
  worker.tool("sayHello", {
30
20
  title: "Say Hello",
31
21
  description: "Return a greeting",
32
22
  schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
- execute: ({ name }, { notion }) => `Hello, ${name}`,
23
+ execute: ({ name }, _context) => `Hello, ${name}`,
34
24
  });
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
25
  ```
44
26
 
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`).
27
+ A worker with one or more tools is attachable to Notion agents. Each `tool` becomes a callable function for the agent:
28
+ - `title` and `description` are used both in the Notion UI as well as a helpful description to your agent.
29
+ - `schema` specifies what data the agent must supply.
48
30
 
49
- ### Sync
50
- #### Strategy and Pagination
31
+ ### OAuth
51
32
 
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
-
59
- **Sync strategy (`mode`):**
60
- - `replace`: each sync cycle must return the full dataset. After the final `hasMore: false`, any records not seen during that cycle are deleted.
61
- - `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.
62
-
63
- **How pagination works:**
64
- 1. Return a batch of changes with `hasMore: true` and a `nextState` value
65
- 2. The runtime calls `execute` again with that state
66
- 3. Continue until you return `hasMore: false`
67
-
68
- **Example replace sync:**
69
-
70
- ```ts
71
- worker.sync("paginatedSync", {
72
- mode: "replace",
73
- primaryKeyProperty: "ID",
74
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
- execute: async (state, { notion }) => {
76
- const page = state?.page ?? 1;
77
- const pageSize = 100;
78
- const { items, hasMore } = await fetchPage(page, pageSize);
79
- return {
80
- changes: items.map((item) => ({
81
- type: "upsert",
82
- key: item.id,
83
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
- })),
85
- hasMore,
86
- nextState: hasMore ? { page: page + 1 } : undefined,
87
- };
88
- },
89
- });
90
33
  ```
91
-
92
- **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.
93
-
94
- **Incremental example (changes only, with deletes):**
95
- ```ts
96
- worker.sync("incrementalSync", {
97
- primaryKeyProperty: "ID",
98
- mode: "incremental",
99
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
- execute: async (state, { notion }) => {
101
- const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
- return {
103
- changes: [
104
- ...upserts.map((item) => ({
105
- type: "upsert",
106
- key: item.id,
107
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
- })),
109
- ...deletes.map((id) => ({ type: "delete", key: id })),
110
- ],
111
- hasMore: Boolean(nextCursor),
112
- nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
- };
34
+ const myOAuth = worker.oauth("myOAuth", {
35
+ name: "my-provider",
36
+ authorizationEndpoint: "https://provider.example.com/oauth/authorize",
37
+ tokenEndpoint: "https://provider.example.com/oauth/token",
38
+ scope: "read write",
39
+ clientId: "1234567890",
40
+ clientSecret: process.env.MY_CUSTOM_OAUTH_CLIENT_SECRET ?? "",
41
+ authorizationParams: {
42
+ access_type: "offline",
43
+ prompt: "consent",
114
44
  },
115
45
  });
116
46
  ```
117
47
 
118
- #### Relations
119
-
120
- Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and `Builder.relation(primaryKey)` entries inside an array.
48
+ The OAuth capability allows you to perform the three legged OAuth flow after specifying paramteres of your OAuth client: `name`, `authorizationEndpoint`, `tokenEndpoint`, `clientId`, `clientSecret`, and `scope` (optional: `authorizationParams`, `callbackUrl`, `accessTokenExpireMs`).
121
49
 
122
- ```ts
123
- worker.sync("projectsSync", {
124
- primaryKeyProperty: "Project ID",
125
- ...
126
- });
50
+ ### Other capabilities
127
51
 
128
- // Example sync worker that syncs sample tasks to a database
129
- worker.sync("tasksSync", {
130
- primaryKeyProperty: "Task ID",
131
- ...
132
- schema: {
133
- ...
134
- properties: {
135
- ...
136
- Project: Schema.relation("projectsSync", {
137
- // Optionally configure a two-way relation. This will automatically create the
138
- // "Tasks" property on the project synced database: there is no need
139
- // to configure "Tasks" on the projectSync capability.
140
- twoWay: true, relatedPropertyName: "Tasks"
141
- }),
142
- },
143
- },
52
+ There are additional capability types in the SDK but these are restricted to a private alpha. Only Agent tools and OAuth are generally available.
144
53
 
145
- execute: async () => {
146
- // Return sample tasks as database entries
147
- const tasks = fetchTasks()
148
- const changes = tasks.map((task) => ({
149
- type: "upsert" as const,
150
- key: task.id,
151
- properties: {
152
- ...
153
- Project: [Builder.relation(task.projectId)],
154
- },
155
- }));
156
-
157
- return {
158
- changes,
159
- hasMore: false,
160
- };
161
- },
162
- });
163
- ```
54
+ | Capability | Availability |
55
+ |------------|--------------|
56
+ | Agent tools | Generally available |
57
+ | OAuth (user-managed) | Generally available |
58
+ | OAuth (Notion-managed) | Private alpha |
59
+ | Syncs | Private alpha |
60
+ | Automations | Private alpha |
164
61
 
165
62
  ## Build, Test, and Development Commands
166
63
  - Node >= 22 and npm >= 10.9.2 (see `package.json` engines).
@@ -169,7 +66,7 @@ worker.sync("tasksSync", {
169
66
  - `npm run check`: type-check only (no emit).
170
67
  - `npx workers auth login [--env=dev]`: connect to a Notion workspace.
171
68
  - `npx workers deploy`: build and publish capabilities.
172
- - `npx workers exec <capability>`: run a sync or tool.
69
+ - `npx workers exec <capability>`: run a sync or tool. Run after `deploy`.
173
70
  - `npx workers pack`: create a tarball; uses Git when available (respects `.gitignore`), always skips `node_modules`, `dist`, `workers.json`, `workers.*.json`, `.env`, and `.env.*`, and works in non-git repos.
174
71
 
175
72
  ## Debugging & Monitoring Runs
@@ -6,161 +6,58 @@
6
6
  - Generated: `dist/` build output, `workers.json` CLI config.
7
7
 
8
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`).
9
+ `@project-ajax/sdk` provides `Worker`, schema helpers, and builders; `@project-ajax/cli` powers `npx workers ...`.
10
+
11
+ ### Agent tool calls
11
12
 
12
13
  ```ts
13
14
  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
15
 
17
16
  const worker = new Worker();
18
17
  export default worker;
19
18
 
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
19
  worker.tool("sayHello", {
30
20
  title: "Say Hello",
31
21
  description: "Return a greeting",
32
22
  schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
- execute: ({ name }, { notion }) => `Hello, ${name}`,
23
+ execute: ({ name }, _context) => `Hello, ${name}`,
34
24
  });
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
25
  ```
44
26
 
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`).
27
+ A worker with one or more tools is attachable to Notion agents. Each `tool` becomes a callable function for the agent:
28
+ - `title` and `description` are used both in the Notion UI as well as a helpful description to your agent.
29
+ - `schema` specifies what data the agent must supply.
48
30
 
49
- ### Sync
50
- #### Strategy and Pagination
31
+ ### OAuth
51
32
 
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
-
59
- **Sync strategy (`mode`):**
60
- - `replace`: each sync cycle must return the full dataset. After the final `hasMore: false`, any records not seen during that cycle are deleted.
61
- - `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.
62
-
63
- **How pagination works:**
64
- 1. Return a batch of changes with `hasMore: true` and a `nextState` value
65
- 2. The runtime calls `execute` again with that state
66
- 3. Continue until you return `hasMore: false`
67
-
68
- **Example replace sync:**
69
-
70
- ```ts
71
- worker.sync("paginatedSync", {
72
- mode: "replace",
73
- primaryKeyProperty: "ID",
74
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
- execute: async (state, { notion }) => {
76
- const page = state?.page ?? 1;
77
- const pageSize = 100;
78
- const { items, hasMore } = await fetchPage(page, pageSize);
79
- return {
80
- changes: items.map((item) => ({
81
- type: "upsert",
82
- key: item.id,
83
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
- })),
85
- hasMore,
86
- nextState: hasMore ? { page: page + 1 } : undefined,
87
- };
88
- },
89
- });
90
33
  ```
91
-
92
- **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.
93
-
94
- **Incremental example (changes only, with deletes):**
95
- ```ts
96
- worker.sync("incrementalSync", {
97
- primaryKeyProperty: "ID",
98
- mode: "incremental",
99
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
- execute: async (state, { notion }) => {
101
- const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
- return {
103
- changes: [
104
- ...upserts.map((item) => ({
105
- type: "upsert",
106
- key: item.id,
107
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
- })),
109
- ...deletes.map((id) => ({ type: "delete", key: id })),
110
- ],
111
- hasMore: Boolean(nextCursor),
112
- nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
- };
34
+ const myOAuth = worker.oauth("myOAuth", {
35
+ name: "my-provider",
36
+ authorizationEndpoint: "https://provider.example.com/oauth/authorize",
37
+ tokenEndpoint: "https://provider.example.com/oauth/token",
38
+ scope: "read write",
39
+ clientId: "1234567890",
40
+ clientSecret: process.env.MY_CUSTOM_OAUTH_CLIENT_SECRET ?? "",
41
+ authorizationParams: {
42
+ access_type: "offline",
43
+ prompt: "consent",
114
44
  },
115
45
  });
116
46
  ```
117
47
 
118
- #### Relations
119
-
120
- Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and `Builder.relation(primaryKey)` entries inside an array.
48
+ The OAuth capability allows you to perform the three legged OAuth flow after specifying paramteres of your OAuth client: `name`, `authorizationEndpoint`, `tokenEndpoint`, `clientId`, `clientSecret`, and `scope` (optional: `authorizationParams`, `callbackUrl`, `accessTokenExpireMs`).
121
49
 
122
- ```ts
123
- worker.sync("projectsSync", {
124
- primaryKeyProperty: "Project ID",
125
- ...
126
- });
50
+ ### Other capabilities
127
51
 
128
- // Example sync worker that syncs sample tasks to a database
129
- worker.sync("tasksSync", {
130
- primaryKeyProperty: "Task ID",
131
- ...
132
- schema: {
133
- ...
134
- properties: {
135
- ...
136
- Project: Schema.relation("projectsSync", {
137
- // Optionally configure a two-way relation. This will automatically create the
138
- // "Tasks" property on the project synced database: there is no need
139
- // to configure "Tasks" on the projectSync capability.
140
- twoWay: true, relatedPropertyName: "Tasks"
141
- }),
142
- },
143
- },
52
+ There are additional capability types in the SDK but these are restricted to a private alpha. Only Agent tools and OAuth are generally available.
144
53
 
145
- execute: async () => {
146
- // Return sample tasks as database entries
147
- const tasks = fetchTasks()
148
- const changes = tasks.map((task) => ({
149
- type: "upsert" as const,
150
- key: task.id,
151
- properties: {
152
- ...
153
- Project: [Builder.relation(task.projectId)],
154
- },
155
- }));
156
-
157
- return {
158
- changes,
159
- hasMore: false,
160
- };
161
- },
162
- });
163
- ```
54
+ | Capability | Availability |
55
+ |------------|--------------|
56
+ | Agent tools | Generally available |
57
+ | OAuth (user-managed) | Generally available |
58
+ | OAuth (Notion-managed) | Private alpha |
59
+ | Syncs | Private alpha |
60
+ | Automations | Private alpha |
164
61
 
165
62
  ## Build, Test, and Development Commands
166
63
  - Node >= 22 and npm >= 10.9.2 (see `package.json` engines).
@@ -169,7 +66,7 @@ worker.sync("tasksSync", {
169
66
  - `npm run check`: type-check only (no emit).
170
67
  - `npx workers auth login [--env=dev]`: connect to a Notion workspace.
171
68
  - `npx workers deploy`: build and publish capabilities.
172
- - `npx workers exec <capability>`: run a sync or tool.
69
+ - `npx workers exec <capability>`: run a sync or tool. Run after `deploy`.
173
70
  - `npx workers pack`: create a tarball; uses Git when available (respects `.gitignore`), always skips `node_modules`, `dist`, `workers.json`, `workers.*.json`, `.env`, and `.env.*`, and works in non-git repos.
174
71
 
175
72
  ## Debugging & Monitoring Runs
@@ -1,258 +1,25 @@
1
1
  import { Worker } from "@project-ajax/sdk";
2
- import * as Builder from "@project-ajax/sdk/builder";
3
- import * as Schema from "@project-ajax/sdk/schema";
4
2
 
5
3
  const worker = new Worker();
6
4
  export default worker;
7
5
 
8
- const projectId = "project-123";
9
- const projectName = "Project 1";
6
+ type HelloInput = { name: string };
10
7
 
11
- // Sample data for demonstration
12
- const sampleTasks = [
13
- {
14
- id: "task-1",
15
- title: "Welcome to Notion Workers",
16
- status: "Completed",
17
- description: "This is a simple hello world example",
18
- projectId,
19
- },
20
- {
21
- id: "task-2",
22
- title: "Build your first worker",
23
- status: "In Progress",
24
- description: "Create a sync or tool worker",
25
- projectId,
26
- },
27
- {
28
- id: "task-3",
29
- title: "Deploy to production",
30
- status: "Todo",
31
- description: "Share your worker with your team",
32
- projectId,
33
- },
34
- ];
35
-
36
- worker.sync("projectsSync", {
37
- primaryKeyProperty: "Project ID",
38
- schema: {
39
- defaultName: "Projects",
40
- databaseIcon: Builder.notionIcon("activity"),
41
- properties: {
42
- "Project Name": Schema.title(),
43
- "Project ID": Schema.richText(),
44
- },
45
- },
46
- execute: async () => {
47
- return {
48
- changes: [
49
- {
50
- type: "upsert" as const,
51
- key: projectId,
52
- icon: Builder.notionIcon("activity"),
53
- properties: {
54
- "Project Name": Builder.title(projectName),
55
- "Project ID": Builder.richText(projectId),
56
- },
57
- },
58
- ],
59
- hasMore: false,
60
- };
61
- },
62
- });
63
-
64
- // Example sync worker that syncs sample tasks to a database
65
- worker.sync("tasksSync", {
66
- primaryKeyProperty: "Task ID",
67
-
68
- // Optional: How often the sync should run. Defaults to "continuous".
69
- // Use intervals like "30m", "1h", "1d" (min: 1m, max: 7d)
70
- schedule: "5m",
71
-
72
- // Sync mode:
73
- // - "replace": Each sync cycle returns the complete dataset. After hasMore:false,
74
- // pages not seen in this sync run are deleted.
75
- // - "incremental": Each sync cycle returns a subset of the complete dataset. Use delete markers
76
- // (e.g., { type: "delete", key: "task-1" }) to remove pages.
77
- // Defaults to "replace".
78
- // mode: "replace",
79
-
80
- schema: {
81
- defaultName: "Sample Tasks",
82
- databaseIcon: Builder.notionIcon("checklist"),
83
- properties: {
84
- "Ticket Title": Schema.title(),
85
- "Task ID": Schema.richText(),
86
- Description: Schema.richText(),
87
- Status: Schema.select([
88
- { name: "Completed", color: "green" },
89
- { name: "In Progress", color: "blue" },
90
- { name: "Todo", color: "default" },
91
- ]),
92
- Project: Schema.relation("projectsSync", {
93
- twoWay: true,
94
- relatedPropertyName: "Tasks",
95
- }),
96
- },
97
- },
98
-
99
- execute: async (_state, { notion: _notion }) => {
100
- const emojiForStatus = (status: string) => {
101
- switch (status) {
102
- case "Completed":
103
- return Builder.notionIcon("checkmark", "green");
104
- case "In Progress":
105
- return Builder.notionIcon("arrow-right", "blue");
106
- case "Todo":
107
- return Builder.notionIcon("clock", "lightgray");
108
- default:
109
- return Builder.notionIcon("question-mark", "lightgray");
110
- }
111
- };
112
- // Return sample tasks as database entries
113
- const changes = sampleTasks.map((task) => ({
114
- type: "upsert" as const,
115
- key: task.id,
116
- icon: emojiForStatus(task.status),
117
- properties: {
118
- "Ticket Title": Builder.title(task.title),
119
- "Task ID": Builder.richText(task.id),
120
- Description: Builder.richText(task.description),
121
- Status: Builder.select(task.status),
122
- Project: [Builder.relation(projectId)],
123
- },
124
- pageContentMarkdown: `## ${task.title}\n\n${task.description}`,
125
- }));
126
-
127
- return {
128
- // List of changes to apply to the Notion database.
129
- changes,
130
- // Indicates whether there is more data to fetch this sync cycle. If true, the runtime will call `execute` again with the nextState.
131
- hasMore: false,
132
- // Optional state data Notion will pass back as `state`
133
- // in the next execution.
134
- // This can be any type of data (cursor, page number, timestamp, etc.).
135
- nextState: undefined,
136
- };
137
- },
138
- });
139
-
140
- type TaskSearchInput = {
141
- taskId?: string | null;
142
- query?: string | null;
143
- };
144
-
145
- type TaskSearchOutput = {
146
- count: number;
147
- tasks: {
148
- id: string;
149
- title: string;
150
- status: string;
151
- description: string;
152
- }[];
153
- };
154
-
155
- // Example agent tool for retrieving task information
156
- worker.tool<TaskSearchInput, TaskSearchOutput>("taskSearchTool", {
157
- title: "Task Search",
158
- description:
159
- "Look up sample tasks by ID or keyword. Helpful for demonstrating agent tool calls.",
8
+ // Example agent tool that returns a greeting
9
+ // Delete this when you're ready to start building your own tools.
10
+ worker.tool<HelloInput, string>("sayHello", {
11
+ title: "Say Hello",
12
+ description: "Returns a friendly greeting for the given name.",
160
13
  schema: {
161
14
  type: "object",
162
15
  properties: {
163
- taskId: {
16
+ name: {
164
17
  type: "string",
165
- nullable: true,
166
- description: "Return a single task that matches the given task ID.",
167
- },
168
- query: {
169
- type: "string",
170
- nullable: true,
171
- description:
172
- "Match search terms against words in the task title or description.",
18
+ description: "The name to greet.",
173
19
  },
174
20
  },
175
- required: [],
21
+ required: ["name"],
176
22
  additionalProperties: false,
177
23
  },
178
- execute: async (input: TaskSearchInput, { notion: _notion }) => {
179
- const { taskId, query } = input;
180
-
181
- let matchingTasks = sampleTasks;
182
-
183
- if (taskId) {
184
- matchingTasks = sampleTasks.filter((task) => task.id === taskId);
185
- } else if (query) {
186
- const normalizedQuery = query.trim().toLowerCase();
187
-
188
- const terms = normalizedQuery.split(/\s+/).filter(Boolean);
189
-
190
- if (terms.length > 0) {
191
- const scoredTasks = sampleTasks
192
- .map((task) => {
193
- const title = task.title.toLowerCase();
194
- const description = task.description.toLowerCase();
195
- const matches = terms.reduce((count, term) => {
196
- return title.includes(term) || description.includes(term)
197
- ? count + 1
198
- : count;
199
- }, 0);
200
-
201
- return { task, matches };
202
- })
203
- .filter(({ matches }) => matches > 0)
204
- .sort((a, b) => b.matches - a.matches);
205
-
206
- matchingTasks = scoredTasks.map(({ task }) => task);
207
- } else {
208
- matchingTasks = [];
209
- }
210
- }
211
-
212
- return {
213
- count: matchingTasks.length,
214
- tasks: matchingTasks.map((task) => ({
215
- id: task.id,
216
- title: task.title,
217
- status: task.status,
218
- description: task.description,
219
- })),
220
- } satisfies TaskSearchOutput;
221
- },
222
- });
223
-
224
- // Example automation that runs when triggered from a database automation
225
- worker.automation("completeTaskAutomation", {
226
- title: "Mark Task Complete",
227
- description: "Automatically marks a task as complete when triggered",
228
- execute: async (event, { notion: _notion }) => {
229
- const { pageId, actionType, pageData } = event;
230
- // The pageData parameter contains the full page object from Notion's Public API
231
- // with all the database properties already encoded and ready to use.
232
-
233
- console.log(`Automation triggered for page: ${pageId}`);
234
- console.log(`Action type: ${actionType}`);
235
-
236
- if (pageData) {
237
- // Access all page properties directly
238
- console.log("Page properties:", pageData.properties);
239
-
240
- // Example: Access specific properties by their name
241
- // const taskName = pageData.properties.Name;
242
- // const status = pageData.properties.Status;
243
- // const assignee = pageData.properties.Assignee;
244
-
245
- // The properties are in Notion's Public API format
246
- // See: https://developers.notion.com/reference/property-value-object
247
- }
248
-
249
- // In a real implementation, you would:
250
- // 1. Use the page properties to determine what action to take
251
- // 2. Update the task status in your system
252
- // 3. Call external APIs, send notifications, etc.
253
-
254
- // Example: You could call an external API, update a database, send notifications, etc.
255
- // For this demo, we just log the execution
256
- console.log("Task marked as complete!");
257
- },
24
+ execute: ({ name }) => `Hello, ${name}!`,
258
25
  });