@notionhq/apps 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/build.d.ts.map +1 -1
  3. package/dist/cli/build.js +12 -7
  4. package/dist/cli/emit-manifest.d.ts +1 -1
  5. package/dist/cli/emit-manifest.d.ts.map +1 -1
  6. package/dist/cli/emit-manifest.js +66 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/notion-as-code/custom-agent.d.ts +1 -0
  11. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  12. package/dist/notion-as-code/custom-agent.js +1 -1
  13. package/dist/notion-as-code/database.d.ts +2 -0
  14. package/dist/notion-as-code/database.d.ts.map +1 -1
  15. package/dist/notion-as-code/database.js +3 -0
  16. package/dist/notion-as-code/date.d.ts +3 -2
  17. package/dist/notion-as-code/date.d.ts.map +1 -1
  18. package/dist/notion-as-code/handles.d.ts +8 -11
  19. package/dist/notion-as-code/handles.d.ts.map +1 -1
  20. package/dist/notion-as-code/handles.js +8 -7
  21. package/dist/notion-as-code/index.d.ts +8 -11
  22. package/dist/notion-as-code/index.d.ts.map +1 -1
  23. package/dist/notion-as-code/page.d.ts +50 -1
  24. package/dist/notion-as-code/page.d.ts.map +1 -1
  25. package/dist/notion-as-code/page.js +1 -0
  26. package/dist/notion-as-code/page.test.d.ts +2 -0
  27. package/dist/notion-as-code/page.test.d.ts.map +1 -0
  28. package/dist/workflow-access-types.test.d.ts +2 -0
  29. package/dist/workflow-access-types.test.d.ts.map +1 -0
  30. package/dist/workflow-access.d.ts +68 -0
  31. package/dist/workflow-access.d.ts.map +1 -0
  32. package/dist/workflow-access.js +135 -0
  33. package/dist/workflow.d.ts +17 -6
  34. package/dist/workflow.d.ts.map +1 -1
  35. package/dist/workflow.js +8 -1
  36. package/docs/BUILD.md +38 -4
  37. package/package.json +1 -1
  38. package/skills/notion-as-code/SKILL.md +16 -3
  39. package/skills/workflow/SKILL.md +53 -16
  40. package/src/cli/build.test.ts +24 -0
  41. package/src/cli/build.ts +13 -8
  42. package/src/cli/emit-manifest.ts +107 -0
  43. package/src/index.ts +20 -1
  44. package/src/notion-as-code/custom-agent.ts +2 -1
  45. package/src/notion-as-code/database.ts +5 -0
  46. package/src/notion-as-code/date.ts +4 -2
  47. package/src/notion-as-code/handles.ts +16 -12
  48. package/src/notion-as-code/index.ts +10 -1
  49. package/src/notion-as-code/page.test.ts +35 -0
  50. package/src/notion-as-code/page.ts +62 -2
  51. package/src/workflow-access-types.test.ts +69 -0
  52. package/src/workflow-access.ts +299 -0
  53. package/src/workflow.test.ts +112 -0
  54. package/src/workflow.ts +50 -7
