pg-workflows 0.11.0 → 0.13.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.d.ts CHANGED
@@ -23,6 +23,8 @@ type WorkflowRun = {
23
23
  parentRunId: string | null;
24
24
  parentStepId: string | null;
25
25
  parentResourceId: string | null;
26
+ /** Set when the run was started by a recurring schedule; the timestamp the schedule fired. */
27
+ scheduledAt: Date | null;
26
28
  };
27
29
  import { StandardSchemaV1 } from "@standard-schema/spec";
28
30
  type DurationObject = {
@@ -33,6 +35,7 @@ type DurationObject = {
33
35
  seconds?: number;
34
36
  };
35
37
  type Duration = string | DurationObject;
38
+ type Schedule = string | Exclude<Duration, string>;
36
39
  declare enum WorkflowStatus {
37
40
  PENDING = "pending",
38
41
  RUNNING = "running",
@@ -63,6 +66,18 @@ type WorkflowOptions<I extends InputParameters> = {
63
66
  timeout?: number;
64
67
  retries?: number;
65
68
  inputSchema?: I;
69
+ /**
70
+ * Recurring schedule. Accepts a cron expression (`'0 9 * * 1-5'`),
71
+ * a duration string (`'5m'`, `'1 hour'`), or a `DurationObject`.
72
+ */
73
+ schedule?: Schedule;
74
+ /** IANA timezone for cron expressions. Defaults to UTC. Ignored for duration-based schedules. */
75
+ timezone?: string;
76
+ };
77
+ /** Metadata about a scheduled fire, exposed on `ctx.schedule` for runs triggered by a schedule. */
78
+ type ScheduleContext = {
79
+ /** Time the schedule fired this run. */
80
+ timestamp: Date;
66
81
  };
67
82
  type StepBaseContext = {
68
83
  run: <T>(stepId: string, handler: () => Promise<T>) => Promise<T>;
@@ -126,7 +141,13 @@ interface WorkflowPlugin<
126
141
  TStepExt = object
127
142
  > {
128
143
  name: string;
129
- methods: (step: TStepBase) => TStepExt;
144
+ methods: (step: TStepBase, context: WorkflowContext) => TStepExt;
145
+ /**
146
+ * Optional middleware around the workflow handler call. Composes in
147
+ * registration order — the first plugin passed to `.use()` wraps everything
148
+ * inside. Implementations MUST call `next()` exactly once.
149
+ */
150
+ wrap?: (context: WorkflowContext, next: () => Promise<unknown>) => Promise<unknown>;
130
151
  }
131
152
  type WorkflowContext<
132
153
  TInput extends InputParameters = InputParameters,
@@ -136,8 +157,14 @@ type WorkflowContext<
136
157
  step: TStep;
137
158
  workflowId: string;
138
159
  runId: string;
160
+ /** Tenant/scope identifier set when the run was started, if any. */
161
+ resourceId?: string;
162
+ /** Zero-based retry attempt number (= `run.retryCount`). */
163
+ attempt: number;
139
164
  timeline: Record<string, unknown>;
140
165
  logger: WorkflowLogger;
166
+ /** Set only for runs triggered by a recurring schedule. */
167
+ schedule?: ScheduleContext;
141
168
  };
142
169
  type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
143
170
  id: string;
@@ -146,6 +173,8 @@ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
146
173
  inputSchema?: TInput;
147
174
  timeout?: number;
148
175
  retries?: number;
176
+ schedule?: Schedule;
177
+ timezone?: string;
149
178
  plugins?: WorkflowPlugin[];
150
179
  };
151
180
  type StepInternalDefinition = {
@@ -321,6 +350,8 @@ declare class WorkflowEngine {
321
350
  batchSize?: number;
322
351
  heartbeatSeconds?: number;
323
352
  }): Promise<void>;
353
+ private registerWorkflowSchedule;
354
+ private unscheduleWorkflow;
324
355
  stop(): Promise<void>;
325
356
  registerWorkflow(definition: WorkflowDefinition<InputParameters>): Promise<WorkflowEngine>;
326
357
  unregisterWorkflow(workflowId: string): Promise<WorkflowEngine>;
@@ -373,6 +404,15 @@ declare class WorkflowEngine {
373
404
  exclusiveLock?: boolean;
374
405
  db?: Db;
375
406
  }): Promise<WorkflowRun>;
