Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1856100
Implement outputs, workflow.output and workflow.fail steps and relate…
VladimirFilonov Feb 18, 2026
0b27d68
address review comments
VladimirFilonov Feb 23, 2026
0a48f18
Enhance workflow output step validation by ensuring minimum length fo…
VladimirFilonov Feb 23, 2026
5e59574
Update error handling in workflow output step implementation to ensur…
VladimirFilonov Feb 24, 2026
31f8c7d
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 4, 2026
b799357
drop unused outputToSchema
VladimirFilonov Mar 5, 2026
00b288f
Add JsonSchema support for workflow.output step
VladimirFilonov Mar 9, 2026
8fec4a8
Merge remote-tracking branch 'upstream/main' into feature/14634-workf…
VladimirFilonov Mar 9, 2026
2bb3434
Adressing comments
VladimirFilonov Mar 11, 2026
b79aa94
Merge remote-tracking branch 'upstream/main' into feature/14634-workf…
VladimirFilonov Mar 11, 2026
be90813
Address review comments (PR #253700):
VladimirFilonov Mar 12, 2026
9c9c063
Merge branch 'main' into feature/14634-workflow-output-and-fail
VladimirFilonov Mar 12, 2026
a024491
Address more comments
VladimirFilonov Mar 12, 2026
bdfac66
Merge branch 'main' into feature/14634-workflow-output-and-fail
VladimirFilonov Mar 13, 2026
84b8ba0
Merge branch 'main' into feature/14634-workflow-output-and-fail
VladimirFilonov Mar 15, 2026
a603eb0
Add icons for output/fail and update icons for execute
VladimirFilonov Mar 16, 2026
6891efd
adjust autocomplete on `with: `
VladimirFilonov Mar 16, 2026
951d5d7
Better autocomplete type suggestions
VladimirFilonov Mar 16, 2026
ea90029
Show output on trigger node in history UI
VladimirFilonov Mar 16, 2026
33c5d6d
Add tests
VladimirFilonov Mar 16, 2026
c7b0d5d
Merge remote-tracking branch 'upstream/main' into feature/14634-workf…
VladimirFilonov Mar 16, 2026
5095d84
Temporary backward compatibility for agent-builder
VladimirFilonov Mar 17, 2026
0b06ff0
Merge branch 'main' into feature/14634-workflow-output-and-fail
VladimirFilonov Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import type {
WhileStep,
WorkflowExecuteAsyncStep,
WorkflowExecuteStep,
WorkflowFailStep,
WorkflowOnFailure,
WorkflowOutputStep,
WorkflowRetry,
WorkflowSettings,
WorkflowYaml,
Expand Down Expand Up @@ -62,6 +64,7 @@ import type {
LoopEnterNode,
WaitForInputGraphNode,
WorkflowGraphType,
WorkflowOutputGraphNode,
} from '../types';
import { isLoopEnterNode } from '../types';
import { createTypedGraph } from '../workflow_graph/create_typed_graph';
Expand Down Expand Up @@ -183,6 +186,20 @@ function visitAbstractStep(currentStep: BaseStep, context: GraphBuildContext): W
return visitWorkflowExecuteAsyncStep(currentStep as WorkflowExecuteAsyncStep, context);
}

if (currentStep.type === 'workflow.output') {
return visitWorkflowOutputStep(currentStep, context);
}

if (currentStep.type === 'workflow.fail') {
const transformedStep: WorkflowOutputStep = {
...currentStep,
type: 'workflow.output',
status: 'failed',
with: (currentStep as WorkflowFailStep).with ?? {},
};
return visitWorkflowOutputStep(transformedStep, context);
}

return visitAtomicStep(currentStep, context);
}

Expand Down Expand Up @@ -288,6 +305,23 @@ export function visitWorkflowExecuteAsyncStep(
return createLeafStepGraph(currentStep, context, 'workflow.executeAsync');
}

export function visitWorkflowOutputStep(
currentStep: BaseStep,
context: GraphBuildContext
): WorkflowGraphType {
const stepId = getStepId(currentStep, context);
const graph = createTypedGraph({ directed: true });
const workflowOutputNode: WorkflowOutputGraphNode = {
id: stepId,
type: 'workflow.output',
stepId,
stepType: 'workflow.output',
configuration: currentStep as WorkflowOutputStep,
};
graph.setNode(workflowOutputNode.id, workflowOutputNode);
return graph;
}

