@upstash/workflow 1.3.0 → 1.3.2-rc

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.
Files changed (50) hide show
  1. package/astro.d.mts +2 -2
  2. package/astro.d.ts +2 -2
  3. package/astro.js +81 -14
  4. package/astro.mjs +1 -1
  5. package/{chunk-G24J5PCC.mjs → chunk-ICDNN4UW.mjs} +84 -14
  6. package/cloudflare.d.mts +2 -2
  7. package/cloudflare.d.ts +2 -2
  8. package/cloudflare.js +81 -14
  9. package/cloudflare.mjs +1 -1
  10. package/express.d.mts +2 -2
  11. package/express.d.ts +2 -2
  12. package/express.js +81 -14
  13. package/express.mjs +1 -1
  14. package/h3.d.mts +2 -2
  15. package/h3.d.ts +2 -2
  16. package/h3.js +81 -14
  17. package/h3.mjs +1 -1
  18. package/hono.d.mts +2 -2
  19. package/hono.d.ts +2 -2
  20. package/hono.js +81 -14
  21. package/hono.mjs +1 -1
  22. package/index.d.mts +43 -7
  23. package/index.d.ts +43 -7
  24. package/index.js +84 -15
  25. package/index.mjs +8 -3
  26. package/nextjs.d.mts +2 -2
  27. package/nextjs.d.ts +2 -2
  28. package/nextjs.js +81 -14
  29. package/nextjs.mjs +1 -1
  30. package/package.json +1 -1
  31. package/react-router.d.mts +2 -2
  32. package/react-router.d.ts +2 -2
  33. package/react-router.js +81 -14
  34. package/react-router.mjs +1 -1
  35. package/{serve-many-D3D9uE4u.d.mts → serve-many-CG3BFvO3.d.mts} +1 -1
  36. package/{serve-many-kPOasiyb.d.ts → serve-many-iJF1IUXk.d.ts} +1 -1
  37. package/solidjs.d.mts +1 -1
  38. package/solidjs.d.ts +1 -1
  39. package/solidjs.js +81 -14
  40. package/solidjs.mjs +1 -1
  41. package/svelte.d.mts +2 -2
  42. package/svelte.d.ts +2 -2
  43. package/svelte.js +81 -14
  44. package/svelte.mjs +1 -1
  45. package/tanstack.d.mts +2 -2
  46. package/tanstack.d.ts +2 -2
  47. package/tanstack.js +81 -14
  48. package/tanstack.mjs +1 -1
  49. package/{types-B_E1VAK6.d.ts → types-CekOpKvz.d.mts} +14 -6
  50. package/{types-B_E1VAK6.d.mts → types-CekOpKvz.d.ts} +14 -6
package/hono.js CHANGED
@@ -103,6 +103,22 @@ var formatWorkflowError = (error) => {
103
103
  message: `An error occured while executing workflow: '${typeof error === "string" ? error : JSON.stringify(error)}'`
104
104
  };
105
105
  };
106
+ var attachStepNameToError = (error, stepName) => {
107
+ if (typeof error !== "object" || error === null) return;
108
+ if ("errorStepName" in error) return;
109
+ if (!Object.isExtensible(error)) return;
110
+ try {
111
+ error.errorStepName = stepName;
112
+ } catch {
113
+ }
114
+ };
115
+ var getStepNameFromError = (error) => {
116
+ if (typeof error === "object" && error !== null) {
117
+ const stepName = error.errorStepName;
118
+ return typeof stepName === "string" ? stepName : void 0;
119
+ }
120
+ return void 0;
121
+ };
106
122
  function getConstructorName(obj) {
107
123
  if (obj === null || obj === void 0) {
108
124
  return null;
@@ -196,12 +212,13 @@ var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
196
212
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
197
213
  var WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk";
198
214
  var WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk";
215
+ var WORKFLOW_ERROR_STEP_NAME_HEADER = "Upstash-Error-Step-Name";
199
216
  var WORKFLOW_PROTOCOL_VERSION = "1";
200
217
  var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
201
218
  var DEFAULT_CONTENT_TYPE = "application/json";
202
219
  var NO_CONCURRENCY = 1;
203
220
  var DEFAULT_RETRIES = 3;
204
- var VERSION = "v1.2.1";
221
+ var VERSION = "v1.3.2";
205
222
  var SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
206
223
  var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
207
224
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
@@ -216,6 +233,31 @@ var import_qstash4 = require("@upstash/qstash");
216
233
  // src/utils.ts
217
234
  var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
218
235
  var NANOID_LENGTH = 21;
236
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
237
+ function validateLabel(label) {
238
+ if (label === void 0) return;
239
+ const labels = Array.isArray(label) ? label : [label];
240
+ if (labels.length === 0) {
241
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
242
+ }
243
+ for (const value of labels) {
244
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
245
+ throw new WorkflowNonRetryableError(
246
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
247
+ );
248
+ }
249
+ }
250
+ }
251
+ function serializeLabel(label) {
252
+ return Array.isArray(label) ? label.join(",") : label;
253
+ }
254
+ function validateFlowControl(flowControl) {
255
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
256
+ throw new WorkflowNonRetryableError(
257
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
258
+ );
259
+ }
260
+ }
219
261
  function getRandomInt() {
220
262
  return Math.floor(Math.random() * NANOID_CHARS.length);
221
263
  }