407
+ /**
408
+ * Fetch the most recently created run for a workflow, optionally scoped to a
409
+ * `resourceId`. Useful for cron-style incremental syncs where the next run
410
+ * needs the previous run's completion timestamp as a cursor.
411
+ */
412
+ getWorkflowLastRun({ workflowId, resourceId }: {
413
+ workflowId: string;
414
+ resourceId?: string;
415
+ }): Promise<WorkflowRun | null>;
376
416
  updateRun({ runId, resourceId, data, expectedStatuses }: {
377
417
  runId: string;
378
418
  resourceId?: string;
@@ -452,4 +492,14 @@ declare class WorkflowEngineError extends Error {
452
492
  declare class WorkflowRunNotFoundError extends WorkflowEngineError {
453
493
  constructor(runId?: string, workflowId?: string);
454
494
  }
455
- export { workflow, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, InputParameters, InferInputParameters, Duration };
495
+ import { AttributeValue, Tracer } from "@opentelemetry/api";
496
+ type OtelPluginOptions = {
497
+ /** Tracer to use. Defaults to `trace.getTracer('pg-workflows')`. */
498
+ tracer?: Tracer;
499
+ /** Prefix for all span names. Defaults to `pg_workflows`. */
500
+ spanNamePrefix?: string;
501
+ /** Extra attributes merged onto the workflow.run span. */
502
+ attributes?: (context: WorkflowContext) => Record<string, AttributeValue>;
503
+ };
504
+ declare function otelPlugin(options?: OtelPluginOptions): WorkflowPlugin<StepBaseContext, object>;
505
+ export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, ScheduleContext, Schedule, OtelPluginOptions, InputParameters, InferInputParameters, Duration };
package/dist/index.js CHANGED
@@ -8,19 +8,21 @@ import {
8
8
  WorkflowRunNotFoundError,
9
9
  WorkflowStatus,
10
10
  createWorkflowRef,
11
+ getWorkflowLastRun,
11
12
  getWorkflowRun,
12
13
  getWorkflowRuns,
13
14
  insertWorkflowRun,
14
15
  invokeChildWorkflowTimelineKey,
15
16
  isInvokeChildWorkflowTimelineEntry,
16
17
  runMigrations,
18
+ scheduleQueueNameFor,
17
19
  updateWorkflowRun,
18
20
  validateResourceId,
19
21
  validateWorkflowId,
20
22
  waitForTimelineKey,
21
23
  withPostgresTransaction,
22
24
  workflow
23
- } from "./shared/chunk-ahxqsytt.js";
25
+ } from "./shared/chunk-5xswmve7.js";
24
26
  // src/engine.ts
25
27
  import { merge } from "es-toolkit";
26
28
  import pg from "pg";
@@ -122,6 +124,56 @@ function parseDuration(duration) {
122
124
  return ms;
123
125
  }
124
126
 
127
+ // src/schedule.ts
128
+ import { CronExpressionParser } from "cron-parser";
129
+ var CRON_TOKEN = /^[0-9*/,?\-LW#]+$/;
130
+ function looksLikeCronString(value) {
131
+ const tokens = value.trim().split(/\s+/);
132
+ if (tokens.length !== 5 && tokens.length !== 6)
133
+ return false;
134
+ return tokens.every((t) => CRON_TOKEN.test(t));
135
+ }
136
+ function validateCronExpression(expression, timezone) {
137
+ try {
138
+ CronExpressionParser.parse(expression, { tz: timezone });
139
+ } catch (e) {
140
+ throw new WorkflowEngineError(`Invalid cron expression "${expression}" (timezone: ${timezone}): ${e instanceof Error ? e.message : String(e)}`);
141
+ }
142
+ }
143
+ function durationMsToCron(ms, original) {
144
+ if (ms < MS_PER_MINUTE) {
145
+ throw new WorkflowEngineError(`Schedule interval must be at least 1 minute; got ${ms}ms from ${JSON.stringify(original)}`);
146
+ }
147
+ if (ms % MS_PER_DAY === 0) {
148
+ const days = ms / MS_PER_DAY;
149
+ if (days === 1)
150
+ return "0 0 * * *";
151
+ throw cronStepError(original, `${days} days`);
152
+ }
153
+ if (ms % MS_PER_HOUR === 0) {
154
+ const hours = ms / MS_PER_HOUR;
155
+ if (24 % hours === 0)
156
+ return `0 */${hours} * * *`;
157
+ throw cronStepError(original, `${hours} hours`);
158
+ }
159
+ const minutes = ms / MS_PER_MINUTE;
160
+ if (Number.isInteger(minutes) && 60 % minutes === 0)
161
+ return `*/${minutes} * * * *`;
162
+ throw cronStepError(original, `${minutes} minutes`);
163
+ }
164
+ function cronStepError(original, label) {
165
+ return new WorkflowEngineError(`Schedule interval ${JSON.stringify(original)} (${label}) doesn't map cleanly to a recurring cron expression. Use a value that divides 60 minutes, 24 hours, or 1 day — or pass an explicit cron string.`);
166
+ }
167
+ function resolveSchedule(schedule, timezone) {
168
+ const tz = timezone ?? "UTC";
169
+ if (typeof schedule === "string" && looksLikeCronString(schedule)) {
170
+ validateCronExpression(schedule, tz);
171
+ return { cron: schedule, timezone: tz };
172
+ }
173
+ const ms = parseDuration(schedule);
174
+ return { cron: durationMsToCron(ms, schedule), timezone: tz };
175
+ }
176
+
125
177
  // src/engine.ts
126
178
  var LOG_PREFIX = "[WorkflowEngine]";
127
179
  var StepTypeToIcon = {
@@ -208,10 +260,41 @@ class WorkflowEngine {
208
260
  await this.boss.work(WORKFLOW_RUN_DLQ_QUEUE_NAME, { pollingIntervalSeconds: 0.5, batchSize: 1 }, (jobs) => this.handleWorkflowRunDlq(jobs));
209
261
  this.logger.log(`Worker started for queue ${WORKFLOW_RUN_DLQ_QUEUE_NAME}`);
210
262
  }
263
+ if (asEngine) {
264
+ const scheduled = Array.from(this.workflows.values()).flatMap((wf) => wf.schedule == null ? [] : [{ id: wf.id, resolved: resolveSchedule(wf.schedule, wf.timezone) }]);
265
+ await Promise.allSettled(scheduled.map(({ id, resolved }) => this.registerWorkflowSchedule(id, resolved).catch((error) => {
266
+ this.logger.error(`Failed to register schedule for "${id}", skipping`, error instanceof Error ? error : new Error(String(error)), { workflowId: id });
267
+ })));
268
+ }
211
269
  this._started = true;
212
270
  this.logger.log("Workflow engine started!");
213
271
  }
272
+ async registerWorkflowSchedule(workflowId, resolvedSchedule) {
273
+ const scheduleQueueName = scheduleQueueNameFor(workflowId);
274
+ await this.boss.createQueue(scheduleQueueName);
275
+ await this.boss.schedule(scheduleQueueName, resolvedSchedule.cron, null, {
276
+ tz: resolvedSchedule.timezone
277
+ });
278
+ await this.boss.work(scheduleQueueName, { batchSize: 1, includeMetadata: true }, async (jobs) => {
279
+ const scheduledAt = jobs[0]?.startAfter ?? new Date;
280
+ try {
281
+ await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
282
+ } catch (error) {
283
+ this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
284
+ throw error;
285
+ }
286
+ });
287
+ this.logger.log(`Schedule registered for workflow "${workflowId}": ${resolvedSchedule.cron} (${resolvedSchedule.timezone})`, { workflowId });
288
+ }
289
+ async unscheduleWorkflow(workflowId) {
290
+ try {
291
+ await this.boss.unschedule(scheduleQueueNameFor(workflowId));
292
+ } catch (error) {
293
+ this.logger.error(`Failed to unschedule "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
294
+ }
295
+ }
214
296
  async stop() {
297
+ await Promise.allSettled(Array.from(this.workflows.values()).filter((wf) => wf.schedule != null).map((wf) => this.unscheduleWorkflow(wf.id)));
215
298
  await this.boss.stop();
216
299
  if (this._ownsPool) {
217
300
  await this.pool.end();
@@ -224,10 +307,14 @@ class WorkflowEngine {
224
307
  throw new WorkflowEngineError(`Workflow ${definition.id} is already registered`, definition.id);
225
308
  }
226
309
  const { steps } = parseWorkflowHandler(definition.handler);
310
+ const resolvedSchedule = definition.schedule ? resolveSchedule(definition.schedule, definition.timezone) : undefined;
227
311
  this.workflows.set(definition.id, {
228
312
  ...definition,
229
313
  steps
230
314
  });
315
+ if (this._started && resolvedSchedule) {
316
+ await this.registerWorkflowSchedule(definition.id, resolvedSchedule);
317
+ }
231
318
  this.logger.log(`Registered workflow "${definition.id}" with steps:`);
232
319
  for (const step of steps.values()) {
233
320
  const tags = [];
@@ -242,10 +329,17 @@ class WorkflowEngine {
242
329
  return this;
243
330
  }
244
331
  async unregisterWorkflow(workflowId) {
332
+ const existing = this.workflows.get(workflowId);
333
+ if (existing?.schedule != null && this._started) {
334
+ await this.unscheduleWorkflow(workflowId);
335
+ }
245
336
  this.workflows.delete(workflowId);
246
337
  return this;
247
338
  }
248
339
  async unregisterAllWorkflows() {
340
+ if (this._started) {
341
+ await Promise.allSettled(Array.from(this.workflows.values()).filter((wf) => wf.schedule != null).map((wf) => this.unscheduleWorkflow(wf.id)));
342
+ }
249
343
  this.workflows.clear();
250
344
  return this;
251
345
  }
@@ -295,6 +389,7 @@ class WorkflowEngine {
295
389
  parentRunId,
296
390
  parentStepId,
297
391
  parentResourceId,
392
+ scheduledAt,
298
393
  enqueue = true,
299
394
  db
300
395
  }) {
@@ -328,7 +423,8 @@ class WorkflowEngine {
328
423
  idempotencyKey,
329
424
  parentRunId,
330
425
  parentStepId,
331
- parentResourceId
426
+ parentResourceId,
427
+ scheduledAt
332
428
  }, targetDb);
333
429
  const insertAndEnqueue = async (targetDb) => {
334
430
  const result = await insertRun(targetDb);
@@ -511,6 +607,14 @@ class WorkflowEngine {
511
607
  }
512
608
  return run;
513
609
  }
610
+ async getWorkflowLastRun({
611
+ workflowId,
612
+ resourceId
613
+ }) {
614
+ validateWorkflowId(workflowId);
615
+ validateResourceId(resourceId);
616
+ return getWorkflowLastRun({ workflowId, resourceId }, this.db);
617
+ }
514
618
  async updateRun({
515
619
  runId,
516
620
  resourceId,
@@ -732,21 +836,33 @@ class WorkflowEngine {
732
836
  };
733
837
  let step = { ...baseStep };
734
838
  const plugins = workflow2.plugins ?? [];
735
- for (const plugin of plugins) {
736
- const extra = plugin.methods(step);
737
- step = { ...step, ...extra };
738
- }
739
839
  const context = {
740
840
  input: run.input,
741
841
  workflowId: run.workflowId,
742
842
  runId: run.id,
843
+ resourceId: run.resourceId ?? undefined,
844
+ attempt: run.retryCount,
743
845
  get timeline() {
744
846
  return run?.timeline ?? {};
745
847
  },
746
848
  logger: this.logger,
747
- step
849
+ step,
850
+ schedule: run.scheduledAt ? { timestamp: run.scheduledAt } : undefined
748
851
  };
749
- const result = await workflow2.handler(context);
852
+ for (const plugin of plugins) {
853
+ const extra = plugin.methods(step, context);
854
+ step = { ...step, ...extra };
855
+ context.step = step;
856
+ }
857
+ let next = () => workflow2.handler(context);
858
+ for (const plugin of [...plugins].reverse()) {
859
+ if (plugin.wrap) {
860
+ const inner = next;
861
+ const wrap = plugin.wrap;
862
+ next = () => wrap(context, inner);
863
+ }
864
+ }
865
+ const result = await next();
750
866
  run = await this.getRun({ runId, resourceId: scopedResourceId });
751
867
  const isLastParsedStep = run.currentStepId === workflow2.steps[workflow2.steps.length - 1]?.id;
752
868
  const hasPluginSteps = (workflow2.plugins?.length ?? 0) > 0;
@@ -1279,8 +1395,183 @@ ${error.stack}` : String(error)
1279
1395
  }, this.db);
1280
1396
  }
1281
1397
  }
1398
+ // src/plugins/otel.ts
1399
+ import {
1400
+ context as otelContext,
1401
+ SpanStatusCode,
1402
+ trace
1403
+ } from "@opentelemetry/api";
1404
+ var DEFAULT_PREFIX = "pg_workflows";
1405
+ function isCachedHit(timeline, stepId, kind) {
1406
+ const entry = timeline[stepId];
1407
+ if (entry && typeof entry === "object" && "output" in entry && entry.output !== undefined) {
1408
+ return true;
1409
+ }
1410
+ if (kind === "invokeChildWorkflow" && timeline[invokeChildWorkflowTimelineKey(stepId)]) {
1411
+ return true;
1412
+ }
1413
+ return false;
1414
+ }
1415
+ function otelPlugin(options = {}) {
1416
+ const tracer = options.tracer ?? trace.getTracer("pg-workflows");
1417
+ const prefix = options.spanNamePrefix ?? DEFAULT_PREFIX;
1418
+ const extraAttrs = options.attributes;
1419
+ return {
1420
+ name: "opentelemetry",
1421
+ methods: (step, context) => {
1422
+ const wrapVoidish = (kind, base) => {
1423
+ return async (stepId, ...args) => {
1424
+ if (isCachedHit(context.timeline, stepId, kind)) {
1425
+ return base(stepId, ...args);
1426
+ }
1427
+ const capturedCtx = otelContext.active();
1428
+ const startTime = new Date;
1429
+ let result;
1430
+ let originalErr;
1431
+ let thrownError;
1432
+ try {
1433
+ result = await base(stepId, ...args);
1434
+ } catch (err) {
1435
+ originalErr = err;
1436
+ thrownError = err instanceof Error ? err : new Error(String(err));
1437
+ }
1438
+ const span = tracer.startSpan(`${prefix}.step.${kind}`, {
1439
+ startTime,
1440
+ attributes: { "step.id": stepId, "step.type": kind }
1441
+ }, capturedCtx);
1442
+ if (thrownError) {
1443
+ span.recordException(thrownError);
1444
+ span.setStatus({ code: SpanStatusCode.ERROR, message: thrownError.message });
1445
+ span.end();
1446
+ throw originalErr;
1447
+ }
1448
+ span.setStatus({ code: SpanStatusCode.OK });
1449
+ span.end();
1450
+ return result;
1451
+ };
1452
+ };
1453
+ return {
1454
+ run: async (stepId, handler) => {
1455
+ if (isCachedHit(context.timeline, stepId, "run")) {
1456
+ return step.run(stepId, handler);
1457
+ }
1458
+ const capturedCtx = otelContext.active();
1459
+ const startTime = new Date;
1460
+ let result;
1461
+ let originalErr;
1462
+ let thrownError;
1463
+ try {
1464
+ result = await step.run(stepId, handler);
1465
+ } catch (err) {
1466
+ originalErr = err;
1467
+ thrownError = err instanceof Error ? err : new Error(String(err));
1468
+ }
1469
+ if (result === undefined && !thrownError) {
1470
+ return;
1471
+ }
1472
+ const span = tracer.startSpan(`${prefix}.step.run`, {
1473
+ startTime,
1474
+ attributes: { "step.id": stepId, "step.type": "run" }
1475
+ }, capturedCtx);
1476
+ if (thrownError) {
1477
+ span.recordException(thrownError);
1478
+ span.setStatus({ code: SpanStatusCode.ERROR, message: thrownError.message });
1479
+ span.end();
1480
+ throw originalErr;
1481
+ }
1482
+ span.setStatus({ code: SpanStatusCode.OK });
1483
+ span.end();
1484
+ return result;
1485
+ },
1486
+ waitFor: wrapVoidish("waitFor", step.waitFor),
1487
+ delay: wrapVoidish("delay", step.delay),
1488
+ sleep: wrapVoidish("delay", step.delay),
1489
+ waitUntil: wrapVoidish("waitUntil", step.waitUntil),
1490
+ pause: wrapVoidish("pause", step.pause),
1491
+ poll: async (stepId, conditionFn, pollOptions) => {
1492
+ const capturedCtx = otelContext.active();
1493
+ const startTime = new Date;
1494
+ let result;
1495
+ let originalErr;
1496
+ let thrownError;
1497
+ try {
1498
+ result = await step.poll(stepId, conditionFn, pollOptions);
1499
+ } catch (err) {
1500
+ originalErr = err;
1501
+ thrownError = err instanceof Error ? err : new Error(String(err));
1502
+ }
1503
+ const span = tracer.startSpan(`${prefix}.step.poll`, {
1504
+ startTime,
1505
+ attributes: { "step.id": stepId, "step.type": "poll" }
1506
+ }, capturedCtx);
1507
+ if (thrownError) {
1508
+ span.recordException(thrownError);
1509
+ span.setStatus({ code: SpanStatusCode.ERROR, message: thrownError.message });
1510
+ span.end();
1511
+ throw originalErr;
1512
+ }
1513
+ span.setStatus({ code: SpanStatusCode.OK });
1514
+ span.end();
1515
+ return result;
1516
+ },
1517
+ invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
1518
+ if (isCachedHit(context.timeline, stepId, "invokeChildWorkflow")) {
1519
+ return step.invokeChildWorkflow(stepId, refOrParams, inputArg, optionsArg);
1520
+ }
1521
+ const capturedCtx = otelContext.active();
1522
+ const startTime = new Date;
1523
+ let result;
1524
+ let originalErr;
1525
+ let thrownError;
1526
+ try {
1527
+ result = await step.invokeChildWorkflow(stepId, refOrParams, inputArg, optionsArg);
1528
+ } catch (err) {
1529
+ originalErr = err;
1530
+ thrownError = err instanceof Error ? err : new Error(String(err));
1531
+ }
1532
+ const span = tracer.startSpan(`${prefix}.step.invokeChildWorkflow`, {
1533
+ startTime,
1534
+ attributes: { "step.id": stepId, "step.type": "invokeChildWorkflow" }
1535
+ }, capturedCtx);
1536
+ if (thrownError) {
1537
+ span.recordException(thrownError);
1538
+ span.setStatus({ code: SpanStatusCode.ERROR, message: thrownError.message });
1539
+ span.end();
1540
+ throw originalErr;
1541
+ }
1542
+ span.setStatus({ code: SpanStatusCode.OK });
1543
+ span.end();
1544
+ return result;
1545
+ }
1546
+ };
1547
+ },
1548
+ wrap: (context, next) => tracer.startActiveSpan(`${prefix}.workflow.run`, {
1549
+ attributes: {
1550
+ "workflow.id": context.workflowId,
1551
+ "workflow.run_id": context.runId,
1552
+ "workflow.attempt": context.attempt,
1553
+ ...context.resourceId ? { "workflow.resource_id": context.resourceId } : {},
1554
+ ...extraAttrs ? extraAttrs(context) : {}
1555
+ }
1556
+ }, async (span) => {
1557
+ try {
1558
+ const result = await next();
1559
+ span.setStatus({ code: SpanStatusCode.OK });
1560
+ return result;
1561
+ } catch (err) {
1562
+ const error = err instanceof Error ? err : new Error(String(err));
1563
+ span.recordException(error);
1564
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
1565
+ throw err;
1566
+ } finally {
1567
+ span.end();
1568
+ }
1569
+ })
1570
+ };
1571
+ }
1282
1572
  export {
1283
1573
  workflow,
1574
+ otelPlugin,
1284
1575
  createWorkflowRef,
1285
1576
  WorkflowStatus,
1286
1577
  WorkflowRunNotFoundError,
@@ -1289,5 +1580,5 @@ export {
1289
1580
  WorkflowClient
1290
1581
  };
1291
1582
 
1292
- //# debugId=F9D6596295A7020B64756E2164756E21
1583
+ //# debugId=5BC543336A07527964756E2164756E21
1293
1584
  //# sourceMappingURL=index.js.map