next interation on failure (#4192)

This commit is contained in:
LawyZheng
2025-12-04 14:51:44 +08:00
committed by GitHub
parent cc2f127308
commit 9888bd27d4
18 changed files with 380 additions and 211 deletions

View File

@@ -23,6 +23,8 @@ export const baseHelpTooltipContent = {
"If you are running multiple workflows at once, you will need to give the block an identifier to know that this TOTP goes with this block.", "If you are running multiple workflows at once, you will need to give the block an identifier to know that this TOTP goes with this block.",
continueOnFailure: continueOnFailure:
"Allow the workflow to continue if it encounters a failure.", "Allow the workflow to continue if it encounters a failure.",
nextIterationOnFailure:
"When inside a for loop, continue to the next iteration if this block fails.",
includeActionHistoryInVerification: includeActionHistoryInVerification:
"Include the action history in the completion verification.", "Include the action history in the completion verification.",
} as const; } as const;
@@ -72,6 +74,8 @@ export const helpTooltips = {
...baseHelpTooltipContent, ...baseHelpTooltipContent,
loopValue: loopValue:
"Define the values to iterate over. Use a parameter reference or natural language (e.g., 'Extract links of the top 2 posts'). Natural language automatically creates an extraction block that generates a list of string values. Use {{ current_value }} in the loop to get the current iteration value.", "Define the values to iterate over. Use a parameter reference or natural language (e.g., 'Extract links of the top 2 posts'). Natural language automatically creates an extraction block that generates a list of string values. Use {{ current_value }} in the loop to get the current iteration value.",
nextIterationOnFailure:
"When enabled, if any block inside the loop fails, the loop will immediately jump to the next iteration instead of stopping.",
}, },
sendEmail: { sendEmail: {
...baseHelpTooltipContent, ...baseHelpTooltipContent,

View File

@@ -23,7 +23,10 @@ import { useRerender } from "@/hooks/useRerender";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { RunEngineSelector } from "@/components/EngineSelector"; import { RunEngineSelector } from "@/components/EngineSelector";
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
import { useUpdate } from "@/routes/workflows/editor/useUpdate"; import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
const urlTooltip = const urlTooltip =
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off."; "The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
@@ -64,6 +68,7 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
const update = useUpdate<ActionNode["data"]>({ id, editable }); const update = useUpdate<ActionNode["data"]>({ id, editable });
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => { useEffect(() => {
setFacing(data.showCode ? "back" : "front"); setFacing(data.showCode ? "back" : "front");
@@ -248,28 +253,19 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
</div> </div>
)} )}
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> editable={editable}
Continue on Failure isInsideForLoop={isInsideForLoop}
</Label> blockType="action"
<HelpTooltip onContinueOnFailureChange={(checked) => {
content={helpTooltips["action"]["continueOnFailure"]} update({ continueOnFailure: checked });
/> }}
</div> onNextIterationOnFailureChange={(checked) => {
<div className="w-52"> update({ nextIterationOnFailure: checked });
<Switch }}
checked={data.continueOnFailure} />
onCheckedChange={(checked) => {
if (!editable) {
return;
}
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -10,7 +10,6 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react"; import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
import { useState } from "react"; import { useState } from "react";
import { dataSchemaExampleValue } from "../types"; import { dataSchemaExampleValue } from "../types";
@@ -20,7 +19,10 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { helpTooltips, placeholders } from "../../helpContent"; import { helpTooltips, placeholders } from "../../helpContent";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup"; import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
@@ -37,6 +39,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { useRerender } from "@/hooks/useRerender"; import { useRerender } from "@/hooks/useRerender";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants"; import { AI_IMPROVE_CONFIGS } from "../../constants";
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) { function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
@@ -58,6 +61,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
const rerender = useRerender({ prefix: "accordian" }); const rerender = useRerender({ prefix: "accordian" });
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<ExtractionNode["data"]>({ id, editable }); const update = useUpdate<ExtractionNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => { useEffect(() => {
setFacing(data.showCode ? "back" : "front"); setFacing(data.showCode ? "back" : "front");
@@ -209,30 +213,19 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
}} }}
/> />
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> editable={editable}
Continue on Failure isInsideForLoop={isInsideForLoop}
</Label> blockType="extraction"
<HelpTooltip onContinueOnFailureChange={(checked) => {
content={ update({ continueOnFailure: checked });
helpTooltips["extraction"]["continueOnFailure"] }}
} onNextIterationOnFailureChange={(checked) => {
/> update({ nextIterationOnFailure: checked });
</div> }}
<div className="w-52"> />
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -11,7 +11,6 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
@@ -22,7 +21,10 @@ import { helpTooltips, placeholders } from "../../helpContent";
import { errorMappingExampleValue } from "../types"; import { errorMappingExampleValue } from "../types";
import type { FileDownloadNode } from "./types"; import type { FileDownloadNode } from "./types";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { RunEngineSelector } from "@/components/EngineSelector"; import { RunEngineSelector } from "@/components/EngineSelector";
@@ -37,6 +39,7 @@ import { useRerender } from "@/hooks/useRerender";
import { BROWSER_DOWNLOAD_TIMEOUT_SECONDS } from "@/api/types"; import { BROWSER_DOWNLOAD_TIMEOUT_SECONDS } from "@/api/types";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants"; import { AI_IMPROVE_CONFIGS } from "../../constants";
const urlTooltip = const urlTooltip =
@@ -65,6 +68,7 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<FileDownloadNode["data"]>({ id, editable }); const update = useUpdate<FileDownloadNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => { useEffect(() => {
setFacing(data.showCode ? "back" : "front"); setFacing(data.showCode ? "back" : "front");
@@ -279,25 +283,19 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
</div> </div>
)} )}
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> editable={editable}
Continue on Failure isInsideForLoop={isInsideForLoop}
</Label> blockType="download"
<HelpTooltip onContinueOnFailureChange={(checked) => {
content={helpTooltips["download"]["continueOnFailure"]} update({ continueOnFailure: checked });
/> }}
</div> onNextIterationOnFailureChange={(checked) => {
<div className="w-52"> update({ nextIterationOnFailure: checked });
<Switch }}
checked={data.continueOnFailure} />
onCheckedChange={(checked) => {
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -11,7 +11,6 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
@@ -22,7 +21,10 @@ import { errorMappingExampleValue } from "../types";
import type { LoginNode } from "./types"; import type { LoginNode } from "./types";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector"; import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector";
import { RunEngineSelector } from "@/components/EngineSelector"; import { RunEngineSelector } from "@/components/EngineSelector";
@@ -36,6 +38,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { useRerender } from "@/hooks/useRerender"; import { useRerender } from "@/hooks/useRerender";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants"; import { AI_IMPROVE_CONFIGS } from "../../constants";
function LoginNode({ id, data, type }: NodeProps<LoginNode>) { function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
@@ -56,6 +59,7 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<LoginNode["data"]>({ id, editable }); const update = useUpdate<LoginNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
// Manage flippable facing state // Manage flippable facing state
const [facing, setFacing] = useState<"front" | "back">("front"); const [facing, setFacing] = useState<"front" | "back">("front");
@@ -277,25 +281,19 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
</div> </div>
)} )}
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> editable={editable}
Continue on Failure isInsideForLoop={isInsideForLoop}
</Label> blockType="login"
<HelpTooltip onContinueOnFailureChange={(checked) => {
content={helpTooltips["login"]["continueOnFailure"]} update({ continueOnFailure: checked });
/> }}
</div> onNextIterationOnFailureChange={(checked) => {
<div className="w-52"> update({ nextIterationOnFailure: checked });
<Switch }}
checked={data.continueOnFailure} />
onCheckedChange={(checked) => {
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -159,6 +159,26 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
<HelpTooltip content="When checked, the loop will continue executing even if one of its iterations fails" /> <HelpTooltip content="When checked, the loop will continue executing even if one of its iterations fails" />
</div> </div>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center gap-2">
<Checkbox
checked={data.nextIterationOnFailure ?? false}
disabled={!data.editable}
onCheckedChange={(checked) => {
update({
nextIterationOnFailure:
checked === "indeterminate" ? false : checked,
});
}}
/>
<Label className="text-xs text-slate-300">
Next Loop on Failure
</Label>
<HelpTooltip
content={helpTooltips["loop"]["nextIterationOnFailure"]}
/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ export type LoopNodeData = NodeBaseData & {
loopVariableReference: string; loopVariableReference: string;
completeIfEmpty: boolean; completeIfEmpty: boolean;
continueOnFailure: boolean; continueOnFailure: boolean;
nextIterationOnFailure?: boolean;
}; };
export type LoopNode = Node<LoopNodeData, "loop">; export type LoopNode = Node<LoopNodeData, "loop">;
@@ -19,6 +20,7 @@ export const loopNodeDefaultData: LoopNodeData = {
loopVariableReference: "", loopVariableReference: "",
completeIfEmpty: false, completeIfEmpty: false,
continueOnFailure: false, continueOnFailure: false,
nextIterationOnFailure: false,
model: null, model: null,
} as const; } as const;

