Skip to content

Commit 04fca14

Browse files
CopilotCopilot
andcommitted
feat(automation): add retry backoff, node timeoutMs/schemas, and version history to flow spec
- Add backoffMultiplier, maxRetryDelayMs, jitter to FlowSchema.errorHandling - Add timeoutMs, inputSchema, outputSchema to FlowNodeSchema - Add FlowVersionHistorySchema for rollback support - Add comprehensive tests for all new fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 346d712 commit 04fca14

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

packages/spec/src/automation/flow.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FlowEdgeSchema,
66
FlowVariableSchema,
77
FlowNodeAction,
8+
FlowVersionHistorySchema,
89
defineFlow,
910
type Flow,
1011
type FlowNode,
@@ -130,6 +131,39 @@ describe('FlowNodeSchema', () => {
130131
expect(() => FlowNodeSchema.parse(node)).not.toThrow();
131132
});
132133
});
134+
135+
it('should accept node with timeoutMs', () => {
136+
const result = FlowNodeSchema.safeParse({
137+
id: 'http_1',
138+
type: 'http_request',
139+
label: 'Call API',
140+
timeoutMs: 5000,
141+
});
142+
expect(result.success).toBe(true);
143+
if (result.success) {
144+
expect(result.data.timeoutMs).toBe(5000);
145+
}
146+
});
147+
148+
it('should accept node with inputSchema and outputSchema', () => {
149+
const result = FlowNodeSchema.safeParse({
150+
id: 'script_1',
151+
type: 'script',
152+
label: 'Process Data',
153+
inputSchema: {
154+
name: { type: 'string', required: true, description: 'User name' },
155+
age: { type: 'number', required: false },
156+
},
157+
outputSchema: {
158+
greeting: { type: 'string', description: 'Generated greeting' },
159+
},
160+
});
161+
expect(result.success).toBe(true);
162+
if (result.success) {
163+
expect(result.data.inputSchema).toBeDefined();
164+
expect(result.data.outputSchema).toBeDefined();
165+
}
166+
});
133167
});
134168

135169
describe('FlowEdgeSchema', () => {
@@ -597,6 +631,53 @@ describe('FlowSchema - errorHandling', () => {
597631
});
598632
expect(result.errorHandling).toBeUndefined();
599633
});
634+
635+
it('should accept exponential backoff configuration', () => {
636+
const result = FlowSchema.safeParse({
637+
name: 'backoff_flow',
638+
label: 'Backoff Flow',
639+
type: 'autolaunched',
640+
nodes: [
641+
{ id: 'start', type: 'start', label: 'Start' },
642+
{ id: 'end', type: 'end', label: 'End' },
643+
],
644+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
645+
errorHandling: {
646+
strategy: 'retry',
647+
maxRetries: 5,
648+
retryDelayMs: 1000,
649+
backoffMultiplier: 2,
650+
maxRetryDelayMs: 30000,
651+
jitter: true,
652+
},
653+
});
654+
expect(result.success).toBe(true);
655+
if (result.success) {
656+
expect(result.data.errorHandling!.backoffMultiplier).toBe(2);
657+
expect(result.data.errorHandling!.maxRetryDelayMs).toBe(30000);
658+
expect(result.data.errorHandling!.jitter).toBe(true);
659+
}
660+
});
661+
662+
it('should use defaults for backoff fields', () => {
663+
const result = FlowSchema.safeParse({
664+
name: 'default_backoff',
665+
label: 'Default Backoff',
666+
type: 'autolaunched',
667+
nodes: [
668+
{ id: 'start', type: 'start', label: 'Start' },
669+
{ id: 'end', type: 'end', label: 'End' },
670+
],
671+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
672+
errorHandling: { strategy: 'retry' },
673+
});
674+
expect(result.success).toBe(true);
675+
if (result.success) {
676+
expect(result.data.errorHandling!.backoffMultiplier).toBe(1);
677+
expect(result.data.errorHandling!.maxRetryDelayMs).toBe(30000);
678+
expect(result.data.errorHandling!.jitter).toBe(false);
679+
}
680+
});
600681
});
601682

