@notionhq/apps 0.0.1

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 (74) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +30 -0
  3. package/dist/cli/build.d.ts +13 -0
  4. package/dist/cli/build.d.ts.map +1 -0
  5. package/dist/cli/build.js +36 -0
  6. package/dist/cli/build.test.d.ts +2 -0
  7. package/dist/cli/build.test.d.ts.map +1 -0
  8. package/dist/cli/codegen.d.ts +10 -0
  9. package/dist/cli/codegen.d.ts.map +1 -0
  10. package/dist/cli/codegen.js +41 -0
  11. package/dist/cli/codegen.test.d.ts +2 -0
  12. package/dist/cli/codegen.test.d.ts.map +1 -0
  13. package/dist/cli/discover.d.ts +32 -0
  14. package/dist/cli/discover.d.ts.map +1 -0
  15. package/dist/cli/discover.js +37 -0
  16. package/dist/cli/discover.test.d.ts +2 -0
  17. package/dist/cli/discover.test.d.ts.map +1 -0
  18. package/dist/cli/emit-manifest.d.ts +10 -0
  19. package/dist/cli/emit-manifest.d.ts.map +1 -0
  20. package/dist/cli/emit-manifest.js +131 -0
  21. package/dist/cli/emit-manifest.test.d.ts +2 -0
  22. package/dist/cli/emit-manifest.test.d.ts.map +1 -0
  23. package/dist/cli/index.d.ts +3 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +36 -0
  26. package/dist/context.d.ts +7 -0
  27. package/dist/context.d.ts.map +1 -0
  28. package/dist/context.js +30 -0
  29. package/dist/error.d.ts +8 -0
  30. package/dist/error.d.ts.map +1 -0
  31. package/dist/error.js +11 -0
  32. package/dist/manifest.d.ts +25 -0
  33. package/dist/manifest.d.ts.map +1 -0
  34. package/dist/manifest.js +0 -0
  35. package/dist/output.d.ts +7 -0
  36. package/dist/output.d.ts.map +1 -0
  37. package/dist/output.js +8 -0
  38. package/dist/runtime-metadata.d.ts +9 -0
  39. package/dist/runtime-metadata.d.ts.map +1 -0
  40. package/dist/runtime-metadata.js +33 -0
  41. package/dist/runtime-metadata.test.d.ts +2 -0
  42. package/dist/runtime-metadata.test.d.ts.map +1 -0
  43. package/dist/triggers.d.ts +365 -0
  44. package/dist/triggers.d.ts.map +1 -0
  45. package/dist/triggers.generated.d.ts +246 -0
  46. package/dist/triggers.generated.d.ts.map +1 -0
  47. package/dist/triggers.generated.js +239 -0
  48. package/dist/triggers.js +0 -0
  49. package/dist/workflow.d.ts +125 -0
  50. package/dist/workflow.d.ts.map +1 -0
  51. package/dist/workflow.js +210 -0
  52. package/dist/workflow.test.d.ts +2 -0
  53. package/dist/workflow.test.d.ts.map +1 -0
  54. package/docs/BUILD.md +66 -0
  55. package/package.json +74 -0
  56. package/src/cli/build.test.ts +73 -0
  57. package/src/cli/build.ts +57 -0
  58. package/src/cli/codegen.test.ts +21 -0
  59. package/src/cli/codegen.ts +51 -0
  60. package/src/cli/discover.test.ts +26 -0
  61. package/src/cli/discover.ts +73 -0
  62. package/src/cli/emit-manifest.test.ts +45 -0
  63. package/src/cli/emit-manifest.ts +190 -0
  64. package/src/cli/index.ts +41 -0
  65. package/src/context.ts +49 -0
  66. package/src/error.ts +12 -0
  67. package/src/manifest.ts +26 -0
  68. package/src/output.ts +8 -0
  69. package/src/runtime-metadata.test.ts +54 -0
  70. package/src/runtime-metadata.ts +46 -0
  71. package/src/triggers.generated.ts +489 -0
  72. package/src/triggers.ts +500 -0
  73. package/src/workflow.test.ts +788 -0
  74. package/src/workflow.ts +418 -0
