diff --git a/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx b/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx
index 22eaee1f..a2ac5ba4 100644
--- a/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx
+++ b/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx
@@ -12,11 +12,12 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { getClient } from "@/api/AxiosClient";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { helpTooltips } from "@/routes/workflows/editor/helpContent";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { AutoResizingTextarea } from "../AutoResizingTextarea/AutoResizingTextarea";
import { Button } from "../ui/button";
import { AxiosError } from "axios";
import { toast } from "../ui/use-toast";
+import { cn } from "@/util/utils";
type Props = {
value: string;
@@ -35,6 +36,42 @@ function WorkflowDataSchemaInputGroup({
const [generateWithAIActive, setGenerateWithAIActive] = useState(false);
const [generateWithAIPrompt, setGenerateWithAIPrompt] = useState("");
+ function computeJsonError(
+ jsonText: string,
+ ): { message: string; line?: number; column?: number } | null {
+ try {
+ JSON.parse(jsonText);
+ return null;
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "Invalid JSON";
+ // Try to extract position and compute line/column for friendlier feedback
+ const match = message.match(/position\s+(\d+)/i);
+ if (!match) {
+ return { message };
+ }
+ const pos = Number(match[1]);
+ if (Number.isNaN(pos)) {
+ return { message };
+ }
+ let line = 1;
+ let col = 1;
+ for (let i = 0; i < Math.min(pos, jsonText.length); i++) {
+ if (jsonText[i] === "\n") {
+ line += 1;
+ col = 1;
+ } else {
+ col += 1;
+ }
+ }
+ return { message, line, column: col };
+ }
+ }
+
+ const jsonError = useMemo(() => {
+ if (value === "null") return null;
+ return computeJsonError(value);
+ }, [value]);
+
const getDataSchemaSuggestionMutation = useMutation({
mutationFn: async () => {
const client = await getClient(credentialGetter);
@@ -121,13 +158,27 @@ function WorkflowDataSchemaInputGroup({
)}
) : null}
-
+
+
+
+ {jsonError && (
+
+ {jsonError.line && jsonError.column
+ ? `Invalid JSON (${jsonError.line}:${jsonError.column}) — ${jsonError.message}`
+ : `Invalid JSON — ${jsonError.message}`}
+
+ )}
)}
diff --git a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx
index 5bfb3a12..22700d87 100644
--- a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx
@@ -79,6 +79,8 @@ import {
getWorkflowSettings,
layout,
} from "./workflowEditorUtils";
+import { getWorkflowErrors } from "./workflowEditorUtils";
+import { toast } from "@/components/ui/use-toast";
import { useAutoPan } from "./useAutoPan";
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -373,8 +375,25 @@ function FlowRenderer({
setGetSaveDataRef.current(constructSaveData);
}, [constructSaveData]);
- async function handleSave() {
- return await saveWorkflow.mutateAsync();
+ async function handleSave(): Promise {
+ // Validate before saving; block if any workflow errors exist
+ const errors = getWorkflowErrors(nodes);
+ if (errors.length > 0) {
+ toast({
+ title: "Can not save workflow because of errors:",
+ description: (
+
+ {errors.map((error) => (
+
{error}
+ ))}
+
+ ),
+ variant: "destructive",
+ });
+ return false;
+ }
+ await saveWorkflow.mutateAsync();
+ return true;
}
function deleteNode(id: string) {
@@ -605,8 +624,10 @@ function FlowRenderer({