@@ -0,0 +1,135 @@
1
+ const ACCESS_ALIAS_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;
2
+ const RESERVED_ACCESS_ALIASES = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3
+ const RESOURCE_TYPES = {
4
+ page: true,
5
+ database: true,
6
+ dataSource: true,
7
+ customAgent: true
8
+ };
9
+ function normalizeWorkflowAccess(declarations) {
10
+ const source = declarations ?? {};
11
+ assertPlainObject(source, "Workflow access must be an object keyed by alias.");
12
+ const keys = Object.keys(source);
13
+ if (Reflect.ownKeys(source).length !== keys.length) {
14
+ throw new Error("Workflow access aliases must be enumerable strings.");
15
+ }
16
+ if (keys.length > 100) {
17
+ throw new Error("Workflows support at most 100 access requirements.");
18
+ }
19
+ const normalized = /* @__PURE__ */ Object.create(null);
20
+ for (const alias of keys) {
21
+ if (!ACCESS_ALIAS_PATTERN.test(alias) || RESERVED_ACCESS_ALIASES.has(alias)) {
22
+ throw new Error(`Invalid workflow access alias: ${alias}`);
23
+ }
24
+ const declaration = source[alias];
25
+ if (!isRecord(declaration) || !Object.hasOwn(declaration, "resource") || !Object.hasOwn(declaration, "level")) {
26
+ throw new Error(`Workflow access "${alias}" must declare a resource and level.`);
27
+ }
28
+ const resource = declaration.resource;
29
+ if (!isRecord(resource) || !Object.hasOwn(resource, "resourceType") || !Object.hasOwn(resource, "resourceId") || typeof resource.resourceId !== "string" || resource.resourceId.length === 0 || !isWorkflowAccessResourceType(resource.resourceType)) {
30
+ throw new Error(
31
+ `Workflow access "${alias}" must reference a supported NaC resource handle.`
32
+ );
33
+ }
34
+ let requirement;
35
+ if (resource.resourceType === "customAgent") {
36
+ if (declaration.level !== "call") {
37
+ throw new Error(
38
+ `Workflow access "${alias}" custom agents only support level "call".`
39
+ );
40
+ }
41
+ requirement = {
42
+ type: resource.resourceType,
43
+ resourceId: resource.resourceId,
44
+ level: declaration.level
45
+ };
46
+ } else {
47
+ if (!isWorkflowAccessResourceLevel(declaration.level)) {
48
+ throw new Error(
49
+ `Workflow access "${alias}" ${resource.resourceType} level must be view, comment, edit, or fullAccess.`
50
+ );
51
+ }
52
+ requirement = {
53
+ type: resource.resourceType,
54
+ resourceId: resource.resourceId,
55
+ level: declaration.level
56
+ };
57
+ }
58
+ normalized[alias] = Object.freeze(requirement);
59
+ }
60
+ return Object.freeze(normalized);
61
+ }
62
+ function hydrateWorkflowAccess(requirements) {
63
+ const expected = requirements ?? {};
64
+ const aliases = Object.keys(expected);
65
+ const empty = /* @__PURE__ */ Object.create(null);
66
+ if (aliases.length === 0) {
67
+ return Object.freeze(empty);
68
+ }
69
+ const rawBindings = process.env.NOTION_ACCESS_BINDINGS;
70
+ if (rawBindings === void 0) {
71
+ throw new Error(
72
+ "Workflow access requirements are not bound. Deploy and configure this requirement before running it."
73
+ );
74
+ }
75
+ if (rawBindings.length === 0) {
76
+ throw new Error("Invalid NOTION_ACCESS_BINDINGS metadata.");
77
+ }
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(rawBindings);
81
+ } catch (error) {
82
+ throw new Error("Invalid NOTION_ACCESS_BINDINGS metadata.", { cause: error });
83
+ }
84
+ assertPlainObject(parsed, "Invalid NOTION_ACCESS_BINDINGS metadata.");
85
+ const bindingKeys = Object.keys(parsed);
86
+ if (Reflect.ownKeys(parsed).length !== bindingKeys.length || bindingKeys.length !== aliases.length || bindingKeys.some((alias) => !Object.hasOwn(expected, alias))) {
87
+ throw new Error(
88
+ "Invalid NOTION_ACCESS_BINDINGS metadata: aliases do not match workflow access."
89
+ );
90
+ }
91
+ const resolved = /* @__PURE__ */ Object.create(null);
92
+ for (const alias of aliases) {
93
+ const requirement = expected[alias];
94
+ if (!requirement) {
95
+ throw new Error(`Workflow access "${alias}" is not declared.`);
96
+ }
97
+ const binding = parsed[alias];
98
+ if (!isBinding(binding)) {
99
+ throw new Error(`Invalid NOTION_ACCESS_BINDINGS metadata for access "${alias}".`);
100
+ }
101
+ if (binding.type !== requirement.type || binding.resourceId !== requirement.resourceId || binding.level !== requirement.level) {
102
+ throw new Error(
103
+ `Workflow access "${alias}" binding does not match its declared type, resourceId, or level.`
104
+ );
105
+ }
106
+ resolved[alias] = Object.freeze({ type: binding.type, id: binding.id });
107
+ }
108
+ return Object.freeze(resolved);
109
+ }
110
+ function isWorkflowAccessResourceType(value) {
111
+ return typeof value === "string" && Object.hasOwn(RESOURCE_TYPES, value);
112
+ }
113
+ function isWorkflowAccessResourceLevel(value) {
114
+ return value === "view" || value === "comment" || value === "edit" || value === "fullAccess";
115
+ }
116
+ function isBinding(value) {
117
+ if (!isRecord(value) || Reflect.ownKeys(value).length !== 4) return false;
118
+ const keys = Object.keys(value);
119
+ if (keys.length !== 4 || !keys.includes("type") || !keys.includes("resourceId") || !keys.includes("level") || !keys.includes("id")) {
120
+ return false;
121
+ }
122
+ return isWorkflowAccessResourceType(value.type) && typeof value.resourceId === "string" && value.resourceId.length > 0 && typeof value.level === "string" && (isWorkflowAccessResourceLevel(value.level) || value.level === "call") && typeof value.id === "string" && value.id.length > 0;
123
+ }
124
+ function assertPlainObject(value, message) {
125
+ if (!isRecord(value)) throw new Error(message);
126
+ const prototype = Object.getPrototypeOf(value);
127
+ if (prototype !== Object.prototype && prototype !== null) throw new Error(message);
128
+ }
129
+ function isRecord(value) {
130
+ return typeof value === "object" && value !== null && !Array.isArray(value);
131
+ }
132
+ export {
133
+ hydrateWorkflowAccess,
134
+ normalizeWorkflowAccess
135
+ };
@@ -1,4 +1,5 @@
1
1
  import { type WorkflowConnectionDeclarations, type WorkflowConnectionRequirement, type WorkflowConnections } from "./connections.js";
