) {
}}
/>
+
+
+
+
+
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts
index a29fda40..d4e234bc 100644
--- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts
+++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts
@@ -17,6 +17,7 @@ export type WorkflowStartNodeData = {
aiFallback: boolean;
runSequentially: boolean;
sequentialKey: string | null;
+ finallyBlockLabel: string | null;
label: "__start_block__";
showCode: boolean;
};
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx
index 33656a34..627bc5ec 100644
--- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx
@@ -533,35 +533,42 @@ function NodeHeader({
inputClassName="text-base"
/>
- {transmutations && transmutations.others.length ? (
-
-
- {transmutations.blockTitle}
+
+ {transmutations && transmutations.others.length ? (
+
+
+ {transmutations.blockTitle}
+
+
+ t.label),
+ ]}
+ selected={transmutations.self}
+ onChange={(label) => {
+ const transmutation = transmutations.others.find(
+ (t) => t.label === label,
+ );
+
+ if (!transmutation) {
+ return;
+ }
+
+ transmuteNodeCallback(nodeId, transmutation.nodeName);
+ }}
+ />
+
+
+ ) : (
+
{blockTitle}
+ )}
+ {workflowSettingsStore.finallyBlockLabel === blockLabel && (
+
+ Runs on any outcome
-
- t.label),
- ]}
- selected={transmutations.self}
- onChange={(label) => {
- const transmutation = transmutations.others.find(
- (t) => t.label === label,
- );
-
- if (!transmutation) {
- return;
- }
-
- transmuteNodeCallback(nodeId, transmutation.nodeName);
- }}
- />
-
-
- ) : (
- {blockTitle}
- )}
+ )}
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx
index 82aaaaf7..6675f812 100644
--- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx
@@ -154,6 +154,7 @@ function getWorkflowElements(version: WorkflowVersion) {
aiFallback: version.ai_fallback ?? true,
runSequentially: version.run_sequentially ?? false,
sequentialKey: version.sequential_key ?? null,
+ finallyBlockLabel: version.workflow_definition?.finally_block_label ?? null,
};
// Deep clone the blocks to ensure complete isolation from main editor
diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
index a9ea8c5f..306fbc04 100644
--- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
+++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
@@ -1373,6 +1373,7 @@ function getElements(
showCode: false,
runSequentially: settings.runSequentially,
sequentialKey: settings.sequentialKey,
+ finallyBlockLabel: settings.finallyBlockLabel ?? null,
}),
);
@@ -2541,6 +2542,7 @@ function getWorkflowSettings(nodes: Array
): WorkflowSettings {
aiFallback: true,
runSequentially: false,
sequentialKey: null,
+ finallyBlockLabel: null,
};
const startNodes = nodes.filter(isStartNode);
const startNodeWithWorkflowSettings = startNodes.find(
@@ -2566,6 +2568,7 @@ function getWorkflowSettings(nodes: Array): WorkflowSettings {
aiFallback: data.aiFallback,
runSequentially: data.runSequentially,
sequentialKey: data.sequentialKey,
+ finallyBlockLabel: data.finallyBlockLabel ?? null,
};
}
return defaultSettings;
@@ -3358,6 +3361,7 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest {
version: workflowDefinitionVersion,
parameters: convertParametersToParameterYAML(userParameters),
blocks: convertBlocksToBlockYAML(workflow.workflow_definition.blocks),
+ finally_block_label: workflow.workflow_definition.finally_block_label,
},
is_saved_task: workflow.is_saved_task,
status: workflow.status,
diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
index c09a95e8..24b6faa5 100644
--- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
+++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
@@ -558,6 +558,7 @@ export type WorkflowDefinition = {
version?: number | null;
parameters: Array;
blocks: Array;
+ finally_block_label?: string | null;
};
export type WorkflowApiResponse = {
@@ -603,6 +604,7 @@ export type WorkflowSettings = {
aiFallback: boolean | null;
runSequentially: boolean;
sequentialKey: string | null;
+ finallyBlockLabel: string | null;
};
export type WorkflowModel = JsonObjectExtendable<{ model_name: string }>;
diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
index ebdda790..7b680c81 100644
--- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
+++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
@@ -27,6 +27,7 @@ export type WorkflowDefinitionYAML = {
version?: number | null;
parameters: Array;
blocks: Array;
+ finally_block_label?: string | null;
};
export type ParameterYAML =
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx
index b3f479ab..20fea86c 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx
@@ -77,10 +77,13 @@ function WorkflowRunOverview() {
}
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
+ const finallyBlockLabel =
+ workflowRun.workflow?.workflow_definition?.finally_block_label ?? null;
const selection = findActiveItem(
workflowRunTimeline,
active,
workflowRunIsFinalized,
+ finallyBlockLabel,
);
const browserSessionId = workflowRun.browser_session_id;
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx
index a179a6f9..a0d8d121 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx
@@ -53,6 +53,9 @@ function WorkflowRunTimeline({
const workflowRunIsNotFinalized = statusIsNotFinalized(workflowRun);
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
+ const finallyBlockLabel =
+ workflowRun.workflow?.workflow_definition?.finally_block_label ?? null;
+
const numberOfActions = workflowRunTimeline.reduce((total, current) => {
if (isTaskVariantBlockItem(current)) {
return total + current.block!.actions!.length;
@@ -104,6 +107,7 @@ function WorkflowRunTimeline({
onActionClick={onActionItemSelected}
onBlockItemClick={onBlockItemSelected}
onThoughtCardClick={onObserverThoughtCardSelected}
+ finallyBlockLabel={finallyBlockLabel}
/>
);
}
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx
index 36e4cceb..815d72d3 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx
@@ -38,6 +38,7 @@ type Props = {
onBlockItemClick: (block: WorkflowRunBlock) => void;
onActionClick: (action: ActionItem) => void;
onThoughtCardClick: (thought: ObserverThought) => void;
+ finallyBlockLabel?: string | null;
};
function WorkflowRunTimelineBlockItem({
@@ -47,8 +48,10 @@ function WorkflowRunTimelineBlockItem({
onBlockItemClick,
onActionClick,
onThoughtCardClick,
+ finallyBlockLabel,
}: Props) {
const actions = block.actions ?? [];
+ const isFinallyBlock = finallyBlockLabel && block.label === finallyBlockLabel;
const hasActiveAction =
isAction(activeItem) &&
@@ -128,6 +131,11 @@ function WorkflowRunTimelineBlockItem({
{block.label}
+ {isFinallyBlock && (
+
+ Execute on any outcome
+
+ )}
@@ -235,6 +243,7 @@ function WorkflowRunTimelineBlockItem({
onActionClick={onActionClick}
onBlockItemClick={onBlockItemClick}
onThoughtCardClick={onThoughtCardClick}
+ finallyBlockLabel={finallyBlockLabel}
/>
);
}
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/useActiveWorkflowRunItem.ts b/skyvern-frontend/src/routes/workflows/workflowRun/useActiveWorkflowRunItem.ts
index c7813960..be606ed6 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/useActiveWorkflowRunItem.ts
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/useActiveWorkflowRunItem.ts
@@ -1,6 +1,7 @@
import { useSearchParams } from "react-router-dom";
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
+import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery";
import { statusIsFinalized } from "@/routes/tasks/types";
import { findActiveItem } from "./workflowTimelineUtils";
import { WorkflowRunOverviewActiveElement } from "./WorkflowRunOverview";
@@ -13,14 +14,19 @@ function useActiveWorkflowRunItem(): [
const active = searchParams.get("active");
const { data: workflowRun } = useWorkflowRunQuery();
+ const { data: workflowRunWithWorkflow } = useWorkflowRunWithWorkflowQuery();
const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery();
const workflowRunIsFinalized = workflowRun && statusIsFinalized(workflowRun);
+ const finallyBlockLabel =
+ workflowRunWithWorkflow?.workflow?.workflow_definition
+ ?.finally_block_label ?? null;
const activeItem = findActiveItem(
workflowRunTimeline ?? [],
active,
!!workflowRunIsFinalized,
+ finallyBlockLabel,
);
function handleSetActiveItem(id: string) {
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/workflowTimelineUtils.ts b/skyvern-frontend/src/routes/workflows/workflowRun/workflowTimelineUtils.ts
index f3569784..f2534e3b 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/workflowTimelineUtils.ts
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/workflowTimelineUtils.ts
@@ -31,11 +31,27 @@ function findActiveItem(
timeline: Array,
target: string | null,
workflowRunIsFinalized: boolean,
+ finallyBlockLabel?: string | null,
): WorkflowRunOverviewActiveElement {
if (target === null) {
if (!workflowRunIsFinalized) {
return "stream";
}
+ // If there's a finally block, try to show it first when workflow is finalized
+ if (finallyBlockLabel && timeline?.length > 0) {
+ const finallyBlock = timeline.find(
+ (item) => isBlockItem(item) && item.block.label === finallyBlockLabel,
+ );
+ if (finallyBlock && isBlockItem(finallyBlock)) {
+ if (
+ finallyBlock.block.actions &&
+ finallyBlock.block.actions.length > 0
+ ) {
+ return finallyBlock.block.actions[0]!;
+ }
+ return finallyBlock.block;
+ }
+ }
if (timeline?.length > 0) {
const timelineItem = timeline![0];
if (isBlockItem(timelineItem)) {
diff --git a/skyvern-frontend/src/store/WorkflowHasChangesStore.ts b/skyvern-frontend/src/store/WorkflowHasChangesStore.ts
index e8db0eef..f78fb1ce 100644
--- a/skyvern-frontend/src/store/WorkflowHasChangesStore.ts
+++ b/skyvern-frontend/src/store/WorkflowHasChangesStore.ts
@@ -143,6 +143,7 @@ const useWorkflowSave = (opts?: WorkflowSaveOpts) => {
version: saveData.workflowDefinitionVersion,
parameters: saveData.parameters,
blocks: saveData.blocks,
+ finally_block_label: saveData.settings.finallyBlockLabel ?? undefined,
},
is_saved_task: saveData.workflow.is_saved_task,
status: opts?.status ?? saveData.workflow.status,
diff --git a/skyvern-frontend/src/store/WorkflowSettingsStore.ts b/skyvern-frontend/src/store/WorkflowSettingsStore.ts
index 05d6f301..6fe443b0 100644
--- a/skyvern-frontend/src/store/WorkflowSettingsStore.ts
+++ b/skyvern-frontend/src/store/WorkflowSettingsStore.ts
@@ -12,6 +12,7 @@ export interface WorkflowSettingsState {
model: WorkflowModel | null;
maxScreenshotScrollingTimes: number | null;
extraHttpHeaders: string | Record | null;
+ finallyBlockLabel: string | null;
setWorkflowSettings: (
settings: Partial<
Omit<
@@ -33,6 +34,7 @@ const defaultState: Omit<
model: null,
maxScreenshotScrollingTimes: null,
extraHttpHeaders: null,
+ finallyBlockLabel: null,
};
export const useWorkflowSettingsStore = create(
diff --git a/skyvern/forge/sdk/workflow/exceptions.py b/skyvern/forge/sdk/workflow/exceptions.py
index f02da543..2593b4a4 100644
--- a/skyvern/forge/sdk/workflow/exceptions.py
+++ b/skyvern/forge/sdk/workflow/exceptions.py
@@ -20,6 +20,24 @@ class WorkflowDefinitionHasDuplicateBlockLabels(BaseWorkflowHTTPException):
)
+class InvalidFinallyBlockLabel(BaseWorkflowHTTPException):
+ def __init__(self, finally_block_label: str, available_labels: list[str]) -> None:
+ super().__init__(
+ f"finally_block_label '{finally_block_label}' does not reference a valid block in the workflow. "
+ f"Available block labels: {', '.join(available_labels) if available_labels else '(none)'}",
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ )
+
+
+class NonTerminalFinallyBlock(BaseWorkflowHTTPException):
+ def __init__(self, finally_block_label: str) -> None:
+ super().__init__(
+ f"finally_block_label '{finally_block_label}' must be a terminal block (next_block_label must be null). "
+ "Only blocks without a next_block_label can be used as finally blocks.",
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ )
+
+
class FailedToCreateWorkflow(BaseWorkflowHTTPException):
def __init__(self, error_message: str) -> None:
super().__init__(
diff --git a/skyvern/forge/sdk/workflow/models/workflow.py b/skyvern/forge/sdk/workflow/models/workflow.py
index 9a831250..e9d3eeb8 100644
--- a/skyvern/forge/sdk/workflow/models/workflow.py
+++ b/skyvern/forge/sdk/workflow/models/workflow.py
@@ -7,7 +7,11 @@ from typing_extensions import deprecated
from skyvern.forge.sdk.schemas.files import FileInfo
from skyvern.forge.sdk.schemas.task_v2 import TaskV2
-from skyvern.forge.sdk.workflow.exceptions import WorkflowDefinitionHasDuplicateBlockLabels
+from skyvern.forge.sdk.workflow.exceptions import (
+ InvalidFinallyBlockLabel,
+ NonTerminalFinallyBlock,
+ WorkflowDefinitionHasDuplicateBlockLabels,
+)
from skyvern.forge.sdk.workflow.models.block import BlockTypeVar
from skyvern.forge.sdk.workflow.models.parameter import PARAMETER_TYPE, OutputParameter
from skyvern.schemas.runs import ProxyLocationInput, ScriptRunResponse
@@ -54,6 +58,7 @@ class WorkflowDefinition(BaseModel):
version: int = 1
parameters: list[PARAMETER_TYPE]
blocks: List[BlockTypeVar]
+ finally_block_label: str | None = None
def validate(self) -> None:
labels: set[str] = set()
@@ -67,6 +72,13 @@ class WorkflowDefinition(BaseModel):
if duplicate_labels:
raise WorkflowDefinitionHasDuplicateBlockLabels(duplicate_labels)
+ if self.finally_block_label:
+ if self.finally_block_label not in labels:
+ raise InvalidFinallyBlockLabel(self.finally_block_label, list(labels))
+ for block in self.blocks:
+ if block.label == self.finally_block_label and block.next_block_label is not None:
+ raise NonTerminalFinallyBlock(self.finally_block_label)
+
class Workflow(BaseModel):
workflow_id: str
diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py
index e67a014a..226ae2bc 100644
--- a/skyvern/forge/sdk/workflow/service.py
+++ b/skyvern/forge/sdk/workflow/service.py
@@ -737,33 +737,59 @@ class WorkflowService:
script=workflow_script,
)
+ # Check if there's a finally block configured
+ finally_block_label = workflow.workflow_definition.finally_block_label
+
if refreshed_workflow_run := await app.DATABASE.get_workflow_run(
workflow_run_id=workflow_run_id,
organization_id=organization_id,
):
workflow_run = refreshed_workflow_run
- if workflow_run.status not in (
- WorkflowRunStatus.canceled,
+
+ pre_finally_status = workflow_run.status
+ pre_finally_failure_reason = workflow_run.failure_reason
+
+ if pre_finally_status not in (
+ WorkflowRunStatus.canceled,
+ WorkflowRunStatus.failed,
+ WorkflowRunStatus.terminated,
+ WorkflowRunStatus.timed_out,
+ ):
+ await self.generate_script_if_needed(
+ workflow=workflow,
+ workflow_run=workflow_run,
+ block_labels=block_labels,
+ blocks_to_update=blocks_to_update,
+ )
+
+ # Execute finally block if configured. Skip for: canceled (user explicitly stopped)
+ should_run_finally = finally_block_label and pre_finally_status != WorkflowRunStatus.canceled
+ if should_run_finally:
+ # Temporarily set to running for terminal workflows (for frontend UX)
+ if pre_finally_status in (
WorkflowRunStatus.failed,
WorkflowRunStatus.terminated,
WorkflowRunStatus.timed_out,
):
- workflow_run = await self.mark_workflow_run_as_completed(
+ workflow_run = await self._update_workflow_run_status(
workflow_run_id=workflow_run_id,
+ status=WorkflowRunStatus.running,
+ failure_reason=None,
)
- await self.generate_script_if_needed(
- workflow=workflow,
- workflow_run=workflow_run,
- block_labels=block_labels,
- blocks_to_update=blocks_to_update,
- )
- else:
- LOG.info(
- "Workflow run is already timed_out, canceled, failed, or terminated, not marking as completed",
- workflow_run_id=workflow_run_id,
- workflow_run_status=workflow_run.status,
- run_with=workflow_run.run_with,
- )
+ await self._execute_finally_block_if_configured(
+ workflow=workflow,
+ workflow_run=workflow_run,
+ organization=organization,
+ browser_session_id=browser_session_id,
+ )
+
+ workflow_run = await self._finalize_workflow_run_status(
+ workflow_run_id=workflow_run_id,
+ workflow_run=workflow_run,
+ pre_finally_status=pre_finally_status,
+ pre_finally_failure_reason=pre_finally_failure_reason,
+ )
+
await self.clean_up_workflow(
workflow=workflow,
workflow_run=workflow_run,
@@ -1340,6 +1366,46 @@ class WorkflowService:
return workflow_run, False
+ async def _execute_finally_block_if_configured(
+ self,
+ workflow: Workflow,
+ workflow_run: WorkflowRun,
+ organization: Organization,
+ browser_session_id: str | None,
+ ) -> None:
+ finally_block_label = workflow.workflow_definition.finally_block_label
+ if not finally_block_label:
+ return
+
+ label_to_block: dict[str, BlockTypeVar] = {block.label: block for block in workflow.workflow_definition.blocks}
+
+ block = label_to_block.get(finally_block_label)
+ if not block:
+ LOG.warning(
+ "Finally block label not found",
+ workflow_run_id=workflow_run.workflow_run_id,
+ finally_block_label=finally_block_label,
+ )
+ return
+
+ try:
+ parameters = block.get_all_parameters(workflow_run.workflow_run_id)
+ await app.WORKFLOW_CONTEXT_MANAGER.register_block_parameters_for_workflow_run(
+ workflow_run.workflow_run_id, parameters, organization
+ )
+ await block.execute_safe(
+ workflow_run_id=workflow_run.workflow_run_id,
+ organization_id=organization.organization_id,
+ browser_session_id=browser_session_id,
+ )
+ except Exception as e:
+ LOG.warning(
+ "Finally block execution failed",
+ workflow_run_id=workflow_run.workflow_run_id,
+ block_label=block.label,
+ error=str(e),
+ )
+
def _build_workflow_graph(
self,
blocks: list[BlockTypeVar],
@@ -2131,6 +2197,35 @@ class WorkflowService:
run_with=run_with,
)
+ async def _finalize_workflow_run_status(
+ self,
+ workflow_run_id: str,
+ workflow_run: WorkflowRun,
+ pre_finally_status: WorkflowRunStatus,
+ pre_finally_failure_reason: str | None,
+ ) -> WorkflowRun:
+ """
+ Set final workflow run status based on pre-finally state.
+ Called unconditionally to ensure unified flow.
+ """
+ if pre_finally_status not in (
+ WorkflowRunStatus.canceled,
+ WorkflowRunStatus.failed,
+ WorkflowRunStatus.terminated,
+ WorkflowRunStatus.timed_out,
+ ):
+ return await self.mark_workflow_run_as_completed(workflow_run_id)
+
+ if workflow_run.status == WorkflowRunStatus.running:
+ # We temporarily set to running for finally block, restore terminal status
+ return await self._update_workflow_run_status(
+ workflow_run_id=workflow_run_id,
+ status=pre_finally_status,
+ failure_reason=pre_finally_failure_reason,
+ )
+
+ return workflow_run
+
async def mark_workflow_run_as_failed(
self,
workflow_run_id: str,
diff --git a/skyvern/forge/sdk/workflow/workflow_definition_converter.py b/skyvern/forge/sdk/workflow/workflow_definition_converter.py
index 9b7b3aed..e6e2346f 100644
--- a/skyvern/forge/sdk/workflow/workflow_definition_converter.py
+++ b/skyvern/forge/sdk/workflow/workflow_definition_converter.py
@@ -302,7 +302,12 @@ def convert_workflow_definition(
if dag_version is None:
dag_version = 2 if _has_dag_metadata(workflow_definition_yaml.blocks) else 1
- workflow_definition = WorkflowDefinition(parameters=parameters.values(), blocks=blocks, version=dag_version)
+ workflow_definition = WorkflowDefinition(
+ parameters=parameters.values(),
+ blocks=blocks,
+ version=dag_version,
+ finally_block_label=workflow_definition_yaml.finally_block_label,
+ )
LOG.info(
f"Created workflow from request, title: {title}",
diff --git a/skyvern/schemas/workflows.py b/skyvern/schemas/workflows.py
index 37367d1d..503bf0dd 100644
--- a/skyvern/schemas/workflows.py
+++ b/skyvern/schemas/workflows.py
@@ -591,6 +591,7 @@ class WorkflowDefinitionYAML(BaseModel):
version: int = 1
parameters: list[PARAMETER_YAML_TYPES]
blocks: list[BLOCK_YAML_TYPES]
+ finally_block_label: str | None = None
@model_validator(mode="after")
def validate_unique_block_labels(cls, workflow: "WorkflowDefinitionYAML") -> "WorkflowDefinitionYAML":
@@ -604,6 +605,12 @@ class WorkflowDefinitionYAML(BaseModel):
f"Found duplicate label(s): {', '.join(unique_duplicates)}"
)
+ if workflow.finally_block_label and workflow.finally_block_label not in labels:
+ raise ValueError(
+ f"finally_block_label '{workflow.finally_block_label}' does not reference a valid block. "
+ f"Available labels: {', '.join(labels) if labels else '(none)'}"
+ )
+
return workflow