windmill-client 1.804.0 → 1.806.0

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.js CHANGED
@@ -126,7 +126,7 @@ const OpenAPI = {
126
126
  PASSWORD: void 0,
127
127
  TOKEN: getEnv$1("WM_TOKEN"),
128
128
  USERNAME: void 0,
129
- VERSION: "1.804.0",
129
+ VERSION: "1.806.0",
130
130
  WITH_CREDENTIALS: !getEnv$1("WM_RAW_APP"),
131
131
  interceptors: {
132
132
  request: new Interceptors(),
@@ -1655,6 +1655,59 @@ var GitSyncService = class {
1655
1655
  });
1656
1656
  }
1657
1657
  /**
1658
+ * List the GitLab projects a token can sync
1659
+ * Lists the projects the supplied GitLab token can push to, so a git repository resource can be filled in without hand-writing a project path. The token is used for this call only and is never stored. Requires workspace admin.
1660
+ * @param data The data for the request.
1661
+ * @param data.workspace
1662
+ * @param data.requestBody
1663
+ * @returns GitlabProject the projects the token can sync
1664
+ * @throws ApiError
1665
+ */
1666
+ static listGitlabProjects(data) {
1667
+ return request(OpenAPI, {
1668
+ method: "POST",
1669
+ url: "/w/{workspace}/git_sync/gitlab/projects",
1670
+ path: { workspace: data.workspace },
1671
+ body: data.requestBody,
1672
+ mediaType: "application/json"
1673
+ });
1674
+ }
1675
+ /**
1676
+ * Where a repository's credential comes from
1677
+ * Whether Windmill holds this repository's access token, and which host it talks to. `held` means this workspace stores it, `borrowed` means an ancestor does and it is not this workspace's to replace. Both absent means the repository authenticates with whatever its URL carries. Returns no secret. Requires workspace admin.
1678
+ * @param data The data for the request.
1679
+ * @param data.workspace
1680
+ * @param data.path Path of the git repository resource, with or without the `$res:` prefix. A path rather than a URL, because a resource URL may carry a token and a URL in a query string lands in logs.
1681
+ * @returns unknown where the credential comes from
1682
+ * @throws ApiError
1683
+ */
1684
+ static getCredentialOrigin(data) {
1685
+ return request(OpenAPI, {
1686
+ method: "GET",
1687
+ url: "/w/{workspace}/git_sync/credential/origin",
1688
+ path: { workspace: data.workspace },
1689
+ query: { path: data.path }
1690
+ });
1691
+ }
1692
+ /**
1693
+ * Store the credential for a git repository
1694
+ * Stores the access token a git repository authenticates with, so it does not have to be written into the repository URL or a workspace variable. The token is write-only: it is served only to a git-sync job that presents its own job token, and a fork of this workspace reads this copy instead of holding one of its own. Requires workspace admin.
1695
+ * @param data The data for the request.
1696
+ * @param data.workspace
1697
+ * @param data.requestBody
1698
+ * @returns string the credential was stored
1699
+ * @throws ApiError
1700
+ */
1701
+ static setGitCredential(data) {
1702
+ return request(OpenAPI, {
1703
+ method: "POST",
1704
+ url: "/w/{workspace}/git_sync/credential",
1705
+ path: { workspace: data.workspace },
1706
+ body: data.requestBody,
1707
+ mediaType: "application/json"
1708
+ });
1709
+ }
1710
+ /**
1658
1711
  * GHES installation callback
1659
1712
  * Register a self-managed GitHub App installation from GitHub Enterprise Server
1660
1713
  * @param data The data for the request.
@@ -1950,11 +2003,11 @@ var WorkspaceService = class {
1950
2003
  });
1951
2004
  }
1952
2005
  /**
1953
- * get github app token
2006
+ * get the git credential for a git-sync job (GitHub App token, or the credential stored for the repository)
1954
2007
  * @param data The data for the request.
1955
2008
  * @param data.workspace
1956
2009
  * @param data.requestBody jwt job token
1957
- * @returns unknown github app token
2010
+ * @returns unknown git credential
1958
2011
  * @throws ApiError
1959
2012
  */
1960
2013
  static getGithubAppToken(data) {
@@ -5677,6 +5730,52 @@ var ResourceService = class {
5677
5730
  });