2
+ import { type WorkflowAccess, type WorkflowAccessDeclarations, type WorkflowAccessRequirements } from "./workflow-access.js";
2
3
  import type { CapabilityContext } from "./context.js";
3
4
  import { type RunMetadata } from "./runtime-metadata.js";
4
5
  import { type WorkflowState } from "./workflow-state.js";
@@ -37,6 +38,7 @@ export type WorkflowWaitUntilResult = {
37
38
  type WorkflowHandlerResult = {
38
39
  status: "success";
39
40
  } | WorkflowWaitUntilResult;
41
+ export type { WorkflowAccess, WorkflowAccessAgentLevel, WorkflowAccessBindings, WorkflowAccessDeclaration, WorkflowAccessDeclarations, WorkflowAccessLevel, WorkflowAccessLevelFor, WorkflowAccessResource, WorkflowAccessResourceLevel, WorkflowAccessResourceType, WorkflowAccessRequirement, WorkflowAccessRequirements, WorkflowAccessValue, } from "./workflow-access.js";
40
42
  export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
41
43
  export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
42
44
  export type WorkflowEventForTriggers<T extends readonly WorkflowTrigger[]> = WorkflowEventForTrigger<T[number]>;
@@ -44,7 +46,7 @@ export { connections, type WorkflowConnection, type WorkflowConnectionDeclaratio
44
46
  /**
45
47
  * Configuration passed to {@link workflow}.
46
48
  */
47
- export type WorkflowConfiguration<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]], TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations> = {
49
+ export type WorkflowConfiguration<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]], TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations, TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations> = {
48
50
  /**
49
51
  * A human-readable name for the workflow, shown in the UI when viewing workflows
50
52
  */
@@ -59,14 +61,20 @@ export type WorkflowConfiguration<TTriggers extends readonly [WorkflowTrigger, .
59
61
  * Each trigger defines a specific event or condition that causes the workflow to run.
60
62
  */
61
63
  triggers: TTriggers;
64
+ /**
65
+ * Notion-as-Code resources whose access is configured during cloud deployment.
66
+ * Deployment reconciles grants through the existing Notion-module permissions
67
+ * and supplies named runtime bindings on `context.access`.
68
+ */
69
+ access?: TAccess;
62
70
  /**
63
71
  * Connections that must be set up before this workflow can run.
64
72
  */
65
73
  connections?: TConnections;
66
- handler: (event: WorkflowEventForTriggers<TTriggers>, context: WorkflowContext<TConnections>) => Promise<void> | void;
74
+ handler: (event: WorkflowEventForTriggers<TTriggers>, context: WorkflowContext<TConnections, TAccess>) => Promise<void> | void;
67
75
  };
68
76
  /** Configuration that checks trigger keys against declared connections. */
69
- export type WorkflowTriggerConfiguration<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]], TConnections extends WorkflowConnectionDeclarations> = Omit<WorkflowConfiguration<TTriggers, TConnections>, "triggers"> & {
77
+ export type WorkflowTriggerConfiguration<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]], TConnections extends WorkflowConnectionDeclarations, TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations> = Omit<WorkflowConfiguration<TTriggers, TConnections, TAccess>, "triggers"> & {
70
78
  triggers: (context: {
71
79
  triggers: WorkflowTriggerCreators<NoInfer<TConnections>>;
72
80
  }) => TTriggers;
@@ -87,6 +95,7 @@ export type Workflow<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTri
87
95
  description: string;
88
96
  triggers: TTriggers;
89
97
  connections?: readonly WorkflowConnectionRequirement[];
98
+ access?: WorkflowAccessRequirements;
90
99
  };