@@ -751,8 +793,8 @@ var triggerFirstInvocation = async (params) => {
751
793
  if (unknownSdk) {
752
794
  headers[WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER] = "true";
753
795
  }
754
- if (workflowContext.label) {
755
- headers[WORKFLOW_LABEL_HEADER] = workflowContext.label;
796
+ if (workflowContext.labels.length > 0) {
797
+ headers[WORKFLOW_LABEL_HEADER] = serializeLabel(workflowContext.labels);
756
798
  }
757
799
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
758
800
  return {
@@ -1554,8 +1596,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1554
1596
  });
1555
1597
  triggerHeaders["Upstash-Workflow-Invoke"] = "true";
1556
1598
  if (label) {
1557
- triggerHeaders[WORKFLOW_LABEL_HEADER] = label;
1558
- triggerHeaders[`upstash-forward-${WORKFLOW_LABEL_HEADER}`] = label;
1599
+ const labelHeader = serializeLabel(label);
1600
+ triggerHeaders[WORKFLOW_LABEL_HEADER] = labelHeader;
1601
+ triggerHeaders[`upstash-forward-${WORKFLOW_LABEL_HEADER}`] = labelHeader;
1559
1602
  }
1560
1603
  return { headers: triggerHeaders, contentType };
1561
1604
  }
