@osolmaz/pi-workflows 0.3.0 → 0.5.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/README.md +11 -7
- package/dist/builtins/catalog.d.ts +2 -0
- package/dist/builtins/catalog.js +32 -0
- package/dist/builtins/catalog.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +2 -2
- package/dist/builtins/monitor.workflow.js +25 -25
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +70 -9
- package/dist/controllers/sqlite.js +209 -35
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/workflow-engine-scheduler.d.ts +2 -2
- package/dist/controllers/workflow-engine-scheduler.js +3 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/extension/executor.d.ts +3 -0
- package/dist/extension/executor.js +11 -1
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +157 -108
- package/dist/extension/index.js.map +1 -1
- package/dist/host/runner.d.ts +1 -0
- package/dist/host/runner.js +68 -20
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +3 -0
- package/dist/render/graph-render.js.map +1 -1
- package/dist/workflows/catalog.d.ts +43 -0
- package/dist/workflows/catalog.js +79 -0
- package/dist/workflows/catalog.js.map +1 -0
- package/dist/workflows/definition.d.ts +2 -1
- package/dist/workflows/definition.js +9 -1
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +93 -33
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -3
- package/dist/workflows/index.js +2 -2
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +18 -16
- package/dist/workflows/loader.js +58 -23
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +42 -0
- package/dist/workflows/migrate-sources.js +133 -0
- package/dist/workflows/migrate-sources.js.map +1 -0
- package/dist/workflows/schema.d.ts +2 -1
- package/dist/workflows/schema.js +14 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +5 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +44 -5
- package/docs/development.md +5 -3
- package/docs/plans/2026-08-12-coordinated-workflow-timeouts-plan.md +74 -0
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +97 -0
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +95 -0
- package/docs/run-bundles.md +22 -4
- package/docs/workflows.md +57 -14
- package/package.json +1 -1
- package/src/builtins/catalog.ts +32 -0
- package/src/builtins/monitor.workflow.ts +33 -26
- package/src/controllers/index.ts +1 -0
- package/src/controllers/sqlite.ts +353 -43
- package/src/controllers/workflow-engine-scheduler.ts +5 -2
- package/src/extension/executor.ts +12 -1
- package/src/extension/index.ts +181 -140
- package/src/host/runner.ts +78 -20
- package/src/render/graph-render.ts +3 -0
- package/src/workflows/catalog.ts +135 -0
- package/src/workflows/definition.ts +11 -0
- package/src/workflows/engine.ts +128 -53
- package/src/workflows/index.ts +7 -0
- package/src/workflows/loader.ts +70 -26
- package/src/workflows/migrate-sources.ts +174 -0
- package/src/workflows/schema.ts +16 -1
- package/src/workflows/store.ts +5 -2
- package/src/workflows/types.ts +43 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { BuiltinWorkflowCatalog } from "./catalog.js";
|
|
5
|
+
import { WorkflowRunStore, listRunBundles } from "./store.js";
|
|
6
|
+
import type { WorkflowRunState, WorkflowSource } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export type LegacySourceMigrationQueue = {
|
|
9
|
+
repairCanonicalWorkflowSourceRun(options: {
|
|
10
|
+
runId: string;
|
|
11
|
+
workflowName: string;
|
|
12
|
+
workflowSourceRef: string;
|
|
13
|
+
runnerId: string;
|
|
14
|
+
claimToken: string;
|
|
15
|
+
leaseMs: number;
|
|
16
|
+
}): "unchanged" | "claimed" | false;
|
|
17
|
+
claimLegacyWorkflowSourceRun(options: {
|
|
18
|
+
runId: string;
|
|
19
|
+
workflowName: string;
|
|
20
|
+
oldWorkflowPath: string;
|
|
21
|
+
workflowSourceRef: string;
|
|
22
|
+
runnerId: string;
|
|
23
|
+
claimToken: string;
|
|
24
|
+
leaseMs: number;
|
|
25
|
+
}): boolean;
|
|
26
|
+
parkWorkflowRun(options: { runId: string; claimToken: string }): boolean;
|
|
27
|
+
getWorkflowRun(runId: string): { status: "claimed" | "parked" | "done" } | undefined;
|
|
28
|
+
setWorkflowRunOriginSession?(runId: string, originSessionId: string): boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MIGRATION_LEASE_MS = 30_000;
|
|
32
|
+
|
|
33
|
+
export type LegacySourceMigrationResult = {
|
|
34
|
+
migratedRunIds: string[];
|
|
35
|
+
blocked: { runId: string; reason: string }[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** One bounded migration for nonterminal runs created before canonical sources. */
|
|
39
|
+
export async function migrateLegacyWorkflowSources(options: {
|
|
40
|
+
catalog: BuiltinWorkflowCatalog;
|
|
41
|
+
store?: WorkflowRunStore;
|
|
42
|
+
queue?: LegacySourceMigrationQueue;
|
|
43
|
+
}): Promise<LegacySourceMigrationResult> {
|
|
44
|
+
const store = options.store ?? new WorkflowRunStore();
|
|
45
|
+
const result: LegacySourceMigrationResult = { migratedRunIds: [], blocked: [] };
|
|
46
|
+
for (const bundle of await listRunBundles(store.outputRoot)) {
|
|
47
|
+
const state = bundle.state;
|
|
48
|
+
if (state.status !== "running" && state.status !== "waiting") continue;
|
|
49
|
+
if (
|
|
50
|
+
options.queue?.setWorkflowRunOriginSession !== undefined &&
|
|
51
|
+
bundle.sessionBinding !== null
|
|
52
|
+
) {
|
|
53
|
+
options.queue.setWorkflowRunOriginSession(state.runId, bundle.sessionBinding.piSessionId);
|
|
54
|
+
}
|
|
55
|
+
if (state.workflowSource !== undefined) {
|
|
56
|
+
if (options.queue === undefined) continue;
|
|
57
|
+
const claimToken = randomUUID();
|
|
58
|
+
const repaired = options.queue.repairCanonicalWorkflowSourceRun({
|
|
59
|
+
runId: state.runId,
|
|
60
|
+
workflowName: state.workflowName,
|
|
61
|
+
workflowSourceRef: sourceRef(state.workflowSource),
|
|
62
|
+
runnerId: `source-migration-${process.pid}`,
|
|
63
|
+
claimToken,
|
|
64
|
+
leaseMs: MIGRATION_LEASE_MS,
|
|
65
|
+
});
|
|
66
|
+
if (repaired === false) {
|
|
67
|
+
// Another runner can own a canonical run during startup. Its source is
|
|
68
|
+
// already safe; the normal queue lease will make it claimable later.
|
|
69
|
+
} else if (
|
|
70
|
+
repaired === "claimed" &&
|
|
71
|
+
!options.queue.parkWorkflowRun({ runId: state.runId, claimToken })
|
|
72
|
+
) {
|
|
73
|
+
result.blocked.push({
|
|
74
|
+
runId: state.runId,
|
|
75
|
+
reason: "canonical queue repair could not release its claim",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (state.workflowPath === undefined || state.workflowHash === undefined) {
|
|
81
|
+
result.blocked.push({ runId: state.runId, reason: "legacy workflow identity is incomplete" });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const legacyPath = {
|
|
85
|
+
workflowName: state.workflowName,
|
|
86
|
+
workflowPath: state.workflowPath,
|
|
87
|
+
};
|
|
88
|
+
const pathEntry = options.catalog.legacyPathEntry(legacyPath);
|
|
89
|
+
let workflowSource: WorkflowSource;
|
|
90
|
+
let workflowSourceRef: string;
|
|
91
|
+
if (pathEntry === undefined) {
|
|
92
|
+
workflowSource = {
|
|
93
|
+
kind: "file",
|
|
94
|
+
path: path.resolve(state.workflowPath),
|
|
95
|
+
hash: state.workflowHash,
|
|
96
|
+
};
|
|
97
|
+
workflowSourceRef = workflowSource.path;
|
|
98
|
+
} else {
|
|
99
|
+
const legacy = options.catalog.matchLegacy({
|
|
100
|
+
...legacyPath,
|
|
101
|
+
workflowHash: state.workflowHash,
|
|
102
|
+
});
|
|
103
|
+
if (legacy === undefined) {
|
|
104
|
+
const reason = `legacy built-in ${pathEntry.id} has an unknown source revision`;
|
|
105
|
+
result.blocked.push({ runId: state.runId, reason });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
workflowSource = {
|
|
109
|
+
kind: "builtin",
|
|
110
|
+
id: legacy.entry.id,
|
|
111
|
+
revision: legacy.revision,
|
|
112
|
+
};
|
|
113
|
+
workflowSourceRef = legacy.entry.ref;
|
|
114
|
+
}
|
|
115
|
+
const claimToken = randomUUID();
|
|
116
|
+
const queueIsDone = options.queue?.getWorkflowRun(state.runId)?.status === "done";
|
|
117
|
+
if (
|
|
118
|
+
options.queue !== undefined &&
|
|
119
|
+
!queueIsDone &&
|
|
120
|
+
!options.queue.claimLegacyWorkflowSourceRun({
|
|
121
|
+
runId: state.runId,
|
|
122
|
+
workflowName: state.workflowName,
|
|
123
|
+
oldWorkflowPath: state.workflowPath,
|
|
124
|
+
workflowSourceRef,
|
|
125
|
+
runnerId: `source-migration-${process.pid}`,
|
|
126
|
+
claimToken,
|
|
127
|
+
leaseMs: MIGRATION_LEASE_MS,
|
|
128
|
+
})
|
|
129
|
+
) {
|
|
130
|
+
result.blocked.push({
|
|
131
|
+
runId: state.runId,
|
|
132
|
+
reason: "matching queue row is active or unavailable",
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const migrated: WorkflowRunState = {
|
|
137
|
+
...state,
|
|
138
|
+
workflowSource,
|
|
139
|
+
};
|
|
140
|
+
delete migrated.workflowPath;
|
|
141
|
+
delete migrated.workflowHash;
|
|
142
|
+
const migratedManifest = {
|
|
143
|
+
...bundle.manifest,
|
|
144
|
+
workflowSource: migrated.workflowSource,
|
|
145
|
+
};
|
|
146
|
+
delete (migratedManifest as typeof migratedManifest & { workflowPath?: string }).workflowPath;
|
|
147
|
+
// State is the migration commit point. Queue and manifest updates are
|
|
148
|
+
// idempotent, so a crash before this write can safely retry.
|
|
149
|
+
await writeJsonAtomic(path.join(bundle.runDir, "manifest.json"), migratedManifest);
|
|
150
|
+
await writeJsonAtomic(path.join(bundle.runDir, "state.json"), migrated);
|
|
151
|
+
if (
|
|
152
|
+
options.queue !== undefined &&
|
|
153
|
+
!queueIsDone &&
|
|
154
|
+
!options.queue.parkWorkflowRun({ runId: state.runId, claimToken })
|
|
155
|
+
) {
|
|
156
|
+
result.blocked.push({
|
|
157
|
+
runId: state.runId,
|
|
158
|
+
reason: "migration completed but its queue claim could not be released",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
result.migratedRunIds.push(state.runId);
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sourceRef(source: WorkflowSource): string {
|
|
167
|
+
return source.kind === "builtin" ? `builtin:${source.id}` : source.path;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {
|
|
171
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.source-migration.tmp`;
|
|
172
|
+
await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
173
|
+
await fs.rename(tempPath, filePath);
|
|
174
|
+
}
|
package/src/workflows/schema.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CheckpointNodeDefinition,
|
|
5
5
|
ComputeNodeDefinition,
|
|
6
6
|
FunctionActionNodeDefinition,
|
|
7
|
+
NotifyNodeDefinition,
|
|
7
8
|
ShellActionNodeDefinition,
|
|
8
9
|
WorkflowDefinition,
|
|
9
10
|
WorkflowEdge,
|
|
@@ -34,9 +35,10 @@ function assertOptionalFunction(value: unknown, description: string): void {
|
|
|
34
35
|
function assertCommonNodeFields(node: WorkflowNodeDefinition, nodeId: string): void {
|
|
35
36
|
if (
|
|
36
37
|
node.timeoutMs !== undefined &&
|
|
38
|
+
typeof node.timeoutMs !== "function" &&
|
|
37
39
|
(typeof node.timeoutMs !== "number" || !Number.isFinite(node.timeoutMs) || node.timeoutMs <= 0)
|
|
38
40
|
) {
|
|
39
|
-
fail(`node ${nodeId} timeoutMs must be a finite positive number`);
|
|
41
|
+
fail(`node ${nodeId} timeoutMs must be a finite positive number or function`);
|
|
40
42
|
}
|
|
41
43
|
if (node.statusDetail !== undefined && typeof node.statusDetail !== "string") {
|
|
42
44
|
fail(`node ${nodeId} statusDetail must be a string`);
|
|
@@ -61,6 +63,16 @@ export function assertValidComputeNode(node: ComputeNodeDefinition, nodeId = "co
|
|
|
61
63
|
assertCommonNodeFields(node, nodeId);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
export function assertValidNotifyNode(node: NotifyNodeDefinition, nodeId = "notify"): void {
|
|
67
|
+
if (typeof node.message !== "function") {
|
|
68
|
+
fail(`node ${nodeId} requires a message function`);
|
|
69
|
+
}
|
|
70
|
+
if (node.kind !== undefined && node.kind !== "progress" && node.kind !== "final") {
|
|
71
|
+
fail(`node ${nodeId} kind must be progress or final`);
|
|
72
|
+
}
|
|
73
|
+
assertCommonNodeFields(node, nodeId);
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
export function assertValidActionNode(node: ActionNodeDefinition, nodeId = "action"): void {
|
|
65
77
|
// Dispatch discriminates with `"exec" in node`, so validation must use the
|
|
66
78
|
// same property semantics: a present-but-invalid `exec` is an error even
|
|
@@ -111,6 +123,9 @@ function assertValidNode(node: WorkflowNodeDefinition, nodeId: string): void {
|
|
|
111
123
|
case "compute":
|
|
112
124
|
assertValidComputeNode(node, nodeId);
|
|
113
125
|
return;
|
|
126
|
+
case "notify":
|
|
127
|
+
assertValidNotifyNode(node, nodeId);
|
|
128
|
+
return;
|
|
114
129
|
case "action":
|
|
115
130
|
assertValidActionNode(node, nodeId);
|
|
116
131
|
return;
|
package/src/workflows/store.ts
CHANGED
|
@@ -1284,7 +1284,7 @@ function createManifest(
|
|
|
1284
1284
|
runId: state.runId,
|
|
1285
1285
|
workflowName: state.workflowName,
|
|
1286
1286
|
...(state.runTitle !== undefined ? { runTitle: state.runTitle } : {}),
|
|
1287
|
-
...(state.
|
|
1287
|
+
...(state.workflowSource !== undefined ? { workflowSource: state.workflowSource } : {}),
|
|
1288
1288
|
startedAt: state.startedAt,
|
|
1289
1289
|
...(state.finishedAt !== undefined ? { finishedAt: state.finishedAt } : {}),
|
|
1290
1290
|
status: state.status,
|
|
@@ -1317,12 +1317,15 @@ export function createDefinitionSnapshot(workflow: WorkflowDefinition): Workflow
|
|
|
1317
1317
|
function snapshotNode(node: WorkflowNodeDefinition): WorkflowNodeSnapshot {
|
|
1318
1318
|
const common: WorkflowNodeSnapshot = {
|
|
1319
1319
|
nodeType: node.nodeType,
|
|
1320
|
-
...(node.timeoutMs
|
|
1320
|
+
...(typeof node.timeoutMs === "number" ? { timeoutMs: node.timeoutMs } : {}),
|
|
1321
1321
|
...(node.statusDetail !== undefined ? { statusDetail: node.statusDetail } : {}),
|
|
1322
1322
|
};
|
|
1323
1323
|
if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
|
|
1324
1324
|
common.expectedOutput = node.expectedOutput;
|
|
1325
1325
|
}
|
|
1326
|
+
if (node.nodeType === "notify") {
|
|
1327
|
+
common.summary = node.kind ?? "progress";
|
|
1328
|
+
}
|
|
1326
1329
|
if (node.nodeType === "checkpoint" && node.summary !== undefined) {
|
|
1327
1330
|
common.summary = node.summary;
|
|
1328
1331
|
}
|
package/src/workflows/types.ts
CHANGED
|
@@ -20,8 +20,11 @@ export type WorkflowNodeContext<TInput = unknown> = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export type WorkflowNodeCommon = {
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Per-node timeout or a callback that derives it from the run context.
|
|
25
|
+
* Falls back to the engine default (15 minutes).
|
|
26
|
+
*/
|
|
27
|
+
timeoutMs?: number | ((context: WorkflowNodeContext) => MaybePromise<number>);
|
|
25
28
|
/** Short human-readable label shown in the viewer while the node runs. */
|
|
26
29
|
statusDetail?: string;
|
|
27
30
|
};
|
|
@@ -65,6 +68,13 @@ export type ComputeNodeDefinition = WorkflowNodeCommon & {
|
|
|
65
68
|
run: (context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
66
69
|
};
|
|
67
70
|
|
|
71
|
+
/** A durable user-facing message addressed by the runtime to the run's origin session. */
|
|
72
|
+
export type NotifyNodeDefinition = WorkflowNodeCommon & {
|
|
73
|
+
nodeType: "notify";
|
|
74
|
+
message: (context: WorkflowNodeContext) => MaybePromise<string>;
|
|
75
|
+
kind?: "progress" | "final";
|
|
76
|
+
};
|
|
77
|
+
|
|
68
78
|
/** A deterministic runtime-owned step implemented as a local function. */
|
|
69
79
|
export type FunctionActionNodeDefinition = WorkflowNodeCommon & {
|
|
70
80
|
nodeType: "action";
|
|
@@ -118,6 +128,7 @@ export type CheckpointNodeDefinition = WorkflowNodeCommon & {
|
|
|
118
128
|
export type WorkflowNodeDefinition =
|
|
119
129
|
| AgentNodeDefinition
|
|
120
130
|
| ComputeNodeDefinition
|
|
131
|
+
| NotifyNodeDefinition
|
|
121
132
|
| ActionNodeDefinition
|
|
122
133
|
| CheckpointNodeDefinition;
|
|
123
134
|
|
|
@@ -230,6 +241,10 @@ export type WorkflowRunStatus =
|
|
|
230
241
|
| "timed_out"
|
|
231
242
|
| "cancelled";
|
|
232
243
|
|
|
244
|
+
export type WorkflowSource =
|
|
245
|
+
| { kind: "builtin"; id: string; revision: string }
|
|
246
|
+
| { kind: "file"; path: string; hash: string };
|
|
247
|
+
|
|
233
248
|
export type WorkflowRunState = {
|
|
234
249
|
schema: "pi-workflows.run-state.v1";
|
|
235
250
|
/**
|
|
@@ -249,8 +264,10 @@ export type WorkflowRunState = {
|
|
|
249
264
|
*/
|
|
250
265
|
carriedStepCount?: number;
|
|
251
266
|
runTitle?: string;
|
|
267
|
+
/** Stable built-in identity or immutable file source used by this run. */
|
|
268
|
+
workflowSource?: WorkflowSource;
|
|
269
|
+
/** Legacy fields accepted only by the bounded built-in migration. */
|
|
252
270
|
workflowPath?: string;
|
|
253
|
-
/** SHA-256 of the workflow source at run start; resume refuses mismatches. */
|
|
254
271
|
workflowHash?: string;
|
|
255
272
|
startedAt: string;
|
|
256
273
|
finishedAt?: string;
|
|
@@ -377,7 +394,7 @@ export type WorkflowRunManifest = {
|
|
|
377
394
|
runId: string;
|
|
378
395
|
workflowName: string;
|
|
379
396
|
runTitle?: string;
|
|
380
|
-
|
|
397
|
+
workflowSource?: WorkflowSource;
|
|
381
398
|
startedAt: string;
|
|
382
399
|
finishedAt?: string;
|
|
383
400
|
status: WorkflowRunStatus;
|
|
@@ -435,8 +452,30 @@ export interface AgentStepExecutor {
|
|
|
435
452
|
runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission>;
|
|
436
453
|
}
|
|
437
454
|
|
|
455
|
+
export type WorkflowNotificationRequest = {
|
|
456
|
+
runId: string;
|
|
457
|
+
workflowName: string;
|
|
458
|
+
nodeId: string;
|
|
459
|
+
attemptId: string;
|
|
460
|
+
/** Stable one-based occurrence of this notify node within the run. */
|
|
461
|
+
notificationIndex: number;
|
|
462
|
+
kind: "progress" | "final";
|
|
463
|
+
content: string;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
export type WorkflowNotificationReceipt = {
|
|
467
|
+
notificationId: string;
|
|
468
|
+
targetSessionId: string;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export interface WorkflowNotificationSink {
|
|
472
|
+
notify(request: WorkflowNotificationRequest): MaybePromise<WorkflowNotificationReceipt>;
|
|
473
|
+
}
|
|
474
|
+
|
|
438
475
|
export type WorkflowEngineOptions = {
|
|
439
476
|
executor: AgentStepExecutor;
|
|
477
|
+
/** Durable destination for notify nodes. Required when a workflow uses one. */
|
|
478
|
+
notificationSink?: WorkflowNotificationSink;
|
|
440
479
|
/** Root directory for run bundles. Defaults to `~/.pi/agent/workflows/runs`. */
|
|
441
480
|
outputRoot?: string;
|
|
442
481
|
/**
|