فهرست منبع

feat(runtime): workflow inputs validate & validate api (#521)

* feat(runtime): json schema validator

* feat(runtime): engine invoke access validation

* test(runtime): inputs validate tests

* refactor(runtime): engine directly depend on validation

* feat(runtime): task validate api

* fix(ci): tsc error

* fix(ci): test error

* fix(runtime): udpate task report api zod define

* refactor(demo): extract common request method to reduce code duplication
Louis Young 6 ماه پیش
والد
کامیت
e0b13f9ac2
32فایلهای تغییر یافته به همراه1585 افزوده شده و 155 حذف شده
  1. 2 2
      apps/demo-free-layout/src/components/testrun/testrun-panel/index.tsx
  2. 2 0
      apps/demo-free-layout/src/plugins/runtime-plugin/client/base-client.ts
  3. 7 0
      apps/demo-free-layout/src/plugins/runtime-plugin/client/browser-client/index.ts
  4. 83 71
      apps/demo-free-layout/src/plugins/runtime-plugin/client/server-client/index.ts
  5. 25 12
      apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts
  6. 1 2
      packages/runtime/interface/src/api/constant.ts
  7. 2 2
      packages/runtime/interface/src/api/define.ts
  8. 1 1
      packages/runtime/interface/src/api/index.ts
  9. 25 5
      packages/runtime/interface/src/api/schema.ts
  10. 2 1
      packages/runtime/interface/src/api/task-report/index.ts
  11. 35 0
      packages/runtime/interface/src/api/task-validate/index.ts
  12. 0 48
      packages/runtime/interface/src/api/validation/index.ts
  13. 5 0
      packages/runtime/interface/src/client/index.ts
  14. 4 4
      packages/runtime/interface/src/runtime/context/index.ts
  15. 2 0
      packages/runtime/interface/src/runtime/engine/index.ts
  16. 2 2
      packages/runtime/interface/src/runtime/validation/index.ts
  17. 3 0
      packages/runtime/interface/src/schema/json-schema.ts
  18. 4 3
      packages/runtime/js-core/src/api/index.ts
  19. 20 0
      packages/runtime/js-core/src/api/task-validate.ts
  20. 9 0
      packages/runtime/js-core/src/application/workflow.ts
  21. 2 0
      packages/runtime/js-core/src/domain/__tests__/schemas/index.ts
  22. 473 0
      packages/runtime/js-core/src/domain/__tests__/schemas/validate-inputs.test.ts
  23. 302 0
      packages/runtime/js-core/src/domain/__tests__/schemas/validate-inputs.ts
  24. 12 0
      packages/runtime/js-core/src/domain/__tests__/utils/array-vo-data.ts
  25. 1 0
      packages/runtime/js-core/src/domain/__tests__/utils/index.ts
  26. 1 0
      packages/runtime/js-core/src/domain/container/index.ts
  27. 3 0
      packages/runtime/js-core/src/domain/engine/index.test.ts
  28. 25 0
      packages/runtime/js-core/src/domain/engine/index.ts
  29. 51 2
      packages/runtime/js-core/src/domain/validation/index.ts
  30. 1 0
      packages/runtime/js-core/src/infrastructure/utils/index.ts
  31. 208 0
      packages/runtime/js-core/src/infrastructure/utils/json-schema-validator.test.ts
  32. 272 0
      packages/runtime/js-core/src/infrastructure/utils/json-schema-validator.ts

+ 2 - 2
apps/demo-free-layout/src/components/testrun/testrun-panel/index.tsx

@@ -57,11 +57,11 @@ export const TestRunSidePanel: FC<TestRunSidePanelProps> = ({ visible, onCancel
   };
 
   useEffect(() => {
-    const disposer = runtimeService.onTerminated(({ result, errors }) => {
+    const disposer = runtimeService.onResultChanged(({ result, errors }) => {
       setRunning(false);
       setResult(result);
       if (errors) {
-        setErrors(errors.map((e) => `${e.nodeID}: ${e.message}`));
+        setErrors(errors);
       } else {
         setErrors(undefined);
       }

+ 2 - 0
apps/demo-free-layout/src/plugins/runtime-plugin/client/base-client.ts

@@ -17,4 +17,6 @@ export class WorkflowRuntimeClient implements IRuntimeClient {
   public [FlowGramAPIName.TaskResult]: IRuntimeClient[FlowGramAPIName.TaskResult];
 
   public [FlowGramAPIName.TaskCancel]: IRuntimeClient[FlowGramAPIName.TaskCancel];
+
+  public [FlowGramAPIName.TaskValidate]: IRuntimeClient[FlowGramAPIName.TaskValidate];
 }

+ 7 - 0
apps/demo-free-layout/src/plugins/runtime-plugin/client/browser-client/index.ts

@@ -36,4 +36,11 @@ export class WorkflowRuntimeBrowserClient implements IRuntimeClient {
     const { TaskCancelAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
     return TaskCancelAPI(input);
   };
+
+  public [FlowGramAPIName.TaskValidate]: IRuntimeClient[FlowGramAPIName.TaskValidate] = async (
+    input
+  ) => {
+    const { TaskValidateAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
+    return TaskValidateAPI(input);
+  };
 }

+ 83 - 71
apps/demo-free-layout/src/plugins/runtime-plugin/client/server-client/index.ts

@@ -6,14 +6,21 @@
 import {
   FlowGramAPIName,
   IRuntimeClient,
+  TaskCancelDefine,
   TaskCancelInput,
   TaskCancelOutput,
+  TaskReportDefine,
   TaskReportInput,
   TaskReportOutput,
+  TaskResultDefine,
   TaskResultInput,
   TaskResultOutput,
+  TaskRunDefine,
   TaskRunInput,
   TaskRunOutput,
+  TaskValidateDefine,
+  TaskValidateInput,
+  TaskValidateOutput,
 } from '@flowgram.ai/runtime-interface';
 import { injectable } from '@flowgram.ai/free-layout-editor';
 
@@ -32,99 +39,104 @@ export class WorkflowRuntimeServerClient implements IRuntimeClient {
   }
 
   public async [FlowGramAPIName.TaskRun](input: TaskRunInput): Promise<TaskRunOutput | undefined> {
-    try {
-      const body = JSON.stringify(input);
-      const response = await fetch(this.getURL('/api/task/run'), {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: body,
-        redirect: 'follow',
-      });
-      const output: TaskRunOutput | ServerError = await response.json();
-      if (this.isError(output)) {
-        console.error('TaskRun failed', output);
-        return;
-      }
-      return output;
-    } catch (e) {
-      console.error(e);
-      return;
-    }
+    return this.request<TaskRunOutput>(TaskRunDefine.path, TaskRunDefine.method, {
+      body: input,
+      errorMessage: 'TaskRun failed',
+    });
   }
 
   public async [FlowGramAPIName.TaskReport](
     input: TaskReportInput
   ): Promise<TaskReportOutput | undefined> {
-    try {
-      const response = await fetch(this.getURL(`/api/task/report?taskID=${input.taskID}`), {
-        method: 'GET',
-        redirect: 'follow',
-      });
-      const output: TaskReportOutput | ServerError = await response.json();
-      if (this.isError(output)) {
-        console.error('TaskReport failed', output);
-        return;
-      }
-      return output;
-    } catch (e) {
-      console.error(e);
-      return;
-    }
+    return this.request<TaskReportOutput>(TaskReportDefine.path, TaskReportDefine.method, {
+      queryParams: { taskID: input.taskID },
+      errorMessage: 'TaskReport failed',
+    });
   }
 
   public async [FlowGramAPIName.TaskResult](
     input: TaskResultInput
   ): Promise<TaskResultOutput | undefined> {
-    try {
-      const response = await fetch(this.getURL(`/api/task/result?taskID=${input.taskID}`), {
-        method: 'GET',
-        redirect: 'follow',
-      });
-      const output: TaskResultOutput | ServerError = await response.json();
-      if (this.isError(output)) {
-        console.error('TaskReport failed', output);
-        return {
-          success: false,
-        };
-      }
-      return output;
-    } catch (e) {
-      console.error(e);
-      return {
-        success: false,
-      };
-    }
+    return this.request<TaskResultOutput>(TaskResultDefine.path, TaskResultDefine.method, {
+      queryParams: { taskID: input.taskID },
+      errorMessage: 'TaskResult failed',
+      fallbackValue: { success: false },
+    });
   }
 
   public async [FlowGramAPIName.TaskCancel](input: TaskCancelInput): Promise<TaskCancelOutput> {
+    const result = await this.request<TaskCancelOutput>(
+      TaskCancelDefine.path,
+      TaskCancelDefine.method,
+      {
+        body: input,
+        errorMessage: 'TaskCancel failed',
+        fallbackValue: { success: false },
+      }
+    );
+    return result ?? { success: false };
+  }
+
+  public async [FlowGramAPIName.TaskValidate](
+    input: TaskValidateInput
+  ): Promise<TaskValidateOutput | undefined> {
+    return this.request<TaskValidateOutput>(TaskValidateDefine.path, TaskValidateDefine.method, {
+      body: input,
+      errorMessage: 'TaskValidate failed',
+    });
+  }
+
+  // Generic request method to reduce code duplication
+  private async request<T>(
+    path: string,
+    method: string,
+    options: {
+      body?: unknown;
+      queryParams?: Record<string, string>;
+      errorMessage: string;
+      fallbackValue?: T;
+    }
+  ): Promise<T | undefined> {
     try {
-      const body = JSON.stringify(input);
-      const response = await fetch(this.getURL(`/api/task/cancel`), {
-        method: 'PUT',
+      const url = this.url(path, options.queryParams);
+      const requestOptions: RequestInit = {
+        method,
         redirect: 'follow',
-        headers: {
+      };
+
+      if (options.body) {
+        requestOptions.headers = {
           'Content-Type': 'application/json',
-        },
-        body,
-      });
-      const output: TaskCancelOutput | ServerError = await response.json();
-      if (this.isError(output)) {
-        console.error('TaskReport failed', output);
-        return {
-          success: false,
         };
+        requestOptions.body = JSON.stringify(options.body);
       }
+
+      const response = await fetch(url, requestOptions);
+      const output: T | ServerError = await response.json();
+
+      if (this.isError(output)) {
+        console.error(options.errorMessage, output);
+        return options.fallbackValue;
+      }
+
       return output;
-    } catch (e) {
-      console.error(e);
-      return {
-        success: false,
-      };
+    } catch (error) {
+      console.error(error);
+      return options.fallbackValue;
     }
   }
 
+  // Build URL with query parameters
+  private url(path: string, queryParams?: Record<string, string>): string {
+    const baseURL = this.getURL(`/api${path}`);
+    if (!queryParams) {
+      return baseURL;
+    }
+
+    const searchParams = new URLSearchParams(queryParams);
+    return `${baseURL}?${searchParams.toString()}`;
+  }
+
   private isError(output: unknown | undefined): output is ServerError {
     return !!output && (output as ServerError).code !== undefined;
   }

+ 25 - 12
apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts

@@ -4,7 +4,6 @@
  */
 
 import {
-  IMessage,
   IReport,
   NodeReport,
   WorkflowInputs,
@@ -52,8 +51,8 @@ export class WorkflowRuntimeService {
 
   private resetEmitter = new Emitter<{}>();
 
-  public terminatedEmitter = new Emitter<{
-    errors?: IMessage[];
+  private resultEmitter = new Emitter<{
+    errors?: string[];
     result?: {
       inputs: WorkflowInputs;
       outputs: WorkflowOutputs;
@@ -66,7 +65,7 @@ export class WorkflowRuntimeService {
 
   public onReset = this.resetEmitter.event;
 
-  public onTerminated = this.terminatedEmitter.event;
+  public onResultChanged = this.resultEmitter.event;
 
   public isFlowingLine(line: WorkflowLineEntity) {
     return this.runningNodes.some((node) =>
@@ -78,16 +77,28 @@ export class WorkflowRuntimeService {
     if (this.taskID) {
       await this.taskCancel();
     }
-    if (!this.validate()) {
+    if (!this.validateForm()) {
+      return;
+    }
+    const inputs = JSON.parse(inputsString) as WorkflowInputs;
+    const schema = this.document.toJSON();
+    const validateResult = await this.runtimeClient.TaskValidate({
+      schema: JSON.stringify(schema),
+      inputs,
+    });
+    if (!validateResult?.valid) {
+      this.resultEmitter.fire({
+        errors: validateResult?.errors ?? ['Internal Server Error'],
+      });
       return;
     }
     this.reset();
     const output = await this.runtimeClient.TaskRun({
-      schema: JSON.stringify(this.document.toJSON()),
-      inputs: JSON.parse(inputsString) as WorkflowInputs,
+      schema: JSON.stringify(schema),
+      inputs,
     });
     if (!output) {
-      this.terminatedEmitter.fire({});
+      this.resultEmitter.fire({});
       return;
     }
     this.taskID = output.taskID;
@@ -105,7 +116,7 @@ export class WorkflowRuntimeService {
     });
   }
 
-  private async validate(): Promise<boolean> {
+  private async validateForm(): Promise<boolean> {
     const allForms = this.document.getAllNodes().map((node) => getNodeForm(node));
     const formValidations = await Promise.all(allForms.map(async (form) => form?.validate()));
     const validations = formValidations.filter((validation) => validation !== undefined);
@@ -139,10 +150,12 @@ export class WorkflowRuntimeService {
     if (workflowStatus.terminated) {
       clearInterval(this.syncTaskReportIntervalID);
       if (Object.keys(outputs).length > 0) {
-        this.terminatedEmitter.fire({ result: { inputs, outputs } });
+        this.resultEmitter.fire({ result: { inputs, outputs } });
       } else {
-        this.terminatedEmitter.fire({
-          errors: messages.error,
+        this.resultEmitter.fire({
+          errors: messages?.error?.map((message) =>
+            message.nodeID ? `${message.nodeID}: ${message.message}` : message.message
+          ),
         });
       }
     }

+ 1 - 2
packages/runtime/interface/src/api/constant.ts

@@ -17,11 +17,10 @@ export enum FlowGramAPIName {
   TaskReport = 'TaskReport',
   TaskResult = 'TaskResult',
   TaskCancel = 'TaskCancel',
-  Validation = 'Validation',
+  TaskValidate = 'TaskValidate',
 }
 
 export enum FlowGramAPIModule {
   Info = 'Info',
   Task = 'Task',
-  Validation = 'Validation',
 }

+ 2 - 2
packages/runtime/interface/src/api/define.ts

@@ -3,8 +3,8 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { ValidationDefine } from './validation';
 import { FlowGramAPIDefines } from './type';
+import { TaskValidateDefine } from './task-validate';
 import { TaskRunDefine } from './task-run';
 import { TaskResultDefine } from './task-result';
 import { TaskReportDefine } from './task-report';
@@ -18,7 +18,7 @@ export const FlowGramAPIs: FlowGramAPIDefines = {
   [FlowGramAPIName.TaskReport]: TaskReportDefine,
   [FlowGramAPIName.TaskResult]: TaskResultDefine,
   [FlowGramAPIName.TaskCancel]: TaskCancelDefine,
-  [FlowGramAPIName.Validation]: ValidationDefine,
+  [FlowGramAPIName.TaskValidate]: TaskValidateDefine,
 };
 
 export const FlowGramAPINames = Object.keys(FlowGramAPIs) as FlowGramAPIName[];

+ 1 - 1
packages/runtime/interface/src/api/index.ts

@@ -10,6 +10,6 @@ export * from './constant';
 export * from './task-run';
 export * from './server-info';
 export * from './task-report';
-export * from './validation';
+export * from './task-validate';
 export * from './task-result';
 export * from './task-cancel';

+ 25 - 5
packages/runtime/interface/src/api/schema.ts

@@ -6,6 +6,7 @@
 import z from 'zod';
 
 const WorkflowIOZodSchema = z.record(z.string(), z.any());
+
 const WorkflowSnapshotZodSchema = z.object({
   id: z.string(),
   nodeID: z.string(),
@@ -14,6 +15,7 @@ const WorkflowSnapshotZodSchema = z.object({
   data: WorkflowIOZodSchema,
   branch: z.string().optional(),
 });
+
 const WorkflowStatusZodShape = {
   status: z.string(),
   terminated: z.boolean(),
@@ -23,14 +25,32 @@ const WorkflowStatusZodShape = {
 };
 const WorkflowStatusZodSchema = z.object(WorkflowStatusZodShape);
 
+const WorkflowNodeReportZodSchema = z.object({
+  id: z.string(),
+  ...WorkflowStatusZodShape,
+  snapshots: z.array(WorkflowSnapshotZodSchema),
+});
+
+const WorkflowReportsZodSchema = z.record(z.string(), WorkflowNodeReportZodSchema);
+
+const WorkflowMessageZodSchema = z.object({
+  id: z.string(),
+  type: z.enum(['log', 'info', 'debug', 'error', 'warning']),
+  message: z.string(),
+  nodeID: z.string().optional(),
+  timestamp: z.number(),
+});
+
+const WorkflowMessagesZodSchema = z.record(
+  z.enum(['log', 'info', 'debug', 'error', 'warning']),
+  z.array(WorkflowMessageZodSchema)
+);
+
 export const WorkflowZodSchema = {
   Inputs: WorkflowIOZodSchema,
   Outputs: WorkflowIOZodSchema,
   Status: WorkflowStatusZodSchema,
   Snapshot: WorkflowSnapshotZodSchema,
-  NodeReport: z.object({
-    id: z.string(),
-    ...WorkflowStatusZodShape,
-    snapshots: z.array(WorkflowSnapshotZodSchema),
-  }),
+  Reports: WorkflowReportsZodSchema,
+  Messages: WorkflowMessagesZodSchema,
 };

+ 2 - 1
packages/runtime/interface/src/api/task-report/index.ts

@@ -30,7 +30,8 @@ export const TaskReportDefine: FlowGramAPIDefine = {
       inputs: WorkflowZodSchema.Inputs,
       outputs: WorkflowZodSchema.Outputs,
       workflowStatus: WorkflowZodSchema.Status,
-      reports: z.record(z.string(), WorkflowZodSchema.NodeReport),
+      reports: WorkflowZodSchema.Reports,
+      messages: WorkflowZodSchema.Messages,
     }),
   },
 };

+ 35 - 0
packages/runtime/interface/src/api/task-validate/index.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import z from 'zod';
+
+import { ValidationResult, WorkflowInputs } from '@runtime/index';
+import { FlowGramAPIDefine } from '@api/type';
+import { WorkflowZodSchema } from '@api/schema';
+import { FlowGramAPIMethod, FlowGramAPIModule, FlowGramAPIName } from '@api/constant';
+
+export interface TaskValidateInput {
+  inputs: WorkflowInputs;
+  schema: string;
+}
+
+export interface TaskValidateOutput extends ValidationResult {}
+
+export const TaskValidateDefine: FlowGramAPIDefine = {
+  name: FlowGramAPIName.TaskValidate,
+  method: FlowGramAPIMethod.POST,
+  path: '/task/validate',
+  module: FlowGramAPIModule.Task,
+  schema: {
+    input: z.object({
+      schema: z.string(),
+      inputs: WorkflowZodSchema.Inputs,
+    }),
+    output: z.object({
+      valid: z.boolean(),
+      errors: z.array(z.string()).optional(),
+    }),
+  },
+};

+ 0 - 48
packages/runtime/interface/src/api/validation/index.ts

@@ -1,48 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import z from 'zod';
-
-import { ValidationResult } from '@runtime/index';
-import { FlowGramAPIDefine } from '@api/type';
-import { FlowGramAPIMethod, FlowGramAPIModule, FlowGramAPIName } from '@api/constant';
-
-export interface ValidationReq {
-  schema: string;
-}
-
-export interface ValidationRes extends ValidationResult {}
-
-export const ValidationDefine: FlowGramAPIDefine = {
-  name: FlowGramAPIName.Validation,
-  method: FlowGramAPIMethod.POST,
-  path: '/validation',
-  module: FlowGramAPIModule.Validation,
-  schema: {
-    input: z.object({
-      schema: z.string(),
-    }),
-    output: z.object({
-      valid: z.boolean(),
-      nodeErrors: z.array(
-        z.object({
-          message: z.string(),
-          nodeID: z.string(),
-        })
-      ),
-      edgeErrors: z.array(
-        z.object({
-          message: z.string(),
-          edge: z.object({
-            sourceNodeID: z.string(),
-            targetNodeID: z.string(),
-            sourcePortID: z.string().optional(),
-            targetPortID: z.string().optional(),
-          }),
-        })
-      ),
-    }),
-  },
-};

+ 5 - 0
packages/runtime/interface/src/client/index.ts

@@ -13,6 +13,8 @@ import type {
   TaskResultOutput,
   TaskRunInput,
   TaskRunOutput,
+  TaskValidateInput,
+  TaskValidateOutput,
 } from '@api/index';
 
 export interface IRuntimeClient {
@@ -20,4 +22,7 @@ export interface IRuntimeClient {
   [FlowGramAPIName.TaskReport]: (input: TaskReportInput) => Promise<TaskReportOutput | undefined>;
   [FlowGramAPIName.TaskResult]: (input: TaskResultInput) => Promise<TaskResultOutput | undefined>;
   [FlowGramAPIName.TaskCancel]: (input: TaskCancelInput) => Promise<TaskCancelOutput | undefined>;
+  [FlowGramAPIName.TaskValidate]: (
+    input: TaskValidateInput
+  ) => Promise<TaskValidateOutput | undefined>;
 }

+ 4 - 4
packages/runtime/interface/src/runtime/context/index.ts

@@ -5,13 +5,13 @@
 
 import { IVariableStore } from '@runtime/variable';
 import { IStatusCenter } from '@runtime/status';
+import { IState } from '@runtime/state';
 import { ISnapshotCenter } from '@runtime/snapshot';
+import { IReporter } from '@runtime/reporter';
 import { IMessageCenter } from '@runtime/message';
 import { IIOCenter } from '@runtime/io-center';
-import { IState } from '../state';
-import { IReporter } from '../reporter';
-import { IDocument } from '../document';
-import { InvokeParams } from '../base';
+import { IDocument } from '@runtime/document';
+import { InvokeParams } from '@runtime/base';
 
 export interface ContextData {
   variableStore: IVariableStore;

+ 2 - 0
packages/runtime/interface/src/runtime/engine/index.ts

@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: MIT
  */
 
+import { IValidation } from '@runtime/validation';
 import { ITask } from '../task';
 import { IExecutor } from '../executor';
 import { INode } from '../document';
@@ -10,6 +11,7 @@ import { IContext } from '../context';
 import { InvokeParams } from '../base';
 
 export interface EngineServices {
+  Validation: IValidation;
   Executor: IExecutor;
 }
 

+ 2 - 2
packages/runtime/interface/src/runtime/validation/index.ts

@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { WorkflowSchema } from '@schema/index';
+import { InvokeParams } from '@runtime/base';
 
 export interface ValidationResult {
   valid: boolean;
@@ -11,7 +11,7 @@ export interface ValidationResult {
 }
 
 export interface IValidation {
-  validate(schema: WorkflowSchema): ValidationResult;
+  invoke(params: InvokeParams): ValidationResult;
 }
 
 export const IValidation = Symbol.for('Validation');

+ 3 - 0
packages/runtime/interface/src/schema/json-schema.ts

@@ -25,6 +25,9 @@ export interface IJsonSchema<T = string> {
   items?: IJsonSchema<T>;
   required?: string[];
   $ref?: string;
+  key?: number;
+  name?: string;
+  isPropertyRequired?: boolean;
   extra?: {
     index?: number;
     // Used in BaseType.isEqualWithJSONSchema, the type comparison will be weak

+ 4 - 3
packages/runtime/js-core/src/api/index.ts

@@ -5,18 +5,19 @@
 
 import { FlowGramAPIName } from '@flowgram.ai/runtime-interface';
 
+import { TaskValidateAPI } from './task-validate';
 import { TaskRunAPI } from './task-run';
 import { TaskResultAPI } from './task-result';
 import { TaskReportAPI } from './task-report';
 import { TaskCancelAPI } from './task-cancel';
 
-export { TaskRunAPI, TaskResultAPI, TaskReportAPI, TaskCancelAPI };
+export { TaskRunAPI, TaskResultAPI, TaskReportAPI, TaskCancelAPI, TaskValidateAPI };
 
 export const WorkflowRuntimeAPIs: Record<FlowGramAPIName, (i: any) => any> = {
+  [FlowGramAPIName.ServerInfo]: () => {}, // TODO
   [FlowGramAPIName.TaskRun]: TaskRunAPI,
   [FlowGramAPIName.TaskReport]: TaskReportAPI,
   [FlowGramAPIName.TaskResult]: TaskResultAPI,
   [FlowGramAPIName.TaskCancel]: TaskCancelAPI,
-  [FlowGramAPIName.ServerInfo]: () => {}, // TODO
-  [FlowGramAPIName.Validation]: () => {}, // TODO
+  [FlowGramAPIName.TaskValidate]: TaskValidateAPI,
 };

+ 20 - 0
packages/runtime/js-core/src/api/task-validate.ts

@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { TaskValidateInput, TaskValidateOutput } from '@flowgram.ai/runtime-interface';
+
+import { WorkflowApplication } from '@application/workflow';
+
+export const TaskValidateAPI = async (input: TaskValidateInput): Promise<TaskValidateOutput> => {
+  const app = WorkflowApplication.instance;
+  const { schema: stringSchema, inputs } = input;
+  const schema = JSON.parse(stringSchema);
+  const result = app.validate({
+    schema,
+    inputs,
+  });
+  const output: TaskValidateOutput = result;
+  return output;
+};

+ 9 - 0
packages/runtime/js-core/src/application/workflow.ts

@@ -11,6 +11,8 @@ import {
   ITask,
   IReport,
   WorkflowOutputs,
+  IValidation,
+  ValidationResult,
 } from '@flowgram.ai/runtime-interface';
 
 import { WorkflowRuntimeContainer } from '@workflow/container';
@@ -69,6 +71,13 @@ export class WorkflowApplication {
     return task.context.ioCenter.outputs;
   }
 
+  public validate(params: InvokeParams): ValidationResult {
+    const validation = this.container.get<IValidation>(IValidation);
+    const result = validation.invoke(params);
+    console.log('> POST TaskValidate - valid: ', result.valid);
+    return result;
+  }
+
   private static _instance: WorkflowApplication;
 
   public static get instance(): WorkflowApplication {

+ 2 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/index.ts

@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: MIT
  */
 
+import { validateInputsSchema } from './validate-inputs';
 import { twoLLMSchema } from './two-llm';
 import { loopSchema } from './loop';
 import { branchTwoLayersSchema } from './branch-two-layers';
@@ -17,4 +18,5 @@ export const TestSchemas = {
   basicLLMSchema,
   loopSchema,
   branchTwoLayersSchema,
+  validateInputsSchema,
 };

+ 473 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/validate-inputs.test.ts

@@ -0,0 +1,473 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { describe, expect, it } from 'vitest';
+import { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';
+
+import { WorkflowRuntimeContainer } from '../../container';
+import { ValidateInputsSchemaInputs } from './validate-inputs';
+import { TestSchemas } from '.';
+
+const container: IContainer = WorkflowRuntimeContainer.instance;
+
+describe('WorkflowRuntime validate inputs success', () => {
+  it('basic inputs', async () => {
+    const inputs: ValidateInputsSchemaInputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: {
+          CEA: 'nested string',
+        },
+        CF: ['item1', 'item2', 'item3'],
+      },
+      DD: [
+        {
+          DA: 'optional string',
+          DB: {
+            DBA: 'deep nested',
+          },
+        },
+      ],
+      EE: {
+        EA: {
+          EAA: 'required nested',
+        },
+        EB: 'optional string',
+      },
+    };
+
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    expect(result).toStrictEqual(inputs);
+  });
+  it('complex inputs', async () => {
+    const inputs: ValidateInputsSchemaInputs = {
+      AA: 'complex example',
+      BB: -999,
+      CC: {
+        CA: 'test',
+        CB: 0,
+        CC: 999999,
+        CD: false,
+        CE: {
+          CEA: 'another nested value',
+        },
+        CF: ['a', 'b', 'c', 'd', 'e'],
+      },
+      DD: [
+        {
+          DA: 'first item',
+          DB: {
+            DBA: 'first nested',
+          },
+        },
+        {
+          DA: 'second item',
+          // DB is optional, omitted here
+        },
+        {
+          // DA is optional, omitted here
+          DB: {
+            DBA: 'third nested',
+          },
+        },
+      ],
+      EE: {
+        EA: {
+          EAA: 'required value',
+        },
+        // EB is optional, omitted here
+      },
+    };
+
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    expect(result).toStrictEqual(inputs);
+  });
+  it('min inputs', async () => {
+    const inputs: ValidateInputsSchemaInputs = {
+      AA: '',
+      BB: 0,
+      CC: {
+        CA: '',
+        CB: 0,
+        CC: 0,
+        CD: false,
+        CE: {
+          CEA: '',
+        },
+        CF: [],
+      },
+      DD: [],
+      EE: {
+        EA: {
+          EAA: '',
+        },
+      },
+    };
+
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    expect(result).toStrictEqual(inputs);
+  });
+  it('full inputs', async () => {
+    const inputs: ValidateInputsSchemaInputs = {
+      AA: 'full example',
+      BB: 12345,
+      CC: {
+        CA: 'complete',
+        CB: 500,
+        CC: 600,
+        CD: true,
+        CE: {
+          CEA: 'full nested',
+        },
+        CF: ['full', 'array', 'example'],
+      },
+      DD: [
+        {
+          DA: 'with all fields',
+          DB: {
+            DBA: 'complete nested',
+          },
+        },
+      ],
+      EE: {
+        EA: {
+          EAA: 'all required',
+        },
+        EB: 'with optional field',
+      },
+    };
+
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    expect(result).toStrictEqual(inputs);
+  });
+  it('edge inputs', async () => {
+    const inputs: ValidateInputsSchemaInputs = {
+      AA: 'a', // single character
+      BB: Number.MAX_SAFE_INTEGER,
+      CC: {
+        CA: 'very long string that tests the boundaries of what might be acceptable in real world scenarios',
+        CB: Number.MIN_SAFE_INTEGER,
+        CC: 1,
+        CD: true,
+        CE: {
+          CEA: 'boundary test',
+        },
+        CF: ['single'],
+      },
+      DD: Array(100).fill({
+        DA: 'repeated',
+        DB: {
+          DBA: 'many items',
+        },
+      }),
+      EE: {
+        EA: {
+          EAA: 'boundary',
+        },
+        EB: '',
+      },
+    };
+
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    expect(result).toStrictEqual(inputs);
+  });
+});
+
+describe('WorkflowRuntime validate inputs failed', () => {
+  it('missing required property "AA"', async () => {
+    const inputs = {
+      // AA: "missing", // ❌ missing required property AA
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: [],
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Missing required property "AA" at root'
+    );
+  });
+  it('property "AA" expected to be string, not number', async () => {
+    const inputs = {
+      AA: 123, // ❌ AA should be string, not number
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: [],
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Expected string at AA, but got: number'
+    );
+  });
+  it('property "BB" expected to be number, not string', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 'test-string', // ❌ BB should be number, not string
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: [],
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Expected integer at BB, but got: "test-string"'
+    );
+  });
+  it('property "CC.CA" expected to be string, not number', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        // CA: 123, // ❌ CA should be string, not number
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: [],
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Missing required property "CA" at CC'
+    );
+  });
+  it('missing required property "CC.CEA"', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: {
+          // CEA: "missing" // ❌ missing required property CEA
+        },
+        CF: ['item1'],
+      },
+      DD: [],
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Missing required property "CEA" at CC.CE'
+    );
+  });
+  it('xxxxxxxxxxxxxxxxx', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: [1, 2, 3], // ❌ CF should be string[], not number[]
+      },
+      DD: [],
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Expected string at CC.CF[0], but got: number'
+    );
+  });
+  it('property "DD" expected to be array, not string', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: 'not an array', // ❌ DD should be array, not string
+      EE: {
+        EA: { EAA: 'required' },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Expected array at DD, but got: string'
+    );
+  });
+  it('missing required property "EE"', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: [],
+      // EE: { ... } // ❌ missing required property EE
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Missing required property "EE" at root'
+    );
+  });
+  it('property "EE.EA.EAA" expected to be string, not boolean', async () => {
+    const inputs = {
+      AA: 'hello',
+      BB: 42,
+      CC: {
+        CA: 'world',
+        CB: 100,
+        CC: 200,
+        CD: true,
+        CE: { CEA: 'nested' },
+        CF: ['item1'],
+      },
+      DD: [],
+      EE: {
+        EA: {
+          EAA: true, // ❌ EAA should be string, not boolean
+        },
+      },
+    };
+    const engine = container.get<IEngine>(IEngine);
+    const { context } = engine.invoke({
+      schema: TestSchemas.validateInputsSchema,
+      inputs,
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);
+    const report = context.reporter.export();
+    expect(report.messages.error[0].message).toBe(
+      'JSON Schema validation failed: Expected string at EE.EA.EAA, but got: boolean'
+    );
+  });
+});

+ 302 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/validate-inputs.ts

@@ -0,0 +1,302 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { WorkflowSchema } from '@flowgram.ai/runtime-interface';
+
+export interface ValidateInputsSchemaInputs {
+  AA: string;
+  BB: number;
+  CC?: {
+    CA: string;
+    CB: number;
+    CC: number;
+    CD: boolean;
+    CE: {
+      CEA: string;
+    };
+    CF: string[];
+  };
+  DD?: Array<{
+    DA?: string;
+    DB?: {
+      DBA: string;
+    };
+  }>;
+  EE: {
+    EA: {
+      EAA: string;
+    };
+    EB?: string;
+  };
+}
+
+export const validateInputsSchema: WorkflowSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      meta: {
+        position: {
+          x: 180,
+          y: 0,
+        },
+      },
+      data: {
+        title: 'Start',
+        outputs: {
+          type: 'object',
+          properties: {
+            AA: {
+              key: 10,
+              name: 'AA',
+              isPropertyRequired: true,
+              type: 'string',
+              extra: {
+                index: 0,
+              },
+            },
+            BB: {
+              key: 11,
+              name: 'BB',
+              isPropertyRequired: false,
+              type: 'integer',
+              extra: {
+                index: 1,
+              },
+            },
+            CC: {
+              key: 12,
+              name: 'CC',
+              isPropertyRequired: false,
+              type: 'object',
+              extra: {
+                index: 2,
+              },
+              properties: {
+                CA: {
+                  key: 13,
+                  name: 'CA',
+                  isPropertyRequired: true,
+                  type: 'string',
+                  extra: {
+                    index: 0,
+                  },
+                },
+                CB: {
+                  key: 14,
+                  name: 'CB',
+                  isPropertyRequired: true,
+                  type: 'integer',
+                  extra: {
+                    index: 1,
+                  },
+                },
+                CC: {
+                  key: 50,
+                  name: 'CC',
+                  type: 'number',
+                  extra: {
+                    index: 3,
+                  },
+                  isPropertyRequired: true,
+                },
+                CD: {
+                  key: 51,
+                  name: 'CD',
+                  type: 'boolean',
+                  extra: {
+                    index: 4,
+                  },
+                  isPropertyRequired: true,
+                },
+                CE: {
+                  key: 52,
+                  name: 'CE',
+                  type: 'object',
+                  extra: {
+                    index: 5,
+                  },
+                  isPropertyRequired: true,
+                  properties: {
+                    CEA: {
+                      key: 53,
+                      name: 'CEA',
+                      type: 'string',
+                      extra: {
+                        index: 1,
+                      },
+                      isPropertyRequired: true,
+                    },
+                  },
+                  required: ['CEA'],
+                },
+                CF: {
+                  key: 54,
+                  name: 'CF',
+                  type: 'array',
+                  extra: {
+                    index: 6,
+                  },
+                  items: {
+                    type: 'string',
+                  },
+                  isPropertyRequired: true,
+                },
+              },
+              required: ['CA', 'CB', 'CC', 'CD', 'CE', 'CF'],
+            },
+            DD: {
+              key: 15,
+              name: 'DD',
+              isPropertyRequired: false,
+              type: 'array',
+              extra: {
+                index: 3,
+              },
+              items: {
+                type: 'object',
+                properties: {
+                  DA: {
+                    key: 16,
+                    name: 'DA',
+                    type: 'string',
+                    extra: {
+                      index: 1,
+                    },
+                  },
+                  DB: {
+                    key: 17,
+                    name: 'DB',
+                    type: 'object',
+                    extra: {
+                      index: 2,
+                    },
+                    properties: {
+                      DBA: {
+                        key: 19,
+                        name: 'DBA',
+                        type: 'string',
+                        extra: {
+                          index: 1,
+                        },
+                        isPropertyRequired: true,
+                      },
+                    },
+                    required: ['DBA'],
+                  },
+                },
+                required: [],
+              },
+            },
+            EE: {
+              key: 20,
+              name: 'EE',
+              isPropertyRequired: true,
+              type: 'object',
+              extra: {
+                index: 4,
+              },
+              properties: {
+                EA: {
+                  key: 21,
+                  name: 'EA',
+                  type: 'object',
+                  extra: {
+                    index: 1,
+                  },
+                  properties: {
+                    EAA: {
+                      key: 22,
+                      name: 'EAA',
+                      isPropertyRequired: true,
+                      type: 'string',
+                      extra: {
+                        index: 1,
+                      },
+                    },
+                  },
+                  required: ['EAA'],
+                  isPropertyRequired: true,
+                },
+                EB: {
+                  key: 23,
+                  name: 'EB',
+                  type: 'string',
+                  extra: {
+                    index: 2,
+                  },
+                  isPropertyRequired: false,
+                },
+              },
+              required: ['EA'],
+            },
+          },
+          required: ['AA', 'EE'],
+        },
+      },
+    },
+    {
+      id: 'end_0',
+      type: 'end',
+      meta: {
+        position: {
+          x: 640,
+          y: 0,
+        },
+      },
+      data: {
+        title: 'End',
+        inputs: {
+          type: 'object',
+          properties: {
+            AA: {
+              type: 'string',
+            },
+            BB: {
+              type: 'integer',
+            },
+            CC: {
+              type: 'object',
+            },
+            DD: {
+              type: 'array',
+            },
+            EE: {
+              type: 'object',
+            },
+          },
+        },
+        inputsValues: {
+          AA: {
+            type: 'ref',
+            content: ['start_0', 'AA'],
+          },
+          BB: {
+            type: 'ref',
+            content: ['start_0', 'BB'],
+          },
+          CC: {
+            type: 'ref',
+            content: ['start_0', 'CC'],
+          },
+          DD: {
+            type: 'ref',
+            content: ['start_0', 'DD'],
+          },
+          EE: {
+            type: 'ref',
+            content: ['start_0', 'EE'],
+          },
+        },
+      },
+    },
+  ],
+  edges: [
+    {
+      sourceNodeID: 'start_0',
+      targetNodeID: 'end_0',
+    },
+  ],
+};

+ 12 - 0
packages/runtime/js-core/src/domain/__tests__/utils/array-vo-data.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { VOData } from '@flowgram.ai/runtime-interface';
+
+export const arrayVOData = <T>(arr: T[]): Array<VOData<T>> =>
+  arr.map((item: any) => {
+    const { id, ...data } = item;
+    return data;
+  });

+ 1 - 0
packages/runtime/js-core/src/domain/__tests__/utils/index.ts

@@ -4,3 +4,4 @@
  */
 
 export { snapshotsToVOData } from './snapshot';
+export { arrayVOData } from './array-vo-data';

+ 1 - 0
packages/runtime/js-core/src/domain/container/index.ts

@@ -39,6 +39,7 @@ export class WorkflowRuntimeContainer implements IContainer {
     const Validation = new WorkflowRuntimeValidation();
     const Executor = new WorkflowRuntimeExecutor(WorkflowRuntimeNodeExecutors);
     const Engine = new WorkflowRuntimeEngine({
+      Validation,
       Executor,
     });
 

+ 3 - 0
packages/runtime/js-core/src/domain/engine/index.test.ts

@@ -5,6 +5,7 @@
 
 import { beforeEach, describe, expect, it } from 'vitest';
 
+import { WorkflowRuntimeValidation } from '@workflow/validation';
 import { TestSchemas } from '@workflow/__tests__/schemas';
 import { MockWorkflowRuntimeNodeExecutors } from '@workflow/__tests__/executor';
 import { WorkflowRuntimeExecutor } from '../executor';
@@ -13,8 +14,10 @@ import { WorkflowRuntimeEngine } from './index';
 let engine: WorkflowRuntimeEngine;
 
 beforeEach(() => {
+  const Validation = new WorkflowRuntimeValidation();
   const Executor = new WorkflowRuntimeExecutor(MockWorkflowRuntimeNodeExecutors);
   engine = new WorkflowRuntimeEngine({
+    Validation,
     Executor,
   });
 });

+ 25 - 0
packages/runtime/js-core/src/domain/engine/index.ts

@@ -13,6 +13,7 @@ import {
   InvokeParams,
   ITask,
   FlowGramNode,
+  IValidation,
 } from '@flowgram.ai/runtime-interface';
 
 import { compareNodeGroups } from '@infra/utils';
@@ -21,15 +22,25 @@ import { WorkflowRuntimeContext } from '../context';
 import { WorkflowRuntimeContainer } from '../container';
 
 export class WorkflowRuntimeEngine implements IEngine {
+  private readonly validation: IValidation;
+
   private readonly executor: IExecutor;
 
   constructor(service: EngineServices) {
+    this.validation = service.Validation;
     this.executor = service.Executor;
   }
 
   public invoke(params: InvokeParams): ITask {
     const context = WorkflowRuntimeContext.create();
     context.init(params);
+    const valid = this.validate(params, context);
+    if (!valid) {
+      return WorkflowRuntimeTask.create({
+        processing: Promise.resolve({}),
+        context,
+      });
+    }
     const processing = this.process(context);
     processing.then(() => {
       context.dispose();
@@ -99,6 +110,20 @@ export class WorkflowRuntimeEngine implements IEngine {
     }
   }
 
+  private validate(params: InvokeParams, context: IContext): boolean {
+    const { valid, errors } = this.validation.invoke(params);
+    if (valid) {
+      return true;
+    }
+    errors?.forEach((message) => {
+      context.messageCenter.error({
+        message,
+      });
+    });
+    context.statusCenter.workflow.fail();
+    return false;
+  }
+
   private canExecuteNode(params: { context: IContext; node: INode }) {
     const { node, context } = params;
     const prevNodes = node.prev;

+ 51 - 2
packages/runtime/js-core/src/domain/validation/index.ts

@@ -3,10 +3,34 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { WorkflowSchema, IValidation, ValidationResult } from '@flowgram.ai/runtime-interface';
+import {
+  WorkflowSchema,
+  IValidation,
+  ValidationResult,
+  InvokeParams,
+  IJsonSchema,
+  FlowGramNode,
+} from '@flowgram.ai/runtime-interface';
+
+import { JSONSchemaValidator } from '@infra/index';
 
 export class WorkflowRuntimeValidation implements IValidation {
-  validate(schema: WorkflowSchema): ValidationResult {
+  public invoke(params: InvokeParams): ValidationResult {
+    const { schema, inputs } = params;
+    const schemaValidationResult = this.schema(schema);
+    if (!schemaValidationResult.valid) {
+      return schemaValidationResult;
+    }
+    const inputsValidationResult = this.inputs(this.getWorkflowInputsDeclare(schema), inputs);
+    if (!inputsValidationResult.valid) {
+      return inputsValidationResult;
+    }
+    return {
+      valid: true,
+    };
+  }
+
+  private schema(schema: WorkflowSchema): ValidationResult {
     // TODO
     // 检查成环
     // 检查边的节点是否存在
@@ -20,4 +44,29 @@ export class WorkflowRuntimeValidation implements IValidation {
       valid: true,
     };
   }
+
+  private inputs(inputsSchema: IJsonSchema, inputs: Record<string, unknown>): ValidationResult {
+    const { result, errorMessage } = JSONSchemaValidator({
+      schema: inputsSchema,
+      value: inputs,
+    });
+    if (!result) {
+      const error = `JSON Schema validation failed: ${errorMessage}`;
+      return {
+        valid: false,
+        errors: [error],
+      };
+    }
+    return {
+      valid: true,
+    };
+  }
+
+  private getWorkflowInputsDeclare(schema: WorkflowSchema): IJsonSchema {
+    const startNode = schema.nodes.find((node) => node.type === FlowGramNode.Start);
+    if (!startNode) {
+      throw new Error('Workflow schema must have a start node');
+    }
+    return startNode.data.outputs;
+  }
 }

+ 1 - 0
packages/runtime/js-core/src/infrastructure/utils/index.ts

@@ -8,3 +8,4 @@ export { uuid } from './uuid';
 export { WorkflowRuntimeType } from './runtime-type';
 export { traverseNodes } from './traverse-nodes';
 export { compareNodeGroups } from './compare-node-groups';
+export { JSONSchemaValidator } from './json-schema-validator';

+ 208 - 0
packages/runtime/js-core/src/infrastructure/utils/json-schema-validator.test.ts

@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { describe, expect, it } from 'vitest';
+import { IJsonSchema } from '@flowgram.ai/runtime-interface';
+
+import { JSONSchemaValidator } from './json-schema-validator';
+
+describe('JSONSchemaValidator', () => {
+  const testSchema: IJsonSchema = {
+    type: 'object',
+    properties: {
+      AA: {
+        type: 'string',
+        isPropertyRequired: true,
+        extra: { index: 0 },
+      },
+      BB: {
+        type: 'integer',
+        isPropertyRequired: false,
+        extra: { index: 1 },
+      },
+      CC: {
+        type: 'object',
+        isPropertyRequired: false,
+        extra: { index: 2 },
+        properties: {
+          CA: {
+            type: 'string',
+            isPropertyRequired: true,
+            extra: { index: 0 },
+          },
+          CB: {
+            type: 'integer',
+            isPropertyRequired: true,
+            extra: { index: 1 },
+          },
+        },
+        required: ['CA', 'CB'],
+      },
+      DD: {
+        type: 'array',
+        isPropertyRequired: false,
+        extra: { index: 3 },
+        items: {
+          type: 'object',
+          properties: {
+            DA: {
+              type: 'string',
+              extra: { index: 1 },
+            },
+            DB: {
+              type: 'object',
+              extra: { index: 2 },
+              properties: {
+                DBA: {
+                  type: 'string',
+                  isPropertyRequired: true,
+                  extra: { index: 1 },
+                },
+              },
+              required: ['DBA'],
+            },
+          },
+          required: [],
+        },
+      },
+    },
+    required: ['AA'],
+  };
+
+  it('should validate valid input successfully', () => {
+    const validValue = {
+      AA: 'test string',
+      BB: 42,
+      CC: {
+        CA: 'nested string',
+        CB: 123,
+      },
+      DD: [
+        {
+          DA: 'array item string',
+          DB: {
+            DBA: 'deep nested string',
+          },
+        },
+      ],
+    };
+
+    const result = JSONSchemaValidator({
+      schema: testSchema,
+      value: validValue,
+    });
+
+    expect(result.result).toBe(true);
+    expect(result.errorMessage).toBeUndefined();
+  });
+
+  it('should fail when required property is missing', () => {
+    const invalidValue = {
+      BB: 42,
+      // Missing required AA
+    };
+
+    const result = JSONSchemaValidator({
+      schema: testSchema,
+      value: invalidValue,
+    });
+
+    expect(result.result).toBe(false);
+    expect(result.errorMessage).toContain('Missing required property "AA"');
+  });
+
+  it('should fail when property type is wrong', () => {
+    const invalidValue = {
+      AA: 123, // Should be string, not number
+    };
+
+    const result = JSONSchemaValidator({
+      schema: testSchema,
+      value: invalidValue,
+    });
+
+    expect(result.result).toBe(false);
+    expect(result.errorMessage).toContain('Expected string at AA, but got: number');
+  });
+
+  it('should fail when nested required property is missing', () => {
+    const invalidValue = {
+      AA: 'test string',
+      CC: {
+        CA: 'nested string',
+        // Missing required CB
+      },
+    };
+
+    const result = JSONSchemaValidator({
+      schema: testSchema,
+      value: invalidValue,
+    });
+
+    expect(result.result).toBe(false);
+    expect(result.errorMessage).toContain('Missing required property "CB"');
+  });
+
+  it('should validate array items correctly', () => {
+    const invalidValue = {
+      AA: 'test string',
+      DD: [
+        {
+          DA: 'array item string',
+          DB: {
+            // Missing required DBA
+          },
+        },
+      ],
+    };
+
+    const result = JSONSchemaValidator({
+      schema: testSchema,
+      value: invalidValue,
+    });
+
+    expect(result.result).toBe(false);
+    expect(result.errorMessage).toContain('Missing required property "DBA"');
+  });
+
+  it('should handle enum validation', () => {
+    const enumSchema: IJsonSchema = {
+      type: 'string',
+      enum: ['option1', 'option2', 'option3'],
+    };
+
+    const validResult = JSONSchemaValidator({
+      schema: enumSchema,
+      value: 'option1',
+    });
+    expect(validResult.result).toBe(true);
+
+    const invalidResult = JSONSchemaValidator({
+      schema: enumSchema,
+      value: 'invalid_option',
+    });
+    expect(invalidResult.result).toBe(false);
+    expect(invalidResult.errorMessage).toContain('must be one of: option1, option2, option3');
+  });
+
+  it('should handle different basic types', () => {
+    const typeTests = [
+      { type: 'boolean', validValue: true, invalidValue: 'not boolean' },
+      { type: 'integer', validValue: 42, invalidValue: 3.14 },
+      { type: 'number', validValue: 3.14, invalidValue: 'not number' },
+      { type: 'array', validValue: [1, 2, 3], invalidValue: 'not array' },
+    ];
+
+    typeTests.forEach(({ type, validValue, invalidValue }) => {
+      const schema: IJsonSchema = { type: type };
+
+      const validResult = JSONSchemaValidator({ schema, value: validValue });
+      expect(validResult.result).toBe(true);
+
+      const invalidResult = JSONSchemaValidator({ schema, value: invalidValue });
+      expect(invalidResult.result).toBe(false);
+    });
+  });
+});

+ 272 - 0
packages/runtime/js-core/src/infrastructure/utils/json-schema-validator.ts

@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { IJsonSchema } from '@flowgram.ai/runtime-interface';
+
+// Define validation result type
+type ValidationResult = {
+  result: boolean;
+  errorMessage?: string;
+};
+
+// Define JSON Schema validator parameters type
+type JSONSchemaValidatorParams = {
+  schema: IJsonSchema;
+  value: unknown;
+};
+
+const ROOT_PATH = 'root';
+
+export const isRootPath = (path: string) => path === ROOT_PATH;
+
+// Recursively validate value against JSON Schema
+const validateValue = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {
+  // Handle $ref references (temporarily skip as no reference resolution mechanism is provided)
+  if (schema.$ref) {
+    return { result: true }; // Temporarily skip reference validation
+  }
+
+  // Check enum values
+  if (schema.enum && schema.enum.length > 0) {
+    if (!schema.enum.includes(value as string | number)) {
+      return {
+        result: false,
+        errorMessage: `Value at ${path} must be one of: ${schema.enum.join(
+          ', '
+        )}, but got: ${JSON.stringify(value)}`,
+      };
+    }
+  }
+
+  // Validate based on type
+  switch (schema.type) {
+    case 'boolean':
+      return validateBoolean(value, path);
+
+    case 'string':
+      return validateString(value, path);
+
+    case 'integer':
+      return validateInteger(value, path);
+
+    case 'number':
+      return validateNumber(value, path);
+
+    case 'object':
+      return validateObject(value, schema, path);
+
+    case 'array':
+      return validateArray(value, schema, path);
+
+    case 'map':
+      return validateMap(value, schema, path);
+
+    default:
+      return {
+        result: false,
+        errorMessage: `Unknown type "${schema.type}" at ${path}`,
+      };
+  }
+};
+
+// Validate boolean value
+const validateBoolean = (value: unknown, path: string): ValidationResult => {
+  if (typeof value !== 'boolean') {
+    return {
+      result: false,
+      errorMessage: `Expected boolean at ${path}, but got: ${typeof value}`,
+    };
+  }
+  return { result: true };
+};
+
+// Validate string value
+const validateString = (value: unknown, path: string): ValidationResult => {
+  if (typeof value !== 'string') {
+    return {
+      result: false,
+      errorMessage: `Expected string at ${path}, but got: ${typeof value}`,
+    };
+  }
+  return { result: true };
+};
+
+// Validate integer value
+const validateInteger = (value: unknown, path: string): ValidationResult => {
+  if (!Number.isInteger(value)) {
+    return {
+      result: false,
+      errorMessage: `Expected integer at ${path}, but got: ${JSON.stringify(value)}`,
+    };
+  }
+  return { result: true };
+};
+
+// Validate number value
+const validateNumber = (value: unknown, path: string): ValidationResult => {
+  if (typeof value !== 'number' || isNaN(value)) {
+    return {
+      result: false,
+      errorMessage: `Expected number at ${path}, but got: ${JSON.stringify(value)}`,
+    };
+  }
+  return { result: true };
+};
+
+// Validate object value
+const validateObject = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {
+  if (value === null || value === undefined) {
+    return {
+      result: false,
+      errorMessage: `Expected object at ${path}, but got: ${value}`,
+    };
+  }
+
+  if (typeof value !== 'object' || Array.isArray(value)) {
+    return {
+      result: false,
+      errorMessage: `Expected object at ${path}, but got: ${
+        Array.isArray(value) ? 'array' : typeof value
+      }`,
+    };
+  }
+
+  const objectValue = value as Record<string, unknown>;
+
+  // Check required properties
+  if (schema.required && schema.required.length > 0) {
+    for (const requiredProperty of schema.required) {
+      if (!(requiredProperty in objectValue)) {
+        return {
+          result: false,
+          errorMessage: `Missing required property "${requiredProperty}" at ${path}`,
+        };
+      }
+    }
+  }
+
+  // Check isPropertyRequired field in properties (if exists)
+  if (schema.properties) {
+    for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {
+      const isRequired =
+        (propertySchema as unknown as Record<string, unknown>).isPropertyRequired === true;
+      if (isRequired && !(propertyName in objectValue)) {
+        return {
+          result: false,
+          errorMessage: `Missing required property "${propertyName}" at ${path}`,
+        };
+      }
+    }
+  }
+
+  // Validate properties
+  if (schema.properties) {
+    for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {
+      if (propertyName in objectValue) {
+        const propertyPath = isRootPath(path) ? propertyName : `${path}.${propertyName}`;
+        const propertyResult = validateValue(
+          objectValue[propertyName],
+          propertySchema,
+          propertyPath
+        );
+        if (!propertyResult.result) {
+          return propertyResult;
+        }
+      }
+    }
+  }
+
+  // Validate additional properties
+  if (schema.additionalProperties) {
+    const definedProperties = new Set(Object.keys(schema.properties || {}));
+    for (const [propertyName, propertyValue] of Object.entries(objectValue)) {
+      if (!definedProperties.has(propertyName)) {
+        const propertyPath = isRootPath(path) ? propertyName : `${path}.${propertyName}`;
+        const propertyResult = validateValue(
+          propertyValue,
+          schema.additionalProperties,
+          propertyPath
+        );
+        if (!propertyResult.result) {
+          return propertyResult;
+        }
+      }
+    }
+  }
+
+  return { result: true };
+};
+
+// Validate array value
+const validateArray = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {
+  if (!Array.isArray(value)) {
+    return {
+      result: false,
+      errorMessage: `Expected array at ${path}, but got: ${typeof value}`,
+    };
+  }
+
+  // Validate array items
+  if (schema.items) {
+    for (const [index, item] of value.entries()) {
+      const itemPath = `${path}[${index}]`;
+      const itemResult = validateValue(item, schema.items, itemPath);
+      if (!itemResult.result) {
+        return itemResult;
+      }
+    }
+  }
+
+  return { result: true };
+};
+
+// Validate map value (similar to object, but all values must conform to the same schema)
+const validateMap = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {
+  if (value === null || value === undefined) {
+    return {
+      result: false,
+      errorMessage: `Expected map at ${path}, but got: ${value}`,
+    };
+  }
+
+  if (typeof value !== 'object' || Array.isArray(value)) {
+    return {
+      result: false,
+      errorMessage: `Expected map at ${path}, but got: ${
+        Array.isArray(value) ? 'array' : typeof value
+      }`,
+    };
+  }
+
+  const mapValue = value as Record<string, unknown>;
+
+  // If additionalProperties exists, validate all values
+  if (schema.additionalProperties) {
+    for (const [key, mapItemValue] of Object.entries(mapValue)) {
+      const keyPath = isRootPath(path) ? key : `${path}.${key}`;
+      const keyResult = validateValue(mapItemValue, schema.additionalProperties, keyPath);
+      if (!keyResult.result) {
+        return keyResult;
+      }
+    }
+  }
+
+  return { result: true };
+};
+
+// Main JSON Schema validator function
+export const JSONSchemaValidator = (params: JSONSchemaValidatorParams): ValidationResult => {
+  const { schema, value } = params;
+
+  try {
+    const validationResult = validateValue(value, schema, ROOT_PATH);
+    return validationResult;
+  } catch (error) {
+    return {
+      result: false,
+      errorMessage: `Validation error: ${error instanceof Error ? error.message : String(error)}`,
+    };
+  }
+};