@notionhq/apps 0.0.15 → 0.0.16

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/AGENTS.md ADDED
@@ -0,0 +1,13 @@
1
+ # Agent instructions
2
+
3
+ Before creating, modifying, or troubleshooting an App capability, read the
4
+ matching skill:
5
+
6
+ - [Workflows](./skills/workflow/SKILL.md)
7
+ - [Connections](./skills/connections/SKILL.md)
8
+ - [Database syncs](./skills/sync/SKILL.md)
9
+ - [Custom blocks](./skills/custom-blocks/SKILL.md)
10
+ - [Notion as Code](./skills/notion-as-code/SKILL.md)
11
+
12
+ Resolve these links relative to this package directory. Check the installed SDK
13
+ declarations for current API details.
package/README.md CHANGED
@@ -63,6 +63,16 @@ uses explicit exports and does not re-export utility types or the custom-block b
63
63
  keep using `@notionhq/apps/custom-blocks` and `@notionhq/apps/react`; the root includes
64
64
  Node-only workflow code. Styles stay in `@notionhq/apps/nds.css`.
65
65
 
66
+ ## Agent skills
67
+
68
+ The package includes skills for [workflows](./skills/workflow/SKILL.md),
69
+ [connections](./skills/connections/SKILL.md),
70
+ [database syncs](./skills/sync/SKILL.md),
71
+ [custom blocks](./skills/custom-blocks/SKILL.md), and
72
+ [Notion as Code](./skills/notion-as-code/SKILL.md). Read the matching skill
73
+ before creating, modifying, or troubleshooting an App capability. Shipping the
74
+ skills with the SDK keeps their instructions up to date with the installed API.
75
+
66
76
  ## Development
67
77
 
68
78
  1. Install [mise](https://mise.jdx.dev/getting-started.html)
@@ -139,6 +149,38 @@ Put each default-exported sync directly in `src/syncs/`. A primary key must be a
139
149
  or text property. Do not include that property in an upsert: the SDK fills it from
140
150
  the change's `key`.
141
151
 
152
+ ## Notion-as-Code databases
153
+
154
+ Use `properties` to declare a database with one data source:
155
+
156
+ ```ts
157
+ import { notion } from "@notionhq/apps/notion-as-code";
158
+
159
+ const issues = notion.database({
160
+ resourceId: "branch-check-issues",
161
+ name: "Issues",
162
+ properties: [
163
+ { resourceId: "issue-title", name: "Name", type: "title" },
164
+ { resourceId: "issue-id", name: "ID", type: "text" },
165
+ ],
166
+ });
167
+ ```
168
+
169
+ Pass `issues.dataSource` as the `dataSource` argument to `sync` from `@notionhq/apps`, or use
170
+ `issues.dataSource.addPage(...)` to declare a page in the source. The database still exposes
171
+ `dataSources` and `addView`.
172
+
173
+ For explicit sources, keep using `notion.database({ resourceId, dataSources: [...] })`
174
+ and access each source through `handle.dataSources[sourceResourceId]`.
175
+ `properties` and `dataSources` are mutually exclusive. Both forms also work with
176
+ page and teamspace `addDatabase` helpers.
177
+
178
+ The shorthand emits the existing database intent with one source named after the
179
+ database and identified by `${resourceId}-source`. That ID must be globally unique.
180
+ When converting an existing explicit declaration, use the shorthand only if its
181
+ source already has that ID; changing the source ID creates a different resource.
182
+ No top-level `properties` field is added to the database intent.
183
+
142
184
  ## Custom blocks
143
185
 
144
186
  Declare browser blocks in `src/customBlocks/<key>.ts`:
@@ -18,6 +18,13 @@ export type DatabaseArgs<DataSources extends readonly DataSourceDefinition[] = r
18
18
  };
19
19
  /** Input for adding a database beneath an existing resource. */
20
20
  export type ChildDatabaseArgs<DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[]> = Omit<DatabaseArgs<DataSources>, "parent">;
21
+ /** Single-source shorthand; the source ID is `${resourceId}-source`. */
22
+ export type SingleSourceDatabaseArgs<Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[]> = Omit<DatabaseArgs, "dataSources" | "name"> & {
23
+ name: string;
24
+ properties: Properties;
25
+ dataSources?: never;
26
+ };
27
+ export type ChildSingleSourceDatabaseArgs<Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[]> = Omit<SingleSourceDatabaseArgs<Properties>, "parent">;
21
28
  /** Resolves one data source ID to the properties declared for it. */
22
29
  type DataSourcePropertiesForResourceId<DataSources extends readonly DataSourceDefinition[], ResourceId extends DataSources[number]["resourceId"]> = Extract<DataSources[number], {
23
30
  resourceId: ResourceId;
@@ -37,8 +44,23 @@ export type DatabaseHandle<DataSources extends readonly DataSourceDefinition[] =
37
44
  };
38
45
  addView: (view: unknown) => void;
39
46
  };
47
+ /** A database whose sole data source is available without an ID lookup. */
48
+ export type SingleSourceDatabaseHandle<Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[]> = DatabaseHandle<readonly DataSourceDefinition<Properties>[]> & {
49
+ readonly dataSource: DataSourceHandle<Properties>;
50
+ };
51
+ export type ChildDatabaseFactory = {
52
+ <const Properties extends readonly NotionAsCodeProperty[]>(args: ChildSingleSourceDatabaseArgs<Properties>): SingleSourceDatabaseHandle<Properties>;
53
+ <const DataSources extends readonly DataSourceDefinition[]>(args: ChildDatabaseArgs<DataSources> & {
54
+ properties?: never;
55
+ }): DatabaseHandle<DataSources>;
56
+ };
40
57
  /** Declare a database, defaulting to a private top-level database in the Apps workspace. */
41
- export declare function database<const DataSources extends readonly DataSourceDefinition[]>(args: DatabaseArgs<DataSources>): DatabaseHandle<DataSources>;
58
+ export declare function database<const Properties extends readonly NotionAsCodeProperty[]>(args: SingleSourceDatabaseArgs<Properties>): SingleSourceDatabaseHandle<Properties>;
59
+ export declare function database<const DataSources extends readonly DataSourceDefinition[]>(args: DatabaseArgs<DataSources> & {
60
+ properties?: never;
61
+ }): DatabaseHandle<DataSources>;
62
+ /** Bind both database declaration forms to an existing parent. */
63
+ export declare function createChildDatabase(parent: Parent): ChildDatabaseFactory;
42
64
  /** Record a database declaration and create handles for each of its data sources. */
43
65
  export declare function createDatabase(args: DatabaseArgs, parent: Parent): DatabaseHandle;
