@multiplier-labs/stepflow 0.3.3 → 0.3.4

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 (69) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  4. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  5. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  6. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  7. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  8. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  9. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  10. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  11. package/.github/workflows/publish.yml +6 -0
  12. package/dist/{chunk-4VOKMGP6.js → chunk-5XJBAPXF.js} +517 -522
  13. package/dist/chunk-5XJBAPXF.js.map +1 -0
  14. package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
  15. package/dist/chunk-FMPJBK2N.js.map +1 -0
  16. package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
  17. package/dist/chunk-JPXPB7U5.js.map +1 -0
  18. package/dist/{chunk-GYPNJBDP.js → chunk-U3PCVK6O.js} +266 -47
  19. package/dist/chunk-U3PCVK6O.js.map +1 -0
  20. package/dist/events/index.d.ts +56 -3
  21. package/dist/events/index.js +1 -1
  22. package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
  23. package/dist/index.d.ts +2 -12
  24. package/dist/index.js +764 -485
  25. package/dist/index.js.map +1 -1
  26. package/dist/scheduler/index.d.ts +1 -1
  27. package/dist/scheduler/index.js +2 -2
  28. package/dist/storage/index.d.ts +313 -47
  29. package/dist/storage/index.js +4 -2
  30. package/docs/stepflow-api-reference.md +3 -1
  31. package/package.json +7 -4
  32. package/src/core/engine/concurrency.ts +229 -0
  33. package/src/core/engine/lifecycle.ts +176 -0
  34. package/src/core/engine.test.ts +70 -0
  35. package/src/core/engine.ts +91 -185
  36. package/src/core/orchestrator/persistence.ts +165 -0
  37. package/src/core/orchestrator/plan-resolution.ts +140 -0
  38. package/src/core/orchestrator/state-transitions.ts +109 -0
  39. package/src/core/orchestrator/step-runner.ts +427 -0
  40. package/src/core/orchestrator.ts +40 -359
  41. package/src/events/webhook.test.ts +292 -0
  42. package/src/events/webhook.ts +320 -77
  43. package/src/planning/evaluate-condition.test.ts +153 -0
  44. package/src/planning/evaluate-condition.ts +105 -0
  45. package/src/planning/planner.ts +1 -83
  46. package/src/planning/registry.ts +3 -85
  47. package/src/scheduler/cron.test.ts +9 -1
  48. package/src/scheduler/cron.ts +26 -2
  49. package/src/scheduler/postgres-persistence.ts +27 -15
  50. package/src/scheduler/sqlite-persistence.ts +28 -3
  51. package/src/storage/postgres-core.ts +550 -0
  52. package/src/storage/postgres-migration.test.ts +69 -0
  53. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  54. package/src/storage/postgres.ts +171 -690
  55. package/src/storage/sqlite-core.ts +151 -0
  56. package/src/storage/sqlite.test.ts +177 -1
  57. package/src/storage/sqlite.ts +183 -93
  58. package/src/utils/index.ts +2 -0
  59. package/src/utils/pinned-fetch.test.ts +84 -0
  60. package/src/utils/pinned-fetch.ts +118 -0
  61. package/src/utils/postgres-deps.ts +16 -0
  62. package/src/utils/safe-json.test.ts +166 -0
  63. package/src/utils/safe-json.ts +107 -0
  64. package/src/utils/webhook-signature.test.ts +111 -0
  65. package/src/utils/webhook-signature.ts +74 -0
  66. package/dist/chunk-4VOKMGP6.js.map +0 -1
  67. package/dist/chunk-GYPNJBDP.js.map +0 -1
  68. package/dist/chunk-KHTUWLVZ.js.map +0 -1
  69. package/dist/chunk-ML35PIHX.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared condition evaluation helpers used by the planner and registry.