91
100
  handler: (event: WorkflowEventForTriggers<TTriggers>, options?: HandlerOptions) => Promise<WorkflowHandlerResult | undefined>;
92
101
  };
@@ -119,8 +128,8 @@ export type Workflow<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTri
119
128
  export declare function workflow<const TTriggers extends readonly [
120
129
  WorkflowTriggerForConnections<NoInfer<TConnections>>,
121
130
  ...WorkflowTriggerForConnections<NoInfer<TConnections>>[]
122
- ], const TConnections extends WorkflowConnectionDeclarations = Record<never, never>>(configuration: WorkflowTriggerConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
123
- export declare function workflow<const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]], const TConnections extends WorkflowConnectionDeclarations = Record<never, never>>(configuration: WorkflowConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
131
+ ], const TConnections extends WorkflowConnectionDeclarations = Record<never, never>, const TAccess extends WorkflowAccessDeclarations = Record<never, never>>(configuration: WorkflowTriggerConfiguration<TTriggers, TConnections, TAccess>): Workflow<TTriggers>;
132
+ export declare function workflow<const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]], const TConnections extends WorkflowConnectionDeclarations = Record<never, never>, const TAccess extends WorkflowAccessDeclarations = Record<never, never>>(configuration: WorkflowConfiguration<TTriggers, TConnections, TAccess>): Workflow<TTriggers>;
124
133
  /** Context passed to a workflow step. */