@@ -1898,7 +1941,13 @@ var submitSingleStep = async ({
1898
1941
  await dispatchLifecycle("beforeExecution", {
1899
1942
  stepName: lazyStep.stepName
1900
1943
  });
1901
- const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1944
+ let resultStep;
1945
+ try {
1946
+ resultStep = await lazyStep.getResultStep(concurrency, stepId);
1947
+ } catch (error) {
1948
+ attachStepNameToError(error, lazyStep.stepName);
1949
+ throw error;
1950
+ }
1902
1951
  const { headers } = lazyStep.getHeaders({
1903
1952
  context,
1904
1953
  step: resultStep,
@@ -2121,9 +2170,11 @@ var AutoExecutor = class _AutoExecutor {
2121
2170
  if (isInstanceOf(error, WorkflowAbort) || isInstanceOf(error, import_qstash5.QstashError) && error.status === 400 || isInstanceOf(error, import_qstash5.QstashError) && error.status === 412) {
2122
2171
  throw error;
2123
2172
  }
2124
- throw new WorkflowError(
2173
+ const wrappedError = new WorkflowError(
2125
2174
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
2126
2175
  );
2176
+ attachStepNameToError(wrappedError, parallelSteps[stepIndex].stepName);
2177
+ throw wrappedError;
2127
2178
  }
2128
2179
  break;
2129
2180
  }
@@ -2696,9 +2747,10 @@ var WorkflowContext = class {
2696
2747
  */
2697
2748
  env;
2698
2749
  /**
2699
- * Label to apply to the workflow run.
2750
+ * Labels attached to the workflow run.
2700
2751
  *
2701
- * Can be used to filter the workflow run logs.
2752
+ * Can be used to filter the workflow run logs. A run can have multiple
2753
+ * labels when triggered with `label: string[]`.
2702
2754
  *
2703
2755
  * Can be set by passing a `label` parameter when triggering the workflow
2704
2756
  * with `client.trigger`:
@@ -2706,11 +2758,20 @@ var WorkflowContext = class {
2706
2758
  * ```ts
2707
2759
  * await client.trigger({
2708
2760
  * url: "https://workflow-endpoint.com",
2709
- * label: "my-label"
2761
+ * label: ["label-1", "label-2"]
2710
2762
  * });
2711
2763
  * ```
2712
2764
  */
2713
- label;
2765
+ labels;
2766
+ /**
2767
+ * Label of the workflow run.
2768
+ *
2769
+ * @deprecated Use `labels` instead. When a run has multiple labels, this
2770
+ * only returns the first one.
2771
+ */
2772
+ get label() {
2773
+ return this.labels[0];
2774
+ }
2714
2775
  /**
2715
2776
  * Number of times QStash has retried delivering the current request.
2716
2777
  *
@@ -2741,7 +2802,7 @@ var WorkflowContext = class {
2741
2802
  this.headers = headers;
2742
2803
  this.requestPayload = initialPayload;
2743
2804
  this.env = env ?? {};
2744
- this.label = label;
2805
+ this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2745
2806
  this.retried = retried ?? 0;
2746
2807
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2747
2808
  middlewareManagerInstance.assignContext(this);
@@ -2822,8 +2883,10 @@ var WorkflowContext = class {
2822
2883
  await this.addStep(new LazySleepUntilStep(this, stepName, time));
2823
2884
  }
2824
2885
  async call(stepName, settings) {
2886
+ validateFlowControl(settings.flowControl);
2825
2887
  let callStep;
2826
2888
  if ("workflow" in settings) {
2889
+ validateLabel(settings.label);
2827
2890
  const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
2828
2891
  const stringBody = typeof settings.body === "string" ? settings.body : settings.body === void 0 ? void 0 : JSON.stringify(settings.body);
2829
2892
  callStep = new LazyCallStep({
@@ -2931,6 +2994,8 @@ var WorkflowContext = class {
2931
2994
  );
2932
2995
  }
2933
2996
  async invoke(stepName, settings) {
2997
+ validateLabel(settings.label);
2998
+ validateFlowControl(settings.flowControl);
2934
2999
  return await this.addStep(
2935
3000
  new LazyInvokeStep(this, stepName, settings)
2936
3001
  );
@@ -3016,7 +3081,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3016
3081
  url: context.url,
3017
3082
  initialPayload: context.requestPayload,
3018
3083
  env: context.env,
3019
- label: context.label,
3084
+ label: context.labels,
3020
3085
  retried: context.retried
3021
3086
  });
3022
3087
  try {
@@ -3854,10 +3919,12 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3854
3919
  await middlewareManager.dispatchDebug("onError", {
3855
3920
  error: isInstanceOf(error, Error) ? error : new Error(formattedError.message)
3856
3921
  });
3922
+ const stepName = getStepNameFromError(error)?.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
3857
3923
  return new Response(JSON.stringify(formattedError), {
3858
3924
  status: 500,
3859
3925
  headers: {
3860
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3926
+ [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
3927
+ ...stepName ? { [WORKFLOW_ERROR_STEP_NAME_HEADER]: stepName } : {}
3861
3928
  }
3862
3929
  });
3863
3930
  }
package/hono.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SDK_TELEMETRY,
3
3
  serveBase,
4
4
  serveManyBase
5
- } from "./chunk-G24J5PCC.mjs";
5
+ } from "./chunk-ICDNN4UW.mjs";
6
6
 
7
7
  // platforms/hono.ts
8
8
  var telemetry = {
package/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as StepType, R as RawStep, W as WorkflowMiddleware, a as WorkflowClient, b as WorkflowReceiver, c as RouteFunction, d as WorkflowServeOptions, T as Telemetry, N as NotifyResponse, e as Waiter } from './types-B_E1VAK6.mjs';
2
- export { A as AsyncStepFunction, C as CallResponse, f as CallSettings, D as DetailedFinishCondition, g as Duration, E as ExclusiveValidationOptions, F as FailureFunctionPayload, h as FinishCondition, H as HeaderParams, I as InvokableWorkflow, i as InvokeStepResponse, j as InvokeWorkflowRequest, L as LazyInvokeStepParams, k as NotifyStepResponse, P as ParallelCallState, Q as QStashClientExtraConfig, l as RequiredExceptFields, m as Step, n as StepFunction, o as StepTypes, p as SyncStepFunction, q as WaitEventOptions, r as WaitRequest, s as WaitStepResponse, t as WorkflowAbort, u as WorkflowContext, v as WorkflowError, w as WorkflowNonRetryableError, x as WorkflowRetryAfterError } from './types-B_E1VAK6.mjs';
1
+ import { S as StepType, R as RawStep, W as WorkflowMiddleware, a as WorkflowClient, b as WorkflowReceiver, c as RouteFunction, d as WorkflowServeOptions, T as Telemetry, N as NotifyResponse, e as Waiter } from './types-CekOpKvz.mjs';
2
+ export { A as AsyncStepFunction, C as CallResponse, f as CallSettings, D as DetailedFinishCondition, g as Duration, E as ExclusiveValidationOptions, F as FailureFunctionPayload, h as FinishCondition, H as HeaderParams, I as InvokableWorkflow, i as InvokeStepResponse, j as InvokeWorkflowRequest, L as LazyInvokeStepParams, k as NotifyStepResponse, P as ParallelCallState, Q as QStashClientExtraConfig, l as RequiredExceptFields, m as Step, n as StepFunction, o as StepTypes, p as SyncStepFunction, q as WaitEventOptions, r as WaitRequest, s as WaitStepResponse, t as WorkflowAbort, u as WorkflowContext, v as WorkflowError, w as WorkflowNonRetryableError, x as WorkflowRetryAfterError } from './types-CekOpKvz.mjs';
3
3
  import { FlowControl, PublishRequest, HTTPMethods, Client as Client$1 } from '@upstash/qstash';
4
4
  import 'zod';
5
5
 
@@ -183,6 +183,13 @@ type StepLogGroup = {
183
183
  */
184
184
  steps: {
185
185
  messageId: string;
186
+ /**
187
+ * name of the step being run.
188
+ *
189
+ * only available when the SDK reported it while the step was failing
190
+ * (via the `Upstash-Error-Step-Name` response header).
191
+ */
192
+ stepName?: string;
186
193
  state: "STEP_PROGRESS" | "STEP_RETRY" | "STEP_FAILED" | "STEP_CANCELED";
187
194
  /**
188
195
  * retries
@@ -247,6 +254,7 @@ type FailureFunctionLog = {
247
254
  * Status code of the response from the failure function
248
255
  */
249
256
  responseStatus?: number;
257
+ nextDeliveryTime?: number;
250
258
  };
251
259
  type WorkflowRunLog = {
252
260
  /**
@@ -317,9 +325,18 @@ type WorkflowRunLog = {
317
325
  */
318
326
  dlqId?: string;
319
327
  /**
320
- * Label of the workflow run
328
+ * Label of the workflow run.
329
+ *
330
+ * @deprecated Use `labels` instead. When a run has multiple labels, this
331
+ * field only contains the first one.
321
332
  */
322
333
  label?: string;
334
+ /**
335
+ * Labels attached to the workflow run.
336
+ *
337
+ * A run can have multiple labels when triggered with `label: string[]`.
338
+ */
339
+ labels?: string[];
323
340
  };
324
341
  type WorkflowRunLogs = {
325
342
  cursor: string;
@@ -403,8 +420,11 @@ type TriggerOptions = {
403
420
  * Label to apply to the workflow run.
404
421
  *
405
422
  * Can be used to filter the workflow run logs.
423
+ *
424
+ * Pass an array to attach multiple labels to a single workflow run; they are
425
+ * sent as a comma-separated value in the `Upstash-Label` header.
406
426
  */
407
- label?: string;
427
+ label?: string | string[];
408
428
  /**
409
429
  * By default, Workflow SDK sends telemetry about SDK version, framework or runtime.
410
430
  *
@@ -526,7 +546,14 @@ type UniversalFilterFields = {
526
546
  fromDate?: Date | number;
527
547
  toDate?: Date | number;
528
548
  callerIp?: string;
529
- label?: string;
549
+ /**
550
+ * Filter by label.
551
+ *
552
+ * Pass an array to match runs that have any of the given labels (OR semantics).
553
+ * For example, with runs labelled `[label_1, label_2]` and `[label_2, label_3]`,
554
+ * filtering by `[label_1, label_2]` returns both.
555
+ */
556
+ label?: string | string[];
530
557
  flowControlKey?: string;
531
558
  };
532
559
  /** Workflow-specific filter fields for DLQ and bulk endpoints. */
@@ -664,11 +691,20 @@ type DLQMessage = {
664
691
  */
665
692
  failureCallbackInfo?: FailureCallbackInfo;
666
693
  /**
667
- * label passed when triggering workflow
694
+ * Label passed when triggering the workflow run.
695
+ *
696
+ * @deprecated Use `labels` instead. When a run has multiple labels, this
697
+ * field only contains the first one.
668
698
  */
669
699
  label?: string;
700
+ /**
701
+ * Labels attached to the workflow run.
702
+ *
703
+ * A run can have multiple labels when triggered with `label: string[]`.
704
+ */
705
+ labels?: string[];
670
706
  };
671
- type PublicDLQMessage = Pick<DLQMessage, "header" | "body" | "maxRetries" | "notBefore" | "createdAt" | "callerIP" | "workflowRunId" | "workflowCreatedAt" | "workflowUrl" | "responseStatus" | "responseHeader" | "responseBody" | "dlqId" | "failureCallback" | "failureCallbackInfo" | "label">;
707
+ type PublicDLQMessage = Pick<DLQMessage, "header" | "body" | "maxRetries" | "notBefore" | "createdAt" | "callerIP" | "workflowRunId" | "workflowCreatedAt" | "workflowUrl" | "responseStatus" | "responseHeader" | "responseBody" | "dlqId" | "failureCallback" | "failureCallbackInfo" | "label" | "labels">;
672
708
  declare class DLQ {
673
709
  private client;
674
710
  constructor(client: Client$1);
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as StepType, R as RawStep, W as WorkflowMiddleware, a as WorkflowClient, b as WorkflowReceiver, c as RouteFunction, d as WorkflowServeOptions, T as Telemetry, N as NotifyResponse, e as Waiter } from './types-B_E1VAK6.js';
2
- export { A as AsyncStepFunction, C as CallResponse, f as CallSettings, D as DetailedFinishCondition, g as Duration, E as ExclusiveValidationOptions, F as FailureFunctionPayload, h as FinishCondition, H as HeaderParams, I as InvokableWorkflow, i as InvokeStepResponse, j as InvokeWorkflowRequest, L as LazyInvokeStepParams, k as NotifyStepResponse, P as ParallelCallState, Q as QStashClientExtraConfig, l as RequiredExceptFields, m as Step, n as StepFunction, o as StepTypes, p as SyncStepFunction, q as WaitEventOptions, r as WaitRequest, s as WaitStepResponse, t as WorkflowAbort, u as WorkflowContext, v as WorkflowError, w as WorkflowNonRetryableError, x as WorkflowRetryAfterError } from './types-B_E1VAK6.js';
1
+ import { S as StepType, R as RawStep, W as WorkflowMiddleware, a as WorkflowClient, b as WorkflowReceiver, c as RouteFunction, d as WorkflowServeOptions, T as Telemetry, N as NotifyResponse, e as Waiter } from './types-CekOpKvz.js';
2
+ export { A as AsyncStepFunction, C as CallResponse, f as CallSettings, D as DetailedFinishCondition, g as Duration, E as ExclusiveValidationOptions, F as FailureFunctionPayload, h as FinishCondition, H as HeaderParams, I as InvokableWorkflow, i as InvokeStepResponse, j as InvokeWorkflowRequest, L as LazyInvokeStepParams, k as NotifyStepResponse, P as ParallelCallState, Q as QStashClientExtraConfig, l as RequiredExceptFields, m as Step, n as StepFunction, o as StepTypes, p as SyncStepFunction, q as WaitEventOptions, r as WaitRequest, s as WaitStepResponse, t as WorkflowAbort, u as WorkflowContext, v as WorkflowError, w as WorkflowNonRetryableError, x as WorkflowRetryAfterError } from './types-CekOpKvz.js';
3
3
  import { FlowControl, PublishRequest, HTTPMethods, Client as Client$1 } from '@upstash/qstash';
4
4
  import 'zod';
5
5
 
@@ -183,6 +183,13 @@ type StepLogGroup = {
183
183
  */
184
184
  steps: {
185
185
  messageId: string;
186
+ /**
187
+ * name of the step being run.
188
+ *
189
+ * only available when the SDK reported it while the step was failing
190
+ * (via the `Upstash-Error-Step-Name` response header).
191
+ */
192
+ stepName?: string;
186
193
  state: "STEP_PROGRESS" | "STEP_RETRY" | "STEP_FAILED" | "STEP_CANCELED";
187
194
  /**
188
195
  * retries
@@ -247,6 +254,7 @@ type FailureFunctionLog = {
247
254
  * Status code of the response from the failure function
248
255
  */
249
256
  responseStatus?: number;
257
+ nextDeliveryTime?: number;
250
258
  };
251
259
  type WorkflowRunLog = {
252
260
  /**
@@ -317,9 +325,18 @@ type WorkflowRunLog = {
317
325
  */
318
326
  dlqId?: string;
319
327
  /**
320
- * Label of the workflow run
328
+ * Label of the workflow run.
329
+ *
330
+ * @deprecated Use `labels` instead. When a run has multiple labels, this
331
+ * field only contains the first one.
321
332
  */
322
333
  label?: string;
334
+ /**
335
+ * Labels attached to the workflow run.
336
+ *
337
+ * A run can have multiple labels when triggered with `label: string[]`.
338
+ */
339
+ labels?: string[];
323
340
  };
324
341
  type WorkflowRunLogs = {
325
342
  cursor: string;
@@ -403,8 +420,11 @@ type TriggerOptions = {
403
420
  * Label to apply to the workflow run.
404
421
  *
405
422
  * Can be used to filter the workflow run logs.
423
+ *
424
+ * Pass an array to attach multiple labels to a single workflow run; they are
425
+ * sent as a comma-separated value in the `Upstash-Label` header.
406
426
  */
407
- label?: string;
427
+ label?: string | string[];
408
428
  /**
409
429
  * By default, Workflow SDK sends telemetry about SDK version, framework or runtime.
410
430
  *
@@ -526,7 +546,14 @@ type UniversalFilterFields = {
526
546
  fromDate?: Date | number;
527
547
  toDate?: Date | number;
528
548
  callerIp?: string;
529
- label?: string;
549
+ /**
550
+ * Filter by label.
551
+ *
552
+ * Pass an array to match runs that have any of the given labels (OR semantics).
553
+ * For example, with runs labelled `[label_1, label_2]` and `[label_2, label_3]`,
554
+ * filtering by `[label_1, label_2]` returns both.
555
+ */
556
+ label?: string | string[];
530
557
  flowControlKey?: string;
531
558
  };
532
559
  /** Workflow-specific filter fields for DLQ and bulk endpoints. */
@@ -664,11 +691,20 @@ type DLQMessage = {
664
691
  */
665
692
  failureCallbackInfo?: FailureCallbackInfo;
666
693
  /**
667
- * label passed when triggering workflow
694
+ * Label passed when triggering the workflow run.
695
+ *
696
+ * @deprecated Use `labels` instead. When a run has multiple labels, this
697
+ * field only contains the first one.
668
698
  */
669
699
  label?: string;
700
+ /**
701
+ * Labels attached to the workflow run.
702
+ *
703
+ * A run can have multiple labels when triggered with `label: string[]`.
704
+ */
705
+ labels?: string[];
670
706
  };
671
- type PublicDLQMessage = Pick<DLQMessage, "header" | "body" | "maxRetries" | "notBefore" | "createdAt" | "callerIP" | "workflowRunId" | "workflowCreatedAt" | "workflowUrl" | "responseStatus" | "responseHeader" | "responseBody" | "dlqId" | "failureCallback" | "failureCallbackInfo" | "label">;
707
+ type PublicDLQMessage = Pick<DLQMessage, "header" | "body" | "maxRetries" | "notBefore" | "createdAt" | "callerIP" | "workflowRunId" | "workflowCreatedAt" | "workflowUrl" | "responseStatus" | "responseHeader" | "responseBody" | "dlqId" | "failureCallback" | "failureCallbackInfo" | "label" | "labels">;
672
708
  declare class DLQ {
673
709
  private client;
674
710
  constructor(client: Client$1);
package/index.js CHANGED
@@ -110,6 +110,22 @@ var formatWorkflowError = (error) => {
110
110
  message: `An error occured while executing workflow: '${typeof error === "string" ? error : JSON.stringify(error)}'`
111
111
  };
112
112
  };
113
+ var attachStepNameToError = (error, stepName) => {
114
+ if (typeof error !== "object" || error === null) return;
115
+ if ("errorStepName" in error) return;
116
+ if (!Object.isExtensible(error)) return;
117
+ try {
118
+ error.errorStepName = stepName;
119
+ } catch {
120
+ }
121
+ };
122
+ var getStepNameFromError = (error) => {
123
+ if (typeof error === "object" && error !== null) {
124
+ const stepName = error.errorStepName;
125
+ return typeof stepName === "string" ? stepName : void 0;
126
+ }
127
+ return void 0;
128
+ };
113
129
  function getConstructorName(obj) {
114
130
  if (obj === null || obj === void 0) {
115
131
  return null;
@@ -270,12 +286,13 @@ var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
270
286
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
271
287
  var WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk";
272
288
  var WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk";
289
+ var WORKFLOW_ERROR_STEP_NAME_HEADER = "Upstash-Error-Step-Name";
273
290
  var WORKFLOW_PROTOCOL_VERSION = "1";
274
291
  var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
275
292
  var DEFAULT_CONTENT_TYPE = "application/json";
276
293
  var NO_CONCURRENCY = 1;
277
294
  var DEFAULT_RETRIES = 3;
278
- var VERSION = "v1.2.1";
295
+ var VERSION = "v1.3.2";
279
296
  var SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
280
297
  var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
281
298
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
@@ -290,6 +307,31 @@ var import_qstash4 = require("@upstash/qstash");
290
307
  // src/utils.ts
291
308
  var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
292
309
  var NANOID_LENGTH = 21;
310
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
311
+ function validateLabel(label) {
312
+ if (label === void 0) return;
313
+ const labels = Array.isArray(label) ? label : [label];
314
+ if (labels.length === 0) {
315
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
316
+ }
317
+ for (const value of labels) {
318
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
319
+ throw new WorkflowNonRetryableError(
320
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
321
+ );
322
+ }
323
+ }
324
+ }
325
+ function serializeLabel(label) {
326
+ return Array.isArray(label) ? label.join(",") : label;
327
+ }
328
+ function validateFlowControl(flowControl) {
329
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
330
+ throw new WorkflowNonRetryableError(
331
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
332
+ );
333
+ }
334
+ }
293
335
  function getRandomInt() {
294
336
  return Math.floor(Math.random() * NANOID_CHARS.length);
295
337
  }
@@ -825,8 +867,8 @@ var triggerFirstInvocation = async (params) => {
825
867
  if (unknownSdk) {
826
868
  headers[WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER] = "true";
827
869
  }
828
- if (workflowContext.label) {
829
- headers[WORKFLOW_LABEL_HEADER] = workflowContext.label;
870
+ if (workflowContext.labels.length > 0) {
871
+ headers[WORKFLOW_LABEL_HEADER] = serializeLabel(workflowContext.labels);
830
872
  }
831
873
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
832
874
  return {
@@ -1628,8 +1670,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1628
1670
  });
1629
1671
  triggerHeaders["Upstash-Workflow-Invoke"] = "true";
1630
1672
  if (label) {
1631
- triggerHeaders[WORKFLOW_LABEL_HEADER] = label;
1632
- triggerHeaders[`upstash-forward-${WORKFLOW_LABEL_HEADER}`] = label;
1673
+ const labelHeader = serializeLabel(label);
1674
+ triggerHeaders[WORKFLOW_LABEL_HEADER] = labelHeader;
1675
+ triggerHeaders[`upstash-forward-${WORKFLOW_LABEL_HEADER}`] = labelHeader;
1633
1676
  }
1634
1677
  return { headers: triggerHeaders, contentType };
1635
1678
  }
@@ -1972,7 +2015,13 @@ var submitSingleStep = async ({
1972
2015
  await dispatchLifecycle("beforeExecution", {
1973
2016
  stepName: lazyStep.stepName
1974
2017
  });
1975
- const resultStep = await lazyStep.getResultStep(concurrency, stepId);
2018
+ let resultStep;
2019
+ try {
2020
+ resultStep = await lazyStep.getResultStep(concurrency, stepId);
2021
+ } catch (error) {
2022
+ attachStepNameToError(error, lazyStep.stepName);
2023
+ throw error;
2024
+ }
1976
2025
  const { headers } = lazyStep.getHeaders({
1977
2026
  context,
1978
2027
  step: resultStep,
@@ -2195,9 +2244,11 @@ var AutoExecutor = class _AutoExecutor {
2195
2244
  if (isInstanceOf(error, WorkflowAbort) || isInstanceOf(error, import_qstash5.QstashError) && error.status === 400 || isInstanceOf(error, import_qstash5.QstashError) && error.status === 412) {
2196
2245
  throw error;
2197
2246
  }
2198
- throw new WorkflowError(
2247
+ const wrappedError = new WorkflowError(
2199
2248
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
2200
2249
  );
2250
+ attachStepNameToError(wrappedError, parallelSteps[stepIndex].stepName);
2251
+ throw wrappedError;
2201
2252
  }
2202
2253
  break;
2203
2254
  }
@@ -2703,9 +2754,10 @@ var WorkflowContext = class {
2703
2754
  */
2704
2755
  env;
2705
2756
  /**
2706
- * Label to apply to the workflow run.
2757
+ * Labels attached to the workflow run.
2707
2758
  *
2708
- * Can be used to filter the workflow run logs.
2759
+ * Can be used to filter the workflow run logs. A run can have multiple
2760
+ * labels when triggered with `label: string[]`.
2709
2761
  *
2710
2762
  * Can be set by passing a `label` parameter when triggering the workflow
2711
2763
  * with `client.trigger`:
@@ -2713,11 +2765,20 @@ var WorkflowContext = class {
2713
2765
  * ```ts
2714
2766
  * await client.trigger({
2715
2767
  * url: "https://workflow-endpoint.com",
2716
- * label: "my-label"
2768
+ * label: ["label-1", "label-2"]
2717
2769
  * });
2718
2770
  * ```
2719
2771
  */
2720
- label;
2772
+ labels;
2773
+ /**
2774
+ * Label of the workflow run.
2775
+ *
2776
+ * @deprecated Use `labels` instead. When a run has multiple labels, this
2777
+ * only returns the first one.
2778
+ */
2779
+ get label() {
2780
+ return this.labels[0];
2781
+ }
2721
2782
  /**
2722
2783
  * Number of times QStash has retried delivering the current request.
2723
2784
  *
@@ -2748,7 +2809,7 @@ var WorkflowContext = class {
2748
2809
  this.headers = headers;
2749
2810
  this.requestPayload = initialPayload;
2750
2811
  this.env = env ?? {};
2751
- this.label = label;
2812
+ this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2752
2813
  this.retried = retried ?? 0;
2753
2814
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2754
2815
  middlewareManagerInstance.assignContext(this);
@@ -2829,8 +2890,10 @@ var WorkflowContext = class {
2829
2890
  await this.addStep(new LazySleepUntilStep(this, stepName, time));
2830
2891
  }
2831
2892
  async call(stepName, settings) {
2893
+ validateFlowControl(settings.flowControl);
2832
2894
  let callStep;
2833
2895
  if ("workflow" in settings) {
2896
+ validateLabel(settings.label);
2834
2897
  const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
2835
2898
  const stringBody = typeof settings.body === "string" ? settings.body : settings.body === void 0 ? void 0 : JSON.stringify(settings.body);
2836
2899
  callStep = new LazyCallStep({
@@ -2938,6 +3001,8 @@ var WorkflowContext = class {
2938
3001
  );
2939
3002
  }
2940
3003
  async invoke(stepName, settings) {
3004
+ validateLabel(settings.label);
3005
+ validateFlowControl(settings.flowControl);
2941
3006
  return await this.addStep(
2942
3007
  new LazyInvokeStep(this, stepName, settings)
2943
3008
  );
@@ -3023,7 +3088,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3023
3088
  url: context.url,
3024
3089
  initialPayload: context.requestPayload,
3025
3090
  env: context.env,
3026
- label: context.label,
3091
+ label: context.labels,
3027
3092
  retried: context.retried
3028
3093
  });
3029
3094
  try {
@@ -3861,10 +3926,12 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3861
3926
  await middlewareManager.dispatchDebug("onError", {
3862
3927
  error: isInstanceOf(error, Error) ? error : new Error(formattedError.message)
3863
3928
  });
3929
+ const stepName = getStepNameFromError(error)?.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
3864
3930
  return new Response(JSON.stringify(formattedError), {
3865
3931
  status: 500,
3866
3932
  headers: {
3867
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3933
+ [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
3934
+ ...stepName ? { [WORKFLOW_ERROR_STEP_NAME_HEADER]: stepName } : {}
3868
3935
  }
3869
3936
  });
3870
3937
  }
@@ -4111,13 +4178,15 @@ var Client4 = class {
4111
4178
  const isBatchInput = Array.isArray(params);
4112
4179
  const options = isBatchInput ? params : [params];
4113
4180
  const invocations = options.map((option) => {
4181
+ validateLabel(option.label);
4182
+ validateFlowControl(option.flowControl);
4114
4183
  const failureUrl = option.failureUrl ?? option.url;
4115
4184
  const finalWorkflowRunId = getWorkflowRunId(option.workflowRunId);
4116
4185
  const context = new WorkflowContext({
4117
4186
  qstashClient: this.client,
4118
4187
  headers: new Headers({
4119
4188
  ...option.headers ?? {},
4120
- ...option.label ? { [WORKFLOW_LABEL_HEADER]: option.label } : {}
4189
+ ...option.label ? { [WORKFLOW_LABEL_HEADER]: serializeLabel(option.label) } : {}
4121
4190
  }),
4122
4191
  initialPayload: option.body,
4123
4192
  steps: [],
package/index.mjs CHANGED
@@ -15,9 +15,12 @@ import {
15
15
  makeNotifyRequest,
16
16
  normalizeCursor,
17
17
  prepareFlowControl,
18
+ serializeLabel,
18
19
  serve,
19
- triggerFirstInvocation
20
- } from "./chunk-G24J5PCC.mjs";
20
+ triggerFirstInvocation,
21
+ validateFlowControl,
22
+ validateLabel
23
+ } from "./chunk-ICDNN4UW.mjs";
21
24
 
22
25
  // src/client/index.ts
23
26
  import { Client as QStashClient } from "@upstash/qstash";
@@ -248,13 +251,15 @@ var Client = class {
248
251
  const isBatchInput = Array.isArray(params);
249
252
  const options = isBatchInput ? params : [params];
250
253
  const invocations = options.map((option) => {
254
+ validateLabel(option.label);
255
+ validateFlowControl(option.flowControl);
251
256
  const failureUrl = option.failureUrl ?? option.url;
252
257
  const finalWorkflowRunId = getWorkflowRunId(option.workflowRunId);
253
258
  const context = new WorkflowContext({
254
259
  qstashClient: this.client,
255
260
  headers: new Headers({
256
261
  ...option.headers ?? {},
257
- ...option.label ? { [WORKFLOW_LABEL_HEADER]: option.label } : {}
262
+ ...option.label ? { [WORKFLOW_LABEL_HEADER]: serializeLabel(option.label) } : {}
258
263
  }),
259
264
  initialPayload: option.body,
260
265
  steps: [],