3
+ *
4
+ * Extracted to a single module so the planner's recipe scoring and the
5
+ * registry's `query({ matchConditions })` filter cannot drift apart.
6
+ */
7
+
8
+ import RE2 from 're2';
9
+ import type { ConditionOperator } from './types';
10
+
11
+ /**
12
+ * Maximum length (in characters) accepted for a `matches` regex pattern.
13
+ *
14
+ * Recipe conditions can come from user-supplied configuration; even with RE2's
15
+ * linear-time guarantee, very large patterns waste CPU/memory at compile time
16
+ * and inflate the per-evaluation cost. Cap the source length so a pathological
17
+ * recipe cannot hand the planner a megabyte of regex.
18
+ */
19
+ export const MAX_REGEX_PATTERN_LENGTH = 512;
20
+
21
+ /**
22
+ * Get a nested value from an object using dot notation.
23
+ */
24
+ export function getNestedValue(
25
+ obj: Record<string, unknown>,
26
+ path: string
27
+ ): unknown {
28
+ return path.split('.').reduce<unknown>((current, key) => {
29
+ if (current && typeof current === 'object' && key in current) {
30
+ return (current as Record<string, unknown>)[key];
31
+ }
32
+ return undefined;
33
+ }, obj);
34
+ }
35
+
36
+ /**
37
+ * Evaluate a single condition against a value.
38
+ *
39
+ * The `matches` operator uses RE2 (not the JS RegExp engine) to avoid
40
+ * catastrophic-backtracking DoS from untrusted recipe inputs.
41
+ */
42
+ export function evaluateCondition(
43
+ operator: ConditionOperator,
44
+ fieldValue: unknown,
45
+ conditionValue: unknown
46
+ ): boolean {
47
+ switch (operator) {
48
+ case 'eq':
49
+ return fieldValue === conditionValue;
50
+
51
+ case 'neq':
52
+ return fieldValue !== conditionValue;
53
+
54
+ case 'gt':
55
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
56
+ ? fieldValue > conditionValue
57
+ : false;
58
+
59
+ case 'gte':
60
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
61
+ ? fieldValue >= conditionValue
62
+ : false;
63
+
64
+ case 'lt':
65
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
66
+ ? fieldValue < conditionValue
67
+ : false;
68
+
69
+ case 'lte':
70
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
71
+ ? fieldValue <= conditionValue
72
+ : false;
73
+
74
+ case 'contains':
75
+ if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
76
+ return fieldValue.includes(conditionValue);
77
+ }
78
+ if (Array.isArray(fieldValue)) {
79
+ return fieldValue.includes(conditionValue);
80
+ }
81
+ return false;
82
+
83
+ case 'matches':
84
+ if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
85
+ if (conditionValue.length > MAX_REGEX_PATTERN_LENGTH) {
86
+ return false;
87
+ }
88
+ try {
89
+ return new RE2(conditionValue).test(fieldValue);
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ return false;
95
+
96
+ case 'exists':
97
+ return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
98
+
99
+ case 'notExists':
100
+ return fieldValue === undefined || fieldValue === null || fieldValue === '';
101
+
102
+ default:
103
+ return false;
104
+ }
105
+ }
@@ -3,10 +3,9 @@
3
3
  * Selects recipes and generates plans based on conditions and input analysis.
4
4
  */
5
5
 
6
- import RE2 from 're2';
7
6
  import { generateId } from '../utils/id';