125
134
  export type StepContext = {
126
135
  /**
@@ -158,7 +167,9 @@ type WorkflowWait = {
158
167
  until(name: string, options: WorkflowWaitUntilOptions): Promise<void>;
159
168
  };
160
169
  /** Context passed to a workflow handler. */
161
- export type WorkflowContext<TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations> = CapabilityContext & {
170
+ export type WorkflowContext<TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations, TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations> = CapabilityContext & {
171
+ /** Resolved NaC bindings for this invocation; UI-granted resources are not added here. */
172
+ readonly access: WorkflowAccess<TAccess>;
162
173
  connections: WorkflowConnections<TConnections>;
163
174
  } & RunMetadata & {
164
175
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
package/dist/workflow.js CHANGED
@@ -3,6 +3,10 @@ import {
3
3
  normalizeConnectionRequirements,
4
4
  validateTriggerConnections
5
5
  } from "./connections.js";
6
+ import {
7
+ hydrateWorkflowAccess,
8
+ normalizeWorkflowAccess
9
+ } from "./workflow-access.js";
6
10
  import { createHash } from "node:crypto";
7
11
  import { readFile } from "node:fs/promises";
8
12
  import { join } from "node:path";
@@ -19,6 +23,7 @@ import {
19
23
  } from "./connections.js";
20
24
  function workflow(configuration) {
21
25
  const requirements = normalizeConnectionRequirements(configuration.connections ?? {});
26
+ const accessRequirements = normalizeWorkflowAccess(configuration.access);
22
27
  const connectionDeclarations = configuration.connections === void 0 ? void 0 : structuredClone(configuration.connections);
23
28
  const triggers = typeof configuration.triggers === "function" ? configuration.triggers({ triggers: createWorkflowTriggers() }) : configuration.triggers;
24
29
  validateTriggerConnections(triggers, requirements);
@@ -28,7 +33,8 @@ function workflow(configuration) {
28
33
  name: configuration.name,
29
34
  description: configuration.description,
30
35
  triggers,
31
- ...configuration.connections === void 0 ? {} : { connections: requirements }
36
+ ...configuration.connections === void 0 ? {} : { connections: requirements },
37
+ ...configuration.access === void 0 ? {} : { access: accessRequirements }
32
38
  },
33
39
  async handler(event, options) {
34
40
  try {
@@ -38,6 +44,7 @@ function workflow(configuration) {
38
44
  const step = createStep(runMetadata);
39
45
  const capabilityContext = {
40
46
  ...baseContext,
47
+ access: hydrateWorkflowAccess(accessRequirements),
41
48
  connections: createWorkflowConnections(
42
49
  baseContext.notion,
43
50
  connectionDeclarations
package/docs/BUILD.md CHANGED
@@ -43,10 +43,11 @@ natural capability owner.
43
43
  1. Discover top-level files in the capability directories.
44
44
  2. Generate `.notion/entry.ts` with workflow and sync imports and a dispatcher.
45
45
  3. Bundle the app's code into `dist/worker.js`, leaving npm packages external.
46
- 4. Evaluate custom-block declarations separately, validate capability exports and
47
- configuration, and write `dist/manifest.json`. Blocks do not enter `worker.js`.
46
+ 4. Evaluate custom-block declarations separately. Blocks do not enter `worker.js`.
48
47
  5. Evaluate worker capabilities in a separate build-only metadata bundle to record
49
- Notion-as-Code declarations, then emit `dist/provisioning.json` if there are any.
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`.
50
51
 
51
52
  Capability modules must therefore be importable without secrets or network access.
52
53
  Read required environment variables and make requests inside handlers or workflow
@@ -170,7 +171,8 @@ uploaded project must declare its SDK dependency and a `build` script that invok
170
171
  Worker using `workersCreateWorker` with `createApp: true`, or updates the existing
171
172
  Worker, then uploads source and calls `workersBuildWorker` with the Worker ID.
172
173
  For App-linked Workers, the build endpoint coordinates capability registration,
173
- Notion-as-Code provisioning, and database attachment before reporting build success.
174
+ Notion-as-Code provisioning, database attachment, workflow binding resolution,
175
+ and workflow permission reconciliation before reporting build success.
174
176
  It returns the normal build result and run ID, not provisioning state. No separate
175
177
  Apps deployment endpoint is required.
176
178
 
@@ -193,6 +195,9 @@ flow and local state, and reconciles attachments. It reads the optional
193
195
  `dist/provisioning.json` locally and does not upload it separately or invoke
194
196
  the cloud build endpoint.
195
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
+
196
201
  Resource bindings are not stored in SDK build output. Local deployments retain a
197
202
  local state file and report `state_file`; cloud deployments keep installation-owned
198
203
  resource mappings and state on the server. Cloud builds do not import local state
@@ -202,6 +207,35 @@ switching between modes may recreate resources rather than reuse existing bindin
202
207
  Cloud failures never automatically retry locally. There is no rollback: code or
203
208
  resources may already have changed when a deployment fails.
204
209
 
210
+ ## Workflow access
211
+
212
+ `workflow({ access: { handbook: { resource: handbook, level: "view" } }, ... })`
213
+ declares access to a NaC resource and a named runtime binding. The manifest
214
+ contains only the alias and its symbolic `{ type, resourceId, level }`
215
+ requirement; live IDs are not build output.
216
+ Build validation rejects references missing from the current provisioning
217
+ declarations or having the wrong resource kind. `resourceId` is required because
218
+ it identifies the declaration used for this validation and binding.
219
+
220
+ After provisioning, cloud deployment resolves requirements against the successful
221
+ NaC apply result. It reconciles code-managed grants through the existing
222
+ Notion-module permissions and saves those permissions together with resolved
223
+ bindings on the workflow's worker module, keyed by capability and alias, in one
224
+ workflow transaction. Bindings are not authorization: the existing permissions
225
+ authorize `context.notion` calls. Redeployment updates or removes code-managed
226
+ grants as declarations change while preserving manually configured UI grants.
227
+
228
+ Execution reads the selected workflow configuration, not the latest NaC state,
229
+ validates the bindings against the capability requirements, and injects only that
230
+ capability's declared bindings. The SDK exposes typed, readonly `{ type, id }`
231
+ entries in `context.access`; UI-granted resources do not appear there. Use live
232
+ record IDs directly in `context.notion` calls for UI-granted resources; those
233
+ calls remain subject to the existing permissions. Missing or mismatched bindings
234
+ fail before the handler executes.
235
+
236
+ See the [workflow skill](../skills/workflow/SKILL.md#resources-created-with-the-app)
237
+ for supported resource types, access levels, and an authoring example.
238
+
205
239
  ## Manifest
206
240
 
207
241
  The workflow-only manifest retains the platform's existing resource fields as empty arrays:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/apps",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "An SDK for building workflow apps for Notion",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -24,6 +24,14 @@ owned by one capability, use a local module such as
24
24
  are shared across capabilities or do not have a natural capability owner. Module
25
25
  evaluation must work without credentials or network access.
26
26
 
27
+ To configure a workflow's access to a declared page, database, data source, or
28
+ custom agent, pass its handle in `access: { alias: { resource: handle, level } }`.
29
+ The handler receives the live identity as `context.access.alias.id`; importing
30
+ a declaration alone does not request access. See the
31
+ [workflow skill](../workflow/SKILL.md#resources-created-with-the-app) for levels
32
+ and examples. Nonempty workflow `access` requires cloud deployment; `--local-build`
33
+ rejects it before upload.
34
+
27
35
  ## Where declarations are picked up
28
36
 
29
37
  The Apps build discovers direct `.ts` children of `src/workflows/` and
@@ -67,7 +75,8 @@ root exports support:
67
75
 
68
76
  - `teamspace({ resourceId, name, accessLevel })`, with `addPage` and
69
77
  `teamspace.addDatabase(id, args)` on the returned handle.
70
- - `page({ resourceId, parent?, properties?, content? })`, with
78
+ - `page({ resourceId, parent?, properties?, content? })`; regular page titles use
79
+ `properties: { title: notion.text("My Page Title") }`. It also has
71
80
  `addPage` and `page.addDatabase(id, args)` for children.
72
81
  - `database(resourceId, { dataSourceResourceId, name, schema, parent?, views? })`,
73
82
  returning a single source as `handle.dataSource`, or
@@ -82,6 +91,11 @@ root exports support:
82
91
  Use property resource IDs in view filters, sorts, and layout options; calendar
83
92
  views require `calendarBy`, and timeline views require `timelineBy`.
84
93
 
94
+ For a regular page, set the title with
95
+ `properties: { title: notion.text("My Page Title") }`. A Markdown heading in
96
+ `content` creates a child heading block; it does not set the page title. For a
97
+ database page, use the title property's schema name instead of `title`.
98
+
85
99
  A database declaration must include at least one data source or at least one
86
100
  view. A linked-only database may omit `datasources`, or use `datasources: {}`,
87
101
  only when `views` is nonempty. `{}`, `{ datasources: {} }`, and
@@ -123,8 +137,7 @@ The Apps SDK exposes a subset of Notion as Code. Data sources are nested inside
123
137
  Apps deployment rejects workspace creation or changes and supplies the App's
124
138
  workspace binding itself. Value helpers and types stay on their existing
125
139
  subpaths. For example, import `{ notion }` from `@notionhq/apps/notion-as-code`
126
- for `notion.text(...)` or `notion.file(resourceId)`. The latter creates a file
127
- reference, not an upload or file declaration. These helpers are not root exports.
140
+ for `notion.text(...)`. These helpers are not root exports.
128
141
  CLI acceptance of a serialized intent envelope alone does not establish server
129
142
  support for its contents.
130
143
 
@@ -110,8 +110,8 @@ configuration, and do not log secrets or private payloads.
110
110
 
111
111
  ## Resources created with the App
112
112
 
113
- Use [Notion as Code](../notion-as-code/SKILL.md) for pages and databases that
114
- should be created during deployment. Prefer it over equivalent manual setup
113
+ Use [Notion as Code](../notion-as-code/SKILL.md) for pages, databases, and custom agents
114
+ that should be created during deployment. Prefer it over equivalent manual setup
115
115
  or API calls that create the App's resources. Use runtime API calls for dynamic
116
116
  data changes, not as a substitute for supported Notion as Code setup.
117
117
  Keep declarations at module scope. A small workflow-specific resource can be
@@ -120,39 +120,76 @@ guide page in `src/workflows/sayHello/lib/notion.ts`:
120
120
 
121
121
  ```ts
122
122
  import { page } from "@notionhq/apps";
123
+ import { notion } from "@notionhq/apps/notion-as-code";
123
124
 
124
125
  export const guide = page({
125
126
  resourceId: "workflow-guide",
126
- content: "# Workflow guide\nThis App runs a scheduled workflow.",
127
+ properties: { title: notion.text("Workflow guide") },
128
+ content: "This App runs a scheduled workflow.",
127
129
  });
128
130
  ```
129
131
 
130
- Import the module from `src/workflows/sayHello.ts` so the build records it:
132
+ Import the handle into the workflow and declare the named binding it needs:
131
133
 
132
134
  ```ts
133
135
  import { workflow } from "@notionhq/apps";
134
136
  import { triggers } from "@notionhq/apps/triggers";
135
137
 
136
- import "./sayHello/lib/notion.js";
138
+ import { guide } from "./sayHello/lib/notion.js";
137
139
 
138
140
  export default workflow({
139
- name: "Say Hello",
140
- description: "Says hello on a recurring schedule.",
141
+ name: "Read guide",
142
+ description: "Reads the App's guide on a recurring schedule.",
141
143
  triggers: [triggers.scheduled()],
144
+ access: { guide: { resource: guide, level: "view" } },
142
145
  handler: async (_event, context) => {
143
- await context.step("Say hello", () => {
144
- console.log("Hello from your workflow!");
145
- });
146
+ await context.step("Read guide", () =>
147
+ context.notion.pages.retrieve({ page_id: context.access.guide.id }),
148
+ );
146
149
  },
147
150
  });
148
151
  ```
149
152
 
150
- The page is provisioned during deployment, not on every workflow run.
151
- Database declarations follow the same import pattern; see the
152
- [sync example](../sync/SKILL.md#choose-the-database-source) for using a Notion as Code
153
- data source in a sync. Notion as Code resource IDs are declaration identities, not live
154
- Notion UUIDs to pass to `context.notion`. Runtime API calls still belong in
155
- durable steps and need actual resolved Notion IDs.
153
+ The page is provisioned during deployment, not on every workflow run. Inline
154
+ declarations in `access` work too. Importing a declaration alone provisions the
155
+ resource but does not create a `context.access` alias. An `access` entry declares
156
+ the needed permission level and a named binding for that NaC resource.
157
+
158
+ `access` accepts page, database, data-source, and custom-agent handles. Pages,
159
+ databases, and data sources support `view`, `comment`, `edit`, and `fullAccess`;
160
+ custom agents support only `call`. Keys are workflow-local aliases. The handler's
161
+ typed `context.access` exposes each declared alias as a readonly `{ type, id }`,
162
+ not the declaration handle or a permission-management API. UI-granted resources
163
+ are permissions only: they do not create `context.access` aliases.
164
+
165
+ For a resource already granted to the workflow in Notion's UI, use its live
166
+ record ID directly in a `context.notion` call:
167
+
168
+ ```ts
169
+ await context.step("Read granted page", () =>
170
+ context.notion.pages.retrieve({ page_id: "page-id" }),
171
+ );
172
+ ```
173
+
174
+ Such a UI grant needs no `access` entry, NaC declaration, or NaC `resourceId`.
175
+ The call remains subject to the workflow's existing Notion-module permissions.
176
+
177
+ Use a data-source handle when calling `context.notion.dataSources`: its resolved
178
+ ID is a collection ID. A database handle resolves to the database block ID;
179
+ these identities are not interchangeable. Notion as Code `resourceId` values
180
+ remain declaration identities, not live Notion UUIDs.
181
+
182
+ Cloud deployment resolves declared handles to live IDs and reconciles their
183
+ grants through the existing Notion-module permissions. It saves permissions and
184
+ bindings together; bindings themselves do not authorize calls. Changing `access`
185
+ updates code-managed grants and bindings while preserving manually configured
186
+ UI grants. Normal draft/publish and runtime authorization still apply. Missing
187
+ or mismatched required bindings fail before the handler runs.
188
+
189
+ Deploy workflows with nonempty `access` using `ntn apps deploy`, without
190
+ `--local-build`. Local-build deployment rejects them before upload. Runtime API
191
+ calls still belong in durable steps. For syncs, see the
192
+ [data-source example](../sync/SKILL.md#choose-the-database-source).
156
193
 
157
194
  ## Review a workflow
158
195
 
@@ -204,6 +204,30 @@ describe("buildApp", () => {
204
204
  /hello.ts.*customBlock/,
205
205
  );
206
206
  });
207
+ it("emits symbolic access requirements for every NaC resource kind", async () => {
208
+ const { manifest } = await buildApp(fixture("workflow-access"));
209
+ expect(manifest.capabilities).toEqual([
210
+ {
211
+ type: "workflow",
212
+ key: "access",
213
+ config: {
214
+ name: "Access",
215
+ description: "Declares all workflow access resource kinds",
216
+ triggers: [{ type: "notion.page.created" }],
217
+ access: {
218
+ home: { type: "page", resourceId: "home", level: "view" },
219
+ records: { type: "database", resourceId: "records", level: "edit" },
220
+ source: {
221
+ type: "dataSource",
222
+ resourceId: "records-source",
223
+ level: "comment",
224
+ },
225
+ agent: { type: "customAgent", resourceId: "agent", level: "call" },
226
+ },
227
+ },
228
+ },
229
+ ]);
230
+ });
207
231
 
208
232
  it("builds a sync manifest with a Notion-as-Code meetings database", async () => {
209
233
  const { manifest } = await buildApp(example("sync"));
package/src/cli/build.ts CHANGED
@@ -54,24 +54,29 @@ export async function buildApp(projectRoot: string): Promise<BuildResult> {
54
54
  }
55
55
 
56
56
  const blockConfigs = await buildBlockDeclarations(projectRoot, capabilities);
57
- const manifest = await extractManifest(bundlePath, capabilities, blockConfigs);
58
- const manifestPath = path.join(projectRoot, "dist", "manifest.json");
59
- await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`);
60
-
61
- await emitProvisioningArtifact(
57
+ const entityProvisioning = await buildMetadata(
62
58
  projectRoot,
63
59
  capabilities.filter((capability) => capability.runtime === "worker"),
64
60
  );
61
+ const manifest = await extractManifest(
62
+ bundlePath,
63
+ capabilities,
64
+ blockConfigs,
65
+ entityProvisioning,
66
+ );
67
+ const manifestPath = path.join(projectRoot, "dist", "manifest.json");
68
+ await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`);
69
+
70
+ await emitProvisioningArtifact(projectRoot, entityProvisioning);
65
71
 
66
72
  return { manifest, bundlePath, manifestPath };
67
73
  }
68
74
 
69
- /** Build Notion-as-Code metadata separately so it does not ship in the Worker bundle. */
75
+ /** Emit the Notion-as-Code artifact recorded by the build-only metadata bundle. */
70
76
  async function emitProvisioningArtifact(
71
77
  projectRoot: string,
72
- capabilities: readonly DiscoveredCapability[],
78
+ entityProvisioning: RecordedProvisioning,
73
79
  ): Promise<void> {
74
- const entityProvisioning = await buildMetadata(projectRoot, capabilities);
75
80
  const provisioningPath = path.join(projectRoot, "dist", "provisioning.json");
76
81
  if (entityProvisioning.intents.length === 0) {
77
82
  await fs.promises.rm(provisioningPath, { force: true });