-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathflow.zod.ts
More file actions
268 lines (241 loc) · 11.5 KB
/
flow.zod.ts
File metadata and controls
268 lines (241 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
/**
* Flow Node Types
*/
export const FlowNodeAction = z.enum([
'start', // Trigger
'end', // Return/Stop
'decision', // If/Else logic
'assignment', // Set Variable
'loop', // For Each
'create_record', // CRUD: Create
'update_record', // CRUD: Update
'delete_record', // CRUD: Delete
'get_record', // CRUD: Get/Query
'http_request', // Webhook/API Call
'script', // Custom Script (JS/TS)
'screen', // Screen / User-Input Element
'wait', // Delay/Sleep
'subflow', // Call another flow
'connector_action', // Zapier-style integration action
'parallel_gateway', // BPMN Parallel Gateway — AND-split (all outgoing branches execute concurrently)
'join_gateway', // BPMN Join Gateway — AND-join (waits for all incoming branches to complete)
'boundary_event', // BPMN Boundary Event — attached to a host node for timer/error/signal interrupts
]);
/**
* Flow Variable Schema
* Variables available within the flow execution context.
*/
export const FlowVariableSchema = z.object({
name: z.string().describe('Variable name'),
type: z.string().describe('Data type (text, number, boolean, object, list)'),
isInput: z.boolean().default(false).describe('Is input parameter'),
isOutput: z.boolean().default(false).describe('Is output parameter'),
});
/**
* Flow Node Schema
* A single step in the visual logic graph.
*
* @example Decision Node
* {
* id: "dec_1",
* type: "decision",
* label: "Is High Value?",
* config: {
* conditions: [
* { label: "Yes", expression: "{amount} > 10000" },
* { label: "No", expression: "true" } // default
* ]
* },
* position: { x: 300, y: 200 }
* }
*/
export const FlowNodeSchema = z.object({
id: z.string().describe('Node unique ID'),
type: FlowNodeAction.describe('Action type'),
label: z.string().describe('Node label'),
/** Node Configuration Options (Specific to type) */
config: z.record(z.string(), z.unknown()).optional().describe('Node configuration'),
/**
* Connector Action Configuration
* Used when type is 'connector_action'
*/
connectorConfig: z.object({
connectorId: z.string(),
actionId: z.string(),
input: z.record(z.string(), z.unknown()).describe('Mapped inputs for the action'),
}).optional(),
/** UI Position (for the canvas) */
position: z.object({ x: z.number(), y: z.number() }).optional(),
/** Node-level execution timeout */
timeoutMs: z.number().int().min(0).optional().describe('Maximum execution time for this node in milliseconds'),
/** Node input schema declaration for Studio form generation and runtime validation */
inputSchema: z.record(z.string(), z.object({
type: z.enum(['string', 'number', 'boolean', 'object', 'array']).describe('Parameter type'),
required: z.boolean().default(false).describe('Whether the parameter is required'),
description: z.string().optional().describe('Parameter description'),
})).optional().describe('Input parameter schema for this node'),
/** Node output schema declaration */
outputSchema: z.record(z.string(), z.object({
type: z.enum(['string', 'number', 'boolean', 'object', 'array']).describe('Output type'),
description: z.string().optional().describe('Output description'),
})).optional().describe('Output schema declaration for this node'),
/**
* Wait Event Configuration (for 'wait' nodes)
* Defines what external event or condition should resume the paused execution.
* Industry alignment: BPMN Intermediate Catch Events, Temporal Signals.
*/
waitEventConfig: z.object({
/** Type of event to wait for */
eventType: z.enum(['timer', 'signal', 'webhook', 'manual', 'condition'])
.describe('What kind of event resumes the execution'),
/** Duration to wait (ISO 8601 duration or milliseconds) — for timer events */
timerDuration: z.string().optional().describe('ISO 8601 duration (e.g., "PT1H") or wait time for timer events'),
/** Signal name to listen for — for signal/webhook events */
signalName: z.string().optional().describe('Named signal or webhook event to wait for'),
/** Timeout before auto-failing or continuing — optional guard */
timeoutMs: z.number().int().min(0).optional().describe('Maximum wait time before timeout (ms)'),
/** Action to take on timeout */
onTimeout: z.enum(['fail', 'continue']).default('fail').describe('Behavior when the wait times out'),
}).optional().describe('Configuration for wait node event resumption'),
/**
* Boundary Event Configuration (for 'boundary_event' nodes)
* Attaches an event handler to a host activity node (BPMN Boundary Event pattern).
* Industry alignment: BPMN Boundary Error/Timer/Signal Events.
*/
boundaryConfig: z.object({
/** ID of the host node this boundary event is attached to */
attachedToNodeId: z.string().describe('Host node ID this boundary event monitors'),
/** Type of boundary event */
eventType: z.enum(['error', 'timer', 'signal', 'cancel'])
.describe('Boundary event trigger type'),
/** Whether the boundary event interrupts the host activity */
interrupting: z.boolean().default(true)
.describe('If true, the host activity is cancelled when this event fires'),
/** Error code filter — only for error boundary events */
errorCode: z.string().optional().describe('Specific error code to catch (empty = catch all errors)'),
/** Timer duration — only for timer boundary events */
timerDuration: z.string().optional().describe('ISO 8601 duration for timer boundary events'),
/** Signal name — only for signal boundary events */
signalName: z.string().optional().describe('Named signal to catch'),
}).optional().describe('Configuration for boundary events attached to host nodes'),
});
/**
* Flow Edge Schema
* Connections between nodes.
*/
export const FlowEdgeSchema = z.object({
id: z.string().describe('Edge unique ID'),
source: z.string().describe('Source Node ID'),
target: z.string().describe('Target Node ID'),
/** Condition for this path (only for decision/branch nodes) */
condition: z.string().optional().describe('Expression returning boolean used for branching'),
type: z.enum(['default', 'fault', 'conditional']).default('default')
.describe('Connection type: default (normal flow), fault (error path), or conditional (expression-guarded)'),
label: z.string().optional().describe('Label on the connector'),
/**
* Default Sequence Flow marker (BPMN Default Flow semantics).
* When true, this edge is taken when no sibling conditional edges match.
* Only meaningful on outgoing edges of decision/gateway nodes.
*/
isDefault: z.boolean().default(false)
.describe('Marks this edge as the default path when no other conditions match'),
});
/**
* Flow Schema
* Visual Business Logic Orchestration.
*
* @example Simple Approval Logic
* {
* name: "approve_order_flow",
* label: "Approve Large Orders",
* type: "record_change",
* status: "active",
* nodes: [
* { id: "start", type: "start", label: "Start", position: {x: 0, y: 0} },
* { id: "check_amount", type: "decision", label: "Check Amount", position: {x: 0, y: 100} },
* { id: "auto_approve", type: "update_record", label: "Auto Approve", position: {x: -100, y: 200} },
* { id: "submit_for_approval", type: "connector_action", label: "Submit", position: {x: 100, y: 200} }
* ],
* edges: [
* { id: "e1", source: "start", target: "check_amount" },
* { id: "e2", source: "check_amount", target: "auto_approve", condition: "{amount} < 500" },
* { id: "e3", source: "check_amount", target: "submit_for_approval", condition: "{amount} >= 500" }
* ]
* }
*/
export const FlowSchema = z.object({
/** Identity */
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Machine name'),
label: z.string().describe('Flow label'),
description: z.string().optional(),
/** Metadata & Versioning */
version: z.number().int().default(1).describe('Version number'),
status: z.enum(['draft', 'active', 'obsolete', 'invalid']).default('draft').describe('Deployment status'),
template: z.boolean().default(false).describe('Is logic template (Subflow)'),
/** Trigger Type */
type: z.enum(['autolaunched', 'record_change', 'schedule', 'screen', 'api']).describe('Flow type'),
/** Configuration Variables */
variables: z.array(FlowVariableSchema).optional().describe('Flow variables'),
/** Graph Definition */
nodes: z.array(FlowNodeSchema).describe('Flow nodes'),
edges: z.array(FlowEdgeSchema).describe('Flow connections'),
/** Execution Config */
active: z.boolean().default(false).describe('Is active (Deprecated: use status)'),
runAs: z.enum(['system', 'user']).default('user').describe('Execution context'),
/** Error Handling Strategy */
errorHandling: z.object({
strategy: z.enum(['fail', 'retry', 'continue']).default('fail').describe('How to handle node execution errors'),
maxRetries: z.number().int().min(0).max(10).default(0).describe('Number of retry attempts (only for retry strategy)'),
retryDelayMs: z.number().int().min(0).default(1000).describe('Delay between retries in milliseconds'),
backoffMultiplier: z.number().min(1).default(1).describe('Multiplier for exponential backoff between retries'),
maxRetryDelayMs: z.number().int().min(0).default(30000).describe('Maximum delay between retries in milliseconds'),
jitter: z.boolean().default(false).describe('Add random jitter to retry delay to avoid thundering herd'),
fallbackNodeId: z.string().optional().describe('Node ID to jump to on unrecoverable error'),
}).optional().describe('Flow-level error handling configuration'),
});
/**
* Type-safe factory for creating flow definitions.
*
* Validates the config at creation time using Zod `.parse()`.
*
* @example
* ```ts
* const onCreateFlow = defineFlow({
* name: 'on_task_create',
* label: 'On Task Create',
* type: 'record_change',
* nodes: [
* { id: 'start', type: 'start', label: 'Start' },
* { id: 'end', type: 'end', label: 'End' },
* ],
* edges: [{ id: 'e1', source: 'start', target: 'end' }],
* });
* ```
*/
export function defineFlow(config: z.input<typeof FlowSchema>): FlowParsed {
return FlowSchema.parse(config);
}
export type Flow = z.input<typeof FlowSchema>;
export type FlowParsed = z.infer<typeof FlowSchema>;
export type FlowNode = z.input<typeof FlowNodeSchema>;
export type FlowNodeParsed = z.infer<typeof FlowNodeSchema>;
export type FlowEdge = z.input<typeof FlowEdgeSchema>;
export type FlowEdgeParsed = z.infer<typeof FlowEdgeSchema>;
/**
* Flow Version History Schema
* Tracks historical versions of flow definitions for rollback support.
*
* Industry alignment: Salesforce Flow Versions, n8n Workflow History.
*/
export const FlowVersionHistorySchema = z.object({
flowName: z.string().describe('Flow machine name'),
version: z.number().int().min(1).describe('Version number'),
definition: FlowSchema.describe('Complete flow definition snapshot'),
createdAt: z.string().datetime().describe('When this version was created'),
createdBy: z.string().optional().describe('User who created this version'),
changeNote: z.string().optional().describe('Description of what changed in this version'),
});
export type FlowVersionHistory = z.input<typeof FlowVersionHistorySchema>;
export type FlowVersionHistoryParsed = z.infer<typeof FlowVersionHistorySchema>;