7
+ import { evaluateCondition, getNestedValue } from './evaluate-condition';
8
8
  import type {
9
- ConditionOperator,
10
9
  Plan,
11
10
  PlannedStep,
12
11
  Planner,
@@ -22,87 +21,6 @@ import type {
22
21
  WorkflowKind,
23
22
  } from './types';
24
23
 
25
- // ============================================================================
26
- // Condition Evaluation
27
- // ============================================================================
28
-
29
- /**
30
- * Get a nested value from an object using dot notation.
31
- */
32
- function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
33
- return path.split('.').reduce<unknown>((current, key) => {
34
- if (current && typeof current === 'object' && key in current) {
35
- return (current as Record<string, unknown>)[key];
36
- }
37
- return undefined;
38
- }, obj);
39
- }
40
-
41
- /**
42
- * Evaluate a single condition against a value.
43
- */
44
- function evaluateCondition(
45
- operator: ConditionOperator,
46
- fieldValue: unknown,
47
- conditionValue: unknown
48
- ): boolean {
49
- switch (operator) {
50
- case 'eq':
51
- return fieldValue === conditionValue;
52
-
53
- case 'neq':
54
- return fieldValue !== conditionValue;
55
-
56
- case 'gt':
57
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
58
- ? fieldValue > conditionValue
59
- : false;
60
-
61
- case 'gte':
62
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
63
- ? fieldValue >= conditionValue
64
- : false;
65
-
66
- case 'lt':
67
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
68
- ? fieldValue < conditionValue
69
- : false;
70
-
71
- case 'lte':
72
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
73
- ? fieldValue <= conditionValue
74
- : false;
75
-
76
- case 'contains':
77
- if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
78
- return fieldValue.includes(conditionValue);
79
- }
80
- if (Array.isArray(fieldValue)) {
81
- return fieldValue.includes(conditionValue);
82
- }
83
- return false;
84
-
85
- case 'matches':
86
- if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
87
- try {
88
- return new RE2(conditionValue).test(fieldValue);
89
- } catch {
90
- return false;
91
- }
92
- }
93
- return false;
94
-
95
- case 'exists':
96
- return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
97
-
98
- case 'notExists':
99
- return fieldValue === undefined || fieldValue === null || fieldValue === '';
100
-
101
- default:
102
- return false;
103
- }
104
- }
105
-
106
24
  /**
107
25
  * Score a recipe's conditions against input.
108
26
  * Returns a score from 0-100 based on how well conditions match.
@@ -3,11 +3,10 @@
3
3
  * Provides storage and retrieval of recipes and handlers.
4
4
  */
5
5
 
6
- import RE2 from 're2';
7
6
  import type { WorkflowStep } from '../core/types';
7
+ import { evaluateCondition, getNestedValue } from './evaluate-condition';
8
8
  import type {
9
9
  Recipe,
10
- RecipeCondition,
11
10
  RecipeRegistry,
12
11
  RecipeQueryOptions,
13
12
  RegisteredStepHandler,
@@ -247,9 +246,6 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
247
246
  /**
248
247
  * Evaluate recipe conditions against an input.
249
248
  * Returns true if all conditions match.
250
- *
251
- * Note: This intentionally mirrors the condition evaluation in planner.ts
252
- * to keep registry queries independent of the planner implementation.
253
249
  */
254
250
  private evaluateConditions(
255
251
  conditions: Recipe['conditions'],
@@ -258,88 +254,10 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
258
254
  if (!conditions || conditions.length === 0) return true;
259
255
 
260
256
  return conditions.every(condition => {
261
- const fieldValue = this.getNestedValue(input, condition.field);
262
- return this.evaluateCondition(condition, fieldValue);
257
+ const fieldValue = getNestedValue(input, condition.field);
258
+ return evaluateCondition(condition.operator, fieldValue, condition.value);
263
259
  });
264
260
  }
265
-
266
- /**
267
- * Get a nested value from an object using dot notation.
268
- */
269
- private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
270
- return path.split('.').reduce<unknown>((current, key) => {
271
- if (current && typeof current === 'object' && key in current) {
272
- return (current as Record<string, unknown>)[key];
273
- }
274
- return undefined;
275
- }, obj);
276
- }
277
-
278
- /**
279
- * Evaluate a single condition.
280
- */
281
- private evaluateCondition(
282
- condition: RecipeCondition,
283
- fieldValue: unknown
284
- ): boolean {
285
- const { operator, value } = condition;
286
-
287
- switch (operator) {
288
- case 'eq':
289
- return fieldValue === value;
290
-
291
- case 'neq':
292
- return fieldValue !== value;
293
-
294
- case 'gt':
295
- return typeof fieldValue === 'number' && typeof value === 'number'
296
- ? fieldValue > value
297
- : false;
298
-
299
- case 'gte':
300
- return typeof fieldValue === 'number' && typeof value === 'number'
301
- ? fieldValue >= value
302
- : false;
303
-
304
- case 'lt':
305
- return typeof fieldValue === 'number' && typeof value === 'number'
306
- ? fieldValue < value
307
- : false;
308
-
309
- case 'lte':
310
- return typeof fieldValue === 'number' && typeof value === 'number'
311
- ? fieldValue <= value
312
- : false;
313
-
314
- case 'contains':
315
- if (typeof fieldValue === 'string' && typeof value === 'string') {
316
- return fieldValue.includes(value);
317
- }
318
- if (Array.isArray(fieldValue)) {
319
- return fieldValue.includes(value);
320
- }
321
- return false;
322
-
323
- case 'matches':
324
- if (typeof fieldValue === 'string' && typeof value === 'string') {
325
- try {
326
- return new RE2(value).test(fieldValue);
327
- } catch {
328
- return false;
329
- }
330
- }
331
- return false;
332
-
333
- case 'exists':
334
- return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
335
-
336
- case 'notExists':
337
- return fieldValue === undefined || fieldValue === null || fieldValue === '';
338
-
339
- default:
340
- return false;
341
- }
342
- }
343
261
  }