5678
5731
  }
5679
5732
  /**
5733
+ * count the workspace's resources per resource type
5734
+ * @param data The data for the request.
5735
+ * @param data.workspace
5736
+ * @returns unknown resource count per resource_type
5737
+ * @throws ApiError
5738
+ */
5739
+ static listResourceCountsByType(data) {
5740
+ return request(OpenAPI, {
5741
+ method: "GET",
5742
+ url: "/w/{workspace}/resources/type/resource_counts",
5743
+ path: { workspace: data.workspace }
5744
+ });
5745
+ }
5746
+ /**
5747
+ * list what the hub knows about its resource types
5748
+ * @param data The data for the request.
5749
+ * @param data.workspace
5750
+ * @returns unknown each hub resource type with its integration and pick count, empty if the hub answers neither read
5751
+ * @throws ApiError
5752
+ */
5753
+ static listHubResourceTypeInfo(data) {
5754
+ return request(OpenAPI, {
5755
+ method: "GET",
5756
+ url: "/w/{workspace}/resources/type/hub/info",
5757
+ path: { workspace: data.workspace }
5758
+ });
5759
+ }
5760
+ /**
5761
+ * record a hub resource type pick
5762
+ * @param data The data for the request.
5763
+ * @param data.workspace
5764
+ * @param data.name
5765
+ * @returns unknown whether the hub recorded the pick
5766
+ * @throws ApiError
5767
+ */
5768
+ static pickHubResourceType(data) {
5769
+ return request(OpenAPI, {
5770
+ method: "POST",
5771
+ url: "/w/{workspace}/resources/type/hub/pick/{name}",
5772
+ path: {
5773
+ workspace: data.workspace,
5774
+ name: data.name
5775
+ }
5776
+ });
5777
+ }
5778
+ /**
5680
5779
  * query resource types by similarity
5681
5780
  * @param data The data for the request.
5682
5781
  * @param data.workspace
@@ -11013,6 +11112,29 @@ var JobService = class {
11013
11112
  });
11014
11113
  }
11015
11114
  /**
11115
+ * Get relations' project column lineage as one run saw it
11116
+ * The same answer as `assets/column_lineage`, for the project version a single job ran — including the dbt editor's parse of its own buffer, whose graph belongs to that job and is reachable no other way. One project answers here, the one the run is of, since the graph this annotates is that project's too. Authorized through the job, the same gate as `dbt_graph`. Reaching the run is not on its own enough to read the project: a caller with no access to the script gets its relations and `ref()` edges from `dbt_graph` and an empty answer here, exactly as that endpoint redacts the model's SQL.
11117
+ *
11118
+ * @param data The data for the request.
11119
+ * @param data.workspace
11120
+ * @param data.id The job whose graph the lineage is read from
11121
+ * @param data.assetPath The `dbt://` relations whose lineage to return. Repeated, once per relation, and answered as one union. At least one, and at most 1000 — a request naming none, or more than that, is refused rather than answered with an empty component.
11122
+ *
11123
+ * @returns DbtColumnLineage the relations' column-level lineage
11124
+ * @throws ApiError
11125
+ */
11126
+ static getDbtRunColumnLineage(data) {
11127
+ return request(OpenAPI, {
11128
+ method: "GET",
11129
+ url: "/w/{workspace}/jobs/dbt_column_lineage/{id}",
11130
+ path: {
11131
+ workspace: data.workspace,
11132
+ id: data.id
11133
+ },
11134
+ query: { asset_path: data.assetPath }
11135
+ });
11136
+ }
11137
+ /**
11016
11138
  * List the per-relation progress one job has recorded so far
11017
11139
  * Live materialization state for the relations a single job writes, keyed by relation. Polled while a run is in flight so a graph can move a node as its model completes. Authorized through the job, so a caller who may not read it is refused rather than told the run has no progress. An empty list means the job exists and has recorded nothing yet, or is unknown to this workspace.
11018
11140
  *
@@ -12966,6 +13088,33 @@ var NativeTriggerService = class {
12966
13088
  });
12967
13089
  }
12968
13090
  /**
13091
+ * set enabled state of native trigger
13092
+ * Enables or disables a native trigger. A disabled trigger stays registered on the
13093
+ * external service but starts no job when it fires.
13094
+ * Requires write access to the script or flow that the trigger is associated with.
13095
+ *
13096
+ * @param data The data for the request.
13097
+ * @param data.workspace
13098
+ * @param data.serviceName
13099
+ * @param data.externalId The external ID of the trigger from the external service
13100
+ * @param data.requestBody updated enabled state
13101
+ * @returns string native trigger enabled state updated
13102
+ * @throws ApiError
13103
+ */
13104
+ static setNativeTriggerEnabled(data) {
13105
+ return request(OpenAPI, {
13106
+ method: "POST",
13107
+ url: "/w/{workspace}/native_triggers/{service_name}/setenabled/{external_id}",
13108
+ path: {
13109
+ workspace: data.workspace,
13110
+ service_name: data.serviceName,
13111
+ external_id: data.externalId
13112
+ },
13113
+ body: data.requestBody,
13114
+ mediaType: "application/json"
13115
+ });
13116
+ }
13117
+ /**
12969
13118
  * list native triggers
12970
13119
  * Lists all native triggers for the specified service in the workspace.
12971
13120
  * @param data The data for the request.
@@ -16751,6 +16900,33 @@ var AssetService = class {
16751
16900
  });
16752
16901
  }
16753
16902
  /**
16903
+ * Column-level lineage of a set of dbt relations
16904
+ * The direct (`copy` / `mod`) column-to-column lineage the given relations' columns sit in — the connected component around them, from the engine's static analysis. Not their own edges, which would stop one hop out since a column trace walks transitively, and not a whole project's, which carries model families the selection cannot reach.
16905
+ * Several relations, answered as one union, because one selection reaches several: a script's output column can derive from columns of several dbt models. Unpinned, the component crosses projects — a relation one project produces is another's source — and the caller's access is decided again for every project it reaches, so a trace ends where their grants do. A pinned answer, by version here or by job on the run route, is one project's.
16906
+ * Its own endpoint rather than a field on the asset graph: the graph is folder-wide and polled by a run page, while this is rendered for one selection at a time. Empty for projects that did not opt into the analysis pass (`column_lineage: true`), which is the ordinary case. The indirect `scan` kind is stored but never served: it reaches every output column of its model.
16907
+ *
16908
+ * @param data The data for the request.
16909
+ * @param data.workspace
16910
+ * @param data.assetPath The `dbt://` relations whose lineage to return. Repeated, once per relation, and answered as one union. At least one, and at most 1000 — a request naming none, or more than that, is refused rather than answered with an empty component.
16911
+ *
16912
+ * @param data.dbtScriptHash The deployed version a view is drawing, when it is drawing one — the dbt editor, which shows a single project as of a single deploy. A version-pinned answer is that version's project alone, the same as a job-pinned one, and only the unpinned answer crosses projects: a pin says which stored graph is on screen, and another project's live graph is not part of it.
16913
+ * A run's or an editor buffer's own graph is not reachable here: that pins to a job, and costs the job-read gate — see `jobs/dbt_column_lineage/{id}`.
16914
+ *
16915
+ * @returns DbtColumnLineage the relations' column-level lineage
16916
+ * @throws ApiError
16917
+ */
16918
+ static getDbtColumnLineage(data) {
16919
+ return request(OpenAPI, {
16920
+ method: "GET",
16921
+ url: "/w/{workspace}/assets/column_lineage",
16922
+ path: { workspace: data.workspace },
16923
+ query: {
16924
+ asset_path: data.assetPath,
16925
+ dbt_script_hash: data.dbtScriptHash
16926
+ }
16927
+ });
16928
+ }
16929
+ /**
16754
16930
  * List every workspace DuckDB macro (deployed `// macros` libraries)
16755
16931
  * @param data The data for the request.
16756
16932
  * @param data.workspace
@@ -18975,6 +19151,35 @@ function checkpointableResult(value) {
18975
19151
  function jsonRoundTrip(value) {
18976
19152
  return JSON.parse(encodeCheckpointPayload({ value: checkpointableResult(value) })).value;
18977
19153
  }
19154
+ /** The worker deserializes a sleep into a `u32` of seconds and fails the whole
19155
+ * job on anything wider, so a delay a multiplier has run away with has to be
19156
+ * capped here rather than sent. */
19157
+ const MAX_SLEEP_SECONDS = 4294967295;
19158
+ /** Every attempt claims its keys before the first one is dispatched, so an
19159
+ * unbounded `attempts` is a workflow that hangs allocating rather than a very
19160
+ * patient one. */
19161
+ const MAX_RETRY_ATTEMPTS = 100;
19162
+ /** Rejected where the policy is written, so a workflow fails at its first line
19163
+ * rather than mid-run on a replay. */
19164
+ function assertUsableRetry(retry) {
19165
+ if (retry === void 0) return;
19166
+ const { attempts } = retry;
19167
+ if (!Number.isInteger(attempts) || attempts < 0 || attempts > MAX_RETRY_ATTEMPTS) throw new Error(`retry.attempts must be a whole number between 0 and ${MAX_RETRY_ATTEMPTS}, got ${attempts}`);
19168
+ }
19169
+ /** How many retries the policy asks for, defended against a value that reached
19170
+ * `_nextStep` without going through `assertUsableRetry`. */
19171
+ function retryAttempts(retry) {
19172
+ const attempts = Math.trunc(retry?.attempts ?? 0) || 0;
19173
+ return Math.min(Math.max(attempts, 0), MAX_RETRY_ATTEMPTS);
19174
+ }
19175
+ /** Seconds to wait before retry number `attempt` (0 is the first retry). */
19176
+ function retryDelaySeconds(retry, attempt) {
19177
+ const base = retry.delay ?? 0;
19178
+ if (!(base > 0)) return 0;
19179
+ const grown = base * Math.pow(retry.multiplier ?? 1, attempt);
19180
+ const seconds = Math.floor(Math.min(retry.max_delay ?? grown, grown, MAX_SLEEP_SECONDS));
19181
+ return seconds > 0 ? seconds : 0;
19182
+ }
18978
19183
  /** A step key travels as one path segment when its URLs are minted, so it must be
18979
19184
  * non-empty and free of `/` and dot segments — otherwise `waitForApproval` would
18980
19185
  * accept a key `getApprovalUrls` can never address. */