View File

@@ -25,7 +25,10 @@ import { errorMappingExampleValue } from "../types";
import type { NavigationNode } from "./types"; import type { NavigationNode } from "./types";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { RunEngineSelector } from "@/components/EngineSelector"; import { RunEngineSelector } from "@/components/EngineSelector";
import { ModelSelector } from "@/components/ModelSelector"; import { ModelSelector } from "@/components/ModelSelector";
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
import { useUpdate } from "@/routes/workflows/editor/useUpdate"; import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants"; import { AI_IMPROVE_CONFIGS } from "../../constants";
function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) { function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
@@ -58,6 +62,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<NavigationNode["data"]>({ id, editable }); const update = useUpdate<NavigationNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => { useEffect(() => {
setFacing(data.showCode ? "back" : "front"); setFacing(data.showCode ? "back" : "front");
@@ -287,51 +292,32 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
</div> </div>
)} )}
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> includeActionHistoryInVerification={
Include Action History data.includeActionHistoryInVerification
</Label> }
<HelpTooltip editable={editable}
content={ isInsideForLoop={isInsideForLoop}
helpTooltips["navigation"][ blockType="navigation"
"includeActionHistoryInVerification" showOptions={{
] continueOnFailure: true,
} nextIterationOnFailure: true,
/> includeActionHistoryInVerification: true,
</div> }}
<div className="w-52"> onContinueOnFailureChange={(checked) => {
<Switch update({ continueOnFailure: checked });
checked={data.includeActionHistoryInVerification} }}
onCheckedChange={(checked) => { onNextIterationOnFailureChange={(checked) => {
update({ update({ nextIterationOnFailure: checked });
includeActionHistoryInVerification: checked, }}
}); onIncludeActionHistoryInVerificationChange={(checked) => {
}} update({
/> includeActionHistoryInVerification: checked,
</div> });
</div> }}
<div className="flex items-center justify-between"> />
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Continue on Failure
</Label>
<HelpTooltip
content={
helpTooltips["navigation"]["continueOnFailure"]
}
/>
</div>
<div className="w-52">
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -22,7 +22,10 @@ import { useState } from "react";
import { AppNode } from ".."; import { AppNode } from "..";
import { helpTooltips, placeholders } from "../../helpContent"; import { helpTooltips, placeholders } from "../../helpContent";
import { AI_IMPROVE_CONFIGS } from "../../constants"; import { AI_IMPROVE_CONFIGS } from "../../constants";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types"; import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
import { ParametersMultiSelect } from "./ParametersMultiSelect"; import { ParametersMultiSelect } from "./ParametersMultiSelect";
import type { TaskNode } from "./types"; import type { TaskNode } from "./types";
@@ -39,6 +42,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { useRerender } from "@/hooks/useRerender"; import { useRerender } from "@/hooks/useRerender";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
function TaskNode({ id, data, type }: NodeProps<TaskNode>) { function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
const [facing, setFacing] = useState<"front" | "back">("front"); const [facing, setFacing] = useState<"front" | "back">("front");
@@ -59,6 +63,7 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<TaskNode["data"]>({ id, editable }); const update = useUpdate<TaskNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => { useEffect(() => {
setFacing(data.showCode ? "back" : "front"); setFacing(data.showCode ? "back" : "front");
@@ -303,49 +308,32 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
</div> </div>
)} )}
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> includeActionHistoryInVerification={
Include Action History data.includeActionHistoryInVerification
</Label> }
<HelpTooltip editable={editable}
content={ isInsideForLoop={isInsideForLoop}
helpTooltips["task"][ blockType="task"
"includeActionHistoryInVerification" showOptions={{
] continueOnFailure: true,
} nextIterationOnFailure: true,
/> includeActionHistoryInVerification: true,
</div> }}
<div className="w-52"> onContinueOnFailureChange={(checked) => {
<Switch update({ continueOnFailure: checked });
checked={data.includeActionHistoryInVerification} }}
onCheckedChange={(checked) => { onNextIterationOnFailureChange={(checked) => {
update({ update({ nextIterationOnFailure: checked });
includeActionHistoryInVerification: checked, }}
}); onIncludeActionHistoryInVerificationChange={(checked) => {
}} update({
/> includeActionHistoryInVerification: checked,
</div> });
</div> }}
<div className="flex items-center justify-between"> />
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Continue on Failure
</Label>
<HelpTooltip
content={helpTooltips["task"]["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -10,7 +10,6 @@ import {
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
@@ -21,7 +20,10 @@ import { helpTooltips } from "../../helpContent";
import { errorMappingExampleValue } from "../types"; import { errorMappingExampleValue } from "../types";
import type { ValidationNode } from "./types"; import type { ValidationNode } from "./types";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { ModelSelector } from "@/components/ModelSelector"; import { ModelSelector } from "@/components/ModelSelector";
@@ -34,6 +36,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { useRerender } from "@/hooks/useRerender"; import { useRerender } from "@/hooks/useRerender";
import { DisableCache } from "../DisableCache"; import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants"; import { AI_IMPROVE_CONFIGS } from "../../constants";
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) { function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
@@ -55,6 +58,7 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<ValidationNode["data"]>({ id, editable }); const update = useUpdate<ValidationNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => { useEffect(() => {
setFacing(data.showCode ? "back" : "front"); setFacing(data.showCode ? "back" : "front");
@@ -212,30 +216,19 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
</div> </div>
)} )}
</div> </div>
<Separator /> <BlockExecutionOptions
<div className="flex items-center justify-between"> continueOnFailure={data.continueOnFailure}
<div className="flex gap-2"> nextIterationOnFailure={data.nextIterationOnFailure}
<Label className="text-xs font-normal text-slate-300"> editable={editable}
Continue on Failure isInsideForLoop={isInsideForLoop}
</Label> blockType="validation"
<HelpTooltip onContinueOnFailureChange={(checked) => {
content={ update({ continueOnFailure: checked });
helpTooltips["validation"]["continueOnFailure"] }}
} onNextIterationOnFailureChange={(checked) => {
/> update({ nextIterationOnFailure: checked });
</div> }}
<div className="w-52"> />
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<DisableCache <DisableCache
disableCache={data.disableCache} disableCache={data.disableCache}
editable={editable} editable={editable}

View File

@@ -0,0 +1,134 @@
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { HelpTooltip } from "@/components/HelpTooltip";
import { helpTooltips } from "../../helpContent";
interface BlockExecutionOptionsProps {
continueOnFailure: boolean;
nextIterationOnFailure?: boolean;
includeActionHistoryInVerification?: boolean;
editable: boolean;
isInsideForLoop: boolean;
blockType: string;
onContinueOnFailureChange: (checked: boolean) => void;
onNextIterationOnFailureChange: (checked: boolean) => void;
onIncludeActionHistoryInVerificationChange?: (checked: boolean) => void;
showOptions?: {
continueOnFailure?: boolean;
nextIterationOnFailure?: boolean;
includeActionHistoryInVerification?: boolean;
};
}
export function BlockExecutionOptions({
continueOnFailure,
nextIterationOnFailure = false,
includeActionHistoryInVerification = false,
editable,
isInsideForLoop,
blockType,
onContinueOnFailureChange,
onNextIterationOnFailureChange,
onIncludeActionHistoryInVerificationChange,
showOptions = {
continueOnFailure: true,
nextIterationOnFailure: true,
includeActionHistoryInVerification: false,
},
}: BlockExecutionOptionsProps) {
const showContinueOnFailure = showOptions.continueOnFailure ?? true;
const showNextIterationOnFailure = showOptions.nextIterationOnFailure ?? true;
const showIncludeActionHistory =
showOptions.includeActionHistoryInVerification ?? false;
return (
<>
<Separator />
{showIncludeActionHistory &&
onIncludeActionHistoryInVerificationChange && (
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Include Action History
</Label>
<HelpTooltip
content={
helpTooltips[blockType as keyof typeof helpTooltips]?.[
"includeActionHistoryInVerification"
] ||
helpTooltips["task"]["includeActionHistoryInVerification"]
}
/>
</div>
<div className="w-52">
<Switch
checked={includeActionHistoryInVerification}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
onIncludeActionHistoryInVerificationChange(checked);
}}
/>
</div>
</div>
)}
{showContinueOnFailure && (
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Continue on Failure
</Label>
<HelpTooltip
content={
helpTooltips[blockType as keyof typeof helpTooltips]?.[
"continueOnFailure"
] || helpTooltips["task"]["continueOnFailure"]
}
/>
</div>
<div className="w-52">
<Switch
checked={continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
onContinueOnFailureChange(checked);
}}
/>
</div>
</div>
)}
{showNextIterationOnFailure && isInsideForLoop && (
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Next Loop on Failure
</Label>
<HelpTooltip
content={
helpTooltips[blockType as keyof typeof helpTooltips]?.[
"nextIterationOnFailure"
] || helpTooltips["task"]["nextIterationOnFailure"]
}
/>
</div>
<div className="w-52">
<Switch
checked={nextIterationOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
onNextIterationOnFailureChange(checked);
}}
/>
</div>
</div>
)}
<Separator />
</>
);
}