export function visitAtomicStep(
currentStep: BaseStep,
context: GraphBuildContext
Expand Down
2 changes: 2 additions & 0 deletions src/platform/packages/shared/kbn-workflows/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type {
WorkflowExecuteGraphNodeSchema,
WorkflowExecuteAsyncGraphNode,
WorkflowExecuteAsyncGraphNodeSchema,
WorkflowOutputGraphNode,
WorkflowOutputGraphNodeSchema,
LoopEnterNode,
} from './types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
KibanaGraphNode,
WaitForInputGraphNode,
WaitGraphNode,
WorkflowOutputGraphNode,
} from './nodes/base';
import type {
EnterConditionBranchNode,
Expand Down Expand Up @@ -60,6 +61,9 @@ export const isWaitForInput = (node: GraphNodeUnion): node is WaitForInputGraphN
export const isDataSet = (node: GraphNodeUnion): node is DataSetGraphNode =>
node.type === 'data.set';

export const isWorkflowOutput = (node: GraphNodeUnion): node is WorkflowOutputGraphNode =>
node.type === 'workflow.output';

export const isEnterIf = (node: GraphNodeUnion): node is EnterIfNode => node.type === 'enter-if';

export const isExitIf = (node: GraphNodeUnion): node is ExitIfNode => node.type === 'exit-if';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export type {
WorkflowExecuteGraphNodeSchema,
WorkflowExecuteAsyncGraphNode,
WorkflowExecuteAsyncGraphNodeSchema,
WorkflowOutputGraphNode,
WorkflowOutputGraphNodeSchema,
} from './nodes/base';
export type {
EnterConditionBranchNode,
Expand Down Expand Up @@ -89,6 +91,7 @@ export {
isKibana,
isWait,
isWaitForInput,
isWorkflowOutput,
isEnterForeach,
isEnterWhile,
isEnterIf,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
WaitStepSchema,
WorkflowExecuteAsyncStepSchema,
WorkflowExecuteStepSchema,
WorkflowOutputStepSchema,
} from '../../../spec/schema';

export const GraphNodeSchema = z.object({
Expand Down Expand Up @@ -84,3 +85,10 @@ export const WorkflowExecuteAsyncGraphNodeSchema = GraphNodeSchema.extend({
configuration: WorkflowExecuteAsyncStepSchema,
});
export type WorkflowExecuteAsyncGraphNode = z.infer<typeof WorkflowExecuteAsyncGraphNodeSchema>;

export const WorkflowOutputGraphNodeSchema = GraphNodeSchema.extend({
id: z.string(),
type: z.literal('workflow.output'),
configuration: WorkflowOutputStepSchema,
});
export type WorkflowOutputGraphNode = z.infer<typeof WorkflowOutputGraphNodeSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
WaitGraphNodeSchema,
WorkflowExecuteAsyncGraphNodeSchema,
WorkflowExecuteGraphNodeSchema,
WorkflowOutputGraphNodeSchema,
} from './base';
import {
EnterConditionBranchNodeSchema,
Expand Down Expand Up @@ -58,6 +59,7 @@ const GraphNodeUnionSchema = z.discriminatedUnion('type', [
WaitForInputGraphNodeSchema,
WorkflowExecuteGraphNodeSchema,
WorkflowExecuteAsyncGraphNodeSchema,
WorkflowOutputGraphNodeSchema,
EnterIfNodeSchema,
ExitIfNodeSchema,
EnterConditionBranchNodeSchema,
Expand Down
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export * from './spec/lib/build_fields_zod_validator';
export * from './spec/lib/build_step_schema_for_agent';
export * from './spec/lib/generate_yaml_schema_from_connectors';
export * from './spec/lib/get_workflow_json_schema';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

import type { JSONSchema7 } from 'json-schema';
Comment thread
rosomri marked this conversation as resolved.
import {
buildInputsZodValidator,
buildFieldsZodValidator,
convertJsonSchemaToZod,
convertJsonSchemaToZodWithRefs,
} from './json_schema_to_zod';
} from './build_fields_zod_validator';

describe('convertJsonSchemaToZod', () => {
it('should convert a string schema to Zod', () => {
Expand Down Expand Up @@ -384,11 +384,11 @@ describe('convertJsonSchemaToZodWithRefs', () => {
});
});

describe('buildInputsZodValidator', () => {
describe('buildFieldsZodValidator', () => {
it('should return empty object schema when schema has no properties', () => {
const validator = buildInputsZodValidator(null);
const validator = buildFieldsZodValidator(null);
expect(validator.parse({})).toEqual({});
const validator2 = buildInputsZodValidator({});
const validator2 = buildFieldsZodValidator({});
expect(validator2.parse({})).toEqual({});
});

Expand All @@ -400,8 +400,8 @@ describe('buildInputsZodValidator', () => {
age: { type: 'number' },
},
required: ['name'],
} as Parameters<typeof buildInputsZodValidator>[0];
const validator = buildInputsZodValidator(schema);
} as Parameters<typeof buildFieldsZodValidator>[0];
const validator = buildFieldsZodValidator(schema);
expect(validator.parse({ name: 'Alice' })).toEqual({ name: 'Alice' });
expect(validator.parse({ name: 'Bob', age: 30 })).toEqual({ name: 'Bob', age: 30 });
expect(validator.safeParse({ age: 30 }).success).toBe(false);
Expand All @@ -414,8 +414,8 @@ describe('buildInputsZodValidator', () => {
name: { type: 'string', default: 'unknown' },
count: { type: 'number', default: 0 },
},
} as Parameters<typeof buildInputsZodValidator>[0];
const validator = buildInputsZodValidator(schema);
} as Parameters<typeof buildFieldsZodValidator>[0];
const validator = buildFieldsZodValidator(schema);
const result = validator.parse({}) as { name: string; count: number };
expect(result.name).toBe('unknown');
expect(result.count).toBe(0);
Expand All @@ -427,8 +427,8 @@ describe('buildInputsZodValidator', () => {
properties: {
enabled: { type: 'boolean' },
},
} as Parameters<typeof buildInputsZodValidator>[0];
const validator = buildInputsZodValidator(schema);
} as Parameters<typeof buildFieldsZodValidator>[0];
const validator = buildFieldsZodValidator(schema);
expect(validator.safeParse({ enabled: 'yes' }).success).toBe(false);
expect(validator.safeParse({ enabled: true }).success).toBe(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,34 @@
*/

import type { JSONSchema7 } from 'json-schema';
import { resolveRef } from '@kbn/workflows/spec/lib/input_conversion';
import { z } from '@kbn/zod/v4';
import { fromJSONSchema } from '@kbn/zod/v4/from_json_schema';
import { resolveRef } from './field_conversion';

/** Root schema type for $ref resolution (same as resolveRef's second parameter). */
type RootSchemaType = Parameters<typeof resolveRef>[1];

/**
* Recursively converts a JSON Schema to a Zod schema
* This is only used as a fallback for $ref references, which fromJSONSchema doesn't handle
* For non-$ref schemas, we use fromJSONSchema directly in the main function
* Converts a JSON Schema to a Zod schema using fromJSONSchema.
* Used as fallback when fromJSONSchema returns undefined or for $ref placeholders.
*/
function convertJsonSchemaToZodRecursive(jsonSchema: JSONSchema7 | null | undefined): z.ZodType {
// Defensive check: handle null/undefined schemas
export function convertJsonSchemaToZod(jsonSchema: JSONSchema7 | null | undefined): z.ZodType {
if (!jsonSchema || typeof jsonSchema !== 'object') {
return z.any();
}

// For $ref schemas, we can't use fromJSONSchema directly
// Return z.any() as a placeholder - $ref should be resolved before calling this
if (jsonSchema.$ref) {
return z.any();
}

// Try fromJSONSchema - it handles most cases
const zodSchema = fromJSONSchema(jsonSchema as Record<string, unknown>);
if (zodSchema !== undefined) {
return zodSchema;
}

// If fromJSONSchema fails, return z.any() as last resort
// This should be rare since fromJSONSchema handles most JSON Schema features
return z.any();
}

/**
* Converts a JSON Schema to a Zod schema using fromJSONSchema polyfill
* @param jsonSchema - The JSON Schema to convert
* @returns A Zod schema equivalent to the JSON Schema
*/
export function convertJsonSchemaToZod(jsonSchema: JSONSchema7 | null | undefined): z.ZodType {
// Defensive check: handle null/undefined schemas (crash prevention)
if (!jsonSchema || typeof jsonSchema !== 'object') {
return z.any();
}

// Note: fromJSONSchema doesn't handle $ref, so we need to resolve them first
// For now, if there's a $ref, fall back to recursive converter
// TODO: Resolve $ref before calling fromJSONSchema when remote ref support is added
if (jsonSchema.$ref) {
return convertJsonSchemaToZodRecursive(jsonSchema);
}

// Use fromJSONSchema polyfill - it handles objects, arrays, strings, numbers, booleans,
// enums, defaults, required fields, validation constraints, etc.
const zodSchema = fromJSONSchema(jsonSchema as Record<string, unknown>);
if (zodSchema !== undefined) {
return zodSchema;
}

// If fromJSONSchema returns undefined (should be rare), fall back to recursive converter
// This is a safety net for edge cases the polyfill might not handle
return convertJsonSchemaToZodRecursive(jsonSchema);
}

/**
* Recursively converts a JSON Schema to a Zod schema, resolving $ref against the root schema.
* Use this when you have a root schema (e.g. workflow inputsSchema) and property schemas that may reference it.
* Use when you have a root schema (e.g. workflow inputs/outputs) and property schemas that may reference it.
*/
export function convertJsonSchemaToZodWithRefs(
jsonSchema: JSONSchema7,
Expand Down Expand Up @@ -114,10 +74,11 @@ export function convertJsonSchemaToZodWithRefs(
}

/**
* Builds a Zod validator for workflow inputs from a normalized JSON Schema (object with properties, required, etc.).
* Handles required/optional, defaults, and $ref resolution. Use after normalizeInputsToJsonSchema when validating input payloads.
* Builds a Zod validator for workflow fields (inputs or outputs) from a normalized JSON Schema
* (object with properties, required, etc.). Handles required/optional, defaults, and $ref resolution.
* Use after normalizeFieldsToJsonSchema when validating field payloads.
*/
export function buildInputsZodValidator(
export function buildFieldsZodValidator(
schema: RootSchemaType | null | undefined
): z.ZodType<Record<string, unknown>> {
if (!schema?.properties || typeof schema.properties !== 'object') {
Expand Down
Loading
Loading