@@ -19006,6 +19211,11 @@ var WorkflowCtx = class {
19006
19211
  * into a `complete` — the parent would then record the caught branch's value as
19007
19212
  * a successful step. Boxed: the thrown value may be any falsy value. */
19008
19213
  _pendingStepFailure = null;
19214
+ /** Failed tasks whose rejection nothing has consumed, by step key. An unawaited
19215
+ * task is still dispatched and still fails, but nothing drives the rejecting
19216
+ * thenable it returned. The first `.then()` on that thenable drops the entry,
19217
+ * so what remains is only what the body never looked at. */
19218
+ _unobservedTaskFailures = new Map();
19009
19219
  /** When set, the task matching this key executes its inner function directly */
19010
19220
  _executingKey;
19011
19221
  /** Serializes fast-path POSTs across concurrent step() calls within one
@@ -19039,52 +19249,90 @@ var WorkflowCtx = class {
19039
19249
  }
19040
19250
  _nextStep(name, script, args = {}, dispatch_type = "inline", options) {
19041
19251
  this._rethrowSwallowed();
19042
- const key = this._allocKey(name || script || "step");
19043
- if (key in this.completed) {
19044
- const value = this.completed[key];
19045
- if (value && typeof value === "object" && value.__wmill_error) {
19046
- const err = taskErrorFromMarker(value, `Task '${name}' failed`);
19047
- return { then: (_resolve, reject) => {
19048
- if (reject) reject(err);
19049
- else throw err;
19050
- } };
19252
+ const stepName = name || script || "step";
19253
+ const maxRetries = retryAttempts(options?.retry);
19254
+ const baseKey = this._allocKey(stepName);
19255
+ const attemptKeys = [baseKey];
19256
+ const backoffKeys = [];
19257
+ for (let i = 0; i < maxRetries; i++) {
19258
+ backoffKeys.push(this._allocKey(`${baseKey}#retry${i + 2}`));
19259
+ attemptKeys.push(this._allocKey(`${baseKey}#${i + 2}`));
19260
+ }
19261
+ for (let attempt = 0;; attempt++) {
19262
+ const key = attemptKeys[attempt];
19263
+ if (key in this.completed) {
19264
+ const value = this.completed[key];
19265
+ if (value && typeof value === "object" && value.__wmill_error) {
19266
+ if (attempt < maxRetries) {
19267
+ this._retryBackoff(backoffKeys[attempt], baseKey, options.retry, attempt);
19268
+ continue;
19269
+ }
19270
+ const err = taskErrorFromMarker(value, `Task '${name}' failed`);
19271
+ this._unobservedTaskFailures.set(baseKey, err);
19272
+ return { then: (_resolve, reject) => {
19273
+ this._unobservedTaskFailures.delete(baseKey);
19274
+ if (reject) reject(err);
19275
+ else throw err;
19276
+ } };
19277
+ }
19278
+ return { then: (resolve$1) => resolve$1(value) };
19051
19279
  }
19052
- return { then: (resolve$1) => resolve$1(value) };
19280
+ if (this._executingKey === key) return {
19281
+ then: (resolve$1) => resolve$1(null),
19282
+ _execute_directly: true
19283
+ };
19284
+ if (this._executingKey !== null) return { then: () => new Promise(() => {}) };
19285
+ const stepInfo = {
19286
+ name: name || key,
19287
+ script: script || key,
19288
+ args,
19289
+ key,
19290
+ dispatch_type
19291
+ };
19292
+ if (options) {
19293
+ if (options.timeout !== void 0) stepInfo.timeout = options.timeout;
19294
+ if (options.tag !== void 0) stepInfo.tag = options.tag;
19295
+ if (options.cache_ttl !== void 0) stepInfo.cache_ttl = options.cache_ttl;
19296
+ if (options.priority !== void 0) stepInfo.priority = options.priority;
19297
+ if (options.concurrency_limit !== void 0) stepInfo.concurrent_limit = options.concurrency_limit;
19298
+ if (options.concurrency_key !== void 0) stepInfo.concurrency_key = options.concurrency_key;
19299
+ if (options.concurrency_time_window_s !== void 0) stepInfo.concurrency_time_window_s = options.concurrency_time_window_s;
19300
+ }
19301
+ this.pending.push(stepInfo);
19302
+ return { then: () => {
19303
+ if (this._suspended) return new Promise(() => {});
19304
+ this._suspended = true;
19305
+ const steps = [...this.pending];
19306
+ this.pending = [];
19307
+ const names = steps.map((s) => s.name).join(", ");
19308
+ console.log(`\n--- WAC: ${names} ---`);
19309
+ this._raiseSuspend({
19310
+ mode: steps.length > 1 ? "parallel" : "sequential",
19311
+ steps
19312
+ });
19313
+ } };
19053
19314
  }
19054
- if (this._executingKey === key) return {
19055
- then: (resolve$1) => resolve$1(null),
19056
- _execute_directly: true
19057
- };
19058
- if (this._executingKey !== null) return { then: () => new Promise(() => {}) };
19059
- const stepInfo = {
19060
- name: name || key,
19061
- script: script || key,
19062
- args,
19315
+ }
19316
+ /** Wait out the backoff between two attempts of a retried task, as a durable
19317
+ * sleep, and return once there is nothing to wait for — no delay configured,
19318
+ * or the sleep already in the checkpoint.
19319
+ *
19320
+ * Raises where it stands, the way `_sleep` does, rather than handing back a
19321
+ * thenable: a task call the body never awaits is still dispatched (the runner
19322
+ * flushes `pending`), so a backoff that only fired when awaited would drop
19323
+ * the retry and let the round report the workflow complete. */
19324
+ _retryBackoff(key, baseKey, retry, attempt) {
19325
+ const seconds = retryDelaySeconds(retry, attempt);
19326
+ if (seconds < 1) return;
19327
+ if (key in this.completed) return;
19328
+ if (this._executingKey !== null) return;
19329
+ console.log(`\n--- WAC: sleep(${key}, ${seconds}s) before retrying ${baseKey} ---`);
19330
+ this._raiseSuspend({
19331
+ mode: "sleep",
19063
19332
  key,
19064
- dispatch_type
19065
- };
19066
- if (options) {
19067
- if (options.timeout !== void 0) stepInfo.timeout = options.timeout;
19068
- if (options.tag !== void 0) stepInfo.tag = options.tag;
19069
- if (options.cache_ttl !== void 0) stepInfo.cache_ttl = options.cache_ttl;
19070
- if (options.priority !== void 0) stepInfo.priority = options.priority;
19071
- if (options.concurrency_limit !== void 0) stepInfo.concurrent_limit = options.concurrency_limit;
19072
- if (options.concurrency_key !== void 0) stepInfo.concurrency_key = options.concurrency_key;
19073
- if (options.concurrency_time_window_s !== void 0) stepInfo.concurrency_time_window_s = options.concurrency_time_window_s;
19074
- }
19075
- this.pending.push(stepInfo);
19076
- return { then: () => {
19077
- if (this._suspended) return new Promise(() => {});
19078
- this._suspended = true;
19079
- const steps = [...this.pending];
19080
- this.pending = [];
19081
- const names = steps.map((s) => s.name).join(", ");
19082
- console.log(`\n--- WAC: ${names} ---`);
19083
- this._raiseSuspend({
19084
- mode: steps.length > 1 ? "parallel" : "sequential",
19085
- steps
19086
- });
19087
- } };
19333
+ seconds,
19334
+ steps: []
19335
+ });
19088
19336
  }