344
262
 
345
263
  // ============================================================================
@@ -223,6 +223,14 @@ describe('CronScheduler', () => {
223
223
  // Run pending timers for poll interval
224
224
  await vi.runOnlyPendingTimersAsync();
225
225
 
226
+ // Stop the scheduler and shut down the engine while fake timers are
227
+ // still active so that any setImmediate/setTimeout handles created
228
+ // under fake timers are cleared with the fake clear* functions.
229
+ // Clearing fake-timer handles with real clearImmediate corrupts
230
+ // Node 20/22's internal timer state and breaks subsequent tests.
231
+ await scheduler.stop();
232
+ await engine.shutdown();
233
+
226
234
  vi.useRealTimers();
227
235
 
228
236
  // Note: With fake timers, the actual workflow may not complete
@@ -460,7 +468,7 @@ describe('CronScheduler', () => {
460
468
  expect(loaded).toBeDefined();
461
469
  expect(loaded!.nextRunAt).toBeUndefined();
462
470
  expect(logger.error).toHaveBeenCalledWith(
463
- expect.stringContaining('Failed to parse cron expression'),
471
+ expect.stringContaining('Invalid cron expression'),
464
472
  expect.anything()
465
473
  );
466
474
 
@@ -117,6 +117,16 @@ export class CronScheduler implements Scheduler {
117
117
  return;
118
118
  }
119
119
 
120
+ // Clean up any leftover subscriptions from a previous start/stop cycle
121
+ if (this.eventUnsubscribe) {
122
+ this.eventUnsubscribe();
123
+ this.eventUnsubscribe = null;
124
+ }
125
+ if (this.pollTimer) {
126
+ clearInterval(this.pollTimer);
127
+ this.pollTimer = null;
128
+ }
129
+
120
130
  this.logger.info('Starting scheduler...');
121
131
 
122
132
  // Load schedules from persistence if available
@@ -128,10 +138,24 @@ export class CronScheduler implements Scheduler {
128
138
  this.logger.info(`Loaded ${loaded.length} schedules from persistence`);
129
139
  }
130
140
 
131
- // Calculate next run times for cron schedules
141
+ // Calculate next run times for cron schedules.
142
+ // Wrap each schedule in try-catch so one invalid persisted cron expression
143
+ // doesn't prevent the rest from loading.
132
144
  for (const schedule of this.schedules.values()) {
133
145
  if (schedule.triggerType === 'cron' && schedule.cronExpression) {
134
- this.updateNextRunTime(schedule);
146
+ try {
147
+ CronExpressionParser.parse(schedule.cronExpression, {
148
+ tz: schedule.timezone,
149
+ });
150
+ this.updateNextRunTime(schedule);
151
+ } catch (error) {
152
+ this.logger.error(
153
+ `Invalid cron expression "${schedule.cronExpression}" for schedule ${schedule.id}, disabling schedule:`,
154
+ error
155
+ );
156
+ schedule.enabled = false;
157
+ schedule.nextRunAt = undefined;
158
+ }
135
159
  }
136
160
  }
137
161
 
@@ -15,6 +15,8 @@ let sql: any;
15
15
  let pgModule: any;
16
16
  import type { WorkflowSchedule } from './types.js';
17
17
  import type { SchedulePersistence } from './cron.js';
18
+ import type { Logger, RunStatus } from '../core/types.js';
19
+ import { safeJsonParse as sharedSafeJsonParse } from '../utils/safe-json.js';
18
20
 
19
21
  // ============================================================================
20
22
  // Database Types (Kysely schema)
@@ -97,6 +99,13 @@ export interface PostgresSchedulePersistenceConfig {
97
99
  * @default true
98
100
  */
99
101
  autoMigrate?: boolean;
102
+
103
+ /**
104
+ * Optional structured logger used to surface JSON corruption events.
105
+ * Without one, parse failures still increment the corruption counter
106
+ * exposed via `getJsonParseCorruptionCount()` but produce no log output.
107
+ */
108
+ logger?: Logger;
100
109
  }
101
110
 
102
111
  // ============================================================================
@@ -141,6 +150,7 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
141
150
  private autoMigrate: boolean;
142
151
  private initialized = false;
143
152
  private config: PostgresSchedulePersistenceConfig;
153
+ private logger: Logger | undefined;
144
154
 
145
155
  constructor(config: PostgresSchedulePersistenceConfig) {
146
156
  this.schema = config.schema ?? 'public';
@@ -159,6 +169,7 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
159
169
  }
160
170
  this.autoMigrate = config.autoMigrate !== false;
161
171
  this.config = config;
172
+ this.logger = config.logger;
162
173
  }
163
174
 
164
175
  /**
@@ -483,6 +494,19 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
483
494
  // Helper Methods
484
495
  // ============================================================================
485
496
 
497
+ private safeParseOptionalField(value: unknown, rowId: string, column: string): unknown {
498
+ if (!value) return undefined;
499
+ if (typeof value === 'string') {
500
+ return sharedSafeJsonParse(value, undefined, {
501
+ component: 'PostgresSchedulePersistence',
502
+ rowId,
503
+ column,
504
+ logger: this.logger,
505
+ });
506
+ }
507
+ return value;
508
+ }
509
+
486
510
  private rowToSchedule(row: WorkflowSchedulesTable): WorkflowSchedule {
487
511
  return {
488
512
  id: row.id,
@@ -491,21 +515,9 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
491
515
  cronExpression: row.cron_expression ?? undefined,
492
516
  timezone: row.timezone ?? undefined,
493
517
  triggerOnWorkflowKind: row.trigger_on_workflow_kind ?? undefined,
494
- triggerOnStatus: row.trigger_on_status
495
- ? (typeof row.trigger_on_status === 'string'
496
- ? JSON.parse(row.trigger_on_status)
497
- : row.trigger_on_status)
498
- : undefined,
499
- input: row.input_json
500
- ? (typeof row.input_json === 'string'
501
- ? JSON.parse(row.input_json)
502
- : row.input_json)
503
- : undefined,
504
- metadata: row.metadata_json
505
- ? (typeof row.metadata_json === 'string'
506
- ? JSON.parse(row.metadata_json)
507
- : row.metadata_json)
508
- : undefined,
518
+ triggerOnStatus: this.safeParseOptionalField(row.trigger_on_status, row.id, 'trigger_on_status') as RunStatus[] | undefined,
519
+ input: this.safeParseOptionalField(row.input_json, row.id, 'input_json') as Record<string, unknown> | undefined,
520
+ metadata: this.safeParseOptionalField(row.metadata_json, row.id, 'metadata_json') as Record<string, unknown> | undefined,
509
521
  enabled: row.enabled,
510
522
  lastRunAt: row.last_run_at ? new Date(row.last_run_at) : undefined,
511
523
  lastRunId: row.last_run_id ?? undefined,
@@ -7,7 +7,9 @@
7
7
  import type { Database, Statement } from 'better-sqlite3';
8
8
  import type { WorkflowSchedule } from './types';
9
9
  import type { SchedulePersistence } from './cron';
10
+ import type { Logger, RunStatus } from '../core/types';
10
11
  import { generateId } from '../utils/id';
12
+ import { safeJsonParse as sharedSafeJsonParse } from '../utils/safe-json';
11
13
 
12
14
  // ============================================================================
13
15
  // Configuration Types
@@ -22,6 +24,13 @@ export interface SQLiteSchedulePersistenceConfig {
22
24
 
23
25
  /** Table name for schedules (default: workflow_schedules) */
24
26
  tableName?: string;
27
+
28
+ /**
29
+ * Optional structured logger used to surface JSON corruption events.
30
+ * Without one, parse failures still increment the corruption counter
31
+ * exposed via `getJsonParseCorruptionCount()` but produce no log output.
32
+ */
33
+ logger?: Logger;
25
34
  }
26
35
 
27
36
  // ============================================================================
@@ -47,6 +56,7 @@ export interface SQLiteSchedulePersistenceConfig {
47
56
  export class SQLiteSchedulePersistence implements SchedulePersistence {
48
57
  private db: Database;
49
58
  private tableName: string;
59
+ private logger: Logger | undefined;
50
60
  private stmts: {
51
61
  insert: Statement;
52
62
  update: Statement;
@@ -61,6 +71,7 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
61
71
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.tableName)) {
62
72
  throw new Error(`Invalid table name: ${this.tableName}`);
63
73
  }
74
+ this.logger = config.logger;
64
75
  this.initializeDatabase();
65
76
  }
66
77
 
@@ -217,6 +228,20 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
217
228
  // Helper Methods
218
229
  // ============================================================================
219
230
 
231
+ private safeJsonParse(
232
+ json: string,
233
+ fallback: unknown,
234
+ rowId: string,
235
+ column: string,
236
+ ): unknown {
237
+ return sharedSafeJsonParse(json, fallback, {
238
+ component: 'SQLiteSchedulePersistence',
239
+ rowId,
240
+ column,
241
+ logger: this.logger,
242
+ });
243
+ }
244
+
220
245
  private rowToSchedule(row: ScheduleRow): WorkflowSchedule {
221
246
  return {
222
247
  id: row.id,
@@ -226,10 +251,10 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
226
251
  timezone: row.timezone ?? undefined,
227
252
  triggerOnWorkflowKind: row.trigger_on_workflow_kind ?? undefined,
228
253
  triggerOnStatus: row.trigger_on_status
229
- ? JSON.parse(row.trigger_on_status)
254
+ ? this.safeJsonParse(row.trigger_on_status, undefined, row.id, 'trigger_on_status') as RunStatus[] | undefined
230
255
  : undefined,
231
- input: row.input ? JSON.parse(row.input) : undefined,
232
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
256
+ input: row.input ? this.safeJsonParse(row.input, undefined, row.id, 'input') as Record<string, unknown> | undefined : undefined,
257
+ metadata: row.metadata ? this.safeJsonParse(row.metadata, undefined, row.id, 'metadata') as Record<string, unknown> | undefined : undefined,
233
258
  enabled: row.enabled === 1,
234
259
  lastRunAt: row.last_run_at ? new Date(row.last_run_at) : undefined,
235
260
  lastRunId: row.last_run_id ?? undefined,