@@ -0,0 +1,239 @@
1
+ const triggers = {
2
+ /**
3
+ * Declare that a workflow can run on `slack.message` events.
4
+ *
5
+ * A message was posted in a subscribed Slack channel.
6
+ */
7
+ slackMessage() {
8
+ return {
9
+ type: "slack.message"
10
+ };
11
+ },
12
+ /**
13
+ * Declare that a workflow can run on `slack.reaction.added` events.
14
+ *
15
+ * A reaction was added to a Slack message.
16
+ */
17
+ slackReactionAdded() {
18
+ return {
19
+ type: "slack.reaction.added"
20
+ };
21
+ },
22
+ /**
23
+ * Declare that a workflow can run on `slack.app.mention` events.
24
+ *
25
+ * The Notion app was mentioned in a Slack message.
26
+ */
27
+ slackAppMention() {
28
+ return {
29
+ type: "slack.app.mention"
30
+ };
31
+ },
32
+ /**
33
+ * Declare that a workflow can run on `discord.interaction` events.
34
+ *
35
+ * A Discord interaction was received.
36
+ */
37
+ discordInteraction() {
38
+ return {
39
+ type: "discord.interaction"
40
+ };
41
+ },
42
+ /**
43
+ * Declare that a workflow can run on `mail.email.received` events.
44
+ *
45
+ * An email was received.
46
+ */
47
+ mailEmailReceived() {
48
+ return {
49
+ type: "mail.email.received"
50
+ };
51
+ },
52
+ /**
53
+ * Declare that a workflow can run on `mail.email.sent` events.
54
+ *
55
+ * An email was sent.
56
+ */
57
+ mailEmailSent() {
58
+ return {
59
+ type: "mail.email.sent"
60
+ };
61
+ },
62
+ /**
63
+ * Declare that a workflow can run on `mail.label.applied` events.
64
+ *
65
+ * A label was applied to an email.
66
+ */
67
+ mailLabelApplied() {
68
+ return {
69
+ type: "mail.label.applied"
70
+ };
71
+ },
72
+ /**
73
+ * Declare that a workflow can run on `calendar.event.created` events.
74
+ *
75
+ * A calendar event was created.
76
+ */
77
+ calendarEventCreated() {
78
+ return {
79
+ type: "calendar.event.created"
80
+ };
81
+ },
82
+ /**
83
+ * Declare that a workflow can run on `calendar.event.updated` events.
84
+ *
85
+ * A calendar event was updated.
86
+ */
87
+ calendarEventUpdated() {
88
+ return {
89
+ type: "calendar.event.updated"
90
+ };
91
+ },
92
+ /**
93
+ * Declare that a workflow can run on `calendar.event.canceled` events.
94
+ *
95
+ * A calendar event was canceled.
96
+ */
97
+ calendarEventCanceled() {
98
+ return {
99
+ type: "calendar.event.canceled"
100
+ };
101
+ },
102
+ /**
103
+ * Declare that a workflow can run on `notion.page.created` events.
104
+ *
105
+ * A page has been added to a database.
106
+ */
107
+ notionPageCreated() {
108
+ return {
109
+ type: "notion.page.created"
110
+ };
111
+ },
112
+ /**
113
+ * Declare that a workflow can run on `notion.page.updated` events.
114
+ *
115
+ * A page was updated.
116
+ */
117
+ notionPageUpdated() {
118
+ return {
119
+ type: "notion.page.updated"
120
+ };
121
+ },
122
+ /**
123
+ * Declare that a workflow can run on `notion.page.deleted` events.
124
+ *
125
+ * A page was deleted.
126
+ */
127
+ notionPageDeleted() {
128
+ return {
129
+ type: "notion.page.deleted"
130
+ };
131
+ },
132
+ /**
133
+ * Declare that a workflow can run on `notion.meetingNote.summary.completed` events.
134
+ *
135
+ * A meeting note's AI summary finished generating.
136
+ */
137
+ notionMeetingNoteSummaryCompleted() {
138
+ return {
139
+ type: "notion.meetingNote.summary.completed"
140
+ };
141
+ },
142
+ /**
143
+ * Declare that a workflow can run on `notion.page.discussion.comment.added` events.
144
+ *
145
+ * A comment was added to a page discussion.
146
+ */
147
+ notionCommentAdded() {
148
+ return {
149
+ type: "notion.page.discussion.comment.added"
150
+ };
151
+ },
152
+ /**
153
+ * Declare that a workflow can run on `notion.agent.mentioned` events.
154
+ *
155
+ * An agent was mentioned in page content or a person property.
156
+ */
157
+ notionAgentMentioned() {
158
+ return {
159
+ type: "notion.agent.mentioned"
160
+ };
161
+ },
162
+ /**
163
+ * Declare that a workflow can run on `notion.button.pressed` events.
164
+ *
165
+ * A button block was pressed.
166
+ */
167
+ notionButtonPressed() {
168
+ return {
169
+ type: "notion.button.pressed"
170
+ };
171
+ },
172
+ /**
173
+ * Declare that a workflow can run on `notion.database.agent.updated` events.
174
+ *
175
+ * A database agent's configuration was updated.
176
+ */
177
+ notionDatabaseAgentUpdated() {
178
+ return {
179
+ type: "notion.database.agent.updated"
180
+ };
181
+ },
182
+ /**
183
+ * Declare that a workflow can run on `webhooks.webhook` events.
184
+ *
185
+ * An incoming webhook request was received.
186
+ */
187
+ webhook() {
188
+ return {
189
+ type: "webhooks.webhook"
190
+ };
191
+ },
192
+ /**
193
+ * Declare that a workflow can run on `googleDriveOauth.filesChanged` events.
194
+ *
195
+ * Files changed in a watched Google Drive folder or drive.
196
+ */
197
+ googleDriveOauthFilesChanged() {
198
+ return {
199
+ type: "googleDriveOauth.filesChanged"
200
+ };
201
+ },
202
+ /**
203
+ * Declare that a workflow can run on `recurrence` events.
204
+ *
205
+ * A recurring schedule fired.
206
+ */
207
+ recurrence() {
208
+ return {
209
+ type: "recurrence"
210
+ };
211
+ }
212
+ };
213
+ const WORKFLOW_TRIGGER_TYPES = [
214
+ "slack.message",
215
+ "slack.reaction.added",
216
+ "slack.app.mention",
217
+ "discord.interaction",
218
+ "mail.email.received",
219
+ "mail.email.sent",
220
+ "mail.label.applied",
221
+ "calendar.event.created",
222
+ "calendar.event.updated",
223
+ "calendar.event.canceled",
224
+ "notion.page.created",
225
+ "notion.page.updated",
226
+ "notion.page.deleted",
227
+ "notion.meetingNote.summary.completed",
228
+ "notion.page.discussion.comment.added",
229
+ "notion.agent.mentioned",
230
+ "notion.button.pressed",
231
+ "notion.database.agent.updated",
232
+ "webhooks.webhook",
233
+ "googleDriveOauth.filesChanged",
234
+ "recurrence"
235
+ ];
236
+ export {
237
+ WORKFLOW_TRIGGER_TYPES,
238
+ triggers
239
+ };
File without changes
@@ -0,0 +1,125 @@
1
+ import type { CapabilityContext } from "./context.js";
2
+ import { type RunMetadata } from "./runtime-metadata.js";
3
+ import type { WorkflowEventMap, WorkflowTrigger } from "./triggers.generated.js";
4
+ type HandlerOptions = {
5
+ /** Return the concrete result instead of writing the output envelope. */
6
+ concreteOutput?: true;
7
+ };
8
+ export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
9
+ export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
10
+ export type WorkflowEventForTriggers<T extends readonly WorkflowTrigger[]> = WorkflowEventForTrigger<T[number]>;
11
+ /**
12
+ * Configuration passed to {@link createWorkflow}.
13
+ */
14
+ export type WorkflowConfiguration<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]]> = {
15
+ /**
16
+ * A human-readable name for the workflow, shown in the UI when viewing workflows
17
+ */
18
+ name: string;
19
+ /**
20
+ * A human-readable description of what the workflow does, shown in the UI when viewing workflows
21
+ */
22
+ description: string;
23
+ /**
24
+ * An array of triggers that can invoke this workflow.
25
+ *
26
+ * Each trigger defines a specific event or condition that causes the workflow to run.
27
+ */
28
+ triggers: TTriggers;
29
+ handler: (event: WorkflowEventForTriggers<TTriggers>, context: WorkflowContext) => Promise<void> | void;
30
+ };
31
+ /**
32
+ * A workflow capability as returned by {@link createWorkflow}.
33
+ *
34
+ * Note that a workflow carries no key of its own — the build tool derives
35
+ * the capability key from the file the workflow is default-exported from.
36
+ */
37
+ export type Workflow<TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]] = readonly [
38
+ WorkflowTrigger,
39
+ ...WorkflowTrigger[]
40
+ ]> = {
41
+ _tag: "workflow";
42
+ config: {
43
+ name: string;
44
+ description: string;
45
+ triggers: TTriggers;
46
+ };
47
+ handler: (event: WorkflowEventForTriggers<TTriggers>, options?: HandlerOptions) => Promise<{
48
+ status: "success";
49
+ } | undefined>;
50
+ };
51
+ /**
52
+ * Create a workflow capability.
53
+ *
54
+ * Default-export the result from a file directly under `src/workflows/` —
55
+ * the file's basename becomes the workflow's key.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * // src/workflows/onPageCreated.ts
60
+ * import { triggers } from "@notionhq/apps/triggers";
61
+ * import { createWorkflow } from "@notionhq/apps/workflow";
62
+ *
63
+ * export default createWorkflow({
64
+ * name: "Send Welcome Email",
65
+ * description: "Sends a welcome email when a new page is added",
66
+ * triggers: [triggers.notionPageCreated()],
67
+ * handler: async (event, context) => {
68
+ * console.log(event.page);
69
+ * await context.step("Process page", async ({ id }) => {
70
+ * console.log(`Running step ${id}`);
71
+ * return processPage(event.page);
72
+ * });
73
+ * },
74
+ * });
75
+ * ```
76
+ */
77
+ export declare function createWorkflow<const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]]>(configuration: WorkflowConfiguration<TTriggers>): Workflow<TTriggers>;
78
+ /** Context passed to a workflow step. */
79
+ export type StepContext = {
80
+ /**
81
+ * An identifier that is stable for this step across attempts of the workflow run.
82
+ * Use this to make side effects idempotent when the workflow is retried.
83
+ */
84
+ id: string;
85
+ };
86
+ type WorkflowStepResult<T> = T extends void ? null : T;
87
+ /** Options for a workflow step. */
88
+ export type WorkflowStepOptions = {
89
+ /**
90
+ * A stable key used to identify this step across attempts of a workflow run.
91
+ * Pass a string for a single key segment or an array of strings for a composite
92
+ * key. Defaults to the step name. Changing the key creates a new logical step.
93
+ */
94
+ key: string | string[];
95
+ };
96
+ type WorkflowStep = {
97
+ <T>(name: string, fn: (context: StepContext) => T | Promise<T>): Promise<WorkflowStepResult<T>>;
98
+ <T>(name: string, options: WorkflowStepOptions, fn: (context: StepContext) => T | Promise<T>): Promise<WorkflowStepResult<T>>;
99
+ };
100
+ /** Context passed to a workflow handler. */
101
+ export type WorkflowContext = CapabilityContext & RunMetadata & {
102
+ /**
103
+ * Run an observable unit of work within the workflow.
104
+ *
105
+ * The callback may be synchronous or asynchronous, but `step` always returns
106
+ * a promise. Its return value must be JSON-serializable so it can be included
107
+ * in the success event written to stdout. A callback that returns `undefined`
108
+ * is normalized to `null`, which keeps first runs and saved replays consistent.
109
+ *
110
+ * The step name is used to derive its stable replay key by default. Step key
111
+ * inputs must be unique within a workflow invocation. Pass an explicit string
112
+ * or array of strings when display names are duplicated or may change.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * const page = await context.step("Fetch page", async ({ id }) => {
117
+ * console.log(`Running step ${id}`);
118
+ * return context.notion.pages.retrieve({ page_id: pageId });
119
+ * });
120
+ * ```
121
+ */
122
+ step: WorkflowStep;
123
+ };
124
+ export {};
125
+ //# sourceMappingURL=workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { createCapabilityContext } from "./context.js";
5
+ import { ExecutionError } from "./error.js";
6
+ import { writeOutput } from "./output.js";
7
+ import { readRunMetadata } from "./runtime-metadata.js";
8
+ function createWorkflow(configuration) {
9
+ return {
10
+ _tag: "workflow",
11
+ config: {
12
+ name: configuration.name,
13
+ description: configuration.description,
14
+ triggers: configuration.triggers
15
+ },
16
+ async handler(event, options) {
17
+ try {
18
+ const runMetadata = readRunMetadata();
19
+ const capabilityContext = {
20
+ ...createCapabilityContext(),
21
+ ...runMetadata,
22
+ step: createStep(runMetadata)
23
+ };
24
+ await configuration.handler(event, capabilityContext);
25
+ if (options?.concreteOutput) {
26
+ return { status: "success" };
27
+ }
28
+ writeOutput({ _tag: "success", value: { status: "success" } });
29
+ } catch (err) {
30
+ const error = new ExecutionError(err);
31
+ if (!options?.concreteOutput) {
32
+ writeOutput({
33
+ _tag: "error",
34
+ error: {
35
+ name: error.name,
36
+ message: error.message,
37
+ trace: error.stack
38
+ }
39
+ });
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ };
45
+ }
46
+ const WORKFLOW_STEP_DIRECTORY_ENV_KEY = "NOTION_WORKFLOW_STEP_DIRECTORY";
47
+ const MAX_WORKFLOW_STEP_KEY_LENGTH = 43;
48
+ const WORKFLOW_STEP_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
49
+ const WORKFLOW_STEP_KEY_HASH_DOMAIN = "notion-workflow-step-key";
50
+ const WORKFLOW_STEP_KEY_HASH_VERSION = 1;
51
+ function writeStepEvent(event, value, startedAt, emittedAt = performance.now()) {
52
+ const tag = `__notion_step_${event}__`;
53
+ process.stdout.write(
54
+ `
55
+ <${tag}>${JSON.stringify({
56
+ ...value,
57
+ timestampMs: performance.timeOrigin + emittedAt,
58
+ durationMs: emittedAt - startedAt
59
+ })}</${tag}>
60
+ `
61
+ );
62
+ }
63
+ function createStep({ runGroupId }) {
64
+ const stepNameByKey = /* @__PURE__ */ new Map();
65
+ async function step(name, optionsOrFn, maybeFn) {
66
+ const options = typeof optionsOrFn === "function" ? void 0 : optionsOrFn;
67
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
68
+ if (!fn) {
69
+ throw new Error(`Workflow step "${name}" is missing its callback.`);
70
+ }
71
+ const keyInput = options?.key ?? name;
72
+ const key = createWorkflowStepKey(typeof keyInput === "string" ? [keyInput] : keyInput);
73
+ const existingStepName = stepNameByKey.get(key);
74
+ if (existingStepName !== void 0) {
75
+ throw new Error(
76
+ `Workflow step "${name}" has the same key as "${existingStepName}". Step names are used as keys by default; provide a unique name or an explicit key.`
77
+ );
78
+ }
79
+ stepNameByKey.set(key, name);
80
+ const context = { id: `${runGroupId}:${key}` };
81
+ const event = { id: context.id, name, key };
82
+ const startedAt = performance.now();
83
+ writeStepEvent("started", event, startedAt, startedAt);
84
+ try {
85
+ const checkpoint = await readWorkflowStepCheckpoint(key);
86
+ if (checkpoint) {
87
+ writeStepEvent(
88
+ "replayed",
89
+ {
90
+ ...event,
91
+ sourceRunId: checkpoint.sourceRunId,
92
+ sourceStepId: checkpoint.sourceStepId
93
+ },
94
+ startedAt
95
+ );
96
+ writeStepEvent(
97
+ "completed",
98
+ {
99
+ ...event,
100
+ type: "success"
101
+ },
102
+ startedAt
103
+ );
104
+ return checkpoint.value;
105
+ }
106
+ const value = normalizeWorkflowStepResult(await fn(context));
107
+ writeStepEvent("success", { ...event, value }, startedAt);
108
+ writeStepEvent(
109
+ "completed",
110
+ {
111
+ ...event,
112
+ type: "success"
113
+ },
114
+ startedAt
115
+ );
116
+ return value;
117
+ } catch (err) {
118
+ const error = new ExecutionError(err);
119
+ writeStepEvent(
120
+ "failure",
121
+ {
122
+ ...event,
123
+ error: {
124
+ name: error.name,
125
+ message: error.message,
126
+ trace: error.stack
127
+ }
128
+ },
129
+ startedAt
130
+ );
131
+ writeStepEvent(
132
+ "completed",
133
+ {
134
+ ...event,
135
+ type: "failure"
136
+ },
137
+ startedAt
138
+ );
139
+ throw err;
140
+ }
141
+ }
142
+ return step;
143
+ }
144
+ function createWorkflowStepKey(segments) {
145
+ if (segments.length === 0) {
146
+ throw new Error("Workflow step keys must contain at least one segment.");
147
+ }
148
+ for (const segment of segments) {
149
+ if (typeof segment !== "string") {
150
+ throw new Error("Workflow step key segments must be strings.");
151
+ }
152
+ if (segment.length === 0) {
153
+ throw new Error("Workflow step key segments must not be empty.");
154
+ }
155
+ }
156
+ const serialized = JSON.stringify([
157
+ WORKFLOW_STEP_KEY_HASH_DOMAIN,
158
+ WORKFLOW_STEP_KEY_HASH_VERSION,
159
+ ...segments
160
+ ]);
161
+ const key = createHash("sha256").update(serialized).digest("base64url");
162
+ if (key.length !== MAX_WORKFLOW_STEP_KEY_LENGTH || !WORKFLOW_STEP_KEY_PATTERN.test(key)) {
163
+ throw new Error(
164
+ `Workflow step keys must be ${MAX_WORKFLOW_STEP_KEY_LENGTH} Base64url characters after hashing.`
165
+ );
166
+ }
167
+ return key;
168
+ }
169
+ function normalizeWorkflowStepResult(value) {
170
+ return value === void 0 ? null : value;
171
+ }
172
+ async function readWorkflowStepCheckpoint(key) {
173
+ const directory = process.env[WORKFLOW_STEP_DIRECTORY_ENV_KEY];
174
+ if (!directory) {
175
+ return void 0;
176
+ }
177
+ const path = join(directory, `${hashWorkflowStepKey(key)}.json`);
178
+ let serialized;
179
+ try {
180
+ serialized = await readFile(path, "utf8");
181
+ } catch (error) {
182
+ if (isNodeError(error) && error.code === "ENOENT") {
183
+ return void 0;
184
+ }
185
+ throw error;
186
+ }
187
+ const parsed = JSON.parse(serialized);
188
+ if (!isRecord(parsed) || parsed.version !== 0 || parsed.key !== key || !Object.hasOwn(parsed, "value") || typeof parsed.sourceRunId !== "string" || typeof parsed.sourceStepId !== "string") {
189
+ throw new Error(`Workflow step checkpoint ${hashWorkflowStepKey(key)} is invalid.`);
190
+ }
191
+ return {
192
+ version: 0,
193
+ key,
194
+ value: parsed.value,
195
+ sourceRunId: parsed.sourceRunId,
196
+ sourceStepId: parsed.sourceStepId
197
+ };
198
+ }
199
+ function hashWorkflowStepKey(key) {
200
+ return createHash("sha256").update(key).digest("hex");
201
+ }
202
+ function isRecord(value) {
203
+ return typeof value === "object" && value !== null && !Array.isArray(value);
204
+ }
205
+ function isNodeError(error) {
206
+ return error instanceof Error && "code" in error;
207
+ }
208
+ export {
209
+ createWorkflow
210
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=workflow.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.test.d.ts","sourceRoot":"","sources":["../src/workflow.test.ts"],"names":[],"mappings":""}
package/docs/BUILD.md ADDED
@@ -0,0 +1,66 @@
1
+ # Workflow build process
2
+
3
+ `notion-apps build` discovers workflow modules and produces two deployable artifacts:
4
+
5
+ - `dist/worker.js`, an ESM bundle containing the app's workflow code.
6
+ - `dist/manifest.json`, a static description of its workflows and triggers.
7
+
8
+ ## Project convention
9
+
10
+ Each top-level TypeScript file under `src/workflows/` must default-export the result of
11
+ `createWorkflow(...)`. The filename becomes the workflow key.
12
+
13
+ ```text
14
+ my-app/
15
+ ├── src/
16
+ │ ├── workflows/
17
+ │ │ └── onPageCreated.ts
18
+ │ └── lib/
19
+ │ └── processPage.ts
20
+ ├── .notion/
21
+ └── dist/
22
+ ├── worker.js
23
+ └── manifest.json
24
+ ```
25
+
26
+ Files elsewhere under `src/` are ordinary modules and enter the bundle only when a workflow
27
+ imports them. Directories for other capability types are not discovered.
28
+
29
+ ## Pipeline
30
+
31
+ 1. Discover top-level `src/workflows/*.ts` files.
32
+ 2. Generate `.notion/entry.ts` with static imports and a workflow dispatcher.
33
+ 3. Bundle the app's code into `dist/worker.js`, leaving npm packages external.
34
+ 4. Import the bundle once, validate every workflow export and its JSON-safe configuration, and
35
+ write `dist/manifest.json`.
36
+
37
+ Workflow modules must therefore be importable without secrets or network access. Read required
38
+ environment variables and make requests inside handlers or workflow steps, not at module scope.
39
+
40
+ ## Manifest
41
+
42
+ The workflow-only manifest retains the platform's existing resource fields as empty arrays:
43
+
44
+ ```json
45
+ {
46
+ "sdkVersion": "0.0.1",
47
+ "databases": [],
48
+ "xldbs": [],
49
+ "pacers": [],
50
+ "capabilities": [
51
+ {
52
+ "type": "workflow",
53
+ "key": "onPageCreated",
54
+ "config": {
55
+ "name": "Log New Pages",
56
+ "description": "Logs every page created in the workspace",
57
+ "triggers": [{ "type": "notion.page.created" }]
58
+ }
59
+ }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ The platform invokes a workflow through the generated bundle's `run("workflow", key, event)`
65
+ dispatcher. Workflow results continue using the existing Notion output envelope, and runtime
66
+ metadata continues using the existing `NOTION_*` environment variables and `workerId` field.