@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.
- package/LICENSE.md +21 -0
- package/README.md +30 -0
- package/dist/cli/build.d.ts +13 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +36 -0
- package/dist/cli/build.test.d.ts +2 -0
- package/dist/cli/build.test.d.ts.map +1 -0
- package/dist/cli/codegen.d.ts +10 -0
- package/dist/cli/codegen.d.ts.map +1 -0
- package/dist/cli/codegen.js +41 -0
- package/dist/cli/codegen.test.d.ts +2 -0
- package/dist/cli/codegen.test.d.ts.map +1 -0
- package/dist/cli/discover.d.ts +32 -0
- package/dist/cli/discover.d.ts.map +1 -0
- package/dist/cli/discover.js +37 -0
- package/dist/cli/discover.test.d.ts +2 -0
- package/dist/cli/discover.test.d.ts.map +1 -0
- package/dist/cli/emit-manifest.d.ts +10 -0
- package/dist/cli/emit-manifest.d.ts.map +1 -0
- package/dist/cli/emit-manifest.js +131 -0
- package/dist/cli/emit-manifest.test.d.ts +2 -0
- package/dist/cli/emit-manifest.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +36 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +30 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +11 -0
- package/dist/manifest.d.ts +25 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +0 -0
- package/dist/output.d.ts +7 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +8 -0
- package/dist/runtime-metadata.d.ts +9 -0
- package/dist/runtime-metadata.d.ts.map +1 -0
- package/dist/runtime-metadata.js +33 -0
- package/dist/runtime-metadata.test.d.ts +2 -0
- package/dist/runtime-metadata.test.d.ts.map +1 -0
- package/dist/triggers.d.ts +365 -0
- package/dist/triggers.d.ts.map +1 -0
- package/dist/triggers.generated.d.ts +246 -0
- package/dist/triggers.generated.d.ts.map +1 -0
- package/dist/triggers.generated.js +239 -0
- package/dist/triggers.js +0 -0
- package/dist/workflow.d.ts +125 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +210 -0
- package/dist/workflow.test.d.ts +2 -0
- package/dist/workflow.test.d.ts.map +1 -0
- package/docs/BUILD.md +66 -0
- package/package.json +74 -0
- package/src/cli/build.test.ts +73 -0
- package/src/cli/build.ts +57 -0
- package/src/cli/codegen.test.ts +21 -0
- package/src/cli/codegen.ts +51 -0
- package/src/cli/discover.test.ts +26 -0
- package/src/cli/discover.ts +73 -0
- package/src/cli/emit-manifest.test.ts +45 -0
- package/src/cli/emit-manifest.ts +190 -0
- package/src/cli/index.ts +41 -0
- package/src/context.ts +49 -0
- package/src/error.ts +12 -0
- package/src/manifest.ts +26 -0
- package/src/output.ts +8 -0
- package/src/runtime-metadata.test.ts +54 -0
- package/src/runtime-metadata.ts +46 -0
- package/src/triggers.generated.ts +489 -0
- package/src/triggers.ts +500 -0
- package/src/workflow.test.ts +788 -0
- 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
|
+
};
|
package/dist/triggers.js
ADDED
|
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":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAEjF,KAAK,cAAc,GAAG;IACrB,yEAAyE;IACzE,cAAc,CAAC,EAAE,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,gBAAgB,CAAC,MAAM,gBAAgB,CAAC,CAAC;AAErE,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,eAAe,IAAI,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAE7F,MAAM,MAAM,wBAAwB,CAAC,CAAC,SAAS,SAAS,eAAe,EAAE,IACxE,uBAAuB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAEpC;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAChC,SAAS,SAAS,SAAS,CAAC,eAAe,EAAE,GAAG,eAAe,EAAE,CAAC,IAC/D;IACH;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB,OAAO,EAAE,CACR,KAAK,EAAE,wBAAwB,CAAC,SAAS,CAAC,EAC1C,OAAO,EAAE,eAAe,KACpB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,CACnB,SAAS,SAAS,SAAS,CAAC,eAAe,EAAE,GAAG,eAAe,EAAE,CAAC,GAAG,SAAS;IAC7E,eAAe;IACf,GAAG,eAAe,EAAE;CACpB,IACE;IACH,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,SAAS,CAAC;KACpB,CAAC;IACF,OAAO,EAAE,CACR,KAAK,EAAE,wBAAwB,CAAC,SAAS,CAAC,EAC1C,OAAO,CAAC,EAAE,cAAc,KACpB,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;CAChD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAC7B,KAAK,CAAC,SAAS,SAAS,SAAS,CAAC,eAAe,EAAE,GAAG,eAAe,EAAE,CAAC,EACvE,aAAa,EAAE,qBAAqB,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CA4CtE;AAED,yCAAyC;AACzC,MAAM,MAAM,WAAW,GAAG;IACzB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,KAAK,kBAAkB,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;AAEvD,mCAAmC;AACnC,MAAM,MAAM,mBAAmB,GAAG;IACjC;;;;OAIG;IACH,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,KAAK,YAAY,GAAG;IACnB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IAChG,CAAC,CAAC,EACD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,mBAAmB,EAC5B,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC1C,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;CAClC,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,eAAe,GAAG,iBAAiB,GAC9C,WAAW,GAAG;IACb;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,EAAE,YAAY,CAAC;CACnB,CAAC"}
|
package/dist/workflow.js
ADDED
|
@@ -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 @@
|
|
|
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.
|