44
66
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAEN,KAAK,oBAAoB,EAEzB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACX,KAAK,UAAU,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAc,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAG5E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE/E,oEAAoE;AACpE,MAAM,MAAM,YAAY,CACvB,WAAW,SAAS,SAAS,oBAAoB,EAAE,GAAG,SAAS,oBAAoB,EAAE,IAClF;IACH,UAAU,EAAE,UAAU,CAAC;IACvB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,gBAAgB,CAAC;IAExB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,iBAAiB,CAC5B,WAAW,SAAS,SAAS,oBAAoB,EAAE,GAAG,SAAS,oBAAoB,EAAE,IAClF,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC;AAE9C,qEAAqE;AACrE,KAAK,iCAAiC,CACrC,WAAW,SAAS,SAAS,oBAAoB,EAAE,EACnD,UAAU,SAAS,WAAW,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,IAEpD,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC,SAAS,oBAAoB,CACpF,MAAM,UAAU,CAChB,GACE,UAAU,GACV,KAAK,CAAC;AAEV,+DAA+D;AAC/D,MAAM,MAAM,gBAAgB,CAC3B,UAAU,SAAS,SAAS,oBAAoB,EAAE,GAAG,SAAS,oBAAoB,EAAE,IACjF;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;IACjE,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,cAAc,CACzB,WAAW,SAAS,SAAS,oBAAoB,EAAE,GAAG,SAAS,oBAAoB,EAAE,IAClF;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE;QACrB,QAAQ,EAAE,UAAU,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,GAAG,gBAAgB,CAC3E,iCAAiC,CAAC,WAAW,EAAE,UAAU,CAAC,CAC1D;KACD,CAAC;IACF,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACjC,CAAC;AAEF,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,KAAK,CAAC,WAAW,SAAS,SAAS,oBAAoB,EAAE,EACjF,IAAI,EAAE,YAAY,CAAC,WAAW,CAAC,GAC7B,cAAc,CAAC,WAAW,CAAC,CAAC;AAY/B,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CA+BjF"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/database.ts"],"names":[],"mappings":"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"}
@@ -13,8 +13,34 @@ function database(args) {
13
13
  type: "resourceId",
14
14
  resourceId: APPS_WORKSPACE_RESOURCE_ID
15
15
  };
16
+ if ("properties" in args) {
17
+ if (args.dataSources !== void 0) {
18
+ throw new Error("Specify either properties or dataSources, not both");
19
+ }
20
+ const { properties, ...databaseArgs } = args;
21
+ const source = {
22
+ resourceId: `${args.resourceId}-source`,
23
+ name: args.name,
24
+ properties
25
+ };
26
+ const handle = createDatabase({ ...databaseArgs, dataSources: [source] }, parent);
27
+ const dataSource = handle.dataSources[source.resourceId];
28
+ if (!dataSource) {
29
+ throw new Error(`Missing data source "${source.resourceId}"`);
30
+ }
31
+ return { ...handle, dataSource };
32
+ }
16
33
  return createDatabase(args, parent);
17
34
  }
35
+ function createChildDatabase(parent) {
36
+ function addDatabase(args) {
37
+ if ("properties" in args) {
38
+ return database({ ...args, parent });
39
+ }
40
+ return database({ ...args, parent });
41
+ }
42
+ return addDatabase;
43
+ }
18
44
  function createDatabase(args, parent) {
19
45
  assertUserResourceId(args.resourceId);
20
46
  const dataSources = args.dataSources ?? [];
@@ -72,6 +98,7 @@ function createDataSourceHandle(dataSource) {
72
98
  };
73
99
  }
74
100
  export {
101
+ createChildDatabase,
75
102
  createDatabase,
76
103
  database
77
104
  };
