@notionhq/apps 0.0.29 → 0.0.31
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/notion-as-code/custom-agent.d.ts +6 -5
- package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
- package/dist/notion-as-code/custom-agent.js +15 -4
- package/dist/notion-as-code/index.d.ts +1 -1
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +7 -1
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/workflow-access.d.ts +8 -3
- package/dist/workflow-access.d.ts.map +1 -1
- package/dist/workflow-access.js +33 -34
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -3
- package/skills/notion-as-code/SKILL.md +10 -7
- package/src/index.ts +1 -1
- package/src/notion-as-code/custom-agent.test.ts +62 -3
- package/src/notion-as-code/custom-agent.ts +19 -10
- package/src/notion-as-code/index.ts +0 -1
- package/src/notion-as-code/intents.ts +4 -1
- package/src/notion-as-code/notion-as-code-types.test.ts +21 -11
- package/src/workflow-access.ts +56 -51
- package/src/workflow.ts +1 -1
- package/AGENTS.md +0 -137
- package/docs/BUILD.md +0 -268
- package/docs/CONNECTIONS.md +0 -213
- package/docs/workflow-inputs.md +0 -28
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResourceAccessLevel } from "../workflow-access.js";
|
|
1
2
|
import type { CustomAgentArgs } from "./custom-agent.js";
|
|
2
3
|
import type { PageArgs } from "./page.js";
|
|
3
4
|
import type { TeamspaceArgs } from "./teamspace.js";
|
|
@@ -346,7 +347,9 @@ export type DatabaseIntent = {
|
|
|
346
347
|
};
|
|
347
348
|
|
|
348
349
|
export type PageIntent = PageArgs & { parent: Parent };
|
|
349
|
-
export type CustomAgentIntent = CustomAgentArgs
|
|
350
|
+
export type CustomAgentIntent = Omit<CustomAgentArgs, "access"> & {
|
|
351
|
+
access?: readonly { resourceId: ResourceId; level: ResourceAccessLevel }[];
|
|
352
|
+
};
|
|
350
353
|
|
|
351
354
|
export type ViewIntent = {
|
|
352
355
|
databaseResourceId: ResourceId;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { expectTypeOf, it } from "vitest";
|
|
2
|
+
import { access } from "../workflow-access.js";
|
|
3
|
+
import { page, database, customAgent } from "../index.js";
|
|
2
4
|
|
|
3
5
|
import type {
|
|
4
6
|
CustomAgentAccess,
|
|
5
|
-
CustomAgentAccessRole,
|
|
6
7
|
CustomAgentArgs,
|
|
7
8
|
CustomAgentPropertyUpdatedTrigger,
|
|
8
9
|
CustomAgentTrustedUrls,
|
|
@@ -16,17 +17,26 @@ import type {
|
|
|
16
17
|
} from "./index.js";
|
|
17
18
|
|
|
18
19
|
it("types custom agent resource access and URL trust", () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
resourceId: "
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
expectTypeOf<
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const home = page({ resourceId: "home" });
|
|
21
|
+
const tasks = database("tasks", {
|
|
22
|
+
dataSourceResourceId: "source",
|
|
23
|
+
name: "Tasks",
|
|
24
|
+
schema: { Title: { resourceId: "title", type: "title" } },
|
|
25
|
+
});
|
|
26
|
+
expectTypeOf(access.view(home)).toExtend<CustomAgentAccess>();
|
|
27
|
+
expectTypeOf(access.edit(tasks)).toExtend<CustomAgentAccess>();
|
|
28
|
+
expectTypeOf(access.view(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
29
|
+
expectTypeOf(access.comment(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
30
|
+
expectTypeOf(access.edit(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
31
|
+
expectTypeOf(access.fullAccess(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
32
|
+
expectTypeOf<{
|
|
33
|
+
resource: typeof tasks.dataSource;
|
|
34
|
+
level: "view";
|
|
29
35
|
}>().not.toExtend<CustomAgentAccess>();
|
|
36
|
+
expectTypeOf<{ resourceId: "tasks"; role: "can_view" }>().not.toExtend<CustomAgentAccess>();
|
|
37
|
+
expectTypeOf(
|
|
38
|
+
access.call(customAgent({ resourceId: "agent", name: "Agent" })),
|
|
39
|
+
).not.toExtend<CustomAgentAccess>();
|
|
30
40
|
expectTypeOf<{ type: "all" }>().toEqualTypeOf<CustomAgentTrustedUrls>();
|
|
31
41
|
expectTypeOf<CustomAgentArgs["webAccess"]>().toEqualTypeOf<boolean | undefined>();
|
|
32
42
|
expectTypeOf<CustomAgentArgs["trustedUrls"]>().toEqualTypeOf<
|
package/src/workflow-access.ts
CHANGED
|
@@ -20,17 +20,17 @@ type WorkflowAccessContentResource = Exclude<WorkflowAccessResource, CustomAgent
|
|
|
20
20
|
export type WorkflowAccessResourceType = WorkflowAccessResource["resourceType"];
|
|
21
21
|
|
|
22
22
|
/** Levels accepted by pages, databases, and data sources. */
|
|
23
|
-
export type
|
|
23
|
+
export type ResourceAccessLevel = "view" | "comment" | "edit" | "fullAccess";
|
|
24
24
|
|
|
25
25
|
/** The only level accepted by a custom agent. */
|
|
26
26
|
export type WorkflowAccessAgentLevel = "call";
|
|
27
27
|
|
|
28
28
|
/** Every level accepted by at least one workflow access resource. */
|
|
29
|
-
export type WorkflowAccessLevel =
|
|
29
|
+
export type WorkflowAccessLevel = ResourceAccessLevel | WorkflowAccessAgentLevel;
|
|
30
30
|
|
|
31
31
|
/** The level set accepted by a particular resource kind. */
|
|
32
32
|
export type WorkflowAccessLevelFor<TType extends WorkflowAccessResourceType> =
|
|
33
|
-
TType extends "customAgent" ? WorkflowAccessAgentLevel :
|
|
33
|
+
TType extends "customAgent" ? WorkflowAccessAgentLevel : ResourceAccessLevel;
|
|
34
34
|
|
|
35
35
|
type AccessResourceFor<TType extends WorkflowAccessResourceType> = Extract<
|
|
36
36
|
WorkflowAccessResource,
|
|
@@ -159,57 +159,62 @@ export function normalizeWorkflowAccess(
|
|
|
159
159
|
if (!ACCESS_ALIAS_PATTERN.test(alias) || RESERVED_ACCESS_ALIASES.has(alias)) {
|
|
160
160
|
throw new Error(`Invalid workflow access alias: ${alias}`);
|
|
161
161
|
}
|
|
162
|
+
normalized[alias] = normalizeAccessDeclaration(source[alias], `Workflow access "${alias}"`);
|
|
163
|
+
}
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
return Object.freeze(normalized);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check one access entry and return its resource type, ID, and level.
|
|
170
|
+
* Workflows check the entry's alias separately; custom-agent grants have no alias.
|
|
171
|
+
*/
|
|
172
|
+
export function normalizeAccessDeclaration(
|
|
173
|
+
declaration: unknown,
|
|
174
|
+
label: string,
|
|
175
|
+
): WorkflowAccessRequirement {
|
|
176
|
+
if (
|
|
177
|
+
!isRecord(declaration) ||
|
|
178
|
+
!Object.hasOwn(declaration, "resource") ||
|
|
179
|
+
!Object.hasOwn(declaration, "level")
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(`${label} must declare a resource and level.`);
|
|
182
|
+
}
|
|
183
|
+
const resource: unknown = declaration.resource;
|
|
184
|
+
if (
|
|
185
|
+
!isRecord(resource) ||
|
|
186
|
+
!Object.hasOwn(resource, "resourceType") ||
|
|
187
|
+
!Object.hasOwn(resource, "resourceId") ||
|
|
188
|
+
typeof resource.resourceId !== "string" ||
|
|
189
|
+
resource.resourceId.length === 0 ||
|
|
190
|
+
!isWorkflowAccessResourceType(resource.resourceType)
|
|
191
|
+
) {
|
|
192
|
+
throw new Error(`${label} must reference a supported NaC resource handle.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let requirement: WorkflowAccessRequirement;
|
|
196
|
+
if (resource.resourceType === "customAgent") {
|
|
197
|
+
if (declaration.level !== "call") {
|
|
198
|
+
throw new Error(`${label} custom agents only support level "call".`);
|
|
170
199
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
!isWorkflowAccessResourceType(resource.resourceType)
|
|
179
|
-
) {
|
|
200
|
+
requirement = {
|
|
201
|
+
type: resource.resourceType,
|
|
202
|
+
resourceId: resource.resourceId,
|
|
203
|
+
level: declaration.level,
|
|
204
|
+
};
|
|
205
|
+
} else {
|
|
206
|
+
if (!isResourceAccessLevel(declaration.level)) {
|
|
180
207
|
throw new Error(
|
|
181
|
-
|
|
208
|
+
`${label} ${resource.resourceType} level must be view, comment, edit, or fullAccess.`,
|
|
182
209
|
);
|
|
183
210
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`Workflow access "${alias}" custom agents only support level "call".`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
requirement = {
|
|
193
|
-
type: resource.resourceType,
|
|
194
|
-
resourceId: resource.resourceId,
|
|
195
|
-
level: declaration.level,
|
|
196
|
-
};
|
|
197
|
-
} else {
|
|
198
|
-
if (!isWorkflowAccessResourceLevel(declaration.level)) {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Workflow access "${alias}" ${resource.resourceType} level must be view, comment, edit, or fullAccess.`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
requirement = {
|
|
204
|
-
type: resource.resourceType,
|
|
205
|
-
resourceId: resource.resourceId,
|
|
206
|
-
level: declaration.level,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
normalized[alias] = Object.freeze(requirement);
|
|
211
|
+
requirement = {
|
|
212
|
+
type: resource.resourceType,
|
|
213
|
+
resourceId: resource.resourceId,
|
|
214
|
+
level: declaration.level,
|
|
215
|
+
};
|
|
210
216
|
}
|
|
211
|
-
|
|
212
|
-
return Object.freeze(normalized);
|
|
217
|
+
return Object.freeze(requirement);
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
/**
|
|
@@ -294,14 +299,14 @@ function isWorkflowAccessResourceType(value: unknown): value is WorkflowAccessRe
|
|
|
294
299
|
return typeof value === "string" && Object.hasOwn(RESOURCE_TYPES, value);
|
|
295
300
|
}
|
|
296
301
|
|
|
297
|
-
function
|
|
302
|
+
function isResourceAccessLevel(value: unknown): value is ResourceAccessLevel {
|
|
298
303
|
return value === "view" || value === "comment" || value === "edit" || value === "fullAccess";
|
|
299
304
|
}
|
|
300
305
|
|
|
301
306
|
function isBinding(value: unknown): value is {
|
|
302
307
|
type: WorkflowAccessResourceType;
|
|
303
308
|
resourceId: string;
|
|
304
|
-
level:
|
|
309
|
+
level: ResourceAccessLevel | WorkflowAccessAgentLevel;
|
|
305
310
|
id: string;
|
|
306
311
|
} {
|
|
307
312
|
if (!isRecord(value) || Reflect.ownKeys(value).length !== 4) return false;
|
|
@@ -320,7 +325,7 @@ function isBinding(value: unknown): value is {
|
|
|
320
325
|
typeof value.resourceId === "string" &&
|
|
321
326
|
value.resourceId.length > 0 &&
|
|
322
327
|
typeof value.level === "string" &&
|
|
323
|
-
(
|
|
328
|
+
(isResourceAccessLevel(value.level) || value.level === "call") &&
|
|
324
329
|
typeof value.id === "string" &&
|
|
325
330
|
value.id.length > 0
|
|
326
331
|
);
|
package/src/workflow.ts
CHANGED
package/AGENTS.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# Notion Apps project guidance
|
|
2
|
-
|
|
3
|
-
This file is included in projects scaffolded from this template. Read it completely
|
|
4
|
-
before designing, implementing, or troubleshooting an App.
|
|
5
|
-
|
|
6
|
-
## Design the App before implementation
|
|
7
|
-
|
|
8
|
-
After the project has been scaffolded, establish what the complete App should
|
|
9
|
-
do. Clarify the desired outcome, source data, triggers, external services, and
|
|
10
|
-
every Notion resource the App needs. Recommend a workflow for most automations;
|
|
11
|
-
use a sync when the goal is to mirror an external collection into a Notion
|
|
12
|
-
database. An App may contain both.
|
|
13
|
-
|
|
14
|
-
Before proposing the design, read only enough to describe each open decision as a
|
|
15
|
-
concrete option: this file and the top-level description of each relevant capability
|
|
16
|
-
(workflow, sync, connections, and Notion as Code). Do not read generated type
|
|
17
|
-
declarations (`*.generated.d.ts`), full provider API surfaces, or other
|
|
18
|
-
implementation-level detail before the user agrees on a direction. Save that
|
|
19
|
-
verification for the selected option during implementation.
|
|
20
|
-
|
|
21
|
-
Present the proposed design concisely and get the user's agreement before
|
|
22
|
-
implementing. Include:
|
|
23
|
-
|
|
24
|
-
- Always include an App home page that explains what the App does and links to
|
|
25
|
-
all of its Notion resources.
|
|
26
|
-
- Every Notion resource the App will create, such as databases, pages, and custom
|
|
27
|
-
agents, with its purpose and the capabilities that depend on it.
|
|
28
|
-
- Every sync, including its third-party source, destination database, and
|
|
29
|
-
synchronization behavior.
|
|
30
|
-
- Every workflow, including its trigger, major actions, resources it reads or changes,
|
|
31
|
-
and external connections.
|
|
32
|
-
- Unresolved decisions and required access.
|
|
33
|
-
|
|
34
|
-
Adapt the format to the App rather than copying this example mechanically:
|
|
35
|
-
|
|
36
|
-
### Example App design
|
|
37
|
-
|
|
38
|
-
**Outcome:** Bring support tickets into Notion and escalate urgent tickets to the
|
|
39
|
-
support team.
|
|
40
|
-
|
|
41
|
-
**Notion resources**
|
|
42
|
-
|
|
43
|
-
| Kind | Name | Purpose | Used by |
|
|
44
|
-
| ------------ | ----------------- | --------------------------------------------------- | -------------------------------- |
|
|
45
|
-
| Database | Support tickets | Store synchronized tickets and triage status | Ticket sync, escalation workflow |
|
|
46
|
-
| Page | Support dashboard | Give the team an operational home and database view | Team members |
|
|
47
|
-
| Custom agent | Ticket triage | Classify urgency and summarize a ticket | Escalation workflow |
|
|
48
|
-
|
|
49
|
-
**Syncs and workflows**
|
|
50
|
-
|
|
51
|
-
| Kind | Name | Source or trigger | Behavior | Dependencies |
|
|
52
|
-
| -------- | ---------------------- | --------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- |
|
|
53
|
-
| Sync | Ticket sync | Support-system tickets | Upsert by stable external ID | Support-system connection, Support tickets database |
|
|
54
|
-
| Workflow | Escalate urgent ticket | Support tickets page created or updated | Run triage, update status, and notify support channel | Ticket triage agent, Support tickets database, messaging connection |
|
|
55
|
-
|
|
56
|
-
**Open questions:** Which support system and messaging channel should the App use?
|
|
57
|
-
|
|
58
|
-
Ask the user to confirm or revise the design. Do not begin implementation until they
|
|
59
|
-
agree. If implementation reveals a material resource or capability not covered by the
|
|
60
|
-
agreed design, update the proposal and confirm the change before adding it.
|
|
61
|
-
|
|
62
|
-
## Implement against the generated project
|
|
63
|
-
|
|
64
|
-
Before implementing the agreed design, compare it with everything included by the
|
|
65
|
-
scaffold. Remove template workflows, syncs, custom blocks, Notion resource declarations,
|
|
66
|
-
sample assets, and supporting code that the App does not need. Do not leave example or
|
|
67
|
-
placeholder capabilities in discovered capability directories: if they remain there,
|
|
68
|
-
the build can include and deploy them. Preserve shared configuration and infrastructure
|
|
69
|
-
that the selected capabilities still require.
|
|
70
|
-
|
|
71
|
-
Feature-specific skills are installed at
|
|
72
|
-
`./node_modules/@notionhq/apps/skills`. Inspect that directory and read every relevant
|
|
73
|
-
`SKILL.md` completely before implementing a feature. At minimum, use the `workflow`
|
|
74
|
-
skill for workflows or the `sync` skill for syncs, plus any additional skills they
|
|
75
|
-
route to, such as connections or Notion as Code. Do not rely on remembered SDK APIs
|
|
76
|
-
when the installed declarations or skills can answer the question.
|
|
77
|
-
|
|
78
|
-
Preserve the template's structure and examples unless the user's App requires a
|
|
79
|
-
change. Use the project's documented check and build commands after edits. Deploy
|
|
80
|
-
with `ntn apps deploy` only when deployment is part of the user's request, and report
|
|
81
|
-
local build success separately from deployment success.
|
|
82
|
-
|
|
83
|
-
## Deploy and hand off
|
|
84
|
-
|
|
85
|
-
When deploying, use `ntn apps deploy --json` (plus any other required arguments) so
|
|
86
|
-
the final deployment result includes `worker_url`, `setup_url`, and `is_update`. These
|
|
87
|
-
are JSON-only field names. Human output shows the worker page URL without a
|
|
88
|
-
`worker_url` label, and non-interactive plain output may show only the worker ID
|
|
89
|
-
followed by a `Finish setup:` line. That line is the setup URL, not the worker URL.
|
|
90
|
-
|
|
91
|
-
For a first deployment (`is_update` is false), show only the onboarding/setup URL from
|
|
92
|
-
`setup_url` and tell the user to open it to finish setting up the App.
|
|
93
|
-
|
|
94
|
-
For a redeployment (`is_update` is true), show both `worker_url` and `setup_url`,
|
|
95
|
-
clearly labeled and clickable. Explain that the worker URL opens the deployed App and
|
|
96
|
-
the setup URL lets them revisit onboarding. Use the URLs returned by the CLI rather
|
|
97
|
-
than constructing or guessing them.
|
|
98
|
-
|
|
99
|
-
## Capability-specific guidance
|
|
100
|
-
|
|
101
|
-
Before creating, modifying, or troubleshooting an App capability, read the matching
|
|
102
|
-
skill:
|
|
103
|
-
|
|
104
|
-
- [Workflows](./skills/workflow/SKILL.md)
|
|
105
|
-
- [Connections](./skills/connections/SKILL.md)
|
|
106
|
-
- [Database syncs](./skills/sync/SKILL.md)
|
|
107
|
-
- [Custom blocks](./skills/custom-blocks/SKILL.md)
|
|
108
|
-
- [Notion as Code](./skills/notion-as-code/SKILL.md)
|
|
109
|
-
|
|
110
|
-
Resolve these links relative to this package directory. Check the installed SDK
|
|
111
|
-
declarations for current API details.
|
|
112
|
-
|
|
113
|
-
## Pull requests for SDK changes
|
|
114
|
-
|
|
115
|
-
For a change to the public SDK, show the user-facing code change in the pull request
|
|
116
|
-
body. Put a short, complete example near the top:
|
|
117
|
-
|
|
118
|
-
````md
|
|
119
|
-
## User code
|
|
120
|
-
|
|
121
|
-
### Before
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
// Existing app code
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### After
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
// Updated app code
|
|
131
|
-
```
|
|
132
|
-
````
|
|
133
|
-
|
|
134
|
-
Use the same small example in both sections so reviewers can compare them
|
|
135
|
-
quickly. Include imports and the call that changed when they matter. Show normal
|
|
136
|
-
app code, not internal types, tests, generated files, or provisioning output. Explain
|
|
137
|
-
any required migration steps in plain language below the example.
|
package/docs/BUILD.md
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
# App build process
|
|
2
|
-
|
|
3
|
-
`notion-apps build` discovers capability modules and produces:
|
|
4
|
-
|
|
5
|
-
- `dist/worker.js`, an ESM bundle containing the app's workflows and syncs.
|
|
6
|
-
- `dist/manifest.json`, the static capability and resource manifest.
|
|
7
|
-
- `dist/provisioning.json`, only when the app declares Notion-as-Code resources.
|
|
8
|
-
|
|
9
|
-
## Project convention
|
|
10
|
-
|
|
11
|
-
Each top-level TypeScript file under a capability directory must default-export the
|
|
12
|
-
corresponding capability. The filename becomes its key:
|
|
13
|
-
|
|
14
|
-
| Directory | Default export |
|
|
15
|
-
| ------------------- | ------------------ |
|
|
16
|
-
| `src/workflows/` | `workflow(...)` |
|
|
17
|
-
| `src/syncs/` | `sync(...)` |
|
|
18
|
-
| `src/customBlocks/` | `customBlock(...)` |
|
|
19
|
-
|
|
20
|
-
```text
|
|
21
|
-
my-app/
|
|
22
|
-
├── src/
|
|
23
|
-
│ ├── notion.ts
|
|
24
|
-
│ ├── workflows/
|
|
25
|
-
│ │ └── onPageCreated.ts
|
|
26
|
-
│ └── lib/
|
|
27
|
-
│ └── processPage.ts
|
|
28
|
-
├── .notion/
|
|
29
|
-
└── dist/
|
|
30
|
-
├── worker.js
|
|
31
|
-
└── manifest.json
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Files elsewhere under `src/` are ordinary modules and enter the build only when imported
|
|
35
|
-
by a capability. Capability discovery does not recurse into subdirectories.
|
|
36
|
-
Notion-as-Code declarations may be inline or in an imported helper. Use a local
|
|
37
|
-
module such as `src/workflows/myWorkflow/lib/notion.ts` for resources owned by one
|
|
38
|
-
capability, and `src/notion.ts` for app-wide declarations or resources without a
|
|
39
|
-
natural capability owner.
|
|
40
|
-
|
|
41
|
-
## Pipeline
|
|
42
|
-
|
|
43
|
-
1. Discover top-level files in the capability directories.
|
|
44
|
-
2. Generate `.notion/entry.ts` with workflow and sync imports and a dispatcher.
|
|
45
|
-
3. Bundle the app's code into `dist/worker.js`, leaving npm packages external.
|
|
46
|
-
4. Evaluate custom-block declarations separately. Blocks do not enter `worker.js`.
|
|
47
|
-
5. Evaluate worker and custom-block capabilities in a separate build-only metadata bundle to record
|
|
48
|
-
Notion-as-Code declarations. Validate workflow access references against those
|
|
49
|
-
declarations before writing the manifest and optional provisioning artifact.
|
|
50
|
-
6. Emit `dist/manifest.json` and, when declarations exist, `dist/provisioning.json`.
|
|
51
|
-
|
|
52
|
-
Capability modules must therefore be importable without secrets or network access.
|
|
53
|
-
Read required environment variables and make requests inside handlers or workflow
|
|
54
|
-
steps, not at module scope.
|
|
55
|
-
|
|
56
|
-
## Declaring Notion-as-Code databases
|
|
57
|
-
|
|
58
|
-
Database declarations use explicit resource IDs and keyed schemas. For a single
|
|
59
|
-
data source, supply the database ID as the first argument and the distinct data
|
|
60
|
-
source ID in `dataSourceResourceId`:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
import { notion } from "@notionhq/apps/notion-as-code";
|
|
64
|
-
|
|
65
|
-
const tasks = notion.database("tasks-db", {
|
|
66
|
-
dataSourceResourceId: "tasks-source",
|
|
67
|
-
name: "Tasks",
|
|
68
|
-
schema: {
|
|
69
|
-
Name: { type: "title", resourceId: "tasks-name" },
|
|
70
|
-
Effort: { type: "text", resourceId: "tasks-effort" },
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Here `name` is the single-source shorthand: it supplies both the database and
|
|
76
|
-
data-source display names. The source handle is `tasks.dataSource`; pass it to
|
|
77
|
-
`sync({ dataSource, ... })` from `@notionhq/apps`.
|
|
78
|
-
|
|
79
|
-
For multiple sources, use `datasources` instead of the top-level `schema`:
|
|
80
|
-
|
|
81
|
-
````ts
|
|
82
|
-
const work = notion.database("work-db", {
|
|
83
|
-
name: "Work",
|
|
84
|
-
datasources: {
|
|
85
|
-
Tasks: {
|
|
86
|
-
resourceId: "work-tasks-source",
|
|
87
|
-
schema: {
|
|
88
|
-
Name: { type: "title", resourceId: "work-tasks-name" },
|
|
89
|
-
Effort: { type: "text", resourceId: "work-tasks-effort" },
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
Projects: {
|
|
93
|
-
resourceId: "work-projects-source",
|
|
94
|
-
schema: {
|
|
95
|
-
Name: { type: "title", resourceId: "work-projects-name" },
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
The keys in `datasources` are the source names, so the handles are indexed by
|
|
102
|
-
those names: `work.datasources.Tasks` and `work.datasources.Projects`. Nested
|
|
103
|
-
data-source configs do not accept `name`. The top-level database `name` remains
|
|
104
|
-
supported and names the database; in a single-source declaration it also
|
|
105
|
-
supplies the source display name. Property configs do not accept `name`; each
|
|
106
|
-
schema key is always the property's name. Typed property access uses
|
|
107
|
-
`work.datasources.Tasks.schema.Effort`; sync primary keys and row properties use
|
|
108
|
-
the string/object key `"Effort"`.
|
|
109
|
-
|
|
110
|
-
Every database, source, and property ID remains explicit and must be unique
|
|
111
|
-
across the provisioning declarations. No IDs are generated from keys or names.
|
|
112
|
-
Preserve explicit IDs when changing authoring keys or display names.
|
|
113
|
-
|
|
114
|
-
Page and teamspace handles accept the same forms through
|
|
115
|
-
`page.addDatabase("database-id", args)` and `teamspace.addDatabase("database-id", args)`.
|
|
116
|
-
Top-level declarations also accept
|
|
117
|
-
`parent: { type: "resourceId", resourceId: "parent-id" }`; omitting it retains the
|
|
118
|
-
private Apps workspace parent. A database must declare at least one data source
|
|
119
|
-
or at least one view. A linked-only declaration may omit `datasources`, or use
|
|
120
|
-
`datasources: {}`, only with a nonempty `views` list. The empty forms `{}`,
|
|
121
|
-
`{ datasources: {} }`, and `{ datasources: {}, views: [] }` are rejected; widened
|
|
122
|
-
or dynamic maps and arrays are checked at runtime. The single-source `schema` and
|
|
123
|
-
multi-source `datasources` forms cannot be combined.
|
|
124
|
-
|
|
125
|
-
Here is a linked-only database whose typed table view references an explicit
|
|
126
|
-
data source resource ID:
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
const linked = notion.database("work-linked-db", {
|
|
130
|
-
views: [
|
|
131
|
-
{
|
|
132
|
-
resourceId: "work-issues-table",
|
|
133
|
-
type: "table",
|
|
134
|
-
dataSourceResourceId: "issues-source",
|
|
135
|
-
properties: [{ property: "issue-title", visible: true }],
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
});
|
|
139
|
-
````
|
|
140
|
-
|
|
141
|
-
The build converts these declarations to the existing serialized database
|
|
142
|
-
intents: `dataSources` and `properties` remain arrays with exact resource IDs
|
|
143
|
-
and names from the authoring keys. Authoring-only `schema`, `datasources`, and
|
|
144
|
-
database-level `dataSourceResourceId` fields do not leak into the serialized
|
|
145
|
-
database intent. The provisioning JSON envelope and server API are unchanged.
|
|
146
|
-
|
|
147
|
-
## Provisioning artifact and deployment
|
|
148
|
-
|
|
149
|
-
The optional provisioning artifact uses `$schema: "notion:apps-provisioning:v1"`,
|
|
150
|
-
`version: 1`, and an `intents` array. These declarations are build metadata, not
|
|
151
|
-
provisioning operations executed by the deployed Worker.
|
|
152
|
-
|
|
153
|
-
After a successful build, the artifact reflects the current declarations. If no
|
|
154
|
-
declarations remain, the build removes any previous `dist/provisioning.json`
|
|
155
|
-
rather than uploading stale intents or emitting an empty artifact. This also
|
|
156
|
-
works when rebuilding into a reused `dist/` directory. Removing the artifact does
|
|
157
|
-
not delete previously provisioned Notion resources or reset their cloud state.
|
|
158
|
-
Do not deploy the output of a failed build.
|
|
159
|
-
|
|
160
|
-
Build and coordination modes use the existing CLI arguments:
|
|
161
|
-
|
|
162
|
-
| Command | Build location | Deployment coordination |
|
|
163
|
-
| ------------------------------------- | ------------------------------------------------ | ----------------------------- |
|
|
164
|
-
| `ntn apps deploy` | Cloud sandbox, using the project's installed SDK | Server (`workersBuildWorker`) |
|
|
165
|
-
| `ntn apps deploy --local-build --yes` | Local `notion-apps build` | CLI, as on `main` |
|
|
166
|
-
|
|
167
|
-
Cloud builds do not require the SDK or Node.js on the invoking machine. The
|
|
168
|
-
uploaded project must declare its SDK dependency and a `build` script that invokes
|
|
169
|
-
`notion-apps build`; the cloud runner uses the existing `npm run build` or
|
|
170
|
-
`pnpm run build` command, not a separate SDK command. The CLI creates an App-linked
|
|
171
|
-
Worker using `workersCreateWorker` with `createApp: true`, or updates the existing
|
|
172
|
-
Worker, then uploads source and calls `workersBuildWorker` with the Worker ID.
|
|
173
|
-
For App-linked Workers, the build endpoint coordinates capability registration,
|
|
174
|
-
Notion-as-Code provisioning, database attachment, workflow binding resolution,
|
|
175
|
-
and workflow permission reconciliation before reporting build success.
|
|
176
|
-
It returns the normal build result and run ID, not provisioning state. No separate
|
|
177
|
-
Apps deployment endpoint is required.
|
|
178
|
-
|
|
179
|
-
For App-linked Workers, the Notion-as-Code build phase prepares a separate
|
|
180
|
-
upload target, like the manifest target, for optional `dist/provisioning.json`.
|
|
181
|
-
The cloud runner uploads the file only if the build emits it; an upload failure
|
|
182
|
-
fails the build. The provisioning hooks read the JSON directly, not from the Worker
|
|
183
|
-
bundle. Provisioning JSON is limited to 5 MiB; a missing object means there are no
|
|
184
|
-
provisioning declarations, while other read errors or invalid JSON fail deployment.
|
|
185
|
-
|
|
186
|
-
This upload is a retained, deployment-scoped object in the existing Workers S3
|
|
187
|
-
bucket. The coordinator leaves it in storage after successful or failed deployment
|
|
188
|
-
attempts. It records build declarations, not the durable installation-owned
|
|
189
|
-
Notion-as-Code resource mappings and state.
|
|
190
|
-
Ordinary Worker deployment APIs, responses, and storage lifecycle are unchanged.
|
|
191
|
-
|
|
192
|
-
`--local-build` preserves the current CLI-coordinated setup. The CLI builds and
|
|
193
|
-
uploads the Worker bundle and manifest, uses the existing Notion-as-Code apply
|
|
194
|
-
flow and local state, and reconciles attachments. It reads the optional
|
|
195
|
-
`dist/provisioning.json` locally and does not upload it separately or invoke
|
|
196
|
-
the cloud build endpoint.
|
|
197
|
-
|
|
198
|
-
Workflows with nonempty `access` require cloud deployment. The local-build path
|
|
199
|
-
rejects them after the SDK build and before uploading or deploying the bundle.
|
|
200
|
-
|
|
201
|
-
Custom-block default bindings are stored in `manifest.json` as resource IDs and
|
|
202
|
-
resolved to live data source and property IDs during deployment. Live resource
|
|
203
|
-
bindings are not stored in SDK build output. Local deployments retain a
|
|
204
|
-
local state file and report `state_file`; cloud deployments keep installation-owned
|
|
205
|
-
resource mappings and state on the server. Cloud builds do not import local state
|
|
206
|
-
or return provisioning state to the CLI. Local and cloud state are independent:
|
|
207
|
-
switching between modes may recreate resources rather than reuse existing bindings.
|
|
208
|
-
|
|
209
|
-
Cloud failures never automatically retry locally. There is no rollback: code or
|
|
210
|
-
resources may already have changed when a deployment fails.
|
|
211
|
-
|
|
212
|
-
## Workflow access
|
|
213
|
-
|
|
214
|
-
`workflow({ access: { handbook: access.view(handbook) }, ... })`
|
|
215
|
-
declares access to a NaC resource and a named runtime binding. The manifest
|
|
216
|
-
contains only the alias and its symbolic `{ type, resourceId, level }`
|
|
217
|
-
requirement; live IDs are not build output.
|
|
218
|
-
Build validation rejects references missing from the current provisioning
|
|
219
|
-
declarations or having the wrong resource kind. `resourceId` is required because
|
|
220
|
-
it identifies the declaration used for this validation and binding.
|
|
221
|
-
|
|
222
|
-
After provisioning, cloud deployment resolves requirements against the successful
|
|
223
|
-
NaC apply result. It reconciles code-managed grants through the existing
|
|
224
|
-
Notion-module permissions and saves those permissions together with resolved
|
|
225
|
-
bindings on the workflow's worker module, keyed by capability and alias, in one
|
|
226
|
-
workflow transaction. Bindings are not authorization: the existing permissions
|
|
227
|
-
authorize `context.notion` calls. Redeployment updates or removes code-managed
|
|
228
|
-
grants as declarations change while preserving manually configured UI grants.
|
|
229
|
-
|
|
230
|
-
Execution reads the selected workflow configuration, not the latest NaC state,
|
|
231
|
-
validates the bindings against the capability requirements, and injects only that
|
|
232
|
-
capability's declared bindings. The SDK exposes typed, readonly `{ type, id }`
|
|
233
|
-
entries in `context.access`; UI-granted resources do not appear there. Use live
|
|
234
|
-
record IDs directly in `context.notion` calls for UI-granted resources; those
|
|
235
|
-
calls remain subject to the existing permissions. Missing or mismatched bindings
|
|
236
|
-
fail before the handler executes.
|
|
237
|
-
|
|
238
|
-
See the [workflow skill](../skills/workflow/SKILL.md#resources-created-with-the-app)
|
|
239
|
-
for supported resource types, access levels, and an authoring example.
|
|
240
|
-
|
|
241
|
-
## Manifest
|
|
242
|
-
|
|
243
|
-
The workflow-only manifest retains the platform's existing resource fields as empty arrays:
|
|
244
|
-
|
|
245
|
-
```json
|
|
246
|
-
{
|
|
247
|
-
"$schema": "notion:apps-manifest:v1",
|
|
248
|
-
"sdkVersion": "0.0.1",
|
|
249
|
-
"databases": [],
|
|
250
|
-
"xldbs": [],
|
|
251
|
-
"pacers": [],
|
|
252
|
-
"capabilities": [
|
|
253
|
-
{
|
|
254
|
-
"type": "workflow",
|
|
255
|
-
"key": "onPageCreated",
|
|
256
|
-
"config": {
|
|
257
|
-
"name": "Log New Pages",
|
|
258
|
-
"description": "Logs every page created in the workspace",
|
|
259
|
-
"triggers": [{ "type": "notion.page.created" }]
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
]
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
The platform invokes a workflow through the generated bundle's `run("workflow", key, event)`
|
|
267
|
-
dispatcher. Workflow results continue using the existing Notion output envelope, and runtime
|
|
268
|
-
metadata continues using the existing `NOTION_*` environment variables and `workerId` field.
|