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.",
continueOnFailure:
"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:
"Include the action history in the completion verification.",
} as const;
@@ -72,6 +74,8 @@ export const helpTooltips = {
...baseHelpTooltipContent,
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.",
nextIterationOnFailure:
"When enabled, if any block inside the loop fails, the loop will immediately jump to the next iteration instead of stopping.",
},
sendEmail: {
...baseHelpTooltipContent,

View File

@@ -23,7 +23,10 @@ import { useRerender } from "@/hooks/useRerender";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { RunEngineSelector } from "@/components/EngineSelector";
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
const urlTooltip =
"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 update = useUpdate<ActionNode["data"]>({ id, editable });
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
@@ -248,28 +253,19 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
</div>
)}
</div>
<Separator />
<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["action"]["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<BlockExecutionOptions
continueOnFailure={data.continueOnFailure}
nextIterationOnFailure={data.nextIterationOnFailure}
editable={editable}
isInsideForLoop={isInsideForLoop}
blockType="action"
onContinueOnFailureChange={(checked) => {
update({ continueOnFailure: checked });
}}
onNextIterationOnFailureChange={(checked) => {
update({ nextIterationOnFailure: checked });
}}
/>
<DisableCache
disableCache={data.disableCache}
editable={editable}

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,10 @@ import { errorMappingExampleValue } from "../types";
import type { NavigationNode } from "./types";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { RunEngineSelector } from "@/components/EngineSelector";
import { ModelSelector } from "@/components/ModelSelector";
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants";
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 isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<NavigationNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
@@ -287,51 +292,32 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
</div>
)}
</div>
<Separator />
<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["navigation"][
"includeActionHistoryInVerification"
]
}
/>
</div>
<div className="w-52">
<Switch
checked={data.includeActionHistoryInVerification}
onCheckedChange={(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>
<BlockExecutionOptions
continueOnFailure={data.continueOnFailure}
nextIterationOnFailure={data.nextIterationOnFailure}
includeActionHistoryInVerification={
data.includeActionHistoryInVerification
}
editable={editable}
isInsideForLoop={isInsideForLoop}
blockType="navigation"
showOptions={{
continueOnFailure: true,
nextIterationOnFailure: true,
includeActionHistoryInVerification: true,
}}
onContinueOnFailureChange={(checked) => {
update({ continueOnFailure: checked });
}}
onNextIterationOnFailureChange={(checked) => {
update({ nextIterationOnFailure: checked });
}}
onIncludeActionHistoryInVerificationChange={(checked) => {
update({
includeActionHistoryInVerification: checked,
});
}}
/>
<DisableCache
disableCache={data.disableCache}
editable={editable}

View File

@@ -22,7 +22,10 @@ import { useState } from "react";
import { AppNode } from "..";
import { helpTooltips, placeholders } from "../../helpContent";
import { AI_IMPROVE_CONFIGS } from "../../constants";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
import { ParametersMultiSelect } from "./ParametersMultiSelect";
import type { TaskNode } from "./types";
@@ -39,6 +42,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { useRerender } from "@/hooks/useRerender";
import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
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 isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<TaskNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
@@ -303,49 +308,32 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
</div>
)}
</div>
<Separator />
<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["task"][
"includeActionHistoryInVerification"
]
}
/>
</div>
<div className="w-52">
<Switch
checked={data.includeActionHistoryInVerification}
onCheckedChange={(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>
<BlockExecutionOptions
continueOnFailure={data.continueOnFailure}
nextIterationOnFailure={data.nextIterationOnFailure}
includeActionHistoryInVerification={
data.includeActionHistoryInVerification
}
editable={editable}
isInsideForLoop={isInsideForLoop}
blockType="task"
showOptions={{
continueOnFailure: true,
nextIterationOnFailure: true,
includeActionHistoryInVerification: true,
}}
onContinueOnFailureChange={(checked) => {
update({ continueOnFailure: checked });
}}
onNextIterationOnFailureChange={(checked) => {
update({ nextIterationOnFailure: checked });
}}
onIncludeActionHistoryInVerificationChange={(checked) => {
update({
includeActionHistoryInVerification: checked,
});
}}
/>
<DisableCache
disableCache={data.disableCache}
editable={editable}

View File

@@ -10,7 +10,6 @@ import {
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
@@ -21,7 +20,10 @@ import { helpTooltips } from "../../helpContent";
import { errorMappingExampleValue } from "../types";
import type { ValidationNode } from "./types";
import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import {
getAvailableOutputParameterKeys,
isNodeInsideForLoop,
} from "../../workflowEditorUtils";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { ModelSelector } from "@/components/ModelSelector";
@@ -34,6 +36,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
import { useRerender } from "@/hooks/useRerender";
import { DisableCache } from "../DisableCache";
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
import { AI_IMPROVE_CONFIGS } from "../../constants";
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 isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const update = useUpdate<ValidationNode["data"]>({ id, editable });
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
@@ -212,30 +216,19 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
</div>
)}
</div>
<Separator />
<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["validation"]["continueOnFailure"]
}
/>
</div>
<div className="w-52">
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
update({ continueOnFailure: checked });
}}
/>
</div>
</div>
<BlockExecutionOptions
continueOnFailure={data.continueOnFailure}
nextIterationOnFailure={data.nextIterationOnFailure}
editable={editable}
isInsideForLoop={isInsideForLoop}
blockType="validation"
onContinueOnFailureChange={(checked) => {
update({ continueOnFailure: checked });
}}
onNextIterationOnFailureChange={(checked) => {
update({ nextIterationOnFailure: checked });
}}
/>
<DisableCache
disableCache={data.disableCache}
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;
label: string;
continueOnFailure: boolean;
nextIterationOnFailure?: boolean;
editable: boolean;
model: WorkflowModel | null;
showCode?: boolean;

