@voyantjs/workflow-runs 0.41.1 → 0.41.2

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/README.md CHANGED
@@ -72,3 +72,51 @@ mountWorkflowRunsAdminRoutes(hono, {
72
72
  ```
73
73
 
74
74
  For self-hosted workflow services, keep runner registration close to the code that mounts the workflow service. The registry should dispatch to your external workflow server instead of importing worker-only runtime code into the admin API process. The resume path sends `ctx.resumeFromStep` plus `ctx.seedResults`; the self-host server starts a new run, pre-populates the journal with the seeded step outputs, and executes from the failed step onward.
75
+
76
+ ## Record `@voyantjs/workflows` executions
77
+
78
+ Use `recordedWorkflow` as a drop-in replacement for `workflow(...)` when a
79
+ workflow should appear in the workflow runs admin UI. The helper records start,
80
+ success, and failure rows in `workflow_runs` without repeating recorder
81
+ boilerplate in every workflow body.
82
+
83
+ ```ts
84
+ import { recordedWorkflow } from "@voyantjs/workflow-runs"
85
+
86
+ export const generatePdfWorkflow = recordedWorkflow({
87
+ id: "products.generate-pdf",
88
+ tags: ["products"],
89
+ async run(input, ctx) {
90
+ const renderer = ctx.services.resolve("products:pdf-renderer")
91
+ return renderer.generate(input)
92
+ },
93
+ })
94
+ ```
95
+
96
+ By default, `recordedWorkflow` resolves a Drizzle database from
97
+ `ctx.services.resolve("db")`. It records the workflow id, trigger, run id as the
98
+ correlation id, configured/runtime tags, input, result, parent run id for child
99
+ workflow triggers, and errors. Recording is best-effort: database or serializer
100
+ failures do not fail the workflow execution.
101
+
102
+ You can customize the database service key or payload serializers:
103
+
104
+ ```ts
105
+ export const syncCatalogWorkflow = recordedWorkflow(
106
+ {
107
+ id: "catalog.sync",
108
+ async run(input, ctx) {
109
+ return ctx.services.resolve("catalog:sync").run(input)
110
+ },
111
+ },
112
+ {
113
+ dbServiceName: "postgres",
114
+ input: ({ input }) => ({ catalogId: input.catalogId }),
115
+ result: ({ output }) => ({ changed: output.changed }),
116
+ },
117
+ )
118
+ ```
119
+
120
+ This helper only records observability data. Rerun and resume support still uses
121
+ `WorkflowRunnerRegistry` registration so apps can choose which workflows are
122
+ safe to dispatch from the admin UI.
package/dist/index.d.ts CHANGED
@@ -21,4 +21,5 @@ export { mountWorkflowRunsAdminRoutes } from "./routes.js";
21
21
  export { type WorkflowIdempotency, type WorkflowRerunContext, type WorkflowResumeContext, type WorkflowRunner, WorkflowRunnerRegistry, } from "./runner.js";
22
22
  export { type NewWorkflowRun, type NewWorkflowRunStep, type WorkflowRun, type WorkflowRunErrorPayload, type WorkflowRunStep, workflowRunStatusEnum, workflowRunStepStatusEnum, workflowRunSteps, workflowRuns, } from "./schema.js";
23
23
  export { type ListWorkflowRunsQuery, type ListWorkflowRunsResult, workflowRunsService, } from "./service.js";
24
+ export { type RecordedWorkflowOptions, type RecordedWorkflowResultContext, type RecordedWorkflowRunContext, recordedWorkflow, } from "./workflows.js";
24
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,mBAAmB,GACpB,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,gBAAgB,GACjB,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -21,3 +21,4 @@ export { mountWorkflowRunsAdminRoutes } from "./routes.js";
21
21
  export { WorkflowRunnerRegistry, } from "./runner.js";
22
22
  export { workflowRunStatusEnum, workflowRunStepStatusEnum, workflowRunSteps, workflowRuns, } from "./schema.js";
23
23
  export { workflowRunsService, } from "./service.js";
24
+ export { recordedWorkflow, } from "./workflows.js";
@@ -71,5 +71,7 @@ export interface WorkflowRunRecorder {
71
71
  * workflow keeps running. The whole point of this layer is
72
72
  * observability, not durability.
73
73
  */
74
- export declare function beginWorkflowRun(db: PostgresJsDatabase, input: BeginWorkflowRunInput): Promise<WorkflowRunRecorder>;
74
+ export declare function beginWorkflowRun(db: PostgresJsDatabase, input: BeginWorkflowRunInput, options?: {
75
+ reuseRunningRun?: boolean;
76
+ }): Promise<WorkflowRunRecorder>;
75
77
  //# sourceMappingURL=recorder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAUjE,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD;;;;;OAKG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAwH9B"}
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAUjE,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD;;;;;OAKG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAgC9B"}
package/dist/recorder.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * persistence failures rather than rethrowing, so observability
25
25
  * outages never break the underlying business operation.
26
26
  */
27
- import { eq } from "drizzle-orm";
27
+ import { and, desc, eq } from "drizzle-orm";
28
28
  import { workflowRunSteps, workflowRuns, } from "./schema.js";
29
29
  /**
30
30
  * Start a run and return a recorder bound to its id. The row is
@@ -35,7 +35,12 @@ import { workflowRunSteps, workflowRuns, } from "./schema.js";
35
35
  * workflow keeps running. The whole point of this layer is
36
36
  * observability, not durability.
37
37
  */
38
- export async function beginWorkflowRun(db, input) {
38
+ export async function beginWorkflowRun(db, input, options = {}) {
39
+ if (options.reuseRunningRun && input.correlationId) {
40
+ const existingRunId = await findRunningWorkflowRun(db, input);
41
+ if (existingRunId)
42
+ return workflowRunRecorder(db, existingRunId);
43
+ }
39
44
  const insert = {
40
45
  workflowName: input.workflowName,
41
46
  trigger: input.trigger ?? "manual",
@@ -57,6 +62,26 @@ export async function beginWorkflowRun(db, input) {
57
62
  }
58
63
  if (!runId)
59
64
  return noopRecorder();
65
+ return workflowRunRecorder(db, runId);
66
+ }
67
+ async function findRunningWorkflowRun(db, input) {
68
+ const correlationId = input.correlationId;
69
+ if (!correlationId)
70
+ return null;
71
+ try {
72
+ const [row] = await db
73
+ .select({ id: workflowRuns.id })
74
+ .from(workflowRuns)
75
+ .where(and(eq(workflowRuns.workflowName, input.workflowName), eq(workflowRuns.correlationId, correlationId), eq(workflowRuns.status, "running")))
76
+ .orderBy(desc(workflowRuns.startedAt))
77
+ .limit(1);
78
+ return row?.id ?? null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ function workflowRunRecorder(db, runId) {
60
85
  const stepStarts = new Map();
61
86
  let nextSequence = 1;
62
87
  return {
@@ -0,0 +1,38 @@
1
+ import { type WorkflowConfig, type WorkflowContext, type WorkflowDefinition } from "@voyantjs/workflows";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ type JsonRecord = Record<string, unknown>;
5
+ export interface RecordedWorkflowRunContext<TInput> {
6
+ input: TInput;
7
+ ctx: WorkflowContext<TInput>;
8
+ config: WorkflowConfig<TInput, unknown>;
9
+ }
10
+ export interface RecordedWorkflowResultContext<TInput, TOutput> extends RecordedWorkflowRunContext<TInput> {
11
+ output: TOutput;
12
+ }
13
+ export interface RecordedWorkflowOptions<TInput, TOutput> {
14
+ /**
15
+ * Database used by the workflow-runs recorder. When omitted, the helper
16
+ * resolves `ctx.services.resolve(dbServiceName)`.
17
+ */
18
+ db?: PostgresJsDatabase | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<PostgresJsDatabase | null | undefined>);
19
+ /** Service-container key used when `db` is omitted. Defaults to `db`. */
20
+ dbServiceName?: string;
21
+ workflowName?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string>);
22
+ trigger?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
23
+ correlationId?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
24
+ tags?: ReadonlyArray<string> | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<ReadonlyArray<string> | null | undefined>);
25
+ input?: false | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<JsonRecord | null | undefined>);
26
+ result?: false | ((args: RecordedWorkflowResultContext<TInput, TOutput>) => MaybePromise<JsonRecord | null | undefined>);
27
+ parentRunId?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
28
+ triggeredByUserId?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
29
+ resumeFromStep?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
30
+ }
31
+ /**
32
+ * Declare a `@voyantjs/workflows` workflow whose run lifecycle is mirrored into
33
+ * the `workflow_runs` observability tables. Recording is best-effort: database,
34
+ * metadata, or serialization failures never change workflow execution behavior.
35
+ */
36
+ export declare function recordedWorkflow<TInput = unknown, TOutput = unknown>(config: WorkflowConfig<TInput, TOutput>, options?: RecordedWorkflowOptions<TInput, TOutput>): WorkflowDefinition<TInput, TOutput>;
37
+ export {};
38
+ //# sourceMappingURL=workflows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ import { workflow, } from "@voyantjs/workflows";
2
+ import { beginWorkflowRun, } from "./recorder.js";
3
+ const WAITPOINT_PENDING = Symbol.for("voyant.workflows.waitpointPending");
4
+ /**
5
+ * Declare a `@voyantjs/workflows` workflow whose run lifecycle is mirrored into
6
+ * the `workflow_runs` observability tables. Recording is best-effort: database,
7
+ * metadata, or serialization failures never change workflow execution behavior.
8
+ */
9
+ export function recordedWorkflow(config, options = {}) {
10
+ return workflow({
11
+ ...config,
12
+ async run(input, ctx) {
13
+ const recorder = await startRecording(config, options, input, ctx);
14
+ try {
15
+ const output = await config.run(input, ctx);
16
+ await completeRecording(recorder, options, config, input, ctx, output);
17
+ return output;
18
+ }
19
+ catch (err) {
20
+ if (isWaitpointPending(err))
21
+ throw err;
22
+ await failRecording(recorder, err);
23
+ throw err;
24
+ }
25
+ },
26
+ });
27
+ }
28
+ async function startRecording(config, options, input, ctx) {
29
+ try {
30
+ const args = {
31
+ input,
32
+ ctx,
33
+ config: config,
34
+ };
35
+ const db = await resolveDb(options, args);
36
+ if (!db)
37
+ return null;
38
+ const beginInput = {
39
+ workflowName: await resolveValue(options.workflowName, args, config.id),
40
+ trigger: await resolveValue(options.trigger, args, triggerName(ctx)),
41
+ correlationId: await resolveValue(options.correlationId, args, ctx.run.id),
42
+ tags: [
43
+ ...dedupe([...(config.tags ?? []), ...ctx.run.tags, ...(await resolveTags(options, args))]),
44
+ ],
45
+ input: await resolveInput(options, args),
46
+ parentRunId: await resolveValue(options.parentRunId, args, parentRunId(ctx)),
47
+ triggeredByUserId: await resolveValue(options.triggeredByUserId, args, triggeredByUserId(ctx)),
48
+ resumeFromStep: await resolveValue(options.resumeFromStep, args, null),
49
+ };
50
+ return await beginWorkflowRun(db, beginInput, { reuseRunningRun: ctx.invocationCount > 1 });
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ async function completeRecording(recorder, options, config, input, ctx, output) {
57
+ if (!recorder)
58
+ return;
59
+ try {
60
+ await recorder.complete(await resolveResult(options, { input, ctx, config, output }));
61
+ }
62
+ catch {
63
+ // best-effort observability only
64
+ }
65
+ }
66
+ async function failRecording(recorder, err) {
67
+ if (!recorder)
68
+ return;
69
+ try {
70
+ await recorder.fail(err);
71
+ }
72
+ catch {
73
+ // best-effort observability only
74
+ }
75
+ }
76
+ async function resolveDb(options, args) {
77
+ if (typeof options.db === "function")
78
+ return options.db(args);
79
+ if (options.db)
80
+ return options.db;
81
+ const serviceName = options.dbServiceName ?? "db";
82
+ return args.ctx.services.resolve(serviceName);
83
+ }
84
+ async function resolveTags(options, args) {
85
+ if (Array.isArray(options.tags))
86
+ return options.tags;
87
+ if (typeof options.tags === "function")
88
+ return (await options.tags(args)) ?? [];
89
+ return [];
90
+ }
91
+ async function resolveInput(options, args) {
92
+ if (options.input === false)
93
+ return null;
94
+ if (typeof options.input === "function")
95
+ return (await options.input(args)) ?? null;
96
+ return toJsonRecord(args.input);
97
+ }
98
+ async function resolveResult(options, args) {
99
+ try {
100
+ if (options.result === false)
101
+ return null;
102
+ if (typeof options.result === "function")
103
+ return (await options.result(args)) ?? null;
104
+ return toJsonRecord(args.output);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ async function resolveValue(value, args, fallback) {
111
+ if (typeof value === "function") {
112
+ return ((await value(args)) ?? fallback);
113
+ }
114
+ return (value ?? fallback);
115
+ }
116
+ function triggerName(ctx) {
117
+ const trigger = ctx.run.triggeredBy;
118
+ if (trigger.kind === "event")
119
+ return trigger.eventType;
120
+ if (trigger.kind === "schedule")
121
+ return `schedule:${trigger.scheduleId}`;
122
+ if (trigger.kind === "parent")
123
+ return "parent";
124
+ return "api";
125
+ }
126
+ function parentRunId(ctx) {
127
+ const trigger = ctx.run.triggeredBy;
128
+ return trigger.kind === "parent" ? trigger.parentRunId : null;
129
+ }
130
+ function triggeredByUserId(ctx) {
131
+ const trigger = ctx.run.triggeredBy;
132
+ return trigger.kind === "api" ? (trigger.actor ?? null) : null;
133
+ }
134
+ function toJsonRecord(value) {
135
+ if (value === undefined || value === null)
136
+ return null;
137
+ if (isPlainRecord(value))
138
+ return value;
139
+ return { value };
140
+ }
141
+ function isPlainRecord(value) {
142
+ if (value === null || typeof value !== "object" || Array.isArray(value))
143
+ return false;
144
+ const proto = Object.getPrototypeOf(value);
145
+ return proto === Object.prototype || proto === null;
146
+ }
147
+ function isWaitpointPending(err) {
148
+ return (typeof err === "object" &&
149
+ err !== null &&
150
+ err[WAITPOINT_PENDING] === true);
151
+ }
152
+ function dedupe(values) {
153
+ return [...new Set(values)];
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/workflow-runs",
3
- "version": "0.41.1",
3
+ "version": "0.41.2",
4
4
  "description": "Workflow run recording, admin routes, and rerun/resume dispatch primitives for Voyant operator apps.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,20 +29,27 @@
29
29
  "types": "./dist/recorder.d.ts",
30
30
  "import": "./dist/recorder.js",
31
31
  "default": "./dist/recorder.js"
32
+ },
33
+ "./workflows": {
34
+ "types": "./dist/workflows.d.ts",
35
+ "import": "./dist/workflows.js",
36
+ "default": "./dist/workflows.js"
32
37
  }
33
38
  },
34
39
  "dependencies": {
35
40
  "drizzle-orm": "^0.45.2",
36
41
  "hono": "^4.12.10",
37
42
  "zod": "^4.3.6",
38
- "@voyantjs/core": "0.41.1",
39
- "@voyantjs/db": "0.41.1",
40
- "@voyantjs/hono": "0.41.1"
43
+ "@voyantjs/core": "0.41.2",
44
+ "@voyantjs/db": "0.41.2",
45
+ "@voyantjs/hono": "0.41.2",
46
+ "@voyantjs/workflows": "0.41.2"
41
47
  },
42
48
  "devDependencies": {
43
49
  "typescript": "^6.0.2",
44
50
  "vitest": "^4.1.2",
45
- "@voyantjs/voyant-typescript-config": "0.1.0"
51
+ "@voyantjs/voyant-typescript-config": "0.1.0",
52
+ "@voyantjs/workflows-orchestrator": "0.41.2"
46
53
  },
47
54
  "files": [
48
55
  "dist"