602683
describe('defineFlow', () => {
@@ -642,3 +723,31 @@ describe('defineFlow', () => {
642723
})).toThrow();
643724
});
644725
});
726+
727+
describe('FlowVersionHistorySchema', () => {
728+
it('should validate a flow version history entry', () => {
729+
const result = FlowVersionHistorySchema.safeParse({
730+
flowName: 'my_flow',
731+
version: 1,
732+
definition: {
733+
name: 'my_flow',
734+
label: 'My Flow',
735+
type: 'autolaunched',
736+
nodes: [
737+
{ id: 'start', type: 'start', label: 'Start' },
738+
{ id: 'end', type: 'end', label: 'End' },
739+
],
740+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
741+
},
742+
createdAt: '2026-01-01T00:00:00Z',
743+
createdBy: 'admin',
744+
changeNote: 'Initial version',
745+
});
746+
expect(result.success).toBe(true);
747+
});
748+
749+
it('should require flowName, version, definition, and createdAt', () => {
750+
const result = FlowVersionHistorySchema.safeParse({});
751+
expect(result.success).toBe(false);
752+
});
753+
});

packages/spec/src/automation/flow.zod.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ export const FlowNodeSchema = z.object({
7272

7373
/** UI Position (for the canvas) */
7474
position: z.object({ x: z.number(), y: z.number() }).optional(),
75+
76+
/** Node-level execution timeout */
77+
timeoutMs: z.number().int().min(0).optional().describe('Maximum execution time for this node in milliseconds'),
78+
79+
/** Node input schema declaration for Studio form generation and runtime validation */
80+
inputSchema: z.record(z.string(), z.object({
81+
type: z.enum(['string', 'number', 'boolean', 'object', 'array']).describe('Parameter type'),
82+
required: z.boolean().default(false).describe('Whether the parameter is required'),
83+
description: z.string().optional().describe('Parameter description'),
84+
})).optional().describe('Input parameter schema for this node'),
85+
86+
/** Node output schema declaration */
87+
outputSchema: z.record(z.string(), z.object({
88+
type: z.enum(['string', 'number', 'boolean', 'object', 'array']).describe('Output type'),
89+
description: z.string().optional().describe('Output description'),
90+
})).optional().describe('Output schema declaration for this node'),
7591
});
7692

7793
/**
@@ -143,6 +159,9 @@ export const FlowSchema = z.object({
143159
strategy: z.enum(['fail', 'retry', 'continue']).default('fail').describe('How to handle node execution errors'),
144160
maxRetries: z.number().int().min(0).max(10).default(0).describe('Number of retry attempts (only for retry strategy)'),
145161
retryDelayMs: z.number().int().min(0).default(1000).describe('Delay between retries in milliseconds'),
162+
backoffMultiplier: z.number().min(1).default(1).describe('Multiplier for exponential backoff between retries'),
163+
maxRetryDelayMs: z.number().int().min(0).default(30000).describe('Maximum delay between retries in milliseconds'),
164+
jitter: z.boolean().default(false).describe('Add random jitter to retry delay to avoid thundering herd'),
146165
fallbackNodeId: z.string().optional().describe('Node ID to jump to on unrecoverable error'),
147166
}).optional().describe('Flow-level error handling configuration'),
148167
});
@@ -176,3 +195,21 @@ export type FlowNode = z.input<typeof FlowNodeSchema>;
176195
export type FlowNodeParsed = z.infer<typeof FlowNodeSchema>;
177196
export type FlowEdge = z.input<typeof FlowEdgeSchema>;
178197
export type FlowEdgeParsed = z.infer<typeof FlowEdgeSchema>;
198+
199+
/**
200+
* Flow Version History Schema
201+
* Tracks historical versions of flow definitions for rollback support.
202+
*
203+
* Industry alignment: Salesforce Flow Versions, n8n Workflow History.
204+
*/
205+
export const FlowVersionHistorySchema = z.object({
206+
flowName: z.string().describe('Flow machine name'),
207+
version: z.number().int().min(1).describe('Version number'),
208+
definition: FlowSchema.describe('Complete flow definition snapshot'),
209+
createdAt: z.string().datetime().describe('When this version was created'),
210+
createdBy: z.string().optional().describe('User who created this version'),
211+
changeNote: z.string().optional().describe('Description of what changed in this version'),
212+
});
213+
214+
export type FlowVersionHistory = z.input<typeof FlowVersionHistorySchema>;
215+
export type FlowVersionHistoryParsed = z.infer<typeof FlowVersionHistorySchema>;

0 commit comments

Comments
 (0)