View File

@@ -223,6 +223,7 @@ function convertToNode(
debuggable: debuggableWorkflowBlockTypes.has(block.block_type),
label: block.label,
continueOnFailure: block.continue_on_failure,
nextIterationOnFailure: block.next_iteration_on_failure,
editable,
model: block.model,
};
@@ -489,6 +490,7 @@ function convertToNode(
loopValue: block.loop_over?.key ?? "",
loopVariableReference: loopVariableReference,
completeIfEmpty: block.complete_if_empty,
nextIterationOnFailure: block.next_iteration_on_failure,
},
};
}
@@ -1073,6 +1075,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
const base = {
label: node.data.label,
continue_on_failure: node.data.continueOnFailure,
next_iteration_on_failure: node.data.nextIterationOnFailure,
model: node.data.model,
};
switch (node.type) {
@@ -1422,6 +1425,7 @@ function getOrderedChildrenBlocks(
block_type: "for_loop",
label: currentNode.data.label,
continue_on_failure: currentNode.data.continueOnFailure,
next_iteration_on_failure: currentNode.data.nextIterationOnFailure,
loop_blocks: loopChildren,
loop_variable_reference: currentNode.data.loopVariableReference,
complete_if_empty: currentNode.data.completeIfEmpty,
@@ -1452,6 +1456,7 @@ function getWorkflowBlocksUtil(
block_type: "for_loop",
label: node.data.label,
continue_on_failure: node.data.continueOnFailure,
next_iteration_on_failure: node.data.nextIterationOnFailure,
loop_blocks: getOrderedChildrenBlocks(nodes, edges, node.id),
loop_variable_reference: node.data.loopVariableReference,
complete_if_empty: node.data.completeIfEmpty,
@@ -1952,6 +1957,7 @@ function convertBlocksToBlockYAML(
const base = {
label: block.label,
continue_on_failure: block.continue_on_failure,
next_iteration_on_failure: block.next_iteration_on_failure,
next_block_label: block.next_block_label,
};
switch (block.block_type) {
@@ -2455,6 +2461,23 @@ function getLabelForWorkflowParameterType(type: WorkflowParameterValueType) {
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 {
convert,
convertEchoParameters,
@@ -2479,6 +2502,7 @@ export {
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
getWorkflowBlocks,
getWorkflowErrors,
isNodeInsideForLoop,
isOutputParameterKey,
layout,
};

View File

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

View File

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

View File

@@ -135,6 +135,10 @@ class Block(BaseModel, abc.ABC):
model: dict[str, Any] | None = None
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
def override_llm_key(self) -> str | None:
"""
@@ -1386,15 +1390,15 @@ class ForLoopBlock(Block):
organization_id=organization_id,
)
block_outputs.append(failure_block_result)
# If continue_on_failure is False, stop the entire loop
if not self.continue_on_failure:
# If next_iteration_on_failure is False, stop the entire loop
if not self.next_iteration_on_failure:
outputs_with_loop_values.append(each_loop_output_values)
return LoopBlockExecutedResult(
outputs_with_loop_values=outputs_with_loop_values,
block_outputs=block_outputs,
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
if block_output.status == BlockStatus.canceled:
@@ -1412,7 +1416,12 @@ class ForLoopBlock(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(
f"ForLoopBlock: Encountered a failure processing block {block_idx} during loop {loop_idx}, terminating early",
block_outputs=block_outputs,
@@ -1421,6 +1430,8 @@ class ForLoopBlock(Block):
loop_over_value=loop_over_value,
loop_block_continue_on_failure=loop_block.continue_on_failure,
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)
return LoopBlockExecutedResult(
@@ -1429,6 +1440,21 @@ class ForLoopBlock(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)
return LoopBlockExecutedResult(

View File

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

View File

@@ -206,6 +206,9 @@ class BlockYAML(BaseModel, abc.ABC):
)
continue_on_failure: bool = False
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")
@classmethod