View File

@@ -5,6 +5,7 @@ export type NodeBaseData = {
debuggable: boolean; debuggable: boolean;
label: string; label: string;
continueOnFailure: boolean; continueOnFailure: boolean;
nextIterationOnFailure?: boolean;
editable: boolean; editable: boolean;
model: WorkflowModel | null; model: WorkflowModel | null;
showCode?: boolean; showCode?: boolean;

View File

@@ -223,6 +223,7 @@ function convertToNode(
debuggable: debuggableWorkflowBlockTypes.has(block.block_type), debuggable: debuggableWorkflowBlockTypes.has(block.block_type),
label: block.label, label: block.label,
continueOnFailure: block.continue_on_failure, continueOnFailure: block.continue_on_failure,
nextIterationOnFailure: block.next_iteration_on_failure,
editable, editable,
model: block.model, model: block.model,
}; };
@@ -489,6 +490,7 @@ function convertToNode(
loopValue: block.loop_over?.key ?? "", loopValue: block.loop_over?.key ?? "",
loopVariableReference: loopVariableReference, loopVariableReference: loopVariableReference,
completeIfEmpty: block.complete_if_empty, completeIfEmpty: block.complete_if_empty,
nextIterationOnFailure: block.next_iteration_on_failure,
}, },
}; };
} }
@@ -1073,6 +1075,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
const base = { const base = {
label: node.data.label, label: node.data.label,
continue_on_failure: node.data.continueOnFailure, continue_on_failure: node.data.continueOnFailure,
next_iteration_on_failure: node.data.nextIterationOnFailure,
model: node.data.model, model: node.data.model,
}; };
switch (node.type) { switch (node.type) {
@@ -1422,6 +1425,7 @@ function getOrderedChildrenBlocks(
block_type: "for_loop", block_type: "for_loop",
label: currentNode.data.label, label: currentNode.data.label,
continue_on_failure: currentNode.data.continueOnFailure, continue_on_failure: currentNode.data.continueOnFailure,
next_iteration_on_failure: currentNode.data.nextIterationOnFailure,
loop_blocks: loopChildren, loop_blocks: loopChildren,
loop_variable_reference: currentNode.data.loopVariableReference, loop_variable_reference: currentNode.data.loopVariableReference,
complete_if_empty: currentNode.data.completeIfEmpty, complete_if_empty: currentNode.data.completeIfEmpty,
@@ -1452,6 +1456,7 @@ function getWorkflowBlocksUtil(
block_type: "for_loop", block_type: "for_loop",
label: node.data.label, label: node.data.label,
continue_on_failure: node.data.continueOnFailure, continue_on_failure: node.data.continueOnFailure,
next_iteration_on_failure: node.data.nextIterationOnFailure,
loop_blocks: getOrderedChildrenBlocks(nodes, edges, node.id), loop_blocks: getOrderedChildrenBlocks(nodes, edges, node.id),
loop_variable_reference: node.data.loopVariableReference, loop_variable_reference: node.data.loopVariableReference,
complete_if_empty: node.data.completeIfEmpty, complete_if_empty: node.data.completeIfEmpty,
@@ -1952,6 +1957,7 @@ function convertBlocksToBlockYAML(
const base = { const base = {
label: block.label, label: block.label,
continue_on_failure: block.continue_on_failure, continue_on_failure: block.continue_on_failure,
next_iteration_on_failure: block.next_iteration_on_failure,
next_block_label: block.next_block_label, next_block_label: block.next_block_label,
}; };
switch (block.block_type) { switch (block.block_type) {
@@ -2455,6 +2461,23 @@ function getLabelForWorkflowParameterType(type: WorkflowParameterValueType) {
return type; return type;
} }
/**
* Check if a node is inside a for loop block
* @param nodes - Array of all nodes in the workflow
* @param nodeId - ID of the node to check
* @returns true if the node is inside a for loop block, false otherwise
*/
function isNodeInsideForLoop(nodes: Array<AppNode>, nodeId: string): boolean {
const currentNode = nodes.find((n) => n.id === nodeId);
if (!currentNode) {
return false;
}
const parentNode = currentNode.parentId
? nodes.find((n) => n.id === currentNode.parentId)
: null;
return parentNode?.type === "loop";
}
export { export {
convert, convert,
convertEchoParameters, convertEchoParameters,
@@ -2479,6 +2502,7 @@ export {
getUpdatedParametersAfterLabelUpdateForSourceParameterKey, getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
getWorkflowBlocks, getWorkflowBlocks,
getWorkflowErrors, getWorkflowErrors,
isNodeInsideForLoop,
isOutputParameterKey, isOutputParameterKey,
layout, layout,
}; };

View File

@@ -287,6 +287,7 @@ export type WorkflowBlockBase = {
block_type: WorkflowBlockType; block_type: WorkflowBlockType;
output_parameter: OutputParameter; output_parameter: OutputParameter;
continue_on_failure: boolean; continue_on_failure: boolean;
next_iteration_on_failure?: boolean;
model: WorkflowModel | null; model: WorkflowModel | null;
next_block_label?: string | null; next_block_label?: string | null;
}; };

View File

@@ -145,6 +145,7 @@ export type BlockYAMLBase = {
block_type: WorkflowBlockType; block_type: WorkflowBlockType;
label: string; label: string;
continue_on_failure?: boolean; continue_on_failure?: boolean;
next_iteration_on_failure?: boolean;
next_block_label?: string | null; next_block_label?: string | null;
}; };

View File

@@ -135,6 +135,10 @@ class Block(BaseModel, abc.ABC):
model: dict[str, Any] | None = None model: dict[str, Any] | None = None
disable_cache: bool = False disable_cache: bool = False
# Only valid for blocks inside a for loop block
# Whether to continue to the next iteration when the block fails
next_iteration_on_failure: bool = False
@property @property
def override_llm_key(self) -> str | None: def override_llm_key(self) -> str | None:
""" """
@@ -1386,15 +1390,15 @@ class ForLoopBlock(Block):
organization_id=organization_id, organization_id=organization_id,
) )
block_outputs.append(failure_block_result) block_outputs.append(failure_block_result)
# If continue_on_failure is False, stop the entire loop # If next_iteration_on_failure is False, stop the entire loop
if not self.continue_on_failure: if not self.next_iteration_on_failure:
outputs_with_loop_values.append(each_loop_output_values) outputs_with_loop_values.append(each_loop_output_values)
return LoopBlockExecutedResult( return LoopBlockExecutedResult(
outputs_with_loop_values=outputs_with_loop_values, outputs_with_loop_values=outputs_with_loop_values,
block_outputs=block_outputs, block_outputs=block_outputs,
last_block=current_block, last_block=current_block,
) )
# If continue_on_failure is True, break out of the block loop for this iteration # If next_iteration_on_failure is True, break out of the block loop for this iteration
break break
if block_output.status == BlockStatus.canceled: if block_output.status == BlockStatus.canceled:
@@ -1412,7 +1416,12 @@ class ForLoopBlock(Block):
last_block=current_block, last_block=current_block,
) )
if not block_output.success and not loop_block.continue_on_failure: if (
not block_output.success
and not loop_block.continue_on_failure
and not loop_block.next_iteration_on_failure
and not self.next_iteration_on_failure
):
LOG.info( LOG.info(
f"ForLoopBlock: Encountered a failure processing block {block_idx} during loop {loop_idx}, terminating early", f"ForLoopBlock: Encountered a failure processing block {block_idx} during loop {loop_idx}, terminating early",
block_outputs=block_outputs, block_outputs=block_outputs,
@@ -1421,6 +1430,8 @@ class ForLoopBlock(Block):
loop_over_value=loop_over_value, loop_over_value=loop_over_value,
loop_block_continue_on_failure=loop_block.continue_on_failure, loop_block_continue_on_failure=loop_block.continue_on_failure,
failure_reason=block_output.failure_reason, failure_reason=block_output.failure_reason,
next_iteration_on_failure=loop_block.next_iteration_on_failure
or self.next_iteration_on_failure,
) )
outputs_with_loop_values.append(each_loop_output_values) outputs_with_loop_values.append(each_loop_output_values)
return LoopBlockExecutedResult( return LoopBlockExecutedResult(
@@ -1429,6 +1440,21 @@ class ForLoopBlock(Block):
last_block=current_block, last_block=current_block,
) )
if block_output.success or loop_block.continue_on_failure:
continue
if loop_block.next_iteration_on_failure or self.next_iteration_on_failure:
LOG.info(
f"ForLoopBlock: Block {block_idx} during loop {loop_idx} failed but will continue to next iteration",
block_outputs=block_outputs,
loop_idx=loop_idx,
block_idx=block_idx,
loop_over_value=loop_over_value,
loop_block_next_iteration_on_failure=loop_block.next_iteration_on_failure
or self.next_iteration_on_failure,
)
break
outputs_with_loop_values.append(each_loop_output_values) outputs_with_loop_values.append(each_loop_output_values)
return LoopBlockExecutedResult( return LoopBlockExecutedResult(

View File

@@ -2951,6 +2951,7 @@ class WorkflowService:
"next_block_label": block_yaml.next_block_label, "next_block_label": block_yaml.next_block_label,
"output_parameter": output_parameter, "output_parameter": output_parameter,
"continue_on_failure": block_yaml.continue_on_failure, "continue_on_failure": block_yaml.continue_on_failure,
"next_iteration_on_failure": block_yaml.next_iteration_on_failure,
"model": block_yaml.model, "model": block_yaml.model,
} }

View File

@@ -206,6 +206,9 @@ class BlockYAML(BaseModel, abc.ABC):
) )
continue_on_failure: bool = False continue_on_failure: bool = False
model: dict[str, Any] | None = None model: dict[str, Any] | None = None
# Only valid for blocks inside a for loop block
# Whether to continue to the next iteration when the block fails
next_iteration_on_failure: bool = False
@field_validator("label") @field_validator("label")
@classmethod @classmethod