@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/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 options = typeof optionsOrFn === "function" ? void 0 : optionsOrFn;
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
- const keyInput = options?.key ?? name;
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 stepState = createWorkflowStepState(metadata, id);
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(await stepState.run(() => fn(context)));
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 (err) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/apps",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "An SDK for building workflow apps for Notion",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -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.
@@ -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;