19089
19337
  /** Return and clear any pending (unawaited) steps. */
19090
19338
  _flushPending() {
@@ -19251,6 +19499,13 @@ var WorkflowCtx = class {
19251
19499
  this._pendingStepFailure = null;
19252
19500
  return f;
19253
19501
  }
19502
+ /** Report the task failures the body never looked at, and forget them. Which
19503
+ * rounds may call this is the runner's constraint, stated where it is enforced. */
19504
+ _warnUnobservedTaskFailures() {
19505
+ if (this._executingKey !== null) return;
19506
+ for (const [key, err] of this._unobservedTaskFailures) console.log(`\n--- WAC: task '${key}' failed but was never awaited, so the workflow result does not reflect it: ${err.message} ---`);
19507
+ this._unobservedTaskFailures.clear();
19508
+ }
19254
19509
  };
19255
19510
  async function sleep(seconds) {
19256
19511
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -19276,9 +19531,11 @@ async function step(name, fn) {
19276
19531
  * @example
19277
19532
  * const extract_data = task(async (url: string) => { ... });
19278
19533
  * const run_external = task("f/external_script", async (x: number) => { ... });
19534
+ * const call_api = task(fetchOrders, { retry: { attempts: 3, delay: 30, multiplier: 2 } });
19279
19535
  *
19280
19536
  * Inside a `workflow()`, calling a task dispatches it as a step.
19281
- * Outside a workflow, the function body executes directly.
19537
+ * Outside a workflow, the function body executes directly and
19538
+ * {@link TaskOptions} — retry included — does not apply.
19282
19539
  *
19283
19540
  * A task runs as its own job, so its result is always encoded as JSON and
19284
19541
  * decoded back before the caller sees it: a `Date` comes back as a string, a
@@ -19296,6 +19553,7 @@ function task(fnOrPath, maybeFnOrOptions, maybeOptions) {
19296
19553
  fn = fnOrPath;
19297
19554
  taskOptions = maybeFnOrOptions;
19298
19555
  }
19556
+ assertUsableRetry(taskOptions?.retry);
19299
19557
  const taskName = fn.name || taskPath || "";
19300
19558
  const wrapper = function(...args) {
19301
19559
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -19354,6 +19612,7 @@ function task(fnOrPath, maybeFnOrOptions, maybeOptions) {
19354
19612
  * // inside workflow: await extract({ url: "https://..." })
19355
19613
  */
19356
19614
  function taskScript(path, options) {
19615
+ assertUsableRetry(options?.retry);
19357
19616
  const name = path.split("/").pop() || path;
19358
19617
  const wrapper = function(...args) {
19359
19618
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -19379,6 +19638,7 @@ function taskScript(path, options) {
19379
19638
  * // inside workflow: await pipeline({ input: data })
19380
19639
  */
19381
19640
  function taskFlow(path, options) {
19641
+ assertUsableRetry(options?.retry);
19382
19642
  const name = path.split("/").pop() || path;
19383
19643
  const wrapper = function(...args) {
19384
19644
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");