@notionhq/apps 0.0.39 → 0.0.41
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/README.md +54 -0
- package/dist/error.d.ts +11 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +20 -1
- package/dist/slack.generated.d.ts +194 -0
- package/dist/slack.generated.d.ts.map +1 -1
- package/dist/slack.generated.js +24 -0
- package/dist/workflow.d.ts +41 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +232 -12
- package/package.json +1 -1
- package/skills/workflow/SKILL.md +46 -0
- package/src/connections.test.ts +51 -0
- package/src/error.ts +25 -0
- package/src/slack.generated.ts +236 -0
- package/src/workflow.test.ts +425 -3
- package/src/workflow.ts +398 -15
package/dist/workflow.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createHash } from "node:crypto";
|
|
|
11
11
|
import { readFile } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { createCapabilityContext } from "./context.js";
|
|
14
|
-
import { ExecutionError } from "./error.js";
|
|
14
|
+
import { ExecutionError, FatalError, RateLimitError, RetryableError } from "./error.js";
|
|
15
15
|
import { writeOutput } from "./output.js";
|
|
16
16
|
import { resolveRuntimeInput } from "./runtime-input.js";
|
|
17
17
|
import { readRunMetadata } from "./runtime-metadata.js";
|
|
@@ -47,16 +47,33 @@ const input = {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
50
|
+
import { FatalError as FatalError2, RetryableError as RetryableError2 } from "./error.js";
|
|
50
51
|
import { access } from "./workflow-access.js";
|
|
51
52
|
import {
|
|
52
53
|
connections
|
|
53
54
|
} from "./connections.js";
|
|
55
|
+
const MAX_WORKFLOW_RETRY_ATTEMPTS = 3;
|
|
56
|
+
const MAX_WORKFLOW_DURATION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
57
|
+
const WORKFLOW_STEP_RETRY_BUDGET_MS = 5 * 60 * 1e3;
|
|
58
|
+
const DEFAULT_WORKFLOW_RETRY_POLICY = {
|
|
59
|
+
maxAttempts: 3,
|
|
60
|
+
initialDelayMs: 1e3,
|
|
61
|
+
maxDelayMs: 5 * 60 * 1e3,
|
|
62
|
+
backoffMultiplier: 2
|
|
63
|
+
};
|
|
54
64
|
function workflow(configuration) {
|
|
55
65
|
const requirements = normalizeConnectionRequirements(configuration.connections ?? {});
|
|
56
66
|
const accessRequirements = normalizeWorkflowAccess(configuration.access);
|
|
57
67
|
const connectionDeclarations = configuration.connections === void 0 ? void 0 : structuredClone(configuration.connections);
|
|
58
68
|
const triggers = typeof configuration.triggers === "function" ? configuration.triggers({ events: createWorkflowEvents() }) : configuration.triggers;
|
|
59
69
|
validateTriggerConnections(triggers, requirements);
|
|
70
|
+
validateWorkflowDeadline(configuration.deadline);
|
|
71
|
+
validateWorkflowRetryPolicy(configuration.retry, "Workflow retry policy");
|
|
72
|
+
if (configuration.retry !== void 0) {
|
|
73
|
+
validateStepRetryBudget(configuration.retry, void 0, "Workflow retry policy");
|
|
74
|
+
}
|
|
75
|
+
const deadline = configuration.deadline === void 0 ? void 0 : structuredClone(configuration.deadline);
|
|
76
|
+
const retry = configuration.retry === void 0 ? void 0 : manifestRetryPolicy(configuration.retry);
|
|
60
77
|
const manualTriggers = triggers.filter(
|
|
61
78
|
(trigger) => trigger.type === "workflow.manual"
|
|
62
79
|
);
|
|
@@ -75,9 +92,13 @@ function workflow(configuration) {
|
|
|
75
92
|
description: configuration.description,
|
|
76
93
|
triggers: manifestTriggers(triggers, configuration.verify !== void 0),
|
|
77
94
|
...configuration.connections === void 0 ? {} : { connections: requirements },
|
|
78
|
-
...configuration.access === void 0 ? {} : { access: accessRequirements }
|
|
95
|
+
...configuration.access === void 0 ? {} : { access: accessRequirements },
|
|
96
|
+
...deadline === void 0 ? {} : { deadline },
|
|
97
|
+
...retry === void 0 ? {} : { retry }
|
|
79
98
|
},
|
|
80
99
|
async handler(input2, options) {
|
|
100
|
+
const invocationStartedAt = Date.now();
|
|
101
|
+
const exhaustedStepErrors = /* @__PURE__ */ new WeakSet();
|
|
81
102
|
try {
|
|
82
103
|
input2 = await resolveRuntimeInput(input2);
|
|
83
104
|
if (isVerifyInvocation(input2)) {
|
|
@@ -99,7 +120,11 @@ function workflow(configuration) {
|
|
|
99
120
|
const event = input2;
|
|
100
121
|
const runMetadata = readRunMetadata();
|
|
101
122
|
const baseContext = createCapabilityContext();
|
|
102
|
-
const step = createStep(runMetadata
|
|
123
|
+
const step = createStep(runMetadata, {
|
|
124
|
+
defaultRetry: configuration.retry,
|
|
125
|
+
exhaustedErrors: exhaustedStepErrors,
|
|
126
|
+
invocationStartedAt
|
|
127
|
+
});
|
|
103
128
|
const capabilityContext = {
|
|
104
129
|
...baseContext,
|
|
105
130
|
access: hydrateWorkflowAccess(accessRequirements),
|
|
@@ -137,10 +162,27 @@ function workflow(configuration) {
|
|
|
137
162
|
return;
|
|
138
163
|
}
|
|
139
164
|
const error = new ExecutionError(err);
|
|
165
|
+
const terminal = err instanceof FatalError || isObject(err) && exhaustedStepErrors.has(err) || !(err instanceof RetryableError) && !isRetryableError(err, configuration.retry);
|
|
140
166
|
if (!options?.concreteOutput) {
|
|
141
167
|
writeOutput({
|
|
142
168
|
_tag: "error",
|
|
143
|
-
error: {
|
|
169
|
+
error: !terminal && err instanceof RateLimitError ? {
|
|
170
|
+
_tag: "rate_limit",
|
|
171
|
+
name: err.name,
|
|
172
|
+
message: err.message,
|
|
173
|
+
...err.retryAfter === void 0 ? {} : { retryAfter: err.retryAfter }
|
|
174
|
+
} : !terminal && err instanceof RetryableError ? {
|
|
175
|
+
_tag: "retryable",
|
|
176
|
+
name: err.name,
|
|
177
|
+
message: err.message,
|
|
178
|
+
trace: err.stack,
|
|
179
|
+
...err.retryAfterMs === void 0 ? {} : { retryAfterMs: err.retryAfterMs }
|
|
180
|
+
} : terminal ? {
|
|
181
|
+
_tag: "terminal",
|
|
182
|
+
name: err instanceof Error ? err.name : error.name,
|
|
183
|
+
message: err instanceof Error ? err.message : error.message,
|
|
184
|
+
trace: err instanceof Error ? err.stack : error.stack
|
|
185
|
+
} : {
|
|
144
186
|
name: error.name,
|
|
145
187
|
message: error.message,
|
|
146
188
|
trace: error.stack
|
|
@@ -177,16 +219,20 @@ function writeStepEvent(event, value, startedAt, emittedAt = performance.now())
|
|
|
177
219
|
`
|
|
178
220
|
);
|
|
179
221
|
}
|
|
180
|
-
function createStep(metadata) {
|
|
222
|
+
function createStep(metadata, options) {
|
|
181
223
|
const { runGroupId } = metadata;
|
|
224
|
+
const { defaultRetry, exhaustedErrors, invocationStartedAt } = options;
|
|
182
225
|
const stepNameByKey = /* @__PURE__ */ new Map();
|
|
183
226
|
async function step(name, optionsOrFn, maybeFn) {
|
|
184
|
-
const
|
|
227
|
+
const stepOptions = typeof optionsOrFn === "function" ? void 0 : optionsOrFn;
|
|
185
228
|
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
186
229
|
if (!fn) {
|
|
187
230
|
throw new Error(`Workflow step "${name}" is missing its callback.`);
|
|
188
231
|
}
|
|
189
|
-
|
|
232
|
+
validateWorkflowStepOptions(stepOptions);
|
|
233
|
+
const retry = stepOptions?.retry ?? defaultRetry ?? DEFAULT_WORKFLOW_RETRY_POLICY;
|
|
234
|
+
validateStepRetryBudget(retry, stepOptions?.timeoutMs, `Workflow step "${name}"`);
|
|
235
|
+
const keyInput = stepOptions?.key ?? name;
|
|
190
236
|
const key = createWorkflowStepKey(typeof keyInput === "string" ? [keyInput] : keyInput);
|
|
191
237
|
const existingStepName = stepNameByKey.get(key);
|
|
192
238
|
if (existingStepName !== void 0) {
|
|
@@ -196,9 +242,7 @@ function createStep(metadata) {
|
|
|
196
242
|
}
|
|
197
243
|
stepNameByKey.set(key, name);
|
|
198
244
|
const id = `${runGroupId}:${key}`;
|
|
199
|
-
const
|
|
200
|
-
const context = { id, state: stepState.state };
|
|
201
|
-
const event = { id: context.id, name, key };
|
|
245
|
+
const event = { id, name, key };
|
|
202
246
|
const startedAt = performance.now();
|
|
203
247
|
writeStepEvent("started", event, startedAt, startedAt);
|
|
204
248
|
try {
|
|
@@ -223,7 +267,16 @@ function createStep(metadata) {
|
|
|
223
267
|
);
|
|
224
268
|
return checkpoint.value;
|
|
225
269
|
}
|
|
226
|
-
const value = normalizeWorkflowStepResult(
|
|
270
|
+
const value = normalizeWorkflowStepResult(
|
|
271
|
+
await runWorkflowStepCallback({
|
|
272
|
+
metadata,
|
|
273
|
+
id,
|
|
274
|
+
fn,
|
|
275
|
+
timeoutMs: stepOptions?.timeoutMs,
|
|
276
|
+
retry,
|
|
277
|
+
invocationStartedAt
|
|
278
|
+
})
|
|
279
|
+
);
|
|
227
280
|
writeStepEvent("success", { ...event, value }, startedAt);
|
|
228
281
|
writeStepEvent(
|
|
229
282
|
"completed",
|
|
@@ -234,7 +287,9 @@ function createStep(metadata) {
|
|
|
234
287
|
startedAt
|
|
235
288
|
);
|
|
236
289
|
return value;
|
|
237
|
-
} catch (
|
|
290
|
+
} catch (caught) {
|
|
291
|
+
const deferred = caught instanceof DeferredStepRetry;
|
|
292
|
+
const err = deferred ? caught.error : caught;
|
|
238
293
|
const error = new ExecutionError(err);
|
|
239
294
|
writeStepEvent(
|
|
240
295
|
"failure",
|
|
@@ -256,11 +311,20 @@ function createStep(metadata) {
|
|
|
256
311
|
},
|
|
257
312
|
startedAt
|
|
258
313
|
);
|
|
314
|
+
if (!deferred && isObject(err)) {
|
|
315
|
+
exhaustedErrors?.add(err);
|
|
316
|
+
}
|
|
259
317
|
throw err;
|
|
260
318
|
}
|
|
261
319
|
}
|
|
262
320
|
return step;
|
|
263
321
|
}
|
|
322
|
+
class DeferredStepRetry {
|
|
323
|
+
constructor(error) {
|
|
324
|
+
this.error = error;
|
|
325
|
+
}
|
|
326
|
+
error;
|
|
327
|
+
}
|
|
264
328
|
class WorkflowWaitInterrupt extends Error {
|
|
265
329
|
constructor(result) {
|
|
266
330
|
super(workflowWaitMessage(result));
|
|
@@ -430,6 +494,160 @@ function waitDurationUnitMultiplier(unit) {
|
|
|
430
494
|
throw new Error(`Unsupported workflow wait duration unit: ${unit}`);
|
|
431
495
|
}
|
|
432
496
|
}
|
|
497
|
+
async function runWorkflowStepCallback(args) {
|
|
498
|
+
let attempt = 1;
|
|
499
|
+
while (true) {
|
|
500
|
+
const stepState = createWorkflowStepState(args.metadata, args.id);
|
|
501
|
+
const context = { id: args.id, state: stepState.state };
|
|
502
|
+
try {
|
|
503
|
+
return await stepState.run(() => runWithTimeout(args.fn(context), args.timeoutMs));
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (attempt >= args.retry.maxAttempts || !isRetryableError(error, args.retry)) {
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
const delayMs = explicitRetryDelay(error) ?? calculateRetryDelay(args.retry, attempt - 1);
|
|
509
|
+
const nextAttemptBudgetMs = delayMs + (args.timeoutMs ?? 0);
|
|
510
|
+
if (Date.now() - args.invocationStartedAt + nextAttemptBudgetMs > WORKFLOW_STEP_RETRY_BUDGET_MS) {
|
|
511
|
+
throw new DeferredStepRetry(error);
|
|
512
|
+
}
|
|
513
|
+
if (delayMs > 0) {
|
|
514
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
515
|
+
}
|
|
516
|
+
attempt += 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function isRetryableError(error, policy) {
|
|
521
|
+
if (error instanceof FatalError) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
if (error instanceof RetryableError || policy?.retryOn === void 0) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
const matchers = typeof policy.retryOn === "function" ? [policy.retryOn] : policy.retryOn;
|
|
528
|
+
return matchers.some(
|
|
529
|
+
(matcher) => isErrorClass(matcher) ? error instanceof matcher : matcher(error)
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
function isErrorClass(matcher) {
|
|
533
|
+
return matcher === Error || matcher.prototype instanceof Error;
|
|
534
|
+
}
|
|
535
|
+
function explicitRetryDelay(error) {
|
|
536
|
+
if (error instanceof RetryableError) {
|
|
537
|
+
return error.retryAfterMs;
|
|
538
|
+
}
|
|
539
|
+
if (error instanceof RateLimitError && error.retryAfter !== void 0) {
|
|
540
|
+
return error.retryAfter * 1e3;
|
|
541
|
+
}
|
|
542
|
+
return void 0;
|
|
543
|
+
}
|
|
544
|
+
async function runWithTimeout(value, timeoutMs) {
|
|
545
|
+
if (timeoutMs === void 0) {
|
|
546
|
+
return await value;
|
|
547
|
+
}
|
|
548
|
+
let timer;
|
|
549
|
+
try {
|
|
550
|
+
return await Promise.race([
|
|
551
|
+
value,
|
|
552
|
+
new Promise((_resolve, reject) => {
|
|
553
|
+
timer = setTimeout(() => {
|
|
554
|
+
const error = new Error(`Workflow step timed out after ${timeoutMs}ms.`);
|
|
555
|
+
error.name = "StepTimeoutError";
|
|
556
|
+
reject(error);
|
|
557
|
+
}, timeoutMs);
|
|
558
|
+
})
|
|
559
|
+
]);
|
|
560
|
+
} finally {
|
|
561
|
+
if (timer !== void 0) {
|
|
562
|
+
clearTimeout(timer);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function calculateRetryDelay(policy, retryIndex) {
|
|
567
|
+
const delayMs = maxRetryDelay(policy, retryIndex);
|
|
568
|
+
return delayMs / 2 + Math.random() * (delayMs / 2);
|
|
569
|
+
}
|
|
570
|
+
function maxRetryDelay(policy, retryIndex) {
|
|
571
|
+
const initialDelayMs = policy.initialDelayMs ?? DEFAULT_WORKFLOW_RETRY_POLICY.initialDelayMs;
|
|
572
|
+
const maxDelayMs = policy.maxDelayMs ?? DEFAULT_WORKFLOW_RETRY_POLICY.maxDelayMs;
|
|
573
|
+
const backoffMultiplier = policy.backoffMultiplier ?? DEFAULT_WORKFLOW_RETRY_POLICY.backoffMultiplier;
|
|
574
|
+
return Math.min(initialDelayMs * backoffMultiplier ** retryIndex, maxDelayMs);
|
|
575
|
+
}
|
|
576
|
+
function validateStepRetryBudget(policy, timeoutMs, label) {
|
|
577
|
+
let worstCaseMs = (timeoutMs ?? 0) * policy.maxAttempts;
|
|
578
|
+
for (let retryIndex = 0; retryIndex < policy.maxAttempts - 1; retryIndex++) {
|
|
579
|
+
worstCaseMs += maxRetryDelay(policy, retryIndex);
|
|
580
|
+
}
|
|
581
|
+
if (worstCaseMs > WORKFLOW_STEP_RETRY_BUDGET_MS) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`${label} can spend up to ${worstCaseMs}ms on retry delays and timeouts, which exceeds the ${WORKFLOW_STEP_RETRY_BUDGET_MS}ms limit for one workflow invocation. Lower maxAttempts, the retry delays, or timeoutMs, or use context.wait.until() for longer waits.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function manifestRetryPolicy(policy) {
|
|
588
|
+
const { retryOn: _retryOn, ...rest } = policy;
|
|
589
|
+
return structuredClone(rest);
|
|
590
|
+
}
|
|
591
|
+
function validateWorkflowDeadline(deadline) {
|
|
592
|
+
if (deadline === void 0) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
assertPositiveFiniteNumber(deadline.afterMs, "Workflow deadline afterMs");
|
|
596
|
+
assertAtMost(deadline.afterMs, MAX_WORKFLOW_DURATION_MS, "Workflow deadline afterMs");
|
|
597
|
+
}
|
|
598
|
+
function validateWorkflowRetryPolicy(policy, label) {
|
|
599
|
+
if (policy === void 0) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!Number.isInteger(policy.maxAttempts) || policy.maxAttempts < 1 || policy.maxAttempts > MAX_WORKFLOW_RETRY_ATTEMPTS) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`${label} maxAttempts must be an integer between 1 and ${MAX_WORKFLOW_RETRY_ATTEMPTS}.`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (policy.initialDelayMs !== void 0) {
|
|
608
|
+
assertNonNegativeFiniteNumber(policy.initialDelayMs, `${label} initialDelayMs`);
|
|
609
|
+
assertAtMost(policy.initialDelayMs, MAX_WORKFLOW_DURATION_MS, `${label} initialDelayMs`);
|
|
610
|
+
}
|
|
611
|
+
if (policy.maxDelayMs !== void 0) {
|
|
612
|
+
assertNonNegativeFiniteNumber(policy.maxDelayMs, `${label} maxDelayMs`);
|
|
613
|
+
assertAtMost(policy.maxDelayMs, MAX_WORKFLOW_DURATION_MS, `${label} maxDelayMs`);
|
|
614
|
+
}
|
|
615
|
+
if (policy.retryOn !== void 0 && typeof policy.retryOn !== "function" && (!Array.isArray(policy.retryOn) || policy.retryOn.some((matcher) => typeof matcher !== "function"))) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`${label} retryOn must be an error class, a predicate, or an array of them.`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (policy.initialDelayMs !== void 0 && policy.maxDelayMs !== void 0 && policy.maxDelayMs < policy.initialDelayMs) {
|
|
621
|
+
throw new Error(`${label} maxDelayMs must be at least initialDelayMs.`);
|
|
622
|
+
}
|
|
623
|
+
if (policy.backoffMultiplier !== void 0 && (!Number.isFinite(policy.backoffMultiplier) || policy.backoffMultiplier < 1)) {
|
|
624
|
+
throw new Error(`${label} backoffMultiplier must be at least one.`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function validateWorkflowStepOptions(options) {
|
|
628
|
+
if (options?.timeoutMs !== void 0) {
|
|
629
|
+
assertPositiveFiniteNumber(options.timeoutMs, "Workflow step timeoutMs");
|
|
630
|
+
}
|
|
631
|
+
validateWorkflowRetryPolicy(options?.retry, "Workflow step retry policy");
|
|
632
|
+
}
|
|
633
|
+
function assertAtMost(value, max, label) {
|
|
634
|
+
if (value > max) {
|
|
635
|
+
throw new Error(`${label} must be at most ${max}.`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function isObject(value) {
|
|
639
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
640
|
+
}
|
|
641
|
+
function assertPositiveFiniteNumber(value, label) {
|
|
642
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
643
|
+
throw new Error(`${label} must be greater than zero.`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function assertNonNegativeFiniteNumber(value, label) {
|
|
647
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
648
|
+
throw new Error(`${label} must be zero or greater.`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
433
651
|
function createWorkflowStepKey(segments) {
|
|
434
652
|
if (segments.length === 0) {
|
|
435
653
|
throw new Error("Workflow step keys must contain at least one segment.");
|
|
@@ -495,6 +713,8 @@ function isNodeError(error) {
|
|
|
495
713
|
return error instanceof Error && "code" in error;
|
|
496
714
|
}
|
|
497
715
|
export {
|
|
716
|
+
FatalError2 as FatalError,
|
|
717
|
+
RetryableError2 as RetryableError,
|
|
498
718
|
access,
|
|
499
719
|
connections,
|
|
500
720
|
input,
|
package/package.json
CHANGED
package/skills/workflow/SKILL.md
CHANGED
|
@@ -99,6 +99,52 @@ await context.step("Process page", { key: ["process-page", page.id] }, async ({
|
|
|
99
99
|
|
|
100
100
|
Keys must be stable across retries and unique within one workflow run.
|
|
101
101
|
|
|
102
|
+
Configure a workflow deadline and retry policy only when the platform defaults do
|
|
103
|
+
not fit the workflow:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
export default workflow({
|
|
107
|
+
name: "Charge customer",
|
|
108
|
+
description: "Charges a customer when an invoice is created",
|
|
109
|
+
triggers: [events.notionPageCreated()],
|
|
110
|
+
deadline: { afterMs: 7 * 24 * 60 * 60 * 1000 },
|
|
111
|
+
retry: {
|
|
112
|
+
maxAttempts: 3,
|
|
113
|
+
initialDelayMs: 5_000,
|
|
114
|
+
maxDelayMs: 5 * 60 * 1000,
|
|
115
|
+
backoffMultiplier: 2,
|
|
116
|
+
},
|
|
117
|
+
handler: async (_event, context) => {
|
|
118
|
+
await context.step(
|
|
119
|
+
"Charge customer",
|
|
120
|
+
{ timeoutMs: 30_000, retry: { maxAttempts: 2 } },
|
|
121
|
+
chargeCustomer,
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The workflow `retry` policy is the default for every step; a step's `retry`
|
|
128
|
+
replaces it rather than stacking. The run itself is retried only for failures
|
|
129
|
+
outside a step. A step that fails after using its retries fails the run.
|
|
130
|
+
|
|
131
|
+
`maxAttempts` includes the initial attempt and is at most 4. Without any policy,
|
|
132
|
+
steps make 3 attempts with jittered backoff starting at 1 second, doubling, and
|
|
133
|
+
capped at 5 minutes. A step retries ordinary errors and `RetryableError`, but
|
|
134
|
+
stops immediately for `FatalError`. Set `retryOn` to an error class, a predicate,
|
|
135
|
+
or an array of them to retry only matching errors, for example
|
|
136
|
+
`retryOn: TimeoutError` or `retryOn: [TimeoutError, RateLimitError]`. Pass
|
|
137
|
+
`retryAfterMs` to `RetryableError` when the failing service provides an explicit
|
|
138
|
+
delay. Workflow deadlines include retry delays and are at most 7 days, and step
|
|
139
|
+
timeouts apply to one callback attempt.
|
|
140
|
+
|
|
141
|
+
Step retries wait inside one workflow invocation, which runs for at most 5
|
|
142
|
+
minutes. The longest possible retry delays plus `timeoutMs` for every attempt
|
|
143
|
+
must fit in 5 minutes, for step policies and for the workflow `retry` policy
|
|
144
|
+
that steps inherit. Earlier work in the invocation counts against the same 5
|
|
145
|
+
minutes; a retry that no longer fits is left to a run retry. Use
|
|
146
|
+
`context.wait.until()` for longer waits.
|
|
147
|
+
|
|
102
148
|
An external effect can succeed before its step result is saved. Pass the
|
|
103
149
|
callback `id` as an idempotency key when supported; otherwise use a stable
|
|
104
150
|
external ID, upsert, or duplicate check.
|
package/src/connections.test.ts
CHANGED
|
@@ -143,6 +143,57 @@ describe("workflow connection runtime", () => {
|
|
|
143
143
|
expect(Reflect.get(runtime, "toString")).toBeUndefined();
|
|
144
144
|
expect(runtime.support).toBe(runtime.support);
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
it("sends newly exposed Slack actions through the declared binding", async () => {
|
|
148
|
+
vi.stubEnv(
|
|
149
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
150
|
+
'[{"key":"support","type":"slack","id":"support-id"}]',
|
|
151
|
+
);
|
|
152
|
+
const notion = new Client({ auth: "test" });
|
|
153
|
+
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: {} });
|
|
154
|
+
const slack = createWorkflowConnections(notion, {
|
|
155
|
+
support: connections.slack(),
|
|
156
|
+
}).support;
|
|
157
|
+
|
|
158
|
+
await slack.getUser({ userId: "U123" });
|
|
159
|
+
await slack.getUserMessages({ email: "person@example.com" });
|
|
160
|
+
await slack.parseSlackUriOrUrl({
|
|
161
|
+
uriOrUrl: "https://example.slack.com/archives/C123/p123",
|
|
162
|
+
});
|
|
163
|
+
await slack.addReactionToMessage({
|
|
164
|
+
prefixedMessageUri: "slackMessage://C123/123",
|
|
165
|
+
reaction: "eyes",
|
|
166
|
+
});
|
|
167
|
+
await slack.removeReactionFromMessage({
|
|
168
|
+
prefixedMessageUri: "slackMessage://C123/123",
|
|
169
|
+
reaction: "eyes",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(request.mock.calls.map(([input]) => input.body)).toEqual([
|
|
173
|
+
{ connection: "support-id", action: "getUser", args: { userId: "U123" } },
|
|
174
|
+
{
|
|
175
|
+
connection: "support-id",
|
|
176
|
+
action: "getUserMessages",
|
|
177
|
+
args: { email: "person@example.com" },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
connection: "support-id",
|
|
181
|
+
action: "parseSlackUriOrUrl",
|
|
182
|
+
args: { uriOrUrl: "https://example.slack.com/archives/C123/p123" },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
connection: "support-id",
|
|
186
|
+
action: "addReactionToMessage",
|
|
187
|
+
args: { prefixedMessageUri: "slackMessage://C123/123", reaction: "eyes" },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
connection: "support-id",
|
|
191
|
+
action: "removeReactionFromMessage",
|
|
192
|
+
args: { prefixedMessageUri: "slackMessage://C123/123", reaction: "eyes" },
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
|
|
146
197
|
it("does not parse missing or malformed bindings until a declared client is used", () => {
|
|
147
198
|
vi.stubEnv("NOTION_CONNECTION_BINDINGS", "not json");
|
|
148
199
|
const runtime = createWorkflowConnections(new Client(), { support: connections.slack() });
|
package/src/error.ts
CHANGED
|
@@ -11,6 +11,31 @@ export class ExecutionError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Explicitly permit another workflow or step attempt. */
|
|
15
|
+
export class RetryableError extends Error {
|
|
16
|
+
readonly retryAfterMs: number | undefined;
|
|
17
|
+
|
|
18
|
+
constructor(message: string, options?: { retryAfterMs?: number }) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "RetryableError";
|
|
21
|
+
if (
|
|
22
|
+
options?.retryAfterMs !== undefined &&
|
|
23
|
+
(!Number.isFinite(options.retryAfterMs) || options.retryAfterMs < 0)
|
|
24
|
+
) {
|
|
25
|
+
throw new Error("RetryableError retryAfterMs must be zero or greater.");
|
|
26
|
+
}
|
|
27
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Skip remaining workflow or step attempts. */
|
|
32
|
+
export class FatalError extends Error {
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "FatalError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
/** Tell the platform that an external service asked the sync to slow down. */
|
|
15
40
|
export class RateLimitError extends ExecutionError {
|
|
16
41
|
readonly retryAfter: number | undefined;
|