@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.
@@ -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
- expectTypeOf<
20
- "full_access" | "can_edit_content" | "can_comment" | "can_view"
21
- >().toEqualTypeOf<CustomAgentAccessRole>();
22
- expectTypeOf<{
23
- resourceId: "tasks";
24
- role: "can_view";
25
- }>().toExtend<CustomAgentAccess>();
26
- expectTypeOf<{
27
- resourceId: "tasks";
28
- role: "read_and_write";
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<
@@ -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 WorkflowAccessResourceLevel = "view" | "comment" | "edit" | "fullAccess";
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 = WorkflowAccessResourceLevel | WorkflowAccessAgentLevel;
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 : WorkflowAccessResourceLevel;
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
- const declaration: unknown = source[alias];
164
- if (
165
- !isRecord(declaration) ||
166
- !Object.hasOwn(declaration, "resource") ||
167
- !Object.hasOwn(declaration, "level")
168
- ) {
169
- throw new Error(`Workflow access "${alias}" must declare a resource and level.`);
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
- const resource: unknown = declaration.resource;
172
- if (
173
- !isRecord(resource) ||
174
- !Object.hasOwn(resource, "resourceType") ||
175
- !Object.hasOwn(resource, "resourceId") ||
176
- typeof resource.resourceId !== "string" ||
177
- resource.resourceId.length === 0 ||
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
- `Workflow access "${alias}" must reference a supported NaC resource handle.`,
208
+ `${label} ${resource.resourceType} level must be view, comment, edit, or fullAccess.`,
182
209
  );
183
210
  }
184
-
185
- let requirement: WorkflowAccessRequirement;
186
- if (resource.resourceType === "customAgent") {
187
- if (declaration.level !== "call") {
188
- throw new Error(
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 isWorkflowAccessResourceLevel(value: unknown): value is WorkflowAccessResourceLevel {
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: WorkflowAccessResourceLevel | WorkflowAccessAgentLevel;
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
- (isWorkflowAccessResourceLevel(value.level) || value.level === "call") &&
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
@@ -191,7 +191,7 @@ export type {
191
191
  WorkflowAccessLevel,
192
192
  WorkflowAccessLevelFor,
193
193
  WorkflowAccessResource,
194
- WorkflowAccessResourceLevel,
194
+ ResourceAccessLevel,
195
195
  WorkflowAccessResourceType,
196
196
  WorkflowAccessRequirement,
197
197
  WorkflowAccessRequirements,
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.