@@ -1,7 +1,7 @@
1
1
  export { APPS_RESERVED_RESOURCE_ID_PREFIX, APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
2
2
  export type { DataSourceDefinition, NotionAsCodeProperty, Parent } from "./intents.js";
3
3
  export type { CustomAgentArgs, CustomAgentHandle } from "./custom-agent.js";
4
- export type { ChildDatabaseArgs, DataSourceHandle, DatabaseArgs, DatabaseHandle, } from "./database.js";
4
+ export type { ChildDatabaseArgs, ChildDatabaseFactory, ChildSingleSourceDatabaseArgs, DataSourceHandle, DatabaseArgs, DatabaseHandle, SingleSourceDatabaseArgs, SingleSourceDatabaseHandle, } from "./database.js";
5
5
  export type { ChildPageArgs, PageArgs, PageHandle } from "./page.js";
6
6
  export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
7
7
  /** Experimental Notion-as-Code facade for Apps SDK project metadata. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gCAAgC,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC5F,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACvF,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC5E,YAAY,EACX,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,cAAc,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACrE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAErE,wEAAwE;AACxE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;CAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gCAAgC,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC5F,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACvF,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC5E,YAAY,EACX,iBAAiB,EACjB,oBAAoB,EACpB,6BAA6B,EAC7B,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,wBAAwB,EACxB,0BAA0B,GAC1B,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACrE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAErE,wEAAwE;AACxE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;CAAiB,CAAC"}
@@ -1,5 +1,5 @@
1
- import { type DataSourceDefinition, type Parent, type ResourceId } from "./intents.js";
2
- import { type ChildDatabaseArgs, type DatabaseHandle } from "./database.js";
1
+ import { type Parent, type ResourceId } from "./intents.js";
2
+ import { type ChildDatabaseFactory } from "./database.js";
3
3
  export type PageArgs = {
4
4
  resourceId: ResourceId;
5
5
  /** Omit to create a private top-level page in the Apps workspace. */
@@ -12,7 +12,7 @@ export type PageArgs = {
12
12
  export type ChildPageArgs = Omit<PageArgs, "parent">;
13
13
  export type PageHandle = {
14
14
  readonly resourceId: string;
15
- addDatabase: <const DataSources extends readonly DataSourceDefinition[]>(args: ChildDatabaseArgs<DataSources>) => DatabaseHandle<DataSources>;
15
+ addDatabase: ChildDatabaseFactory;
16
16
  addPage: (args: ChildPageArgs) => PageHandle;
17
17
  };
18
18
  /** Declare a page, defaulting to a private top-level page in the Apps workspace. */
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,oBAAoB,EAEzB,KAAK,MAAM,EACX,KAAK,UAAU,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAY,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAItF,MAAM,MAAM,QAAQ,GAAG;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAErD,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,CAAC,KAAK,CAAC,WAAW,SAAS,SAAS,oBAAoB,EAAE,EACtE,IAAI,EAAE,iBAAiB,CAAC,WAAW,CAAC,KAChC,cAAc,CAAC,WAAW,CAAC,CAAC;IACjC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,oFAAoF;AACpF,wBAAgB,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAS/C;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAoBrF"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,MAAM,EACX,KAAK,UAAU,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAI/E,MAAM,MAAM,QAAQ,GAAG;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAErD,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,oFAAoF;AACpF,wBAAgB,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAS/C;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAerF"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  APPS_WORKSPACE_RESOURCE_ID
3
3
  } from "./intents.js";
4
- import { database } from "./database.js";
4
+ import { createChildDatabase } from "./database.js";
5
5
  import { recordIntent } from "./recorder.js";
6
6
  import { assertParent, assertUserResourceId } from "./resource.js";
7
7
  function page(args) {
@@ -23,12 +23,7 @@ function createPage(args, parent) {
23
23
  });
24
24
  return {
25
25
  resourceId: args.resourceId,
26
- addDatabase(databaseArgs) {
27
- return database({
28
- ...databaseArgs,
29
- parent: { type: "resourceId", resourceId: args.resourceId }
30
- });
31
- },
26
+ addDatabase: createChildDatabase({ type: "resourceId", resourceId: args.resourceId }),
32
27
  addPage(pageArgs) {
33
28
  return createPage(pageArgs, { type: "resourceId", resourceId: args.resourceId });
34
29
  }
@@ -1,5 +1,5 @@
1
- import { type DataSourceDefinition, type NotionAsCodeIcon, type ResourceId } from "./intents.js";
2
- import { type ChildDatabaseArgs, type DatabaseHandle } from "./database.js";
1
+ import { type NotionAsCodeIcon, type ResourceId } from "./intents.js";
2
+ import { type ChildDatabaseFactory } from "./database.js";
3
3
  import { type ChildPageArgs, type PageHandle } from "./page.js";
4
4
  export type TeamspaceArgs = {
5
5
  resourceId: ResourceId;
@@ -10,7 +10,7 @@ export type TeamspaceArgs = {
10
10
  };
11
11
  export type TeamspaceHandle = {
12
12
  readonly resourceId: string;
13
- addDatabase: <const DataSources extends readonly DataSourceDefinition[]>(args: ChildDatabaseArgs<DataSources>) => DatabaseHandle<DataSources>;
13
+ addDatabase: ChildDatabaseFactory;
14
14
  addPage: (args: ChildPageArgs) => PageHandle;
15
15
  };
16
16
  /** Declare a teamspace that Apps automatically creates in its workspace. */
@@ -1 +1 @@
1
- {"version":3,"file":"teamspace.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/teamspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAEf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAY,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,EAAc,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAI5E,MAAM,MAAM,aAAa,GAAG;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,CAAC,KAAK,CAAC,WAAW,SAAS,SAAS,oBAAoB,EAAE,EACtE,IAAI,EAAE,iBAAiB,CAAC,WAAW,CAAC,KAChC,cAAc,CAAC,WAAW,CAAC,CAAC;IACjC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,CAoB9D"}
1
+ {"version":3,"file":"teamspace.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/teamspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAEf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAc,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAI5E,MAAM,MAAM,aAAa,GAAG;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,CAe9D"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  APPS_WORKSPACE_RESOURCE_ID
3
3
  } from "./intents.js";
4
- import { database } from "./database.js";
4
+ import { createChildDatabase } from "./database.js";
5
5
  import { createPage } from "./page.js";
6
6
  import { recordIntent } from "./recorder.js";
7
7
  import { assertUserResourceId } from "./resource.js";
@@ -14,12 +14,7 @@ function teamspace(args) {
14
14
  });
15
15
  return {
16
16
  resourceId: args.resourceId,
17
- addDatabase(databaseArgs) {
18
- return database({
19
- ...databaseArgs,
20
- parent: { type: "resourceId", resourceId: args.resourceId }
21
- });
22
- },
17
+ addDatabase: createChildDatabase({ type: "resourceId", resourceId: args.resourceId }),
23
18
  addPage(pageArgs) {
24
19
  return createPage(pageArgs, { type: "resourceId", resourceId: args.resourceId });
25
20
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@notionhq/apps",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "An SDK for building workflow apps for Notion",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "notion-apps": "./dist/cli/index.js"
8
8
  },
9
9
  "files": [
10
+ "skills/",
11
+ "AGENTS.md",
10
12
  "dist/",
11
13
  "src/",
12
14
  "docs/",
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: connections
3
+ description: Configure and use typed provider connections and connection-bound triggers in Notion App workflows.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Workflow connections
8
+
9
+ Import `{ workflow }` from `@notionhq/apps` and
10
+ `connections` from `@notionhq/apps/workflow`. Connections are not root exports.
11
+ Declare requirements on the workflow, then use the corresponding typed client
12
+ inside an awaited durable step. Access a named connection through its provider
13
+ client, such as `context.connections.slack("support")`.
14
+
15
+ Read the installed provider declarations before choosing methods and inputs.
16
+ The SDK handles transport, credentials, and runtime bindings. Do not construct
17
+ raw tools API envelopes or call internal endpoints. Check the installed version
18
+ supports the typed methods; resolve version mismatches before using them.
19
+ Provider clients are inferred from the declared requirements. Keys default to
20
+ the provider name; custom keys distinguish multiple connections to one provider.
21
+ Keep keys unique and ensure connection-trigger keys match a declared provider.
22
+
23
+ A declaration requests setup; it grants no access. Deploy and configure the
24
+ requirement before execution. The runtime resolves bindings and credentials.
25
+ Do not manufacture connection IDs, set runtime binding metadata to bypass setup,
26
+ or copy Worker auth interceptors into an App.
27
+
28
+ Availability and permissions are decided by the server. If a provider is
29
+ unavailable or a write needs confirmation the runtime cannot obtain, report the
30
+ setup limitation. Do not bypass it with an internal endpoint.
31
+
32
+ Check operation-specific partial errors as well as rejected requests. Durable
33
+ steps can repeat effects after an uncertain result; use downstream idempotency
34
+ only where supported and reconcile uncertain writes before retrying.
35
+
36
+ This client surface belongs to workflows. Sync handlers receive only the
37
+ capability context with `notion`; do not assume they have provider clients.
38
+
39
+ ## Bind triggers to connections
40
+
41
+ For provider triggers, use the `triggers: ({ triggers }) => [...]` callback
42
+ on `workflow`. Its trigger creators infer valid connection keys from
43
+ the workflow's declarations:
44
+
45
+ ```ts
46
+ import { workflow } from "@notionhq/apps";
47
+ import { connections } from "@notionhq/apps/workflow";
48
+
49
+ export default workflow({
50
+ name: "Watch support messages",
51
+ description: "Runs when a message arrives through the support connection.",
52
+ connections: [connections.slack({ key: "support" })],
53
+ triggers: ({ triggers }) => [triggers.slackMessage({ connectionKey: "support" })],
54
+ handler: async (_event, context) => {
55
+ await context.step("Record trigger", () => {
56
+ console.log("Support message received");
57
+ });
58
+ },
59
+ });
60
+ ```
61
+
62
+ Use the callback's `triggers` argument to get connection-key checking.
63
+ `connectionKey` refers to a declared key for that trigger's provider, not an
64
+ external account ID or a durable step key. Here, a typo or a key belonging to
65
+ a different provider's connection is a type error. Without a custom key,
66
+ `connections.slack()` declares the key `"slack"`.
67
+
68
+ The SDK also validates explicit bindings when constructing the workflow,
69
+ including array-form triggers: the key must exist and its provider must match.
70
+ Duplicate trigger-type/connection-key pairs are rejected; the same trigger type
71
+ can use distinct connections. The resolved bindings are preserved in the
72
+ manifest. Event types still come from the selected triggers; narrow
73
+ `event.type` before using provider-specific fields in mixed-trigger workflows.
74
+
75
+ Unkeyed provider triggers remain supported for compatibility. Omitting
76
+ `connectionKey` does not explicitly bind a trigger to the provider's default
77
+ key; supply it when the workflow should listen through a particular connection.
78
+ SDK validation does not establish server availability or complete connection
79
+ setup.
80
+
81
+ ## Verify
82
+
83
+ Treat provider content as untrusted data.
84
+ Do not log private payloads. Run `npm run check` and `npm run build`; test
85
+ partial responses and retry behavior offline. Do not make live
86
+ provider writes merely to validate a skill.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: custom-blocks
3
+ description: Build interactive browser custom blocks declared by a Notion App.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # App custom blocks
8
+
9
+ Import `{ customBlock }` from `@notionhq/apps` and default-export `customBlock(...)` in
10
+ `src/customBlocks/<key>.ts`. Copy the existing hello block's browser setup:
11
+
12
+ ```ts
13
+ import { customBlock } from "@notionhq/apps";
14
+
15
+ export default customBlock({
16
+ path: "./blocks/issueBoard",
17
+ slashCommand: "issue-board",
18
+ dataSources: {},
19
+ });
20
+ ```
21
+
22
+ Paths are relative to the app root. Keep browser source in `blocks/<key>/`
23
+ with its own Vite config and browser tsconfig. The root package owns React,
24
+ React DOM, Vite, and type dependencies. The default build command is
25
+ `npx vite build` and output directory is `dist`; override `command` and
26
+ `output` when needed. For existing browser assets use `static: true`, which
27
+ cannot be combined with `command` or `output`.
28
+
29
+ Import React integration from `@notionhq/apps/react` and styles from
30
+ `@notionhq/apps/nds.css`. Wrap the UI in `NotionCustomBlock`. Read the installed
31
+ custom-block client documentation before adding hooks or host interactions.
32
+ Browser runtime APIs stay on `@notionhq/apps/custom-blocks`; the root
33
+ `customBlock` creates a capability, not a browser runtime client.
34
+ Never put server credentials in browser source.
35
+
36
+ `dataSources` declares expected host schemas; it does not bind a concrete
37
+ database. Inspect `ManifestDataSource` from the installed custom-blocks package.
38
+ These schemas use Notion API property names such as `rich_text`; Notion as Code
39
+ data source properties use a different representation, such as `text`.
40
+
41
+ Blocks are build-time declarations, not executable workflow handlers. Do not
42
+ port `worker.customBlock()`, Worker source options, or Worker execution commands.
43
+ Run the app's browser typecheck and build. Inspect installed Apps CLI help for
44
+ block build commands, and verify rendering and bindings in an available host
45
+ when requested; a server bundle build alone does not test browser behavior.
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: notion-as-code
3
+ description: Declare Notion pages, databases, teamspaces, and custom agents in an App, including data sources used by syncs.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Notion as Code for Apps
8
+
9
+ Prefer Notion as Code whenever it supports the requested resource setup. Use declarations
10
+ for pages, databases, teamspaces, and custom agents instead of equivalent
11
+ manual setup or runtime creation calls. Respect explicit user choices and
12
+ existing resource attachments; use other methods for unsupported operations
13
+ or runtime data changes.
14
+
15
+ Import the resource creators you use, such as
16
+ `import { database, page } from "@notionhq/apps"`. These functions declare
17
+ resources for deployment; they do not make Notion API requests when called.
18
+ Use `context.notion` for runtime API operations instead.
19
+
20
+ Put shared declarations in `src/lib/`, or capability-specific declarations in
21
+ a directory such as `src/syncs/lib/`. Import them from a workflow or sync
22
+ module so the build's metadata evaluation reaches them. An unimported resource
23
+ file is not discovered automatically. Declare resources at module scope, not
24
+ inside handlers or durable steps. Module evaluation must work without
25
+ credentials or network access.
26
+
27
+ ## Where declarations are picked up
28
+
29
+ The Apps build discovers direct `.ts` children of `src/workflows/` and
30
+ `src/syncs/`, then evaluates their imports with the Notion as Code recorder active.
31
+ Pages, databases, data sources, teamspaces, and custom agents all follow this
32
+ same rule. Their resource type does not determine their file location.
33
+
34
+ ```text
35
+ src/
36
+ lib/
37
+ resources.ts Shared page/database declarations
38
+ syncs/
39
+ issues.ts Default-exported sync; imports ../lib/resources
40
+ lib/
41
+ issueSchema.ts Sync-specific helper; must be imported
42
+ workflows/
43
+ notify.ts Default-exported workflow
44
+ ```
45
+
46
+ For example, export a database handle from `src/lib/resources.ts` and import it
47
+ in `src/syncs/issues.ts` for `sync`. A page-only declaration
48
+ module can be loaded with a side-effect import such as
49
+ `import "../lib/pages"` from a workflow or sync. Keep those imports at module
50
+ scope so build evaluation runs the declarations.
51
+
52
+ There is no automatically scanned `src/pages/`, `src/databases/`, or Notion as Code
53
+ entrypoint. Helpers under `src/syncs/lib/` are not discovered on their own.
54
+ Every direct file in a capability directory must still default-export that
55
+ capability, so do not put a resource-only file there. Imports reached only
56
+ through `src/customBlocks/` or browser code do not enter the Notion as Code recording
57
+ phase. An App containing only unimported resource declarations is not a
58
+ standalone Notion as Code project and has no discovered capabilities.
59
+
60
+ ## Declare resources
61
+
62
+ Read the installed Notion as Code types before choosing fields. The Apps
63
+ root exports support:
64
+
65
+ - `teamspace({ resourceId, name, accessLevel })`, with `addPage` and
66
+ `addDatabase` on the returned handle.
67
+ - `page({ resourceId, parent?, properties?, content? })`, with
68
+ `addPage` and `addDatabase` for children.
69
+ - `database({ resourceId, parent?, name?, dataSources? })`, returning
70
+ data source handles indexed by their resource IDs. A data source's
71
+ `addPage` declares a row; the database handle also exposes `addView`.
72
+ - `customAgent({ resourceId, name, instructions?, sharedResources? })`.
73
+ Shared resources are declared resource IDs; inspect the installed types
74
+ before specifying models or triggers.
75
+
76
+ Keep resource IDs stable across builds. They identify declarations, not live
77
+ Notion UUIDs. Avoid the reserved `__notion_apps_` prefix. Explicit parents
78
+ use `{ type: "resourceId", resourceId: parent.resourceId }`; child helpers set
79
+ this reference for you. Pages and databases without a parent default to private
80
+ top-level resources in the Apps workspace.
81
+
82
+ Several fields, including views, covers, page layouts, and custom-agent
83
+ triggers, still have incomplete types. A field typed as `unknown` is not proof
84
+ that any payload is supported. Check implementation and examples before using
85
+ it; do not assume parity with other Notion as Code packages.
86
+
87
+ The Apps SDK exposes a subset of Notion as Code. Data sources are nested inside
88
+ `database({ dataSources: [...] })`, not declared by a separate
89
+ `dataSource` function. It has no `space` workspace declaration:
90
+ Apps deployment rejects workspace creation or changes and supplies the App's
91
+ workspace binding itself. Value helpers and types stay on their existing
92
+ subpaths. For example, import `{ notion }` from `@notionhq/apps/notion-as-code`
93
+ for `notion.text(...)` or `notion.file(resourceId)`. The latter creates a file
94
+ reference, not an upload or file declaration. These helpers are not root exports.
95
+ CLI acceptance of
96
+ an intent envelope alone does not establish server support for its contents.
97
+
98
+ ## Use a declared data source in a sync
99
+
100
+ This example can live directly in `src/syncs/issues.ts`. Move the resource
101
+ declaration into an imported helper when sharing it with other capabilities.
102
+
103
+ ```ts
104
+ import { database, sync } from "@notionhq/apps";
105
+ import { Builder } from "@notionhq/apps/builder";
106
+
107
+ const issues = database({
108
+ resourceId: "issues-db",
109
+ name: "Issues",
110
+ dataSources: [
111
+ {
112
+ resourceId: "issues-source",
113
+ name: "Issues",
114
+ properties: [
115
+ { resourceId: "issue-name", name: "Name", type: "title" },
116
+ { resourceId: "issue-id", name: "External ID", type: "text" },
117
+ ],
118
+ },
119
+ ],
120
+ });
121
+
122
+ export default sync({
123
+ dataSource: issues.dataSources["issues-source"],
124
+ primaryKey: "External ID",
125
+ mode: "incremental",
126
+ handler: async () => ({
127
+ changes: [
128
+ {
129
+ type: "upsert",
130
+ key: "example-123",
131
+ properties: { Name: Builder.title("Example issue") },
132
+ },
133
+ ],
134
+ hasMore: false,
135
+ }),
136
+ });
137
+ ```
138
+
139
+ Pass a data source handle, not the whole database handle or a UUID. The SDK
140
+ derives the sync schema from that handle. `primaryKey` names a title or text
141
+ property by its display name, not its resource ID. Omit that property from
142
+ upsert values; the SDK fills it from `key`. Use Apps `Builder` for sync values.
143
+ Follow the [sync skill](../sync/SKILL.md) for pagination and reconciliation.
144
+
145
+ Notion as Code properties are an array with resource IDs and names.
146
+ Each data source needs exactly one title property
147
+ and unique property names and IDs. The current adapter supports title, text,
148
+ number, select, multi-select, status, date, checkbox, URL, email, phone, and file
149
+ properties. It rejects other kinds, including relation, formula, rollup, and
150
+ person, even though some appear in the declaration type. Status options use
151
+ `todo`, `inProgress`, and `complete` arrays. Read the installed adapter before
152
+ extending a schema.
153
+
154
+ ## Build and deployment
155
+
156
+ Run the app's check and build commands, then inspect `dist/provisioning.json`
157
+ alongside the manifest. The provisioning artifact contains recorded resource
158
+ declarations. Building it does not create live resources. Do not hand-edit
159
+ generated artifacts or deploy output from a failed build.
160
+
161
+ Deployment applies provisioning and connects resources to the app. Cloud
162
+ deployment stores resource mappings on the server; local-build deployment
163
+ uses local state. Switching modes can recreate resources because their state
164
+ is independent. A failed deployment can leave partial changes; there is no
165
+ automatic rollback. Report the result before attempting recovery.
166
+
167
+ Use `ntn apps deploy` for the App workflow. The default path uploads source
168
+ for a cloud build and server-side provisioning. With `--local-build`, the
169
+ CLI builds the App, reads `dist/provisioning.json` and `dist/manifest.json`,
170
+ deploys code, applies Notion as Code intents, and reconciles sync attachments. It matches
171
+ each sync's manifest `databaseKey` to a declared data source's `resourceId`,
172
+ then resolves the resulting live data source from provisioning state.
173
+ `sync` supplies this matching key from the handle. An existing
174
+ binding to a different database causes an error instead of silent rebinding.
175
+
176
+ The standalone command `ntn notion-as-code apply <dir>` is a different
177
+ project flow: it builds that directory and reads `dist/intents.json`.
178
+ Do not use it as a substitute for Apps deployment or rename the Apps artifact
179
+ to fit it. It does not perform the App's sync attachment reconciliation.
180
+
181
+ Local deployment defaults to state named `apps-<worker-id>` in the CLI's
182
+ environment/workspace-scoped config store. The
183
+ `--notion-as-code-state-name` option requires `--local-build`. Preserve that
184
+ state when updating; local and cloud mappings are not interchangeable.
185
+
186
+ Removing all declarations removes the build artifact, but does not delete
187
+ previously provisioned resources. Keep local deployment state out of Git.
188
+ Validate declarations and sync transforms offline; perform deployment only
189
+ as part of a requested live task.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: sync
3
+ description: Build, debug, or review Notion App database syncs with stable keys and resumable pagination.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # App database syncs
8
+
9
+ Read the installed `@notionhq/apps/sync`, `notion-as-code`, and `builder`
10
+ declarations before adapting a Worker sync. Default-export one sync directly in
11
+ `src/syncs/<key>.ts`. Apps use standalone declarations, not `worker.sync()`.
12
+ Put sync-specific helpers in `src/syncs/lib/` and shared helpers in `src/lib/`.
13
+
14
+ ## Choose the database source
15
+
16
+ Prefer Notion as Code for the sync's database when Apps Notion as Code supports the required schema.
17
+ Do not choose manual database setup or a separate attached-database declaration
18
+ when Notion as Code can provide the same resource.
19
+
20
+ For a database the App creates, declare and provision it with
21
+ `database(...)` and `sync({ dataSource, ... })`.
22
+ Read the [Notion as Code skill](../notion-as-code/SKILL.md) for that path,
23
+ including declaration discovery and the supported schema types.
24
+
25
+ This example declares an Issues database and syncs into its data source:
26
+
27
+ ```ts
28
+ import { database, sync } from "@notionhq/apps";
29
+ import { Builder } from "@notionhq/apps/builder";
30
+
31
+ const issues = database({
32
+ resourceId: "issues-db",
33
+ name: "Issues",
34
+ dataSources: [
35
+ {
36
+ resourceId: "issues-source",
37
+ name: "Issues",
38
+ properties: [
39
+ { resourceId: "issue-name", name: "Name", type: "title" },
40
+ { resourceId: "issue-id", name: "External ID", type: "text" },
41
+ ],
42
+ },
43
+ ],
44
+ });
45
+
46
+ export default sync({
47
+ dataSource: issues.dataSources["issues-source"],
48
+ primaryKey: "External ID",
49
+ mode: "incremental",
50
+ handler: async () => ({
51
+ changes: [
52
+ {
53
+ type: "upsert",
54
+ key: "example-123",
55
+ properties: { Name: Builder.title("Example issue") },
56
+ },
57
+ ],
58
+ hasMore: false,
59
+ }),
60
+ });
61
+ ```
62
+
63
+ Set `primaryKey` on the sync; it names a title or text property by its display
64
+ name. Omit it from upsert properties: the SDK supplies it from `change.key`.
65
+ Use Apps `Builder` values for sync results. Keep resource IDs stable and reuse
66
+ the declared data source handle. Shared declarations can live in
67
+ `src/lib/resources.ts`, imported by the sync.
68
+
69
+ This skill covers only syncs backed by Notion as Code data sources. If the user
70
+ asks to attach an existing database, explain that this recipe does not cover
71
+ that setup and clarify the next step. Do not silently create a replacement
72
+ database.
73
+
74
+ ## Pagination and reconciliation
75
+
76
+ The handler receives `(state, context)` and returns `changes`, `hasMore`, and
77
+ `nextState`. State can be undefined initially. Persist a serializable cursor
78
+ and advance it only after successfully processing the corresponding page.
79
+ Return `hasMore: true` while pages remain; an empty filtered page is not proof
80
+ that the upstream listing ended. Keep record keys deterministic across pages
81
+ and runs. Throw on failed requests instead of emitting a false empty success.
82
+
83
+ Choose and document replace versus incremental behavior explicitly. Replace
84
+ requires a complete snapshot; incremental requires explicit deletion handling
85
+ when upstream records disappear. Test multiple pages, empty pages with a next
86
+ cursor, retrying the same cursor, and deletion handling offline.
87
+
88
+ The context supplies `notion`, not workflow steps or connection clients.
89
+ Configure required external credentials separately using names and safe
90
+ placeholders in `.env.example`; do not copy Worker auth registration.
91
+
92
+ ## Review and verify
93
+
94
+ Check stable keys, schema/value compatibility, cursor progress and exhaustion,
95
+ partial failures, and whether the documented mode matches the emitted changes.
96
+ Report findings with file, line, impact, and fix. Run the app's offline tests,
97
+ `npm run check`, and `npm run build`. Live execution can change database rows;
98
+ use it only as part of the requested live task.
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: workflow
3
+ description: Build or review Notion App workflows for typed triggers, durable replay-safe steps, and idempotent effects.
4
+ user-invocable: true
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Workflow
9
+
10
+ Use this skill to add or review a workflow in this template.
11
+
12
+ ## Build or change a workflow
13
+
14
+ 1. Read `AGENTS.md` and the existing files in `src/workflows/`.
15
+ 2. Inspect the installed workflow and trigger declarations.
16
+ 3. Choose the trigger, outcome, step boundaries, and required configuration.
17
+ 4. Create one camelCase file directly in `src/workflows/`.
18
+ 5. Default-export `workflow(...)` and use typed trigger creators.
19
+ 6. Put all non-deterministic work in awaited `context.step(...)` calls.
20
+ 7. Give each step a stable display name. For repeated steps, keep the name
21
+ constant and pass a stable, unique composite `key`, such as
22
+ `{ key: ["process-page", page.id] }`.
23
+ 8. Return JSON-safe values needed by later steps.
24
+ 9. Use the step `id` as an idempotency key when supported.
25
+ 10. Run `npm run check` and `npm run build`.
26
+
27
+ Each direct `src/workflows/*.ts` file must default-export `workflow(...)`.
28
+ The camelCase file name becomes its workflow key.
29
+ Keep workflow-specific helpers in `src/workflows/lib/` and shared helpers in
30
+ `src/lib/`; direct children of `src/workflows/` are discovered as workflows.
31
+
32
+ For provider triggers, follow [trigger connection binding](../connections/SKILL.md#bind-triggers-to-connections).
33
+ Use the typed trigger callback to check `connectionKey` against the declared
34
+ provider connections.
35
+
36
+ Use a step for every value that can change and every external effect: network
37
+ and Notion API calls, mutable state reads, timestamps, random values, generated
38
+ IDs, messages, creates, and updates. Keep deterministic transforms of the event
39
+ and completed step results outside a step.
40
+
41
+ Completed steps replay saved results. Do not rely on in-memory mutations inside
42
+ a step. Return the values required by later code. Keep calls in a stable order
43
+ and give every step a stable display name. The name is the replay key by
44
+ default. For a repeated step, do not interpolate an item ID or loop index into
45
+ the name; pass a stable composite key instead:
46
+
47
+ ```ts
48
+ await context.step("Process page", { key: ["process-page", page.id] }, async ({ id }) =>
49
+ processPage(page, { idempotencyKey: id }),
50
+ );
51
+ ```
52
+
53
+ Keys must be stable across retries and unique within one workflow run.
54
+
55
+ An external effect can succeed before its step result is saved. Pass the
56
+ callback `id` as an idempotency key when supported; otherwise use a stable
57
+ external ID, upsert, or duplicate check.
58
+
59
+ Do not write credentials. Add only environment variable names and safe
60
+ placeholders to `.env.example` when configuration is required. Return only
61
+ JSON-serializable step values, throw on failed requests and missing required
62
+ configuration, and do not log secrets or private payloads.
63
+
64
+ ## Resources created with the App
65
+
66
+ Use [Notion as Code](../notion-as-code/SKILL.md) for pages and databases that
67
+ should be created during deployment. Prefer it over equivalent manual setup
68
+ or API calls that create the App's resources. Use runtime API calls for dynamic
69
+ data changes, not as a substitute for supported Notion as Code setup.
70
+ Keep declarations at module scope in
71
+ an imported helper. For example, declare a guide page in `src/lib/resources.ts`:
72
+
73
+ ```ts
74
+ import { page } from "@notionhq/apps";
75
+
76
+ export const guide = page({
77
+ resourceId: "workflow-guide",
78
+ content: "# Workflow guide\nThis App runs a scheduled workflow.",
79
+ });
80
+ ```
81
+
82
+ Import the module from `src/workflows/sayHello.ts` so the build records it:
83
+
84
+ ```ts
85
+ import "../lib/resources";
86
+ import { workflow } from "@notionhq/apps";
87
+ import { triggers } from "@notionhq/apps/triggers";
88
+
89
+ export default workflow({
90
+ name: "Say Hello",
91
+ description: "Says hello on a recurring schedule.",
92
+ triggers: [triggers.scheduled()],
93
+ handler: async (_event, context) => {
94
+ await context.step("Say hello", () => {
95
+ console.log("Hello from your workflow!");
96
+ });
97
+ },
98
+ });
99
+ ```
100
+
101
+ The page is provisioned during deployment, not on every workflow run.
102
+ Database declarations follow the same import pattern; see the
103
+ [sync example](../sync/SKILL.md#choose-the-database-source) for using a Notion as Code
104
+ data source in a sync. Notion as Code resource IDs are declaration identities, not live
105
+ Notion UUIDs to pass to `context.notion`. Runtime API calls still belong in
106
+ durable steps and need actual resolved Notion IDs.
107
+
108
+ ## Review a workflow
109
+
110
+ Review every file in `src/workflows/` and the modules it calls. Report each
111
+ finding with its file, line, impact, and fix. Treat these as errors:
112
+
113
+ 1. A workflow is not a direct file or does not default-export `workflow`.
114
+ 2. Trigger-specific event fields are used without type narrowing.
115
+ 3. Non-deterministic work occurs outside an awaited step.
116
+ 4. Step order or names can change between retries.
117
+ 5. A step result is not JSON-serializable, or later code needs an in-memory mutation.
118
+ 6. A retry-sensitive write lacks an idempotency key or duplicate guard.
119
+ 7. External failures are ignored, or credentials are hard-coded or logged.
120
+ 8. An explicit trigger connection key is undeclared, belongs to the wrong
121
+ provider, or repeats the same trigger-type/key pair.
122
+
123
+ Run `npm run check` and `npm run build` when dependencies are installed.
@@ -2,9 +2,101 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { afterEach, describe, expect, expectTypeOf, it } from "vitest";
6
6
 
7
7
  import { buildApp } from "./build.js";
8
+ import {
9
+ notion,
10
+ type DataSourceHandle,
11
+ type SingleSourceDatabaseHandle,
12
+ } from "../notion-as-code/index.js";
13
+ import { finishMetadataRecording, startMetadataRecording } from "../notion-as-code/recorder.js";
14
+
15
+ describe("Notion-as-Code database shorthand", () => {
16
+ it("keeps provisioning and sync identity equivalent to an explicit source", () => {
17
+ const recorder = startMetadataRecording();
18
+ const database = notion.database({
19
+ resourceId: "issues",
20
+ name: "Issues",
21
+ properties: [
22
+ { resourceId: "title", name: "Name", type: "title" },
23
+ { resourceId: "count", name: "Count", type: "number" },
24
+ ],
25
+ });
26
+ expectTypeOf(database).toExtend<
27
+ SingleSourceDatabaseHandle<typeof database.dataSource.schema>
28
+ >();
29
+ expectTypeOf(
30
+ database.dataSource.database.config.schema.Count.type,
31
+ ).toEqualTypeOf<"number">();
32
+ database.dataSource.addPage({ resourceId: "issue" });
33
+ const shorthand = finishMetadataRecording(recorder).intents;
34
+
35
+ const explicitRecorder = startMetadataRecording();
36
+ const explicit = notion.database({
37
+ resourceId: "issues",
38
+ name: "Issues",
39
+ dataSources: [
40
+ {
41
+ resourceId: "issues-source",
42
+ name: "Issues",
43
+ properties: database.dataSource.schema,
44
+ },
45
+ ],
46
+ });
47
+ explicit.dataSources["issues-source"].addPage({ resourceId: "issue" });
48
+ // @ts-expect-error Explicit databases do not guarantee a single source.
49
+ void explicit.dataSource;
50
+ expect(shorthand).toEqual(finishMetadataRecording(explicitRecorder).intents);
51
+ expect(database.dataSource.database.key).toBe("issues-source");
52
+ expect(shorthand[1]).toEqual({
53
+ type: "page",
54
+ resourceId: "issue",
55
+ parent: { type: "resourceId", resourceId: "issues-source" },
56
+ });
57
+ });
58
+
59
+ it("preserves parent binding and inferred source types through child helpers", () => {
60
+ const recorder = startMetadataRecording();
61
+ const page = notion.page({ resourceId: "page" });
62
+ const team = notion.teamspace({ resourceId: "team", name: "Team", accessLevel: "private" });
63
+ for (const parent of [page, team]) {
64
+ const database = parent.addDatabase({
65
+ resourceId: `${parent.resourceId}-db`,
66
+ name: "Child",
67
+ properties: [
68
+ { resourceId: `${parent.resourceId}-title`, name: "Title", type: "title" },
69
+ ],
70
+ });
71
+ expectTypeOf(database.dataSource).toExtend<
72
+ DataSourceHandle<typeof database.dataSource.schema>
73
+ >();
74
+ expectTypeOf(database.dataSource.schema[0].name).toEqualTypeOf<"Title">();
75
+ }
76
+ const intents = finishMetadataRecording(recorder).intents;
77
+ expect(
78
+ intents.filter((intent) => intent.type === "database").map((intent) => intent.parent),
79
+ ).toEqual([
80
+ { type: "resourceId", resourceId: "page" },
81
+ { type: "resourceId", resourceId: "team" },
82
+ ]);
83
+ });
84
+
85
+ it("rejects mixed forms before recording an intent", () => {
86
+ const recorder = startMetadataRecording();
87
+ const declareMixedDatabase = () => {
88
+ // @ts-expect-error The shorthand and explicit forms are mutually exclusive.
89
+ notion.database({
90
+ resourceId: "mixed",
91
+ name: "Mixed",
92
+ properties: [],
93
+ dataSources: [],
94
+ });
95
+ };
96
+ expect(declareMixedDatabase).toThrow();
97
+ expect(finishMetadataRecording(recorder).intents).toEqual([]);
98
+ });
99
+ });
8
100
 
9
101
  const FIXTURES = fileURLToPath(new URL("../../test/fixtures/", import.meta.url));
10
102
  const EXAMPLES = fileURLToPath(new URL("../../examples/", import.meta.url));
@@ -89,7 +181,7 @@ describe("buildApp", () => {
89
181
  },
90
182
  },
91
183
  {
92
- key: "homebase-meetings-source",
184
+ key: "homebase-meetings-db-source",
93
185
  config: {
94
186
  initialTitle: "Meetings",
95
187
  schema: {
@@ -120,7 +212,7 @@ describe("buildApp", () => {
120
212
  type: "sync",
121
213
  key: "notionAsCodeIssues",
122
214
  config: {
123
- databaseKey: "homebase-meetings-source",
215
+ databaseKey: "homebase-meetings-db-source",
124
216
  primaryKeyProperty: "Meeting ID",
125
217
  mode: "incremental",
126
218
  schedule: { type: "interval", intervalMs: 3_600_000 },
@@ -37,6 +37,19 @@ export type ChildDatabaseArgs<
37
37
  DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
38
38
  > = Omit<DatabaseArgs<DataSources>, "parent">;
39
39
 
40
+ /** Single-source shorthand; the source ID is `${resourceId}-source`. */
41
+ export type SingleSourceDatabaseArgs<
42
+ Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
43
+ > = Omit<DatabaseArgs, "dataSources" | "name"> & {
44
+ name: string;
45
+ properties: Properties;
46
+ dataSources?: never;
47
+ };
48
+
49
+ export type ChildSingleSourceDatabaseArgs<
50
+ Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
51
+ > = Omit<SingleSourceDatabaseArgs<Properties>, "parent">;
52
+
40
53
  /** Resolves one data source ID to the properties declared for it. */
41
54
  type DataSourcePropertiesForResourceId<
42
55
  DataSources extends readonly DataSourceDefinition[],
@@ -71,11 +84,32 @@ export type DatabaseHandle<
71
84
  addView: (view: unknown) => void;
72
85
  };
73
86
 
87
+ /** A database whose sole data source is available without an ID lookup. */
88
+ export type SingleSourceDatabaseHandle<
89
+ Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
90
+ > = DatabaseHandle<readonly DataSourceDefinition<Properties>[]> & {
91
+ readonly dataSource: DataSourceHandle<Properties>;
92
+ };
93
+
94
+ export type ChildDatabaseFactory = {
95
+ <const Properties extends readonly NotionAsCodeProperty[]>(
96
+ args: ChildSingleSourceDatabaseArgs<Properties>,
97
+ ): SingleSourceDatabaseHandle<Properties>;
98
+ <const DataSources extends readonly DataSourceDefinition[]>(
99
+ args: ChildDatabaseArgs<DataSources> & { properties?: never },
100
+ ): DatabaseHandle<DataSources>;
101
+ };
102
+
74
103
  /** Declare a database, defaulting to a private top-level database in the Apps workspace. */
104
+ export function database<const Properties extends readonly NotionAsCodeProperty[]>(
105
+ args: SingleSourceDatabaseArgs<Properties>,
106
+ ): SingleSourceDatabaseHandle<Properties>;
75
107
  export function database<const DataSources extends readonly DataSourceDefinition[]>(
76
- args: DatabaseArgs<DataSources>,
108
+ args: DatabaseArgs<DataSources> & { properties?: never },
77
109
  ): DatabaseHandle<DataSources>;
78
- export function database(args: DatabaseArgs): DatabaseHandle {
110
+ export function database(
111
+ args: DatabaseArgs | SingleSourceDatabaseArgs,
112
+ ): DatabaseHandle | SingleSourceDatabaseHandle {
79
113
  if (args.parent) {
80
114
  assertParent(args.parent);
81
115
  }
@@ -83,9 +117,45 @@ export function database(args: DatabaseArgs): DatabaseHandle {
83
117
  type: "resourceId",
84
118
  resourceId: APPS_WORKSPACE_RESOURCE_ID,
85
119
  };
120
+ if ("properties" in args) {
121
+ if (args.dataSources !== undefined) {
122
+ throw new Error("Specify either properties or dataSources, not both");
123
+ }
124
+ const { properties, ...databaseArgs } = args;
125
+ const source: DataSourceDefinition = {
126
+ resourceId: `${args.resourceId}-source`,
127
+ name: args.name,
128
+ properties,
129
+ };
130
+ const handle = createDatabase({ ...databaseArgs, dataSources: [source] }, parent);
131
+ const dataSource = handle.dataSources[source.resourceId];
132
+ if (!dataSource) {
133
+ throw new Error(`Missing data source "${source.resourceId}"`);
134
+ }
135
+ return { ...handle, dataSource };
136
+ }
86
137
  return createDatabase(args, parent);
87
138
  }
88
139
 
140
+ /** Bind both database declaration forms to an existing parent. */
141
+ export function createChildDatabase(parent: Parent): ChildDatabaseFactory {
142
+ function addDatabase<const Properties extends readonly NotionAsCodeProperty[]>(
143
+ args: ChildSingleSourceDatabaseArgs<Properties>,
144
+ ): SingleSourceDatabaseHandle<Properties>;
145
+ function addDatabase<const DataSources extends readonly DataSourceDefinition[]>(
146
+ args: ChildDatabaseArgs<DataSources> & { properties?: never },
147
+ ): DatabaseHandle<DataSources>;
148
+ function addDatabase(
149
+ args: ChildDatabaseArgs | ChildSingleSourceDatabaseArgs,
150
+ ): DatabaseHandle | SingleSourceDatabaseHandle {
151
+ if ("properties" in args) {
152
+ return database({ ...args, parent });
153
+ }
154
+ return database({ ...args, parent });
155
+ }
156
+ return addDatabase;
157
+ }
158
+
89
159
  /** Record a database declaration and create handles for each of its data sources. */
90
160
  export function createDatabase(args: DatabaseArgs, parent: Parent): DatabaseHandle {
91
161
  assertUserResourceId(args.resourceId);
@@ -5,9 +5,13 @@ export type { DataSourceDefinition, NotionAsCodeProperty, Parent } from "./inten
5
5
  export type { CustomAgentArgs, CustomAgentHandle } from "./custom-agent.js";
6
6
  export type {
7
7
  ChildDatabaseArgs,
8
+ ChildDatabaseFactory,
9
+ ChildSingleSourceDatabaseArgs,
8
10
  DataSourceHandle,
9
11
  DatabaseArgs,
10
12
  DatabaseHandle,
13
+ SingleSourceDatabaseArgs,
14
+ SingleSourceDatabaseHandle,
11
15
  } from "./database.js";
12
16
  export type { ChildPageArgs, PageArgs, PageHandle } from "./page.js";
13
17
  export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
@@ -1,11 +1,10 @@
1
1
  import {
2
2
  APPS_WORKSPACE_RESOURCE_ID,
3
- type DataSourceDefinition,
4
3
  type PageIntent,
5
4
  type Parent,
6
5
  type ResourceId,
7
6
  } from "./intents.js";
8
- import { database, type ChildDatabaseArgs, type DatabaseHandle } from "./database.js";
7
+ import { createChildDatabase, type ChildDatabaseFactory } from "./database.js";
9
8
  import { recordIntent } from "./recorder.js";
10
9
  import { assertParent, assertUserResourceId } from "./resource.js";
11
10
 
@@ -25,9 +24,7 @@ export type ChildPageArgs = Omit<PageArgs, "parent">;
25
24
 
26
25
  export type PageHandle = {
27
26
  readonly resourceId: string;
28
- addDatabase: <const DataSources extends readonly DataSourceDefinition[]>(
29
- args: ChildDatabaseArgs<DataSources>,
30
- ) => DatabaseHandle<DataSources>;
27
+ addDatabase: ChildDatabaseFactory;
31
28
  addPage: (args: ChildPageArgs) => PageHandle;
32
29
  };
33
30
 
@@ -54,12 +51,7 @@ export function createPage(args: ChildPageArgs | PageArgs, parent: Parent): Page
54
51
 
55
52
  return {
56
53
  resourceId: args.resourceId,
57
- addDatabase(databaseArgs) {
58
- return database({
59
- ...databaseArgs,
60
- parent: { type: "resourceId", resourceId: args.resourceId },
61
- });
62
- },
54
+ addDatabase: createChildDatabase({ type: "resourceId", resourceId: args.resourceId }),
63
55
  addPage(pageArgs) {
64
56
  return createPage(pageArgs, { type: "resourceId", resourceId: args.resourceId });
65
57
  },
@@ -1,11 +1,10 @@
1
1
  import {
2
2
  APPS_WORKSPACE_RESOURCE_ID,
3
- type DataSourceDefinition,
4
3
  type NotionAsCodeIcon,
5
4
  type ResourceId,
6
5
  type TeamspaceIntent,
7
6
  } from "./intents.js";
8
- import { database, type ChildDatabaseArgs, type DatabaseHandle } from "./database.js";
7
+ import { createChildDatabase, type ChildDatabaseFactory } from "./database.js";
9
8
  import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
10
9
  import { recordIntent } from "./recorder.js";
11
10
  import { assertUserResourceId } from "./resource.js";
@@ -20,9 +19,7 @@ export type TeamspaceArgs = {
20
19
 
21
20
  export type TeamspaceHandle = {
22
21
  readonly resourceId: string;
23
- addDatabase: <const DataSources extends readonly DataSourceDefinition[]>(
24
- args: ChildDatabaseArgs<DataSources>,
25
- ) => DatabaseHandle<DataSources>;
22
+ addDatabase: ChildDatabaseFactory;
26
23
  addPage: (args: ChildPageArgs) => PageHandle;
27
24
  };
28
25
 
@@ -37,12 +34,7 @@ export function teamspace(args: TeamspaceArgs): TeamspaceHandle {
37
34
 
38
35
  return {
39
36
  resourceId: args.resourceId,
40
- addDatabase(databaseArgs) {
41
- return database({
42
- ...databaseArgs,
43
- parent: { type: "resourceId", resourceId: args.resourceId },
44
- });
45
- },
37
+ addDatabase: createChildDatabase({ type: "resourceId", resourceId: args.resourceId }),
46
38
  addPage(pageArgs) {
47
39
  return createPage(pageArgs, { type: "resourceId", resourceId: args.resourceId });
48
40
  },