Needs (wait for background tasks)
@@ -252,20 +266,44 @@ export const PropertiesPanel = ({ selectedNode, connectedNodes, allNodes, onClos
-
If Condition
-
Run Condition
+
onUpdateIfCondition(selectedNode.id, e.target.value)}
- placeholder="e.g., success() or steps.step1.status == 'pass'"
- />
+ value={
+ isCustomCondition ? 'custom'
+ : !selectedNode.data.ifCondition || selectedNode.data.ifCondition.trim() === '' || selectedNode.data.ifCondition.trim() === 'success()' ? ''
+ : selectedNode.data.ifCondition.trim() === 'failure()' ? 'failure()'
+ : selectedNode.data.ifCondition.trim() === 'always()' ? 'always()'
+ : 'custom'
+ }
+ onChange={(e) => {
+ const val = e.target.value;
+ if (val === 'custom') {
+ setIsCustomCondition(true);
+ onUpdateIfCondition(selectedNode.id, selectedNode.data.ifCondition || '');
+ } else {
+ setIsCustomCondition(false);
+ onUpdateIfCondition(selectedNode.id, val);
+ }
+ }}
+ >
+ Default (runs on success)
+ Runs on failure
+ Always runs
+ Custom expression…
+
+ {(isCustomCondition || (selectedNode.data.ifCondition && !['', 'success()', 'failure()', 'always()'].includes(selectedNode.data.ifCondition.trim()))) && (
+
onUpdateIfCondition(selectedNode.id, e.target.value)}
+ placeholder="steps.step1.status == 'success'"
+ />
+ )}
- Conditional expression (GitHub Actions-style). Examples:
- • success() - run if previous step succeeded
- • failure() - run if previous step failed
- • always() - always run
- • steps.step1.status == 'pass' - check specific step
+ Controls when this step executes. Steps marked "Continue on Error" are excluded from success/failure evaluation. Custom expressions support status checks like steps.step_id.status == 'success'
diff --git a/web-editor/src/components/ScenarioEditor/__tests__/PropertiesPanel.test.tsx b/web-editor/src/components/ScenarioEditor/__tests__/PropertiesPanel.test.tsx
index 0778de7b..e369b698 100644
--- a/web-editor/src/components/ScenarioEditor/__tests__/PropertiesPanel.test.tsx
+++ b/web-editor/src/components/ScenarioEditor/__tests__/PropertiesPanel.test.tsx
@@ -29,6 +29,7 @@ describe('PropertiesPanel', () => {
onUpdateIfCondition: vi.fn(),
onUpdateLoop: vi.fn(),
onUpdateNeeds: vi.fn(),
+ onUpdateContinueOnError: vi.fn(),
};
it('renders correctly', () => {
diff --git a/web-editor/src/styles/Node.module.css b/web-editor/src/styles/Node.module.css
index 721e2c80..1857ee4b 100644
--- a/web-editor/src/styles/Node.module.css
+++ b/web-editor/src/styles/Node.module.css
@@ -120,7 +120,8 @@
.loopBadge,
.conditionBadge,
-.backgroundBadge {
+.backgroundBadge,
+.continueOnErrorBadge {
display: flex;
align-items: center;
gap: 3px;
@@ -161,6 +162,37 @@
background-color: rgba(59, 130, 246, 0.28);
}
+.continueOnErrorBadge {
+ background-color: rgba(245, 158, 11, 0.2);
+ color: #f59e0b;
+ border: 1px solid rgba(245, 158, 11, 0.4);
+}
+
+.continueOnErrorBadge:hover {
+ background-color: rgba(245, 158, 11, 0.3);
+}
+
+.conditionText {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+ color: #22c55e;
+ padding: 2px 4px;
+ margin-top: -4px;
+ margin-bottom: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100%;
+}
+
+.conditionText span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
/* Group container label */
.groupContainerLabel {
position: absolute;
diff --git a/web-editor/src/types/scenario.ts b/web-editor/src/types/scenario.ts
index ffcc7fbc..24691d3a 100644
--- a/web-editor/src/types/scenario.ts
+++ b/web-editor/src/types/scenario.ts
@@ -28,6 +28,7 @@ export interface NodeData extends Record
{
ifCondition?: string;
loop?: LoopConfig;
needs?: string[];
+ continueOnError?: boolean;
isGroupReference?: boolean;
isGroupContainer?: boolean;
isPhaseContainer?: boolean;
@@ -61,6 +62,7 @@ export interface ScenarioStep {
description?: string;
if?: string;
loop?: LoopConfig;
+ 'continue-on-error'?: boolean;
}
export interface ScenarioDefinition {
diff --git a/web-editor/src/utils/__tests__/scenarioConversion.test.ts b/web-editor/src/utils/__tests__/scenarioConversion.test.ts
index 441780a3..22a23acc 100644
--- a/web-editor/src/utils/__tests__/scenarioConversion.test.ts
+++ b/web-editor/src/utils/__tests__/scenarioConversion.test.ts
@@ -145,3 +145,58 @@ describe('YAML Conversion - Reference Normalization', () => {
expect(scenario.groups?.my_group?.steps?.[1]?.arguments?.data).toBe('${{ group.fetch.result }}');
});
});
+
+describe('YAML Conversion - continue-on-error', () => {
+ it('includes continue-on-error=true in YAML output', () => {
+ const node: Node = {
+ id: 'node1',
+ position: { x: 0, y: 0 },
+ data: {
+ label: 'Validate Metrics',
+ stepId: 'validate',
+ operationId: 'validate_op',
+ parameters: [],
+ continueOnError: true
+ }
+ };
+
+ const scenario = convertGraphToYaml([node], [], [], 'test', 'test');
+ const step = scenario.steps[0];
+ expect(step['continue-on-error']).toBe(true);
+ });
+
+ it('omits continue-on-error when set to false (default)', () => {
+ const node: Node = {
+ id: 'node1',
+ position: { x: 0, y: 0 },
+ data: {
+ label: 'Setup',
+ stepId: 'setup',
+ operationId: 'setup_op',
+ parameters: [],
+ continueOnError: false
+ }
+ };
+
+ const scenario = convertGraphToYaml([node], [], [], 'test', 'test');
+ const step = scenario.steps[0];
+ expect(step['continue-on-error']).toBeUndefined();
+ });
+
+ it('omits continue-on-error when not set', () => {
+ const node: Node = {
+ id: 'node1',
+ position: { x: 0, y: 0 },
+ data: {
+ label: 'Setup',
+ stepId: 'setup',
+ operationId: 'setup_op',
+ parameters: []
+ }
+ };
+
+ const scenario = convertGraphToYaml([node], [], [], 'test', 'test');
+ const step = scenario.steps[0];
+ expect(step['continue-on-error']).toBeUndefined();
+ });
+});
diff --git a/web-editor/src/utils/scenarioConversion.ts b/web-editor/src/utils/scenarioConversion.ts
index 68655e2e..bfb3d86f 100644
--- a/web-editor/src/utils/scenarioConversion.ts
+++ b/web-editor/src/utils/scenarioConversion.ts
@@ -79,6 +79,7 @@ export const convertYamlToGraph = (
runInBackground: step.background,
ifCondition: step.if,
loop: step.loop,
+ continueOnError: step['continue-on-error'],
isGroupReference: true,
isGroupContainer: true
},
@@ -124,7 +125,8 @@ export const convertYamlToGraph = (
operationId: groupOperation?.id,
description: groupStep.description || groupOperation?.description || '',
phase: groupOperation?.phase,
- parameters: groupParameters
+ parameters: groupParameters,
+ continueOnError: groupStep['continue-on-error'],
}
};
@@ -187,6 +189,7 @@ export const convertYamlToGraph = (
ifCondition: step.if,
loop: step.loop,
needs: needsList,
+ continueOnError: step['continue-on-error'],
isGroupReference: false
}
};
@@ -313,6 +316,10 @@ export const convertGraphToYaml = (
step.loop = node.data.loop;
}
+ if (node.data.continueOnError) {
+ step['continue-on-error'] = true;
+ }
+
return step;
}
@@ -390,6 +397,10 @@ export const convertGraphToYaml = (
step.loop = node.data.loop;
}
+ if (node.data.continueOnError) {
+ step['continue-on-error'] = true;
+ }
+
// Dependencies (needs) come from dotted edges into this node
const deps = dependencyEdges
.filter(e => e.target === node.id)