@notionhq/apps 0.0.25 → 0.0.27
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/workflow.d.ts +61 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +109 -1
- package/docs/workflow-inputs.md +28 -0
- package/package.json +1 -1
- package/skills/workflow/SKILL.md +5 -0
- package/src/index.ts +1 -0
- package/src/workflow.test.ts +210 -0
- package/src/workflow.ts +226 -3
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ export { page } from "./notion-as-code/page.js";
|
|
|
5
5
|
export { teamspace } from "./notion-as-code/teamspace.js";
|
|
6
6
|
export { view } from "./notion-as-code/view.js";
|
|
7
7
|
export { sync } from "./sync.js";
|
|
8
|
-
export { access, workflow, type Workflow, type WorkflowAccess, type WorkflowAccessAgentLevel, type WorkflowAccessBindings, type WorkflowAccessDeclaration, type WorkflowAccessDeclarations, type WorkflowAccessLevel, type WorkflowAccessLevelFor, type WorkflowAccessResource, type WorkflowAccessResourceLevel, type WorkflowAccessResourceType, type WorkflowAccessRequirement, type WorkflowAccessRequirements, type WorkflowAccessValue, type WorkflowConfiguration, type WorkflowContext, type WorkflowTriggerConfiguration, } from "./workflow.js";
|
|
8
|
+
export { access, input, workflow, type Workflow, type WorkflowAccess, type WorkflowAccessAgentLevel, type WorkflowAccessBindings, type WorkflowAccessDeclaration, type WorkflowAccessDeclarations, type WorkflowAccessLevel, type WorkflowAccessLevelFor, type WorkflowAccessResource, type WorkflowAccessResourceLevel, type WorkflowAccessResourceType, type WorkflowAccessRequirement, type WorkflowAccessRequirements, type WorkflowAccessValue, type WorkflowConfiguration, type WorkflowContext, type WorkflowTriggerConfiguration, } from "./workflow.js";
|
|
9
9
|
export type { WorkflowState, WorkflowStateValue } from "./workflow-state.js";
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACN,MAAM,EACN,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,4BAA4B,GACjC,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACN,MAAM,EACN,KAAK,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,4BAA4B,GACjC,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { view } from "./notion-as-code/view.js";
|
|
|
7
7
|
import { sync } from "./sync.js";
|
|
8
8
|
import {
|
|
9
9
|
access,
|
|
10
|
+
input,
|
|
10
11
|
workflow
|
|
11
12
|
} from "./workflow.js";
|
|
12
13
|
export {
|
|
@@ -14,6 +15,7 @@ export {
|
|
|
14
15
|
customAgent,
|
|
15
16
|
customBlock,
|
|
16
17
|
database,
|
|
18
|
+
input,
|
|
17
19
|
page,
|
|
18
20
|
sync,
|
|
19
21
|
teamspace,
|
package/dist/workflow.d.ts
CHANGED
|
@@ -20,6 +20,50 @@ export type WorkflowWaitDuration = {
|
|
|
20
20
|
[Unit in keyof WorkflowWaitDurationValues]-?: Required<Pick<WorkflowWaitDurationValues, Unit>> & WorkflowWaitDurationValues;
|
|
21
21
|
}[keyof WorkflowWaitDurationValues];
|
|
22
22
|
type WorkflowWaitKey = string | string[];
|
|
23
|
+
type WorkflowInputFieldOptions = {
|
|
24
|
+
optional?: boolean;
|
|
25
|
+
};
|
|
26
|
+
declare const workflowInputValueType: unique symbol;
|
|
27
|
+
export type WorkflowInputField<T> = {
|
|
28
|
+
type: "text" | "number" | "checkbox" | "select" | "multi_select" | "date" | "person" | "files";
|
|
29
|
+
optional?: boolean;
|
|
30
|
+
options?: readonly string[];
|
|
31
|
+
readonly [workflowInputValueType]?: T;
|
|
32
|
+
};
|
|
33
|
+
type WorkflowInputValue<T, TOptions extends WorkflowInputFieldOptions | undefined> = TOptions extends {
|
|
34
|
+
optional: true;
|
|
35
|
+
} ? T | undefined : T;
|
|
36
|
+
/** Date-only or local date-time endpoints, with the selected IANA time zone. */
|
|
37
|
+
export type WorkflowInputDate = {
|
|
38
|
+
start: string;
|
|
39
|
+
end?: string;
|
|
40
|
+
timeZone?: string;
|
|
41
|
+
};
|
|
42
|
+
export declare const input: {
|
|
43
|
+
text<const TOptions extends WorkflowInputFieldOptions | undefined>(options?: TOptions): WorkflowInputField<WorkflowInputValue<string, TOptions>>;
|
|
44
|
+
number<const TOptions extends WorkflowInputFieldOptions | undefined>(options?: TOptions): WorkflowInputField<WorkflowInputValue<number, TOptions>>;
|
|
45
|
+
checkbox<const TOptions extends WorkflowInputFieldOptions | undefined>(options?: TOptions): WorkflowInputField<WorkflowInputValue<boolean, TOptions>>;
|
|
46
|
+
select<const TOptions extends readonly [string, ...string[]], const TFieldOptions extends WorkflowInputFieldOptions | undefined>(values: TOptions, options?: TFieldOptions): WorkflowInputField<WorkflowInputValue<TOptions[number], TFieldOptions>>;
|
|
47
|
+
date<const TOptions extends WorkflowInputFieldOptions | undefined>(options?: TOptions): WorkflowInputField<WorkflowInputValue<WorkflowInputDate, TOptions>>;
|
|
48
|
+
multiSelect<const TOptions extends readonly [string, ...string[]], const TFieldOptions extends WorkflowInputFieldOptions | undefined>(values: TOptions, options?: TFieldOptions): WorkflowInputField<WorkflowInputValue<TOptions[number][], TFieldOptions>>;
|
|
49
|
+
/** Returns the user ID of every selected person. */
|
|
50
|
+
person<const TOptions extends WorkflowInputFieldOptions | undefined>(options?: TOptions): WorkflowInputField<WorkflowInputValue<string[], TOptions>>;
|
|
51
|
+
/** Returns each attachment's URL or Notion attachment block ID. */
|
|
52
|
+
files<const TOptions extends WorkflowInputFieldOptions | undefined>(options?: TOptions): WorkflowInputField<WorkflowInputValue<string[], TOptions>>;
|
|
53
|
+
};
|
|
54
|
+
export type WorkflowInputSchema = Record<string, WorkflowInputField<unknown>>;
|
|
55
|
+
export type WorkflowInputValues<TSchema extends WorkflowInputSchema> = {
|
|
56
|
+
[Key in keyof TSchema]: TSchema[Key] extends WorkflowInputField<infer Value> ? Value : never;
|
|
57
|
+
};
|
|
58
|
+
export type WorkflowInputResponse<TSchema extends WorkflowInputSchema> = {
|
|
59
|
+
value: WorkflowInputValues<TSchema>;
|
|
60
|
+
respondedBy: string;
|
|
61
|
+
respondedAt: string;
|
|
62
|
+
};
|
|
63
|
+
export type WorkflowWaitForInputOptions<TSchema extends WorkflowInputSchema> = {
|
|
64
|
+
input: TSchema;
|
|
65
|
+
key?: WorkflowWaitKey;
|
|
66
|
+
};
|
|
23
67
|
export type WorkflowWaitUntilOptions = {
|
|
24
68
|
at: Date;
|
|
25
69
|
key?: WorkflowWaitKey;
|
|
@@ -35,9 +79,19 @@ export type WorkflowWaitUntilResult = {
|
|
|
35
79
|
resumeAtMs: number;
|
|
36
80
|
};
|
|
37
81
|
};
|
|
82
|
+
export type WorkflowWaitForInputResult = {
|
|
83
|
+
status: "waiting";
|
|
84
|
+
wait: {
|
|
85
|
+
type: "input";
|
|
86
|
+
name: string;
|
|
87
|
+
stepKey: string;
|
|
88
|
+
input: WorkflowInputSchema;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type WorkflowWaitResult = WorkflowWaitUntilResult | WorkflowWaitForInputResult;
|
|
38
92
|
type WorkflowHandlerResult = {
|
|
39
93
|
status: "success";
|
|
40
|
-
} |
|
|
94
|
+
} | WorkflowWaitResult;
|
|
41
95
|
export type { WorkflowAccess, WorkflowAccessAgentLevel, WorkflowAccessBindings, WorkflowAccessDeclaration, WorkflowAccessDeclarations, WorkflowAccessLevel, WorkflowAccessLevelFor, WorkflowAccessResource, WorkflowAccessResourceLevel, WorkflowAccessResourceType, WorkflowAccessRequirement, WorkflowAccessRequirements, WorkflowAccessValue, } from "./workflow-access.js";
|
|
42
96
|
export { access } from "./workflow-access.js";
|
|
43
97
|
export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
|
|
@@ -166,6 +220,12 @@ type WorkflowWait = {
|
|
|
166
220
|
* date has been reached.
|
|
167
221
|
*/
|
|
168
222
|
until(name: string, options: WorkflowWaitUntilOptions): Promise<void>;
|
|
223
|
+
/**
|
|
224
|
+
* Suspend the workflow until a person submits values matching the input
|
|
225
|
+
* schema. The returned value is restored from the durable workflow
|
|
226
|
+
* checkpoint when the request is fulfilled.
|
|
227
|
+
*/
|
|
228
|
+
forInput<const TSchema extends WorkflowInputSchema>(name: string, options: WorkflowWaitForInputOptions<TSchema>): Promise<WorkflowInputResponse<TSchema>>;
|
|
169
229
|
};
|
|
170
230
|
/** Context passed to a workflow handler. */
|
|
171
231
|
export type WorkflowContext<TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations, TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations> = CapabilityContext & {
|
package/dist/workflow.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGN,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAC/B,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAKtD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAE1E,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EACX,gBAAgB,EAChB,eAAe,EACf,uBAAuB,EACvB,6BAA6B,EAC7B,MAAM,yBAAyB,CAAC;AAEjC,KAAK,cAAc,GAAG;IACrB,yEAAyE;IACzE,cAAc,CAAC,EAAE,IAAI,CAAC;CACtB,CAAC;AAEF,KAAK,0BAA0B,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;KACjC,IAAI,IAAI,MAAM,0BAA0B,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,0BAA0B,EAAE,IAAI,CAAC,CAAC,GAC7F,0BAA0B;CAC3B,CAAC,MAAM,0BAA0B,CAAC,CAAC;AAEpC,KAAK,eAAe,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;AAEzC,MAAM,MAAM,wBAAwB,GACjC;IACA,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,CAAC,EAAE,eAAe,CAAC;CACrB,GACD;IACA,KAAK,EAAE,oBAAoB,CAAC;IAC5B,GAAG,CAAC,EAAE,eAAe,CAAC;CACrB,CAAC;AAEL,MAAM,MAAM,uBAAuB,GAAG;IACrC,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE;QACL,IAAI,EAAE,OAAO,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;KACnB,CAAC;CACF,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
|
package/dist/workflow.js
CHANGED
|
@@ -17,6 +17,34 @@ import { resolveRuntimeInput } from "./runtime-input.js";
|
|
|
17
17
|
import { readRunMetadata } from "./runtime-metadata.js";
|
|
18
18
|
import { createWorkflowTriggers } from "./triggers.generated.js";
|
|
19
19
|
import { createWorkflowStepState } from "./workflow-state.js";
|
|
20
|
+
const input = {
|
|
21
|
+
text(options) {
|
|
22
|
+
return { type: "text", ...options };
|
|
23
|
+
},
|
|
24
|
+
number(options) {
|
|
25
|
+
return { type: "number", ...options };
|
|
26
|
+
},
|
|
27
|
+
checkbox(options) {
|
|
28
|
+
return { type: "checkbox", ...options };
|
|
29
|
+
},
|
|
30
|
+
select(values, options) {
|
|
31
|
+
return { type: "select", options: values, ...options };
|
|
32
|
+
},
|
|
33
|
+
date(options) {
|
|
34
|
+
return { type: "date", ...options };
|
|
35
|
+
},
|
|
36
|
+
multiSelect(values, options) {
|
|
37
|
+
return { type: "multi_select", options: values, ...options };
|
|
38
|
+
},
|
|
39
|
+
/** Returns the user ID of every selected person. */
|
|
40
|
+
person(options) {
|
|
41
|
+
return { type: "person", ...options };
|
|
42
|
+
},
|
|
43
|
+
/** Returns each attachment's URL or Notion attachment block ID. */
|
|
44
|
+
files(options) {
|
|
45
|
+
return { type: "files", ...options };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
20
48
|
const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
21
49
|
import { access } from "./workflow-access.js";
|
|
22
50
|
import {
|
|
@@ -189,13 +217,20 @@ function createStep(metadata) {
|
|
|
189
217
|
}
|
|
190
218
|
class WorkflowWaitInterrupt extends Error {
|
|
191
219
|
constructor(result) {
|
|
192
|
-
super(
|
|
220
|
+
super(workflowWaitMessage(result));
|
|
193
221
|
this.result = result;
|
|
194
222
|
this.name = "WorkflowWaitInterrupt";
|
|
195
223
|
}
|
|
196
224
|
result;
|
|
197
225
|
}
|
|
226
|
+
function workflowWaitMessage(result) {
|
|
227
|
+
if (result.wait.type === "until") {
|
|
228
|
+
return `Workflow is waiting until ${new Date(result.wait.resumeAtMs).toISOString()}`;
|
|
229
|
+
}
|
|
230
|
+
return `Workflow is waiting for input: ${result.wait.name}`;
|
|
231
|
+
}
|
|
198
232
|
function createWait(step) {
|
|
233
|
+
const inputWaitNameByKey = /* @__PURE__ */ new Map();
|
|
199
234
|
return {
|
|
200
235
|
async until(name, options) {
|
|
201
236
|
const keyInput = options.key ?? name;
|
|
@@ -220,9 +255,81 @@ function createWait(step) {
|
|
|
220
255
|
resumeAtMs
|
|
221
256
|
}
|
|
222
257
|
});
|
|
258
|
+
},
|
|
259
|
+
async forInput(name, options) {
|
|
260
|
+
const keyInput = options.key ?? name;
|
|
261
|
+
const stepKey = createWorkflowStepKey([
|
|
262
|
+
"wait",
|
|
263
|
+
"input",
|
|
264
|
+
...typeof keyInput === "string" ? [keyInput] : keyInput
|
|
265
|
+
]);
|
|
266
|
+
const existingName = inputWaitNameByKey.get(stepKey);
|
|
267
|
+
if (existingName !== void 0) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Workflow input wait "${name}" has the same key as "${existingName}". Provide a unique name or an explicit key.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
inputWaitNameByKey.set(stepKey, name);
|
|
273
|
+
const checkpoint = await readWorkflowStepCheckpoint(stepKey);
|
|
274
|
+
if (checkpoint) {
|
|
275
|
+
if (!isWorkflowInputResponse(checkpoint.value, options.input)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Workflow input checkpoint ${hashWorkflowStepKey(stepKey)} does not match its schema.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return checkpoint.value;
|
|
281
|
+
}
|
|
282
|
+
throw new WorkflowWaitInterrupt({
|
|
283
|
+
status: "waiting",
|
|
284
|
+
wait: {
|
|
285
|
+
type: "input",
|
|
286
|
+
name,
|
|
287
|
+
stepKey,
|
|
288
|
+
input: options.input
|
|
289
|
+
}
|
|
290
|
+
});
|
|
223
291
|
}
|
|
224
292
|
};
|
|
225
293
|
}
|
|
294
|
+
function isWorkflowInputResponse(value, schema) {
|
|
295
|
+
if (!isRecord(value) || !isRecord(value.value) || typeof value.respondedBy !== "string" || typeof value.respondedAt !== "string") {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (Object.keys(value.value).some((key) => !Object.hasOwn(schema, key))) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
302
|
+
const fieldValue = value.value[key];
|
|
303
|
+
if (fieldValue === void 0 && field.optional === true) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!isWorkflowInputFieldValue(fieldValue, field)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function isWorkflowInputFieldValue(value, field) {
|
|
313
|
+
switch (field.type) {
|
|
314
|
+
case "text":
|
|
315
|
+
return typeof value === "string";
|
|
316
|
+
case "date":
|
|
317
|
+
return isRecord(value) && typeof value.start === "string" && (value.end === void 0 || typeof value.end === "string") && (value.timeZone === void 0 || typeof value.timeZone === "string");
|
|
318
|
+
case "number":
|
|
319
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
320
|
+
case "checkbox":
|
|
321
|
+
return typeof value === "boolean";
|
|
322
|
+
case "select":
|
|
323
|
+
return typeof value === "string" && field.options !== void 0 && field.options.includes(value);
|
|
324
|
+
case "person":
|
|
325
|
+
case "files":
|
|
326
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
327
|
+
case "multi_select": {
|
|
328
|
+
const options = field.options;
|
|
329
|
+
return Array.isArray(value) && options !== void 0 && value.every((item) => typeof item === "string" && options.includes(item));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
226
333
|
function resolveWaitUntilMs(options) {
|
|
227
334
|
const now = Date.now();
|
|
228
335
|
if ("at" in options) {
|
|
@@ -344,5 +451,6 @@ function isNodeError(error) {
|
|
|
344
451
|
export {
|
|
345
452
|
access,
|
|
346
453
|
connections,
|
|
454
|
+
input,
|
|
347
455
|
workflow
|
|
348
456
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Workflow input values
|
|
2
|
+
|
|
3
|
+
Use `input` from `@notionhq/apps` to describe a `context.wait.forInput` form:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const response = await context.wait.forInput("Launch details", {
|
|
7
|
+
input: {
|
|
8
|
+
teams: input.multiSelect(["Design", "Engineering"]),
|
|
9
|
+
owners: input.person(),
|
|
10
|
+
window: input.date(),
|
|
11
|
+
attachments: input.files({ optional: true }),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `teams` is an array of the declared options, typed as `("Design" | "Engineering")[]`.
|
|
17
|
+
- `owners` is a `string[]` of all selected user IDs, including when only one person
|
|
18
|
+
is selected. This replaces the previous scalar `string` response.
|
|
19
|
+
- `window` is `{ start: string; end?: string; timeZone?: string }`, not a string.
|
|
20
|
+
Date-only endpoints use `YYYY-MM-DD`; timed endpoints use local `YYYY-MM-DDTHH:mm`
|
|
21
|
+
together with the selected IANA `timeZone`. Ranges retain both endpoints.
|
|
22
|
+
The type is available as `WorkflowInputDate` from `@notionhq/apps/workflow`.
|
|
23
|
+
- `attachments` is a `string[]` with one URL or Notion attachment block ID per item,
|
|
24
|
+
in property order. Attachment display names are not returned as URLs.
|
|
25
|
+
|
|
26
|
+
Empty required multi-select, person, and file inputs keep the request waiting. Empty optional
|
|
27
|
+
inputs are omitted. Selections outside the declared options keep the request waiting
|
|
28
|
+
so the respondent can correct them.
|
package/package.json
CHANGED
package/skills/workflow/SKILL.md
CHANGED
|
@@ -155,6 +155,11 @@ declarations in `access` work too. Importing a declaration alone provisions the
|
|
|
155
155
|
resource but does not create a `context.access` alias. An `access` entry declares
|
|
156
156
|
the needed permission level and a named binding for that NaC resource.
|
|
157
157
|
|
|
158
|
+
For a workflow that uses `triggers.scheduled()`, also declare a "triggers"
|
|
159
|
+
database with a page-added trigger bound to it. This lets the user manually
|
|
160
|
+
trigger a run at any time by adding a page to that database, without waiting
|
|
161
|
+
for the schedule.
|
|
162
|
+
|
|
158
163
|
`access.view()`, `access.comment()`, `access.edit()`, and `access.fullAccess()`
|
|
159
164
|
accept page, database, and data-source handles. `access.call()` accepts custom-agent
|
|
160
165
|
handles. Keys are workflow-local aliases. The handler's
|
package/src/index.ts
CHANGED
package/src/workflow.test.ts
CHANGED
|
@@ -14,9 +14,11 @@ import { page } from "./notion-as-code/page.js";
|
|
|
14
14
|
import {
|
|
15
15
|
access,
|
|
16
16
|
connections,
|
|
17
|
+
input,
|
|
17
18
|
workflow,
|
|
18
19
|
type StepContext,
|
|
19
20
|
type WorkflowContext,
|
|
21
|
+
type WorkflowInputDate,
|
|
20
22
|
type WorkflowWaitDuration,
|
|
21
23
|
} from "./workflow.js";
|
|
22
24
|
|
|
@@ -1017,6 +1019,214 @@ describe("wait.until", () => {
|
|
|
1017
1019
|
});
|
|
1018
1020
|
});
|
|
1019
1021
|
|
|
1022
|
+
describe("wait.forInput", () => {
|
|
1023
|
+
it("stops the workflow with a typed form schema", async () => {
|
|
1024
|
+
let continued = false;
|
|
1025
|
+
const definition = workflow({
|
|
1026
|
+
name: "Collect launch details",
|
|
1027
|
+
description: "Waits for a person to provide launch details",
|
|
1028
|
+
triggers: [triggers.notionPageCreated()],
|
|
1029
|
+
handler: async (_event, context) => {
|
|
1030
|
+
await context.wait.forInput("Launch details", {
|
|
1031
|
+
input: {
|
|
1032
|
+
summary: input.text(),
|
|
1033
|
+
priority: input.select(["low", "medium", "high"]),
|
|
1034
|
+
notes: input.text({ optional: true }),
|
|
1035
|
+
teams: input.multiSelect(["Design", "Engineering"]),
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
continued = true;
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
|
|
1043
|
+
status: "waiting",
|
|
1044
|
+
wait: {
|
|
1045
|
+
type: "input",
|
|
1046
|
+
name: "Launch details",
|
|
1047
|
+
stepKey: workflowStepKey(["wait", "input", "Launch details"]),
|
|
1048
|
+
input: {
|
|
1049
|
+
summary: { type: "text" },
|
|
1050
|
+
priority: {
|
|
1051
|
+
type: "select",
|
|
1052
|
+
options: ["low", "medium", "high"],
|
|
1053
|
+
},
|
|
1054
|
+
notes: { type: "text", optional: true },
|
|
1055
|
+
teams: { type: "multi_select", options: ["Design", "Engineering"] },
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
expect(continued).toBe(false);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it("returns the typed response stored in its durable checkpoint", async () => {
|
|
1063
|
+
const directory = await createCheckpointDirectory();
|
|
1064
|
+
const stepKey = workflowStepKey(["wait", "input", "Launch details"]);
|
|
1065
|
+
await writeFile(
|
|
1066
|
+
checkpointPath(directory, stepKey),
|
|
1067
|
+
JSON.stringify({
|
|
1068
|
+
version: 0,
|
|
1069
|
+
key: stepKey,
|
|
1070
|
+
value: {
|
|
1071
|
+
value: {
|
|
1072
|
+
summary: "Ship the new workflow",
|
|
1073
|
+
priority: "high",
|
|
1074
|
+
labels: ["Design", "Engineering"],
|
|
1075
|
+
owners: ["first-user-id", "second-user-id"],
|
|
1076
|
+
window: {
|
|
1077
|
+
start: "2026-09-22T09:00",
|
|
1078
|
+
end: "2026-09-23T17:00",
|
|
1079
|
+
timeZone: "America/Los_Angeles",
|
|
1080
|
+
},
|
|
1081
|
+
attachments: ["https://example.com/plan.pdf", "attachment-block-id"],
|
|
1082
|
+
},
|
|
1083
|
+
respondedBy: "user-id",
|
|
1084
|
+
respondedAt: "2026-09-18T12:00:00.000Z",
|
|
1085
|
+
},
|
|
1086
|
+
sourceRunId: "input-run",
|
|
1087
|
+
sourceStepId: "input-step",
|
|
1088
|
+
}),
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
let capturedPriority: "low" | "medium" | "high" | undefined;
|
|
1092
|
+
let capturedInputs: unknown;
|
|
1093
|
+
const definition = workflow({
|
|
1094
|
+
name: "Collect launch details",
|
|
1095
|
+
description: "Waits for a person to provide launch details",
|
|
1096
|
+
triggers: [triggers.notionPageCreated()],
|
|
1097
|
+
handler: async (_event, context) => {
|
|
1098
|
+
const response = await context.wait.forInput("Launch details", {
|
|
1099
|
+
input: {
|
|
1100
|
+
summary: input.text(),
|
|
1101
|
+
priority: input.select(["low", "medium", "high"]),
|
|
1102
|
+
labels: input.multiSelect(["Design", "Engineering"]),
|
|
1103
|
+
owners: input.person(),
|
|
1104
|
+
notes: input.text({ optional: true }),
|
|
1105
|
+
window: input.date(),
|
|
1106
|
+
attachments: input.files({ optional: true }),
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
expectTypeOf(response.value.summary).toEqualTypeOf<string>();
|
|
1110
|
+
expectTypeOf(response.value.priority).toEqualTypeOf<"low" | "medium" | "high">();
|
|
1111
|
+
capturedPriority = response.value.priority;
|
|
1112
|
+
expectTypeOf(response.value.labels).toEqualTypeOf<("Design" | "Engineering")[]>();
|
|
1113
|
+
expectTypeOf(response.value.owners).toEqualTypeOf<string[]>();
|
|
1114
|
+
expectTypeOf(response.value.window).toEqualTypeOf<WorkflowInputDate>();
|
|
1115
|
+
expectTypeOf(response.value.attachments).toEqualTypeOf<string[] | undefined>();
|
|
1116
|
+
capturedInputs = response.value;
|
|
1117
|
+
},
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
|
|
1121
|
+
status: "success",
|
|
1122
|
+
});
|
|
1123
|
+
expect(capturedPriority).toBe("high");
|
|
1124
|
+
expect(capturedInputs).toEqual({
|
|
1125
|
+
summary: "Ship the new workflow",
|
|
1126
|
+
priority: "high",
|
|
1127
|
+
labels: ["Design", "Engineering"],
|
|
1128
|
+
owners: ["first-user-id", "second-user-id"],
|
|
1129
|
+
window: {
|
|
1130
|
+
start: "2026-09-22T09:00",
|
|
1131
|
+
end: "2026-09-23T17:00",
|
|
1132
|
+
timeZone: "America/Los_Angeles",
|
|
1133
|
+
},
|
|
1134
|
+
attachments: ["https://example.com/plan.pdf", "attachment-block-id"],
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it.each([
|
|
1139
|
+
{ extra: { admin: true } },
|
|
1140
|
+
{ extra: { options: { admin: true } } },
|
|
1141
|
+
{ extra: { ["__proto__"]: { admin: true } } },
|
|
1142
|
+
])("rejects undeclared response fields: $extra", async ({ extra }) => {
|
|
1143
|
+
const directory = await createCheckpointDirectory();
|
|
1144
|
+
const stepKey = workflowStepKey(["wait", "input", "Review"]);
|
|
1145
|
+
await writeFile(
|
|
1146
|
+
checkpointPath(directory, stepKey),
|
|
1147
|
+
JSON.stringify({
|
|
1148
|
+
version: 0,
|
|
1149
|
+
key: stepKey,
|
|
1150
|
+
value: {
|
|
1151
|
+
value: { priority: "high", ...extra },
|
|
1152
|
+
respondedBy: "user-id",
|
|
1153
|
+
respondedAt: "2026-09-18T12:00:00.000Z",
|
|
1154
|
+
},
|
|
1155
|
+
sourceRunId: "input-run",
|
|
1156
|
+
sourceStepId: "input-step",
|
|
1157
|
+
}),
|
|
1158
|
+
);
|
|
1159
|
+
const definition = workflow({
|
|
1160
|
+
name: "Review",
|
|
1161
|
+
description: "Rejects undeclared input",
|
|
1162
|
+
triggers: [triggers.notionPageCreated()],
|
|
1163
|
+
handler: async (_event, context) => {
|
|
1164
|
+
await context.wait.forInput("Review", {
|
|
1165
|
+
input: {
|
|
1166
|
+
priority: input.select(["low", "high"]),
|
|
1167
|
+
notes: input.text({ optional: true }),
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
},
|
|
1171
|
+
});
|
|
1172
|
+
await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
|
|
1173
|
+
"does not match its schema",
|
|
1174
|
+
);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it.each([
|
|
1178
|
+
{ name: "unknown select", field: input.select(["low", "medium", "high"]), value: "urgent" },
|
|
1179
|
+
{
|
|
1180
|
+
name: "unknown multi-select",
|
|
1181
|
+
field: input.multiSelect(["Design", "Engineering"]),
|
|
1182
|
+
value: ["Design", "Other"],
|
|
1183
|
+
},
|
|
1184
|
+
{ name: "scalar multi-select", field: input.multiSelect(["Design"]), value: "Design" },
|
|
1185
|
+
{ name: "scalar person", field: input.person(), value: "user-id" },
|
|
1186
|
+
{ name: "invalid person array", field: input.person(), value: ["user-id", 123] },
|
|
1187
|
+
{ name: "date string", field: input.date(), value: "2026-09-22" },
|
|
1188
|
+
{
|
|
1189
|
+
name: "invalid date timezone",
|
|
1190
|
+
field: input.date(),
|
|
1191
|
+
value: { start: "2026-09-22", timeZone: 123 },
|
|
1192
|
+
},
|
|
1193
|
+
])("rejects a checkpoint with $name", async ({ field, value }) => {
|
|
1194
|
+
const directory = await createCheckpointDirectory();
|
|
1195
|
+
const stepKey = workflowStepKey(["wait", "input", "Launch details"]);
|
|
1196
|
+
await writeFile(
|
|
1197
|
+
checkpointPath(directory, stepKey),
|
|
1198
|
+
JSON.stringify({
|
|
1199
|
+
version: 0,
|
|
1200
|
+
key: stepKey,
|
|
1201
|
+
value: {
|
|
1202
|
+
value: { priority: value },
|
|
1203
|
+
respondedBy: "user-id",
|
|
1204
|
+
respondedAt: "2026-09-18T12:00:00.000Z",
|
|
1205
|
+
},
|
|
1206
|
+
sourceRunId: "input-run",
|
|
1207
|
+
sourceStepId: "input-step",
|
|
1208
|
+
}),
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const definition = workflow({
|
|
1212
|
+
name: "Collect launch details",
|
|
1213
|
+
description: "Waits for a person to provide launch details",
|
|
1214
|
+
triggers: [triggers.notionPageCreated()],
|
|
1215
|
+
handler: async (_event, context) => {
|
|
1216
|
+
await context.wait.forInput("Launch details", {
|
|
1217
|
+
input: {
|
|
1218
|
+
priority: field,
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
|
|
1225
|
+
"does not match its schema",
|
|
1226
|
+
);
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1020
1230
|
async function createCheckpointDirectory(): Promise<string> {
|
|
1021
1231
|
checkpointDirectory = await mkdtemp(join(tmpdir(), "apps-step-checkpoints-"));
|
|
1022
1232
|
process.env.NOTION_WORKFLOW_STEP_DIRECTORY = checkpointDirectory;
|
package/src/workflow.ts
CHANGED
|
@@ -53,6 +53,100 @@ export type WorkflowWaitDuration = {
|
|
|
53
53
|
|
|
54
54
|
type WorkflowWaitKey = string | string[];
|
|
55
55
|
|
|
56
|
+
type WorkflowInputFieldOptions = {
|
|
57
|
+
optional?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
declare const workflowInputValueType: unique symbol;
|
|
61
|
+
|
|
62
|
+
export type WorkflowInputField<T> = {
|
|
63
|
+
type: "text" | "number" | "checkbox" | "select" | "multi_select" | "date" | "person" | "files";
|
|
64
|
+
optional?: boolean;
|
|
65
|
+
options?: readonly string[];
|
|
66
|
+
readonly [workflowInputValueType]?: T;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type WorkflowInputValue<
|
|
70
|
+
T,
|
|
71
|
+
TOptions extends WorkflowInputFieldOptions | undefined,
|
|
72
|
+
> = TOptions extends { optional: true } ? T | undefined : T;
|
|
73
|
+
|
|
74
|
+
/** Date-only or local date-time endpoints, with the selected IANA time zone. */
|
|
75
|
+
export type WorkflowInputDate = {
|
|
76
|
+
start: string;
|
|
77
|
+
end?: string;
|
|
78
|
+
timeZone?: string;
|
|
79
|
+
};
|
|
80
|
+
export const input = {
|
|
81
|
+
text<const TOptions extends WorkflowInputFieldOptions | undefined>(
|
|
82
|
+
options?: TOptions,
|
|
83
|
+
): WorkflowInputField<WorkflowInputValue<string, TOptions>> {
|
|
84
|
+
return { type: "text", ...options };
|
|
85
|
+
},
|
|
86
|
+
number<const TOptions extends WorkflowInputFieldOptions | undefined>(
|
|
87
|
+
options?: TOptions,
|
|
88
|
+
): WorkflowInputField<WorkflowInputValue<number, TOptions>> {
|
|
89
|
+
return { type: "number", ...options };
|
|
90
|
+
},
|
|
91
|
+
checkbox<const TOptions extends WorkflowInputFieldOptions | undefined>(
|
|
92
|
+
options?: TOptions,
|
|
93
|
+
): WorkflowInputField<WorkflowInputValue<boolean, TOptions>> {
|
|
94
|
+
return { type: "checkbox", ...options };
|
|
95
|
+
},
|
|
96
|
+
select<
|
|
97
|
+
const TOptions extends readonly [string, ...string[]],
|
|
98
|
+
const TFieldOptions extends WorkflowInputFieldOptions | undefined,
|
|
99
|
+
>(
|
|
100
|
+
values: TOptions,
|
|
101
|
+
options?: TFieldOptions,
|
|
102
|
+
): WorkflowInputField<WorkflowInputValue<TOptions[number], TFieldOptions>> {
|
|
103
|
+
return { type: "select", options: values, ...options };
|
|
104
|
+
},
|
|
105
|
+
date<const TOptions extends WorkflowInputFieldOptions | undefined>(
|
|
106
|
+
options?: TOptions,
|
|
107
|
+
): WorkflowInputField<WorkflowInputValue<WorkflowInputDate, TOptions>> {
|
|
108
|
+
return { type: "date", ...options };
|
|
109
|
+
},
|
|
110
|
+
multiSelect<
|
|
111
|
+
const TOptions extends readonly [string, ...string[]],
|
|
112
|
+
const TFieldOptions extends WorkflowInputFieldOptions | undefined,
|
|
113
|
+
>(
|
|
114
|
+
values: TOptions,
|
|
115
|
+
options?: TFieldOptions,
|
|
116
|
+
): WorkflowInputField<WorkflowInputValue<TOptions[number][], TFieldOptions>> {
|
|
117
|
+
return { type: "multi_select", options: values, ...options };
|
|
118
|
+
},
|
|
119
|
+
/** Returns the user ID of every selected person. */
|
|
120
|
+
person<const TOptions extends WorkflowInputFieldOptions | undefined>(
|
|
121
|
+
options?: TOptions,
|
|
122
|
+
): WorkflowInputField<WorkflowInputValue<string[], TOptions>> {
|
|
123
|
+
return { type: "person", ...options };
|
|
124
|
+
},
|
|
125
|
+
/** Returns each attachment's URL or Notion attachment block ID. */
|
|
126
|
+
files<const TOptions extends WorkflowInputFieldOptions | undefined>(
|
|
127
|
+
options?: TOptions,
|
|
128
|
+
): WorkflowInputField<WorkflowInputValue<string[], TOptions>> {
|
|
129
|
+
return { type: "files", ...options };
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type WorkflowInputSchema = Record<string, WorkflowInputField<unknown>>;
|
|
134
|
+
|
|
135
|
+
export type WorkflowInputValues<TSchema extends WorkflowInputSchema> = {
|
|
136
|
+
[Key in keyof TSchema]: TSchema[Key] extends WorkflowInputField<infer Value> ? Value : never;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type WorkflowInputResponse<TSchema extends WorkflowInputSchema> = {
|
|
140
|
+
value: WorkflowInputValues<TSchema>;
|
|
141
|
+
respondedBy: string;
|
|
142
|
+
respondedAt: string;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type WorkflowWaitForInputOptions<TSchema extends WorkflowInputSchema> = {
|
|
146
|
+
input: TSchema;
|
|
147
|
+
key?: WorkflowWaitKey;
|
|
148
|
+
};
|
|
149
|
+
|
|
56
150
|
export type WorkflowWaitUntilOptions =
|
|
57
151
|
| {
|
|
58
152
|
at: Date;
|
|
@@ -72,7 +166,19 @@ export type WorkflowWaitUntilResult = {
|
|
|
72
166
|
};
|
|
73
167
|
};
|
|
74
168
|
|
|
75
|
-
type
|
|
169
|
+
export type WorkflowWaitForInputResult = {
|
|
170
|
+
status: "waiting";
|
|
171
|
+
wait: {
|
|
172
|
+
type: "input";
|
|
173
|
+
name: string;
|
|
174
|
+
stepKey: string;
|
|
175
|
+
input: WorkflowInputSchema;
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
type WorkflowWaitResult = WorkflowWaitUntilResult | WorkflowWaitForInputResult;
|
|
180
|
+
|
|
181
|
+
type WorkflowHandlerResult = { status: "success" } | WorkflowWaitResult;
|
|
76
182
|
|
|
77
183
|
const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
78
184
|
|
|
@@ -360,6 +466,15 @@ type WorkflowWait = {
|
|
|
360
466
|
* date has been reached.
|
|
361
467
|
*/
|
|
362
468
|
until(name: string, options: WorkflowWaitUntilOptions): Promise<void>;
|
|
469
|
+
/**
|
|
470
|
+
* Suspend the workflow until a person submits values matching the input
|
|
471
|
+
* schema. The returned value is restored from the durable workflow
|
|
472
|
+
* checkpoint when the request is fulfilled.
|
|
473
|
+
*/
|
|
474
|
+
forInput<const TSchema extends WorkflowInputSchema>(
|
|
475
|
+
name: string,
|
|
476
|
+
options: WorkflowWaitForInputOptions<TSchema>,
|
|
477
|
+
): Promise<WorkflowInputResponse<TSchema>>;
|
|
363
478
|
};
|
|
364
479
|
|
|
365
480
|
/** Context passed to a workflow handler. */
|
|
@@ -528,13 +643,22 @@ function createStep(
|
|
|
528
643
|
}
|
|
529
644
|
|
|
530
645
|
class WorkflowWaitInterrupt extends Error {
|
|
531
|
-
constructor(readonly result:
|
|
532
|
-
super(
|
|
646
|
+
constructor(readonly result: WorkflowWaitResult) {
|
|
647
|
+
super(workflowWaitMessage(result));
|
|
533
648
|
this.name = "WorkflowWaitInterrupt";
|
|
534
649
|
}
|
|
535
650
|
}
|
|
536
651
|
|
|
652
|
+
function workflowWaitMessage(result: WorkflowWaitResult): string {
|
|
653
|
+
if (result.wait.type === "until") {
|
|
654
|
+
return `Workflow is waiting until ${new Date(result.wait.resumeAtMs).toISOString()}`;
|
|
655
|
+
}
|
|
656
|
+
return `Workflow is waiting for input: ${result.wait.name}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
537
659
|
function createWait(step: WorkflowStep): WorkflowWait {
|
|
660
|
+
const inputWaitNameByKey = new Map<string, string>();
|
|
661
|
+
|
|
538
662
|
return {
|
|
539
663
|
async until(name, options) {
|
|
540
664
|
const keyInput = options.key ?? name;
|
|
@@ -560,9 +684,108 @@ function createWait(step: WorkflowStep): WorkflowWait {
|
|
|
560
684
|
},
|
|
561
685
|
});
|
|
562
686
|
},
|
|
687
|
+
async forInput(name, options) {
|
|
688
|
+
const keyInput = options.key ?? name;
|
|
689
|
+
const stepKey = createWorkflowStepKey([
|
|
690
|
+
"wait",
|
|
691
|
+
"input",
|
|
692
|
+
...(typeof keyInput === "string" ? [keyInput] : keyInput),
|
|
693
|
+
]);
|
|
694
|
+
const existingName = inputWaitNameByKey.get(stepKey);
|
|
695
|
+
if (existingName !== undefined) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Workflow input wait "${name}" has the same key as "${existingName}". Provide a unique name or an explicit key.`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
inputWaitNameByKey.set(stepKey, name);
|
|
701
|
+
|
|
702
|
+
const checkpoint = await readWorkflowStepCheckpoint(stepKey);
|
|
703
|
+
if (checkpoint) {
|
|
704
|
+
if (!isWorkflowInputResponse(checkpoint.value, options.input)) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`Workflow input checkpoint ${hashWorkflowStepKey(stepKey)} does not match its schema.`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return checkpoint.value;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
throw new WorkflowWaitInterrupt({
|
|
713
|
+
status: "waiting",
|
|
714
|
+
wait: {
|
|
715
|
+
type: "input",
|
|
716
|
+
name,
|
|
717
|
+
stepKey,
|
|
718
|
+
input: options.input,
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
},
|
|
563
722
|
};
|
|
564
723
|
}
|
|
565
724
|
|
|
725
|
+
function isWorkflowInputResponse<TSchema extends WorkflowInputSchema>(
|
|
726
|
+
value: unknown,
|
|
727
|
+
schema: TSchema,
|
|
728
|
+
): value is WorkflowInputResponse<TSchema> {
|
|
729
|
+
if (
|
|
730
|
+
!isRecord(value) ||
|
|
731
|
+
!isRecord(value.value) ||
|
|
732
|
+
typeof value.respondedBy !== "string" ||
|
|
733
|
+
typeof value.respondedAt !== "string"
|
|
734
|
+
) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (Object.keys(value.value).some((key) => !Object.hasOwn(schema, key))) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
743
|
+
const fieldValue = value.value[key];
|
|
744
|
+
if (fieldValue === undefined && field.optional === true) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (!isWorkflowInputFieldValue(fieldValue, field)) {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function isWorkflowInputFieldValue(value: unknown, field: WorkflowInputField<unknown>): boolean {
|
|
755
|
+
switch (field.type) {
|
|
756
|
+
case "text":
|
|
757
|
+
return typeof value === "string";
|
|
758
|
+
case "date":
|
|
759
|
+
return (
|
|
760
|
+
isRecord(value) &&
|
|
761
|
+
typeof value.start === "string" &&
|
|
762
|
+
(value.end === undefined || typeof value.end === "string") &&
|
|
763
|
+
(value.timeZone === undefined || typeof value.timeZone === "string")
|
|
764
|
+
);
|
|
765
|
+
case "number":
|
|
766
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
767
|
+
case "checkbox":
|
|
768
|
+
return typeof value === "boolean";
|
|
769
|
+
case "select":
|
|
770
|
+
return (
|
|
771
|
+
typeof value === "string" &&
|
|
772
|
+
field.options !== undefined &&
|
|
773
|
+
field.options.includes(value)
|
|
774
|
+
);
|
|
775
|
+
case "person":
|
|
776
|
+
case "files":
|
|
777
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
778
|
+
case "multi_select": {
|
|
779
|
+
const options = field.options;
|
|
780
|
+
return (
|
|
781
|
+
Array.isArray(value) &&
|
|
782
|
+
options !== undefined &&
|
|
783
|
+
value.every((item) => typeof item === "string" && options.includes(item))
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
566
789
|
function resolveWaitUntilMs(options: WorkflowWaitUntilOptions): number {
|
|
567
790
|
const now = Date.now();
|
|
568
791
|
if ("at" in options) {
|