2024-11-29 16:05:44 +08:00
import asyncio
2025-10-14 16:17:03 -07:00
import importlib . util
2024-03-01 10:09:30 -08:00
import json
2025-10-14 16:17:03 -07:00
import os
import textwrap
2025-09-03 16:55:15 -04:00
import uuid
2025-10-01 13:49:42 -07:00
from collections import deque
2025-11-05 15:26:11 +08:00
from collections . abc import Sequence
from dataclasses import dataclass , field
2025-02-02 03:10:38 +08:00
from datetime import UTC , datetime
2025-10-14 12:00:17 -06:00
from typing import Any , Literal , cast
2024-03-01 10:09:30 -08:00
2024-11-01 15:13:41 -07:00
import httpx
2024-03-01 10:09:30 -08:00
import structlog
2025-11-29 02:57:20 +03:00
from sqlalchemy . exc import IntegrityError , SQLAlchemyError
2024-03-01 10:09:30 -08:00
2025-10-14 16:17:03 -07:00
import skyvern
2024-03-06 19:06:15 -08:00
from skyvern import analytics
2025-08-28 20:05:24 -04:00
from skyvern . client . types . output_parameter import OutputParameter as BlockOutputParameter
2024-12-02 15:01:22 -08:00
from skyvern . config import settings
2024-11-29 16:05:44 +08:00
from skyvern . constants import GET_DOWNLOADED_FILES_TIMEOUT , SAVE_DOWNLOADED_FILES_TIMEOUT
2024-11-15 11:07:44 +08:00
from skyvern . exceptions import (
2025-07-04 15:34:15 -04:00
BlockNotFound ,
2025-11-06 01:24:39 -08:00
BrowserProfileNotFound ,
2025-07-04 01:49:51 -07:00
BrowserSessionNotFound ,
2025-10-07 16:56:53 -07:00
CannotUpdateWorkflowDueToCodeCache ,
2024-11-15 11:07:44 +08:00
FailedToSendWebhook ,
2025-06-18 00:33:16 -04:00
InvalidCredentialId ,
2024-11-15 11:07:44 +08:00
MissingValueForParameter ,
2025-10-15 17:12:51 -07:00
ScriptTerminationException ,
2024-11-15 11:07:44 +08:00
SkyvernException ,
WorkflowNotFound ,
2025-11-04 18:30:17 -05:00
WorkflowNotFoundForWorkflowRun ,
2024-11-15 11:07:44 +08:00
WorkflowRunNotFound ,
2025-11-29 02:57:20 +03:00
WorkflowRunParameterPersistenceError ,
2024-11-15 11:07:44 +08:00
)
2024-03-01 10:09:30 -08:00
from skyvern . forge import app
2025-09-03 16:55:15 -04:00
from skyvern . forge . prompts import prompt_engine
2025-12-05 12:30:05 -08:00
from skyvern . forge . sdk . artifact . models import Artifact , ArtifactType
2024-03-01 10:09:30 -08:00
from skyvern . forge . sdk . core import skyvern_context
2025-11-04 11:29:14 +08:00
from skyvern . forge . sdk . core . security import generate_skyvern_webhook_signature
2024-03-01 10:09:30 -08:00
from skyvern . forge . sdk . core . skyvern_context import SkyvernContext
2024-12-31 11:24:09 -08:00
from skyvern . forge . sdk . models import Step , StepStatus
2025-02-26 17:19:05 -08:00
from skyvern . forge . sdk . schemas . files import FileInfo
2024-12-06 17:15:11 -08:00
from skyvern . forge . sdk . schemas . organizations import Organization
2025-10-24 16:34:14 -04:00
from skyvern . forge . sdk . schemas . persistent_browser_sessions import PersistentBrowserSession
2025-03-24 15:15:21 -07:00
from skyvern . forge . sdk . schemas . tasks import Task
2024-12-22 20:54:53 -08:00
from skyvern . forge . sdk . schemas . workflow_runs import WorkflowRunBlock , WorkflowRunTimeline , WorkflowRunTimelineType
2025-07-07 14:43:10 +08:00
from skyvern . forge . sdk . trace import TraceManager
2024-04-16 15:41:44 -07:00
from skyvern . forge . sdk . workflow . exceptions import (
2024-05-16 13:08:24 -07:00
InvalidWorkflowDefinition ,
2024-04-16 15:41:44 -07:00
)
2024-03-25 00:57:37 -07:00
from skyvern . forge . sdk . workflow . models . block import (
BlockTypeVar ,
2025-12-05 22:20:28 -05:00
ConditionalBlock ,
2024-11-22 14:44:22 +08:00
ExtractionBlock ,
NavigationBlock ,
2025-01-28 16:59:54 +08:00
TaskV2Block ,
2025-07-30 08:37:45 -04:00
get_all_blocks ,
2024-03-25 00:57:37 -07:00
)
2024-03-21 17:16:56 -07:00
from skyvern . forge . sdk . workflow . models . parameter import (
AWSSecretParameter ,
2025-09-12 11:01:57 -06:00
AzureVaultCredentialParameter ,
2025-02-03 23:18:41 +08:00
BitwardenCreditCardDataParameter ,
BitwardenLoginCredentialParameter ,
BitwardenSensitiveInformationParameter ,
2024-04-09 00:39:12 -07:00
ContextParameter ,
2025-02-14 00:00:19 +08:00
CredentialParameter ,
2025-06-12 04:20:27 -04:00
OnePasswordCredentialParameter ,
2024-03-21 17:16:56 -07:00
OutputParameter ,
WorkflowParameter ,
WorkflowParameterType ,
)
2024-03-01 10:09:30 -08:00
from skyvern . forge . sdk . workflow . models . workflow import (
Workflow ,
WorkflowDefinition ,
WorkflowRequestBody ,
WorkflowRun ,
2024-03-21 17:16:56 -07:00
WorkflowRunOutputParameter ,
2024-03-01 10:09:30 -08:00
WorkflowRunParameter ,
2025-04-01 15:52:35 -04:00
WorkflowRunResponseBase ,
2024-03-01 10:09:30 -08:00
WorkflowRunStatus ,
)
2026-01-13 15:31:33 -07:00
from skyvern . forge . sdk . workflow . workflow_definition_converter import convert_workflow_definition
2025-11-28 14:24:44 -08:00
from skyvern . schemas . runs import (
ProxyLocationInput ,
RunStatus ,
RunType ,
WorkflowRunRequest ,
WorkflowRunResponse ,
)
2025-11-05 15:26:11 +08:00
from skyvern . schemas . scripts import Script , ScriptBlock , ScriptStatus , WorkflowScript
2025-08-18 16:18:50 -07:00
from skyvern . schemas . workflows import (
2024-11-28 10:26:15 -08:00
BLOCK_YAML_TYPES ,
2025-10-14 16:17:03 -07:00
BlockResult ,
2025-08-18 16:18:50 -07:00
BlockStatus ,
BlockType ,
2024-11-28 10:26:15 -08:00
WorkflowCreateYAMLRequest ,
WorkflowDefinitionYAML ,
2025-08-18 16:18:50 -07:00
WorkflowStatus ,
2024-11-28 10:26:15 -08:00
)
2025-09-19 08:50:21 -07:00
from skyvern . services import script_service , workflow_script_service
2025-12-02 11:08:38 -07:00
from skyvern . webeye . browser_state import BrowserState
2024-03-01 10:09:30 -08:00
LOG = structlog . get_logger ( )
2025-09-03 16:55:15 -04:00
DEFAULT_FIRST_BLOCK_LABEL = " block_1 "
DEFAULT_WORKFLOW_TITLE = " New Workflow "
2025-11-05 15:26:11 +08:00
CacheInvalidationReason = Literal [ " updated_block " , " new_block " , " removed_block " ]
2025-11-21 20:18:31 -08:00
BLOCK_TYPES_THAT_SHOULD_BE_CACHED = {
BlockType . TASK ,
BlockType . TaskV2 ,
BlockType . ACTION ,
BlockType . NAVIGATION ,
BlockType . EXTRACTION ,
BlockType . LOGIN ,
BlockType . FILE_DOWNLOAD ,
}
2025-11-05 15:26:11 +08:00
@dataclass
class CacheInvalidationPlan :
reason : CacheInvalidationReason | None = None
label : str | None = None
previous_index : int | None = None
new_index : int | None = None
block_labels_to_disable : list [ str ] = field ( default_factory = list )
@property
def has_targets ( self ) - > bool :
return bool ( self . block_labels_to_disable )
@dataclass
class CachedScriptBlocks :
workflow_script : WorkflowScript
script : Script
blocks_to_clear : list [ ScriptBlock ]
2024-03-01 10:09:30 -08:00
2025-10-07 16:56:53 -07:00
def _get_workflow_definition_core_data ( workflow_definition : WorkflowDefinition ) - > dict [ str , Any ] :
2025-10-01 13:49:42 -07:00
"""
2025-10-07 16:56:53 -07:00
This function dumps the workflow definition and removes the irrelevant data to the definition , like created_at and modified_at fields inside :
2025-10-01 13:49:42 -07:00
- list of blocks
- list of parameters
And return the dumped workflow definition as a python dictionary .
"""
# Convert the workflow definition to a dictionary
workflow_dict = workflow_definition . model_dump ( )
fields_to_remove = [
" created_at " ,
" modified_at " ,
" deleted_at " ,
" output_parameter_id " ,
" workflow_id " ,
" workflow_parameter_id " ,
2025-10-07 16:56:53 -07:00
" aws_secret_parameter_id " ,
" bitwarden_login_credential_parameter_id " ,
" bitwarden_sensitive_information_parameter_id " ,
" bitwarden_credit_card_data_parameter_id " ,
" credential_parameter_id " ,
" onepassword_credential_parameter_id " ,
" azure_vault_credential_parameter_id " ,
2025-10-15 08:14:07 -07:00
" disable_cache " ,
2025-11-21 17:23:39 -08:00
" next_block_label " ,
" version " ,
2025-10-01 13:49:42 -07:00
]
# Use BFS to recursively remove fields from all nested objects
# Queue to store objects to process
queue = deque ( [ workflow_dict ] )
while queue :
current_obj = queue . popleft ( )
if isinstance ( current_obj , dict ) :
# Remove specified fields from current dictionary
for field in fields_to_remove :
if field : # Skip empty string
current_obj . pop ( field , None )
# Add all nested dictionaries and lists to queue for processing
for value in current_obj . values ( ) :
if isinstance ( value , ( dict , list ) ) :
queue . append ( value )
elif isinstance ( current_obj , list ) :
# Add all items in the list to queue for processing
for item in current_obj :
if isinstance ( item , ( dict , list ) ) :
queue . append ( item )
return workflow_dict
2024-03-01 10:09:30 -08:00
class WorkflowService :
2025-11-05 15:26:11 +08:00
@staticmethod
def _determine_cache_invalidation (
previous_blocks : list [ dict [ str , Any ] ] ,
new_blocks : list [ dict [ str , Any ] ] ,
) - > CacheInvalidationPlan :
""" Return which block index triggered the change and the labels that need cache invalidation. """
plan = CacheInvalidationPlan ( )
prev_labels : list [ str ] = [ ]
for blocks in previous_blocks :
label = blocks . get ( " label " )
if label and isinstance ( label , str ) :
prev_labels . append ( label )
new_labels : list [ str ] = [ ]
for blocks in new_blocks :
label = blocks . get ( " label " )
if label and isinstance ( label , str ) :
new_labels . append ( label )
for idx , ( prev_block , new_block ) in enumerate ( zip ( previous_blocks , new_blocks ) ) :
prev_label = prev_block . get ( " label " )
new_label = new_block . get ( " label " )
if prev_label and prev_label == new_label and prev_block != new_block :
plan . reason = " updated_block "
plan . label = new_label
plan . previous_index = idx
break
if plan . reason is None :
previous_label_set = set ( prev_labels )
for idx , label in enumerate ( new_labels ) :
if label and label not in previous_label_set :
plan . reason = " new_block "
plan . label = label
plan . new_index = idx
plan . previous_index = min ( idx , len ( prev_labels ) )
break
if plan . reason is None :
new_label_set = set ( new_labels )
for idx , label in enumerate ( prev_labels ) :
if label not in new_label_set :
plan . reason = " removed_block "
plan . label = label
plan . previous_index = idx
break
if plan . reason == " removed_block " :
new_label_set = set ( new_labels )
plan . block_labels_to_disable = [ label for label in prev_labels if label and label not in new_label_set ]
elif plan . previous_index is not None :
plan . block_labels_to_disable = prev_labels [ plan . previous_index : ]
return plan
async def _partition_cached_blocks (
self ,
* ,
organization_id : str ,
candidates : Sequence [ WorkflowScript ] ,
block_labels_to_disable : Sequence [ str ] ,
) - > tuple [ list [ CachedScriptBlocks ] , list [ CachedScriptBlocks ] ] :
""" Split cached scripts into published vs draft buckets and collect blocks that should be cleared. """
cached_groups : list [ CachedScriptBlocks ] = [ ]
published_groups : list [ CachedScriptBlocks ] = [ ]
target_labels = set ( block_labels_to_disable )
for candidate in candidates :
script = await app . DATABASE . get_script (
script_id = candidate . script_id ,
organization_id = organization_id ,
)
if not script :
continue
script_blocks = await app . DATABASE . get_script_blocks_by_script_revision_id (
script_revision_id = script . script_revision_id ,
organization_id = organization_id ,
)
blocks_to_clear = [
block for block in script_blocks if block . script_block_label in target_labels and block . run_signature
]
if not blocks_to_clear :
continue
group = CachedScriptBlocks ( workflow_script = candidate , script = script , blocks_to_clear = blocks_to_clear )
if candidate . status == ScriptStatus . published :
published_groups . append ( group )
else :
cached_groups . append ( group )
return cached_groups , published_groups
async def _clear_cached_block_groups (
self ,
* ,
organization_id : str ,
workflow : Workflow ,
previous_workflow : Workflow ,
plan : CacheInvalidationPlan ,
groups : Sequence [ CachedScriptBlocks ] ,
) - > None :
""" Remove cached run signatures for the supplied block groups to force regeneration. """
for group in groups :
for block in group . blocks_to_clear :
await app . DATABASE . update_script_block (
script_block_id = block . script_block_id ,
organization_id = organization_id ,
clear_run_signature = True ,
)
LOG . info (
" Cleared cached script blocks after workflow block change " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_workflow . version ,
new_version = workflow . version ,
invalidate_reason = plan . reason ,
invalidate_label = plan . label ,
invalidate_index_prev = plan . previous_index ,
invalidate_index_new = plan . new_index ,
script_id = group . script . script_id ,
script_revision_id = group . script . script_revision_id ,
cleared_block_labels = [ block . script_block_label for block in group . blocks_to_clear ] ,
cleared_block_count = len ( group . blocks_to_clear ) ,
)
2025-06-12 05:04:26 -04:00
@staticmethod
def _collect_extracted_information ( value : Any ) - > list [ Any ] :
""" Recursively collect extracted_information values from nested outputs. """
results : list [ Any ] = [ ]
if isinstance ( value , dict ) :
if " extracted_information " in value and value [ " extracted_information " ] is not None :
extracted = value [ " extracted_information " ]
if isinstance ( extracted , list ) :
results . extend ( extracted )
else :
results . append ( extracted )
else :
for v in value . values ( ) :
results . extend ( WorkflowService . _collect_extracted_information ( v ) )
elif isinstance ( value , list ) :
for item in value :
results . extend ( WorkflowService . _collect_extracted_information ( item ) )
return results
2025-06-18 00:33:16 -04:00
async def _validate_credential_id ( self , credential_id : str , organization : Organization ) - > None :
credential = await app . DATABASE . get_credential ( credential_id , organization_id = organization . organization_id )
if credential is None :
raise InvalidCredentialId ( credential_id )
2024-03-01 10:09:30 -08:00
async def setup_workflow_run (
self ,
request_id : str | None ,
workflow_request : WorkflowRequestBody ,
2024-05-25 19:32:25 -07:00
workflow_permanent_id : str ,
2025-04-29 03:55:52 +08:00
organization : Organization ,
2025-01-28 15:04:18 +08:00
is_template_workflow : bool = False ,
2024-05-25 19:32:25 -07:00
version : int | None = None ,
2024-03-01 10:09:30 -08:00
max_steps_override : int | None = None ,
2025-01-28 16:59:54 +08:00
parent_workflow_run_id : str | None = None ,
2025-10-01 07:21:08 -04:00
debug_session_id : str | None = None ,
2025-10-02 16:06:54 -07:00
code_gen : bool | None = None ,
2024-03-01 10:09:30 -08:00
) - > WorkflowRun :
"""
Create a workflow run and its parameters . Validate the workflow and the organization . If there are missing
parameters with no default value , mark the workflow run as failed .
: param request_id : The request id for the workflow run .
: param workflow_request : The request body for the workflow run , containing the parameters and the config .
: param workflow_id : The workflow id to run .
: param organization_id : The organization id for the workflow .
: param max_steps_override : The max steps override for the workflow run , if any .
: return : The created workflow run .
"""
# Validate the workflow and the organization
2024-05-25 19:32:25 -07:00
workflow = await self . get_workflow_by_permanent_id (
workflow_permanent_id = workflow_permanent_id ,
2025-04-29 03:55:52 +08:00
organization_id = None if is_template_workflow else organization . organization_id ,
2024-05-25 19:32:25 -07:00
version = version ,
)
2024-03-01 10:09:30 -08:00
if workflow is None :
2024-05-25 19:32:25 -07:00
LOG . error ( f " Workflow { workflow_permanent_id } not found " , workflow_version = version )
raise WorkflowNotFound ( workflow_permanent_id = workflow_permanent_id , version = version )
workflow_id = workflow . workflow_id
2024-05-16 10:51:22 -07:00
if workflow_request . proxy_location is None and workflow . proxy_location is not None :
workflow_request . proxy_location = workflow . proxy_location
if workflow_request . webhook_callback_url is None and workflow . webhook_callback_url is not None :
workflow_request . webhook_callback_url = workflow . webhook_callback_url
2025-11-28 13:14:04 +08:00
2024-03-01 10:09:30 -08:00
# Create the workflow run and set skyvern context
2024-07-09 11:26:44 -07:00
workflow_run = await self . create_workflow_run (
workflow_request = workflow_request ,
workflow_permanent_id = workflow_permanent_id ,
workflow_id = workflow_id ,
2025-04-29 03:55:52 +08:00
organization_id = organization . organization_id ,
2025-01-28 16:59:54 +08:00
parent_workflow_run_id = parent_workflow_run_id ,
2025-09-24 11:50:24 +08:00
sequential_key = workflow . sequential_key ,
2025-10-01 07:21:08 -04:00
debug_session_id = debug_session_id ,
2025-10-02 16:06:54 -07:00
code_gen = code_gen ,
2024-07-09 11:26:44 -07:00
)
2024-03-01 10:09:30 -08:00
LOG . info (
f " Created workflow run { workflow_run . workflow_run_id } for workflow { workflow . workflow_id } " ,
request_id = request_id ,
workflow_run_id = workflow_run . workflow_run_id ,
workflow_id = workflow . workflow_id ,
2025-01-24 18:04:07 +08:00
organization_id = workflow . organization_id ,
2024-03-01 10:09:30 -08:00
proxy_location = workflow_request . proxy_location ,
2024-05-16 10:51:22 -07:00
webhook_callback_url = workflow_request . webhook_callback_url ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolling_times = workflow_request . max_screenshot_scrolls ,
2025-10-01 14:13:56 -07:00
ai_fallback = workflow_request . ai_fallback ,
run_with = workflow_request . run_with ,
2025-10-02 16:06:54 -07:00
code_gen = code_gen ,
2024-03-01 10:09:30 -08:00
)
2025-06-27 00:27:48 +09:00
context : skyvern_context . SkyvernContext | None = skyvern_context . current ( )
current_run_id = context . run_id if context and context . run_id else workflow_run . workflow_run_id
2025-10-02 21:21:02 -07:00
root_workflow_run_id = (
context . root_workflow_run_id if context and context . root_workflow_run_id else workflow_run . workflow_run_id
)
2024-03-01 10:09:30 -08:00
skyvern_context . set (
SkyvernContext (
2025-04-29 03:55:52 +08:00
organization_id = organization . organization_id ,
organization_name = organization . organization_name ,
2024-03-01 10:09:30 -08:00
request_id = request_id ,
workflow_id = workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
2025-10-02 21:21:02 -07:00
root_workflow_run_id = root_workflow_run_id ,
2025-06-27 00:27:48 +09:00
run_id = current_run_id ,
workflow_permanent_id = workflow_run . workflow_permanent_id ,
2024-03-01 10:09:30 -08:00
max_steps_override = max_steps_override ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolls = workflow_request . max_screenshot_scrolls ,
2024-03-01 10:09:30 -08:00
)
)
# Create all the workflow run parameters, AWSSecretParameter won't have workflow run parameters created.
all_workflow_parameters = await self . get_workflow_parameters ( workflow_id = workflow . workflow_id )
2024-10-24 09:03:38 -07:00
try :
2026-01-02 21:23:44 +03:00
missing_parameters : list [ str ] = [ ]
2024-10-24 09:03:38 -07:00
for workflow_parameter in all_workflow_parameters :
if workflow_request . data and workflow_parameter . key in workflow_request . data :
request_body_value = workflow_request . data [ workflow_parameter . key ]
2026-01-15 21:39:59 +03:00
# Fall back to default value if the request explicitly sends null
# This supports API clients (e.g., n8n) that include the key with null value
if request_body_value is None and workflow_parameter . default_value is not None :
request_body_value = workflow_parameter . default_value
2026-01-02 21:23:44 +03:00
if self . _is_missing_required_value ( workflow_parameter , request_body_value ) :
missing_parameters . append ( workflow_parameter . key )
continue
2025-06-18 00:33:16 -04:00
if workflow_parameter . workflow_parameter_type == WorkflowParameterType . CREDENTIAL_ID :
await self . _validate_credential_id ( str ( request_body_value ) , organization )
2025-11-29 02:57:20 +03:00
try :
await self . create_workflow_run_parameter (
workflow_run_id = workflow_run . workflow_run_id ,
workflow_parameter = workflow_parameter ,
value = request_body_value ,
)
except SQLAlchemyError as parameter_error :
raise WorkflowRunParameterPersistenceError (
parameter_key = workflow_parameter . key ,
workflow_id = workflow . workflow_permanent_id ,
workflow_run_id = workflow_run . workflow_run_id ,
reason = self . _format_parameter_persistence_error ( parameter_error ) ,
) from parameter_error
2024-10-24 09:03:38 -07:00
elif workflow_parameter . default_value is not None :
2025-06-18 00:33:16 -04:00
if workflow_parameter . workflow_parameter_type == WorkflowParameterType . CREDENTIAL_ID :
await self . _validate_credential_id ( str ( workflow_parameter . default_value ) , organization )
2025-11-29 02:57:20 +03:00
try :
await self . create_workflow_run_parameter (
workflow_run_id = workflow_run . workflow_run_id ,
workflow_parameter = workflow_parameter ,
value = workflow_parameter . default_value ,
)
except SQLAlchemyError as parameter_error :
raise WorkflowRunParameterPersistenceError (
parameter_key = workflow_parameter . key ,
workflow_id = workflow . workflow_permanent_id ,
workflow_run_id = workflow_run . workflow_run_id ,
reason = self . _format_parameter_persistence_error ( parameter_error ) ,
) from parameter_error
2024-10-24 09:03:38 -07:00
else :
2026-01-02 21:23:44 +03:00
missing_parameters . append ( workflow_parameter . key )
if missing_parameters :
missing_list = " , " . join ( sorted ( missing_parameters ) )
raise MissingValueForParameter (
parameter_key = missing_list ,
workflow_id = workflow . workflow_permanent_id ,
workflow_run_id = workflow_run . workflow_run_id ,
)
2024-10-24 09:03:38 -07:00
except Exception as e :
LOG . exception (
f " Error while setting up workflow run { workflow_run . workflow_run_id } " ,
workflow_run_id = workflow_run . workflow_run_id ,
)
2024-11-15 11:07:44 +08:00
2024-12-16 12:22:52 -08:00
failure_reason = f " Setup workflow failed due to an unexpected exception: { str ( e ) } "
2024-11-15 11:07:44 +08:00
if isinstance ( e , SkyvernException ) :
2024-12-16 12:22:52 -08:00
failure_reason = f " Setup workflow failed due to an SkyvernException( { e . __class__ . __name__ } ): { str ( e ) } "
2024-11-15 11:07:44 +08:00
2025-05-26 08:49:42 -07:00
workflow_run = await self . mark_workflow_run_as_failed (
2024-11-15 11:07:44 +08:00
workflow_run_id = workflow_run . workflow_run_id , failure_reason = failure_reason
)
2024-10-24 09:03:38 -07:00
raise e
2024-03-01 10:09:30 -08:00
return workflow_run
2025-11-29 02:57:20 +03:00
@staticmethod
def _format_parameter_persistence_error ( error : SQLAlchemyError ) - > str :
if isinstance ( error , IntegrityError ) :
2025-12-10 21:46:35 -08:00
return " value cannot be null "
2025-11-29 02:57:20 +03:00
return " database error while saving parameter value "
2026-01-02 21:23:44 +03:00
@staticmethod
def _is_missing_required_value ( workflow_parameter : WorkflowParameter , value : Any ) - > bool :
"""
Determine if a provided value should be treated as missing for a required parameter .
Rules :
- None / null is always missing .
- String parameters may be empty strings ( per UI behavior ) .
- JSON parameters treat empty / whitespace - only strings as missing .
- Boolean / integer / float parameters treat empty strings as missing .
- File URL treats empty strings , empty dicts , or dicts with empty s3uri as missing .
- Credential ID treats empty / whitespace - only strings as missing .
"""
if value is None :
return True
param_type = workflow_parameter . workflow_parameter_type
if param_type == WorkflowParameterType . STRING :
return False
if param_type == WorkflowParameterType . JSON :
return isinstance ( value , str ) and value . strip ( ) == " "
if param_type in (
WorkflowParameterType . BOOLEAN ,
WorkflowParameterType . INTEGER ,
WorkflowParameterType . FLOAT ,
) :
return isinstance ( value , str ) and value . strip ( ) == " "
if param_type == WorkflowParameterType . FILE_URL :
if isinstance ( value , str ) :
return value . strip ( ) == " "
if isinstance ( value , dict ) :
if not value :
return True
if " s3uri " in value :
return not bool ( value . get ( " s3uri " ) )
return False
if param_type == WorkflowParameterType . CREDENTIAL_ID :
return isinstance ( value , str ) and value . strip ( ) == " "
return False
2025-10-24 16:34:14 -04:00
async def auto_create_browser_session_if_needed (
self ,
organization_id : str ,
workflow : Workflow ,
* ,
browser_session_id : str | None = None ,
2025-11-28 14:24:44 -08:00
proxy_location : ProxyLocationInput = None ,
2025-10-24 16:34:14 -04:00
) - > PersistentBrowserSession | None :
if browser_session_id : # the user has supplied an id, so no need to create one
return None
workflow_definition = workflow . workflow_definition
blocks = workflow_definition . blocks
human_interaction_blocks = [ block for block in blocks if block . block_type == BlockType . HUMAN_INTERACTION ]
if human_interaction_blocks :
timeouts = [ getattr ( block , " timeout_seconds " , 60 * 60 ) for block in human_interaction_blocks ]
timeout_seconds = sum ( timeouts ) + 60 * 60
browser_session = await app . PERSISTENT_SESSIONS_MANAGER . create_session (
organization_id = organization_id ,
timeout_minutes = timeout_seconds / / 60 ,
proxy_location = proxy_location ,
)
return browser_session
return None
2025-07-07 14:43:10 +08:00
@TraceManager.traced_async ( ignore_inputs = [ " organization " , " api_key " ] )
2024-03-01 10:09:30 -08:00
async def execute_workflow (
self ,
workflow_run_id : str ,
api_key : str ,
2024-10-02 15:16:08 -07:00
organization : Organization ,
2025-07-04 15:34:15 -04:00
block_labels : list [ str ] | None = None ,
2025-08-28 20:05:24 -04:00
block_outputs : dict [ str , Any ] | None = None ,
2025-01-09 22:04:53 +01:00
browser_session_id : str | None = None ,
2024-03-01 10:09:30 -08:00
) - > WorkflowRun :
""" Execute a workflow. """
2024-10-02 15:16:08 -07:00
organization_id = organization . organization_id
2025-10-08 14:58:50 -07:00
2025-01-09 22:04:53 +01:00
LOG . info (
" Executing workflow " ,
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
browser_session_id = browser_session_id ,
2025-08-18 13:25:54 -07:00
block_labels = block_labels ,
2025-08-28 20:05:24 -04:00
block_outputs = block_outputs ,
2025-01-09 22:04:53 +01:00
)
2024-12-22 17:49:33 -08:00
workflow_run = await self . get_workflow_run ( workflow_run_id = workflow_run_id , organization_id = organization_id )
2025-01-28 15:04:18 +08:00
workflow = await self . get_workflow_by_permanent_id ( workflow_permanent_id = workflow_run . workflow_permanent_id )
2025-11-06 01:24:39 -08:00
browser_profile_id = workflow_run . browser_profile_id
2025-08-21 11:16:22 +08:00
close_browser_on_completion = browser_session_id is None and not workflow_run . browser_address
2025-08-19 13:32:39 -07:00
2024-03-12 22:28:16 -07:00
# Set workflow run status to running, create workflow run parameters
2025-09-03 17:40:13 +08:00
workflow_run = await self . mark_workflow_run_as_running ( workflow_run_id = workflow_run_id )
2024-03-01 10:09:30 -08:00
2024-04-16 15:41:44 -07:00
# Get all context parameters from the workflow definition
context_parameters = [
parameter
for parameter in workflow . workflow_definition . parameters
if isinstance ( parameter , ContextParameter )
]
2025-02-03 23:18:41 +08:00
secret_parameters = [
parameter
for parameter in workflow . workflow_definition . parameters
if isinstance (
parameter ,
(
AWSSecretParameter ,
BitwardenLoginCredentialParameter ,
BitwardenCreditCardDataParameter ,
BitwardenSensitiveInformationParameter ,
2025-06-12 04:20:27 -04:00
OnePasswordCredentialParameter ,
2025-09-12 11:01:57 -06:00
AzureVaultCredentialParameter ,
2025-02-14 00:00:19 +08:00
CredentialParameter ,
2025-02-03 23:18:41 +08:00
) ,
)
]
2024-03-01 10:09:30 -08:00
# Get all <workflow parameter, workflow run parameter> tuples
2025-09-03 17:40:13 +08:00
wp_wps_tuples = await self . get_workflow_run_parameter_tuples ( workflow_run_id = workflow_run_id )
2024-03-21 17:16:56 -07:00
workflow_output_parameters = await self . get_workflow_output_parameters ( workflow_id = workflow . workflow_id )
2025-02-03 23:18:41 +08:00
try :
await app . WORKFLOW_CONTEXT_MANAGER . initialize_workflow_run_context (
organization ,
workflow_run_id ,
2025-09-29 19:30:21 -07:00
workflow . title ,
workflow . workflow_id ,
workflow . workflow_permanent_id ,
2025-02-03 23:18:41 +08:00
wp_wps_tuples ,
workflow_output_parameters ,
context_parameters ,
secret_parameters ,
2025-08-28 20:05:24 -04:00
block_outputs ,
2025-12-19 02:54:09 +08:00
workflow ,
2025-02-03 23:18:41 +08:00
)
except Exception as e :
LOG . exception (
2025-09-03 17:40:13 +08:00
f " Error while initializing workflow run context for workflow run { workflow_run_id } " ,
workflow_run_id = workflow_run_id ,
2025-02-03 23:18:41 +08:00
)
exception_message = f " Unexpected error: { str ( e ) } "
if isinstance ( e , SkyvernException ) :
exception_message = f " unexpected SkyvernException( { e . __class__ . __name__ } ): { str ( e ) } "
failure_reason = f " Failed to initialize workflow run context. failure reason: { exception_message } "
2025-05-26 08:49:42 -07:00
workflow_run = await self . mark_workflow_run_as_failed (
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id , failure_reason = failure_reason
2025-02-03 23:18:41 +08:00
)
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
browser_session_id = browser_session_id ,
2025-08-21 11:16:22 +08:00
close_browser_on_completion = close_browser_on_completion ,
2025-02-03 23:18:41 +08:00
)
return workflow_run
2025-11-06 01:24:39 -08:00
browser_session = None
if not browser_profile_id :
browser_session = await self . auto_create_browser_session_if_needed (
organization . organization_id ,
workflow ,
browser_session_id = browser_session_id ,
proxy_location = workflow_run . proxy_location ,
)
2025-10-24 16:34:14 -04:00
if browser_session :
browser_session_id = browser_session . persistent_browser_session_id
close_browser_on_completion = True
2025-10-28 10:18:12 -04:00
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run . workflow_run_id ,
browser_session_id = browser_session_id ,
)
2025-10-24 16:34:14 -04:00
2025-12-04 00:25:35 +08:00
if browser_session_id :
try :
await app . PERSISTENT_SESSIONS_MANAGER . begin_session (
browser_session_id = browser_session_id ,
runnable_type = " workflow_run " ,
runnable_id = workflow_run_id ,
organization_id = organization . organization_id ,
)
except Exception as e :
LOG . exception (
" Failed to begin browser session for workflow run " ,
browser_session_id = browser_session_id ,
workflow_run_id = workflow_run_id ,
)
failure_reason = f " Failed to begin browser session for workflow run: { str ( e ) } "
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run_id ,
failure_reason = failure_reason ,
)
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
browser_session_id = browser_session_id ,
close_browser_on_completion = close_browser_on_completion ,
)
return workflow_run
2025-08-22 11:24:09 -07:00
# Check if there's a related workflow script that should be used instead
2025-09-19 08:50:21 -07:00
workflow_script , _ = await workflow_script_service . get_workflow_script ( workflow , workflow_run , block_labels )
2025-10-01 15:05:53 -07:00
current_context = skyvern_context . current ( )
2025-10-02 16:06:54 -07:00
if current_context :
if workflow_script :
current_context . generate_script = False
if workflow_run . code_gen :
current_context . generate_script = True
2025-11-05 19:57:11 +08:00
workflow_run , blocks_to_update = await self . _execute_workflow_blocks (
2025-10-14 16:17:03 -07:00
workflow = workflow ,
workflow_run = workflow_run ,
organization = organization ,
browser_session_id = browser_session_id ,
2025-11-06 01:24:39 -08:00
browser_profile_id = browser_profile_id ,
2025-10-14 16:17:03 -07:00
block_labels = block_labels ,
block_outputs = block_outputs ,
2025-12-16 10:08:19 +08:00
script = workflow_script ,
2025-10-14 16:17:03 -07:00
)
2025-08-22 11:24:09 -07:00
2026-01-13 16:56:06 -08:00
# Check if there's a finally block configured
finally_block_label = workflow . workflow_definition . finally_block_label
2025-09-03 17:40:13 +08:00
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
2026-01-13 16:56:06 -08:00
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 (
2025-09-03 17:40:13 +08:00
WorkflowRunStatus . failed ,
WorkflowRunStatus . terminated ,
WorkflowRunStatus . timed_out ,
) :
2026-01-13 16:56:06 -08:00
workflow_run = await self . _update_workflow_run_status (
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id ,
2026-01-13 16:56:06 -08:00
status = WorkflowRunStatus . running ,
failure_reason = None ,
2025-09-03 17:40:13 +08:00
)
2026-01-13 16:56:06 -08:00
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 ,
)
2025-09-03 17:40:13 +08:00
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
browser_session_id = browser_session_id ,
close_browser_on_completion = close_browser_on_completion ,
)
return workflow_run
async def _execute_workflow_blocks (
self ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
organization : Organization ,
browser_session_id : str | None = None ,
2025-11-06 01:24:39 -08:00
browser_profile_id : str | None = None ,
2025-09-03 17:40:13 +08:00
block_labels : list [ str ] | None = None ,
block_outputs : dict [ str , Any ] | None = None ,
2025-12-16 10:08:19 +08:00
script : Script | None = None ,
2025-11-05 19:57:11 +08:00
) - > tuple [ WorkflowRun , set [ str ] ] :
2025-09-03 17:40:13 +08:00
organization_id = organization . organization_id
workflow_run_id = workflow_run . workflow_run_id
2025-07-30 08:37:45 -04:00
top_level_blocks = workflow . workflow_definition . blocks
all_blocks = get_all_blocks ( top_level_blocks )
2025-10-14 16:17:03 -07:00
2025-12-16 10:08:19 +08:00
# Load script blocks if script is provided
2025-10-14 16:17:03 -07:00
script_blocks_by_label : dict [ str , Any ] = { }
loaded_script_module = None
2025-11-05 19:57:11 +08:00
blocks_to_update : set [ str ] = set ( )
2025-10-14 16:17:03 -07:00
2025-11-25 10:28:24 -08:00
is_script_run = self . should_run_script ( workflow , workflow_run )
2025-12-16 10:08:19 +08:00
if script :
2025-10-14 16:17:03 -07:00
LOG . info (
" Loading script blocks for workflow execution " ,
workflow_run_id = workflow_run_id ,
2025-12-16 10:08:19 +08:00
script_id = script . script_id ,
script_revision_id = script . script_revision_id ,
2025-10-14 16:17:03 -07:00
)
2025-12-16 10:08:19 +08:00
context = skyvern_context . ensure_context ( )
context . script_id = script . script_id
context . script_revision_id = script . script_revision_id
2025-10-14 16:17:03 -07:00
try :
2025-12-16 10:08:19 +08:00
script_blocks = await app . DATABASE . get_script_blocks_by_script_revision_id (
script_revision_id = script . script_revision_id ,
2025-10-14 16:17:03 -07:00
organization_id = organization_id ,
)
2025-12-16 10:08:19 +08:00
# Create mapping from block label to script block
for script_block in script_blocks :
if script_block . run_signature :
script_blocks_by_label [ script_block . script_block_label ] = script_block
if is_script_run :
# load the script files
script_files = await app . DATABASE . get_script_files (
2025-10-14 16:17:03 -07:00
script_revision_id = script . script_revision_id ,
organization_id = organization_id ,
)
2025-12-16 10:08:19 +08:00
await script_service . load_scripts ( script , script_files )
2025-10-14 16:17:03 -07:00
2025-12-16 10:08:19 +08:00
script_path = os . path . join ( settings . TEMP_PATH , script . script_id , " main.py " )
if os . path . exists ( script_path ) :
# setup script run
parameter_tuples = await app . DATABASE . get_workflow_run_parameters (
workflow_run_id = workflow_run . workflow_run_id
2025-10-14 16:17:03 -07:00
)
2025-12-16 10:08:19 +08:00
script_parameters = { wf_param . key : run_param . value for wf_param , run_param in parameter_tuples }
spec = importlib . util . spec_from_file_location ( " user_script " , script_path )
if spec and spec . loader :
loaded_script_module = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( loaded_script_module )
await skyvern . setup (
script_parameters ,
generated_parameter_cls = loaded_script_module . GeneratedWorkflowParameters ,
2025-10-14 16:17:03 -07:00
)
2025-12-16 10:08:19 +08:00
LOG . info (
" Successfully loaded script module " ,
script_id = script . script_id ,
block_count = len ( script_blocks_by_label ) ,
2025-10-14 16:17:03 -07:00
)
2025-12-16 10:08:19 +08:00
else :
LOG . warning (
" Script file not found at path " ,
script_path = script_path ,
script_id = script . script_id ,
)
2025-10-14 16:17:03 -07:00
except Exception as e :
LOG . warning (
" Failed to load script blocks, will fallback to normal execution " ,
error = str ( e ) ,
exc_info = True ,
workflow_run_id = workflow_run_id ,
2025-12-16 10:08:19 +08:00
script_id = script . script_id ,
2025-10-14 16:17:03 -07:00
)
script_blocks_by_label = { }
loaded_script_module = None
# Mark workflow as running with appropriate engine
2025-12-16 10:08:19 +08:00
run_with = " code " if script and is_script_run and script_blocks_by_label else " agent "
2025-10-14 16:17:03 -07:00
await self . mark_workflow_run_as_running ( workflow_run_id = workflow_run_id , run_with = run_with )
2025-07-04 15:34:15 -04:00
if block_labels and len ( block_labels ) :
blocks : list [ BlockTypeVar ] = [ ]
all_labels = { block . label : block for block in all_blocks }
for label in block_labels :
if label not in all_labels :
raise BlockNotFound ( block_label = label )
blocks . append ( all_labels [ label ] )
LOG . info (
" Executing workflow blocks via whitelist " ,
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id ,
2025-07-04 15:34:15 -04:00
block_cnt = len ( blocks ) ,
block_labels = block_labels ,
2025-08-28 20:05:24 -04:00
block_outputs = block_outputs ,
2025-07-04 15:34:15 -04:00
)
else :
2025-07-30 08:37:45 -04:00
blocks = top_level_blocks
2025-07-04 15:34:15 -04:00
if not blocks :
raise SkyvernException ( f " No blocks found for the given block labels: { block_labels } " )
2025-12-05 22:20:28 -05:00
workflow_version = workflow . workflow_definition . version or 1
if workflow_version > = 2 and not block_labels :
return await self . _execute_workflow_blocks_dag (
workflow = workflow ,
workflow_run = workflow_run ,
organization = organization ,
browser_session_id = browser_session_id ,
script_blocks_by_label = script_blocks_by_label ,
loaded_script_module = loaded_script_module ,
is_script_run = is_script_run ,
blocks_to_update = blocks_to_update ,
)
2025-10-24 16:34:14 -04:00
#
2024-03-01 10:09:30 -08:00
# Execute workflow blocks
2024-11-01 15:13:41 -07:00
blocks_cnt = len ( blocks )
2024-04-04 19:09:19 -07:00
block_result = None
for block_idx , block in enumerate ( blocks ) :
2025-12-05 22:20:28 -05:00
(
workflow_run ,
blocks_to_update ,
block_result ,
should_stop ,
_ ,
) = await self . _execute_single_block (
block = block ,
block_idx = block_idx ,
blocks_cnt = blocks_cnt ,
workflow_run = workflow_run ,
organization = organization ,
workflow_run_id = workflow_run_id ,
browser_session_id = browser_session_id ,
script_blocks_by_label = script_blocks_by_label ,
loaded_script_module = loaded_script_module ,
is_script_run = is_script_run ,
blocks_to_update = blocks_to_update ,
)
2025-01-22 13:23:10 +08:00
2025-12-05 22:20:28 -05:00
if should_stop :
break
return workflow_run , blocks_to_update
async def _execute_workflow_blocks_dag (
self ,
* ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
organization : Organization ,
browser_session_id : str | None ,
script_blocks_by_label : dict [ str , Any ] ,
loaded_script_module : Any ,
is_script_run : bool ,
blocks_to_update : set [ str ] ,
) - > tuple [ WorkflowRun , set [ str ] ] :
try :
start_label , label_to_block , default_next_map = self . _build_workflow_graph (
workflow . workflow_definition . blocks
)
except InvalidWorkflowDefinition as exc :
LOG . error ( " Workflow graph validation failed " , error = str ( exc ) , workflow_id = workflow . workflow_id )
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id ,
failure_reason = str ( exc ) ,
)
return workflow_run , blocks_to_update
visited_labels : set [ str ] = set ( )
current_label = start_label
block_idx = 0
total_blocks = len ( label_to_block )
2025-01-22 13:23:10 +08:00
2025-12-05 22:20:28 -05:00
while current_label :
block = label_to_block . get ( current_label )
if not block :
2025-12-07 12:37:00 -08:00
LOG . error (
" Unable to find block with label in workflow graph " ,
workflow_run_id = workflow_run . workflow_run_id ,
current_label = current_label ,
)
2025-12-05 22:20:28 -05:00
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id ,
failure_reason = f " Unable to find block with label { current_label } " ,
2024-03-12 22:28:16 -07:00
)
2025-12-05 22:20:28 -05:00
break
(
workflow_run ,
blocks_to_update ,
block_result ,
should_stop ,
branch_metadata ,
) = await self . _execute_single_block (
block = block ,
block_idx = block_idx ,
blocks_cnt = total_blocks ,
workflow_run = workflow_run ,
organization = organization ,
workflow_run_id = workflow_run . workflow_run_id ,
browser_session_id = browser_session_id ,
script_blocks_by_label = script_blocks_by_label ,
loaded_script_module = loaded_script_module ,
is_script_run = is_script_run ,
blocks_to_update = blocks_to_update ,
)
visited_labels . add ( current_label )
if should_stop :
break
next_label = None
2025-12-07 12:37:00 -08:00
if block . block_type == BlockType . CONDITIONAL :
2025-12-05 22:20:28 -05:00
next_label = ( branch_metadata or { } ) . get ( " next_block_label " )
else :
next_label = default_next_map . get ( block . label )
if not next_label :
2024-03-12 22:28:16 -07:00
LOG . info (
2025-12-05 22:20:28 -05:00
" DAG traversal reached terminal node " ,
workflow_run_id = workflow_run . workflow_run_id ,
2024-11-14 01:32:53 -08:00
block_label = block . label ,
2024-03-12 22:28:16 -07:00
)
2025-12-05 22:20:28 -05:00
break
2025-10-14 16:17:03 -07:00
2025-12-05 22:20:28 -05:00
if next_label not in label_to_block :
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id ,
failure_reason = f " Next block label { next_label } not found in workflow definition " ,
2025-11-25 10:28:24 -08:00
)
2025-12-05 22:20:28 -05:00
break
2025-10-14 16:17:03 -07:00
2025-12-05 22:20:28 -05:00
if next_label in visited_labels :
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id ,
failure_reason = f " Cycle detected while traversing workflow definition at block { next_label } " ,
)
break
2025-10-14 16:17:03 -07:00
2025-12-05 22:20:28 -05:00
block_idx + = 1
current_label = next_label
2025-10-14 16:17:03 -07:00
2025-12-05 22:20:28 -05:00
return workflow_run , blocks_to_update
2025-10-14 16:17:03 -07:00
2025-12-05 22:20:28 -05:00
async def _execute_single_block (
self ,
* ,
block : BlockTypeVar ,
block_idx : int ,
blocks_cnt : int ,
workflow_run : WorkflowRun ,
organization : Organization ,
workflow_run_id : str ,
browser_session_id : str | None ,
script_blocks_by_label : dict [ str , Any ] ,
loaded_script_module : Any ,
is_script_run : bool ,
blocks_to_update : set [ str ] ,
) - > tuple [ WorkflowRun , set [ str ] , BlockResult | None , bool , dict [ str , Any ] | None ] :
organization_id = organization . organization_id
workflow_run_block_result : BlockResult | None = None
branch_metadata : dict [ str , Any ] | None = None
block_executed_with_code = False
try :
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 == WorkflowRunStatus . canceled :
2024-10-08 23:09:41 -07:00
LOG . info (
2025-12-05 22:20:28 -05:00
" Workflow run is canceled, stopping execution inside workflow execution loop " ,
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id ,
2024-10-08 23:09:41 -07:00
block_idx = block_idx ,
2024-12-02 15:45:16 +08:00
block_type = block . block_type ,
block_label = block . label ,
)
2025-12-05 22:20:28 -05:00
return workflow_run , blocks_to_update , workflow_run_block_result , True , branch_metadata
2024-12-02 15:45:16 +08:00
2025-12-05 22:20:28 -05:00
if workflow_run . status == WorkflowRunStatus . timed_out :
2024-10-08 23:09:41 -07:00
LOG . info (
2025-12-05 22:20:28 -05:00
" Workflow run is timed out, stopping execution inside workflow execution loop " ,
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id ,
2024-10-08 23:09:41 -07:00
block_idx = block_idx ,
2025-12-05 22:20:28 -05:00
block_type = block . block_type ,
2024-11-14 01:32:53 -08:00
block_label = block . label ,
2024-10-08 23:09:41 -07:00
)
2025-12-05 22:20:28 -05:00
return workflow_run , blocks_to_update , workflow_run_block_result , True , branch_metadata
2024-12-02 15:45:16 +08:00
2025-12-05 22:20:28 -05:00
parameters = block . get_all_parameters ( workflow_run_id )
await app . WORKFLOW_CONTEXT_MANAGER . register_block_parameters_for_workflow_run (
workflow_run_id , parameters , organization
)
LOG . info (
f " Executing root block { block . block_type } at index { block_idx } / { blocks_cnt - 1 } for workflow run { workflow_run_id } " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_type_var = block . block_type ,
block_label = block . label ,
model = block . model ,
)
2024-12-02 15:45:16 +08:00
2025-12-05 22:20:28 -05:00
valid_to_run_code = (
is_script_run and block . label and block . label in script_blocks_by_label and not block . disable_cache
)
if valid_to_run_code :
script_block = script_blocks_by_label [ block . label ]
LOG . info (
" Attempting to execute block with script code " ,
block_label = block . label ,
run_signature = script_block . run_signature ,
)
try :
vars_dict = vars ( loaded_script_module ) if loaded_script_module else { }
exec_globals = {
* * vars_dict ,
" skyvern " : skyvern ,
" __builtins__ " : __builtins__ ,
}
2024-12-02 15:45:16 +08:00
2025-12-05 22:20:28 -05:00
assert script_block . run_signature is not None
normalized_signature = textwrap . dedent ( script_block . run_signature ) . strip ( )
indented_signature = textwrap . indent ( normalized_signature , " " )
wrapper_code = f " async def __run_signature_wrapper(): \n return ( \n { indented_signature } \n ) \n "
LOG . debug ( " Executing run_signature wrapper " , wrapper_code = wrapper_code )
2025-01-22 13:23:10 +08:00
2025-12-05 22:20:28 -05:00
try :
exec_code = compile ( wrapper_code , " <run_signature> " , " exec " )
exec ( exec_code , exec_globals )
output_value = await exec_globals [ " __run_signature_wrapper " ] ( )
except ScriptTerminationException as e :
LOG . warning (
" Script termination " ,
block_label = block . label ,
error = str ( e ) ,
exc_info = True ,
2025-01-22 13:23:10 +08:00
)
2025-12-05 22:20:28 -05:00
workflow_run_blocks = await app . DATABASE . get_workflow_run_blocks (
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id ,
2025-12-05 22:20:28 -05:00
organization_id = organization_id ,
)
matching_blocks = [ b for b in workflow_run_blocks if b . label == block . label ]
if matching_blocks :
latest_block = max ( matching_blocks , key = lambda b : b . created_at )
workflow_run_block_result = BlockResult (
success = latest_block . status == BlockStatus . completed ,
failure_reason = latest_block . failure_reason ,
output_parameter = block . output_parameter ,
output_parameter_value = latest_block . output ,
status = BlockStatus ( latest_block . status ) if latest_block . status else BlockStatus . failed ,
workflow_run_block_id = latest_block . workflow_run_block_id ,
)
block_executed_with_code = True
LOG . info (
" Successfully executed block with script code " ,
block_label = block . label ,
block_status = workflow_run_block_result . status ,
has_output = output_value is not None ,
)
else :
LOG . warning (
" Block executed with code but no workflow run block found " ,
block_label = block . label ,
)
block_executed_with_code = False
except Exception as e :
LOG . warning (
" Failed to execute block with script code, falling back to AI " ,
2025-01-22 13:23:10 +08:00
block_label = block . label ,
2025-12-05 22:20:28 -05:00
error = str ( e ) ,
exc_info = True ,
2025-01-22 13:23:10 +08:00
)
2025-12-05 22:20:28 -05:00
block_executed_with_code = False
2025-01-22 13:23:10 +08:00
2025-12-05 22:20:28 -05:00
if not block_executed_with_code :
LOG . info (
" Executing block " ,
2024-11-14 01:32:53 -08:00
block_label = block . label ,
2025-12-05 22:20:28 -05:00
block_type = block . block_type ,
)
workflow_run_block_result = await block . execute_safe (
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
browser_session_id = browser_session_id ,
2024-03-21 17:16:56 -07:00
)
2024-11-15 11:07:44 +08:00
2025-12-05 22:20:28 -05:00
# Extract branch metadata for conditional blocks
if isinstance ( block , ConditionalBlock ) and workflow_run_block_result :
branch_metadata = cast ( dict [ str , Any ] | None , workflow_run_block_result . output_parameter_value )
2024-11-15 11:07:44 +08:00
2025-12-05 22:20:28 -05:00
if not workflow_run_block_result :
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run_id , failure_reason = " Block result is None "
)
return workflow_run , blocks_to_update , workflow_run_block_result , True , branch_metadata
if (
not block_executed_with_code
and block . label
and block . label not in script_blocks_by_label
and workflow_run_block_result . status == BlockStatus . completed
and block . block_type in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
) :
blocks_to_update . add ( block . label )
workflow_run , should_stop = await self . _handle_block_result_status (
block = block ,
block_idx = block_idx ,
blocks_cnt = blocks_cnt ,
block_result = workflow_run_block_result ,
workflow_run = workflow_run ,
workflow_run_id = workflow_run_id ,
)
return workflow_run , blocks_to_update , workflow_run_block_result , should_stop , branch_metadata
except Exception as e :
LOG . exception (
f " Error while executing workflow run { workflow_run_id } " ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_type = block . block_type ,
block_label = block . label ,
)
exception_message = f " Unexpected error: { str ( e ) } "
if isinstance ( e , SkyvernException ) :
exception_message = f " unexpected SkyvernException( { e . __class__ . __name__ } ): { str ( e ) } "
failure_reason = f " { block . block_type } block failed. failure reason: { exception_message } "
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run_id , failure_reason = failure_reason
)
return workflow_run , blocks_to_update , workflow_run_block_result , True , branch_metadata
async def _handle_block_result_status (
self ,
* ,
block : BlockTypeVar ,
block_idx : int ,
blocks_cnt : int ,
block_result : BlockResult ,
workflow_run : WorkflowRun ,
workflow_run_id : str ,
) - > tuple [ WorkflowRun , bool ] :
if block_result . status == BlockStatus . canceled :
LOG . info (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } was canceled for workflow run { workflow_run_id } , cancelling workflow run " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
block_type_var = block . block_type ,
block_label = block . label ,
)
workflow_run = await self . mark_workflow_run_as_canceled ( workflow_run_id = workflow_run_id )
return workflow_run , True
if block_result . status == BlockStatus . failed :
LOG . error (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } failed for workflow run { workflow_run_id } " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
block_type_var = block . block_type ,
block_label = block . label ,
)
if not block . continue_on_failure :
failure_reason = f " { block . block_type } block failed. failure reason: { block_result . failure_reason } "
2025-05-26 08:49:42 -07:00
workflow_run = await self . mark_workflow_run_as_failed (
2025-09-03 17:40:13 +08:00
workflow_run_id = workflow_run_id , failure_reason = failure_reason
2025-01-09 22:04:53 +01:00
)
2025-12-05 22:20:28 -05:00
return workflow_run , True
LOG . warning (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } failed but will continue executing the workflow run { workflow_run_id } " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
continue_on_failure = block . continue_on_failure ,
block_type_var = block . block_type ,
block_label = block . label ,
)
return workflow_run , False
if block_result . status == BlockStatus . terminated :
LOG . info (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } was terminated for workflow run { workflow_run_id } , marking workflow run as terminated " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
block_type_var = block . block_type ,
block_label = block . label ,
)
if not block . continue_on_failure :
failure_reason = f " { block . block_type } block terminated. Reason: { block_result . failure_reason } "
workflow_run = await self . mark_workflow_run_as_terminated (
workflow_run_id = workflow_run_id , failure_reason = failure_reason
)
return workflow_run , True
LOG . warning (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } was terminated for workflow run { workflow_run_id } , but will continue executing the workflow run " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
continue_on_failure = block . continue_on_failure ,
block_type_var = block . block_type ,
block_label = block . label ,
)
return workflow_run , False
if block_result . status == BlockStatus . timed_out :
LOG . info (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } timed out for workflow run { workflow_run_id } , marking workflow run as failed " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
block_type_var = block . block_type ,
block_label = block . label ,
)
if not block . continue_on_failure :
failure_reason = f " { block . block_type } block timed out. Reason: { block_result . failure_reason } "
workflow_run = await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run_id , failure_reason = failure_reason
)
return workflow_run , True
LOG . warning (
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } timed out for workflow run { workflow_run_id } , but will continue executing the workflow run " ,
block_type = block . block_type ,
workflow_run_id = workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
continue_on_failure = block . continue_on_failure ,
block_type_var = block . block_type ,
block_label = block . label ,
)
return workflow_run , False
return workflow_run , False
2026-01-13 16:56:06 -08:00
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 ) ,
)
2025-12-05 22:20:28 -05:00
def _build_workflow_graph (
self ,
blocks : list [ BlockTypeVar ] ,
) - > tuple [ str , dict [ str , BlockTypeVar ] , dict [ str , str | None ] ] :
2025-12-08 11:35:54 -08:00
all_blocks = blocks
2025-12-05 22:20:28 -05:00
label_to_block : dict [ str , BlockTypeVar ] = { }
default_next_map : dict [ str , str | None ] = { }
for block in all_blocks :
if block . label in label_to_block :
raise InvalidWorkflowDefinition ( f " Duplicate block label detected: { block . label } " )
label_to_block [ block . label ] = block
default_next_map [ block . label ] = block . next_block_label
# Only apply sequential defaulting if there are no conditional blocks
# Conditional blocks break sequential ordering since they have multiple branches
has_conditional_blocks = any ( isinstance ( block , ConditionalBlock ) for block in all_blocks )
if not has_conditional_blocks :
for idx , block in enumerate ( blocks [ : - 1 ] ) :
if default_next_map . get ( block . label ) is None :
default_next_map [ block . label ] = blocks [ idx + 1 ] . label
adjacency : dict [ str , set [ str ] ] = { label : set ( ) for label in label_to_block }
incoming : dict [ str , int ] = { label : 0 for label in label_to_block }
def _add_edge ( source : str , target : str | None ) - > None :
if not target :
return
if target not in label_to_block :
raise InvalidWorkflowDefinition ( f " Block { source } references unknown next_block_label { target } " )
2025-12-16 01:29:57 +08:00
# Only increment incoming count if this is a new edge
# (multiple branches of a conditional block may point to the same target)
if target not in adjacency [ source ] :
adjacency [ source ] . add ( target )
incoming [ target ] + = 1
2025-12-05 22:20:28 -05:00
for label , block in label_to_block . items ( ) :
if isinstance ( block , ConditionalBlock ) :
for branch in block . ordered_branches :
_add_edge ( label , branch . next_block_label )
else :
_add_edge ( label , default_next_map . get ( label ) )
roots = [ label for label , count in incoming . items ( ) if count == 0 ]
if not roots :
raise InvalidWorkflowDefinition ( " No entry block found for workflow definition " )
if len ( roots ) > 1 :
raise InvalidWorkflowDefinition (
f " Multiple entry blocks detected ( { ' , ' . join ( sorted ( roots ) ) } ); only one entry block is supported. "
)
# Kahn's algorithm for cycle detection
queue : deque [ str ] = deque ( [ roots [ 0 ] ] )
visited_count = 0
in_degree = dict ( incoming )
while queue :
node = queue . popleft ( )
visited_count + = 1
for neighbor in adjacency [ node ] :
in_degree [ neighbor ] - = 1
if in_degree [ neighbor ] == 0 :
queue . append ( neighbor )
if visited_count != len ( label_to_block ) :
raise InvalidWorkflowDefinition ( " Workflow definition contains a cycle; DAG traversal is required. " )
return roots [ 0 ] , label_to_block , default_next_map
2024-03-01 10:09:30 -08:00
async def create_workflow (
self ,
organization_id : str ,
title : str ,
workflow_definition : WorkflowDefinition ,
description : str | None = None ,
2025-11-28 14:24:44 -08:00
proxy_location : ProxyLocationInput = None ,
2025-06-13 23:59:50 -07:00
max_screenshot_scrolling_times : int | None = None ,
2024-05-16 10:51:22 -07:00
webhook_callback_url : str | None = None ,
2024-07-11 21:34:00 -07:00
totp_verification_url : str | None = None ,
2024-09-08 15:07:03 -07:00
totp_identifier : str | None = None ,
2024-09-06 12:01:56 -07:00
persist_browser_session : bool = False ,
2025-05-29 06:15:04 -07:00
model : dict [ str , Any ] | None = None ,
2024-05-16 10:51:22 -07:00
workflow_permanent_id : str | None = None ,
version : int | None = None ,
2024-06-27 12:53:08 -07:00
is_saved_task : bool = False ,
2025-01-25 04:08:51 +08:00
status : WorkflowStatus = WorkflowStatus . published ,
2025-06-19 00:42:34 -07:00
extra_http_headers : dict [ str , str ] | None = None ,
2025-09-29 15:14:15 -04:00
run_with : str | None = None ,
2025-08-06 08:32:14 -07:00
cache_key : str | None = None ,
2025-08-29 16:18:22 -04:00
ai_fallback : bool | None = None ,
2025-09-18 13:32:55 +08:00
run_sequentially : bool = False ,
2025-09-24 11:50:24 +08:00
sequential_key : str | None = None ,
2025-11-05 18:37:18 +03:00
folder_id : str | None = None ,
2024-03-01 10:09:30 -08:00
) - > Workflow :
return await app . DATABASE . create_workflow (
title = title ,
2024-05-15 08:43:36 -07:00
workflow_definition = workflow_definition . model_dump ( ) ,
2024-05-16 10:51:22 -07:00
organization_id = organization_id ,
description = description ,
proxy_location = proxy_location ,
webhook_callback_url = webhook_callback_url ,
2025-06-13 23:59:50 -07:00
max_screenshot_scrolling_times = max_screenshot_scrolling_times ,
2024-07-11 21:34:00 -07:00
totp_verification_url = totp_verification_url ,
2024-09-08 15:07:03 -07:00
totp_identifier = totp_identifier ,
2024-09-06 12:01:56 -07:00
persist_browser_session = persist_browser_session ,
2025-05-29 06:15:04 -07:00
model = model ,
2024-05-16 10:51:22 -07:00
workflow_permanent_id = workflow_permanent_id ,
version = version ,
2024-06-27 12:53:08 -07:00
is_saved_task = is_saved_task ,
2025-01-25 04:08:51 +08:00
status = status ,
2025-06-19 00:42:34 -07:00
extra_http_headers = extra_http_headers ,
2025-09-29 15:14:15 -04:00
run_with = run_with ,
2025-08-06 08:32:14 -07:00
cache_key = cache_key ,
2025-08-29 16:18:22 -04:00
ai_fallback = False if ai_fallback is None else ai_fallback ,
2025-09-18 13:32:55 +08:00
run_sequentially = run_sequentially ,
2025-09-24 11:50:24 +08:00
sequential_key = sequential_key ,
2025-11-05 18:37:18 +03:00
folder_id = folder_id ,
2024-03-01 10:09:30 -08:00
)
2025-09-03 16:55:15 -04:00
async def create_workflow_from_prompt (
self ,
organization : Organization ,
user_prompt : str ,
totp_identifier : str | None = None ,
totp_verification_url : str | None = None ,
webhook_callback_url : str | None = None ,
2025-11-28 14:24:44 -08:00
proxy_location : ProxyLocationInput = None ,
2025-09-03 16:55:15 -04:00
max_screenshot_scrolling_times : int | None = None ,
extra_http_headers : dict [ str , str ] | None = None ,
max_iterations : int | None = None ,
max_steps : int | None = None ,
2025-10-01 18:46:23 -07:00
status : WorkflowStatus = WorkflowStatus . auto_generated ,
2025-09-29 15:14:15 -04:00
run_with : str | None = None ,
2025-09-19 15:15:19 -04:00
ai_fallback : bool = True ,
2025-10-09 08:52:31 -04:00
task_version : Literal [ " v1 " , " v2 " ] = " v2 " ,
2025-09-03 16:55:15 -04:00
) - > Workflow :
metadata_prompt = prompt_engine . load_prompt (
" conversational_ui_goal " ,
user_goal = user_prompt ,
)
metadata_response = await app . LLM_API_HANDLER (
prompt = metadata_prompt ,
prompt_name = " conversational_ui_goal " ,
2025-09-11 18:10:05 -07:00
organization_id = organization . organization_id ,
2025-09-03 16:55:15 -04:00
)
2025-10-14 15:47:52 -04:00
block_label : str = metadata_response . get ( " block_label " , None ) or DEFAULT_FIRST_BLOCK_LABEL
title : str = metadata_response . get ( " title " , None ) or DEFAULT_WORKFLOW_TITLE
2025-09-03 16:55:15 -04:00
2025-10-09 08:52:31 -04:00
if task_version == " v1 " :
task_prompt = prompt_engine . load_prompt (
" generate-task " ,
user_prompt = user_prompt ,
)
task_response = await app . LLM_API_HANDLER (
prompt = task_prompt ,
prompt_name = " generate-task " ,
organization_id = organization . organization_id ,
)
data_extraction_goal : str | None = task_response . get ( " data_extraction_goal " )
2025-10-14 15:47:52 -04:00
navigation_goal : str = task_response . get ( " navigation_goal " , None ) or user_prompt
url : str = task_response . get ( " url " , None ) or " "
2025-10-09 08:52:31 -04:00
blocks = [
NavigationBlock (
url = url ,
label = block_label ,
title = title ,
navigation_goal = navigation_goal ,
max_steps_per_run = max_steps or settings . MAX_STEPS_PER_RUN ,
totp_verification_url = totp_verification_url ,
totp_identifier = totp_identifier ,
output_parameter = OutputParameter (
output_parameter_id = str ( uuid . uuid4 ( ) ) ,
key = f " { block_label } _output " ,
workflow_id = " " ,
created_at = datetime . now ( UTC ) ,
modified_at = datetime . now ( UTC ) ,
) ,
) ,
]
if data_extraction_goal :
blocks . append (
ExtractionBlock (
label = " extract_data " ,
title = " Extract Data " ,
data_extraction_goal = data_extraction_goal ,
output_parameter = OutputParameter (
output_parameter_id = str ( uuid . uuid4 ( ) ) ,
key = " extract_data_output " ,
workflow_id = " " ,
created_at = datetime . now ( UTC ) ,
modified_at = datetime . now ( UTC ) ,
) ,
max_steps_per_run = max_steps or settings . MAX_STEPS_PER_RUN ,
totp_verification_url = totp_verification_url ,
totp_identifier = totp_identifier ,
)
)
elif task_version == " v2 " :
blocks = [
TaskV2Block (
prompt = user_prompt ,
totp_identifier = totp_identifier ,
totp_verification_url = totp_verification_url ,
label = block_label ,
max_iterations = max_iterations or settings . MAX_ITERATIONS_PER_TASK_V2 ,
max_steps = max_steps or settings . MAX_STEPS_PER_TASK_V2 ,
output_parameter = OutputParameter (
output_parameter_id = str ( uuid . uuid4 ( ) ) ,
key = f " { block_label } _output " ,
workflow_id = " " ,
created_at = datetime . now ( UTC ) ,
modified_at = datetime . now ( UTC ) ,
) ,
)
]
2025-09-03 16:55:15 -04:00
new_workflow = await self . create_workflow (
title = title ,
2025-10-09 08:52:31 -04:00
workflow_definition = WorkflowDefinition ( parameters = [ ] , blocks = blocks ) ,
2025-09-03 16:55:15 -04:00
organization_id = organization . organization_id ,
proxy_location = proxy_location ,
webhook_callback_url = webhook_callback_url ,
totp_verification_url = totp_verification_url ,
totp_identifier = totp_identifier ,
max_screenshot_scrolling_times = max_screenshot_scrolling_times ,
extra_http_headers = extra_http_headers ,
2025-10-01 18:46:23 -07:00
status = status ,
2025-09-29 15:14:15 -04:00
run_with = run_with ,
2025-09-19 15:15:19 -04:00
ai_fallback = ai_fallback ,
2025-09-03 16:55:15 -04:00
)
return new_workflow
2024-05-15 08:43:36 -07:00
async def get_workflow ( self , workflow_id : str , organization_id : str | None = None ) - > Workflow :
workflow = await app . DATABASE . get_workflow ( workflow_id = workflow_id , organization_id = organization_id )
2024-03-01 10:09:30 -08:00
if not workflow :
2024-05-16 10:51:22 -07:00
raise WorkflowNotFound ( workflow_id = workflow_id )
return workflow
async def get_workflow_by_permanent_id (
self ,
workflow_permanent_id : str ,
organization_id : str | None = None ,
version : int | None = None ,
2024-09-19 11:15:07 -07:00
exclude_deleted : bool = True ,
2024-05-16 10:51:22 -07:00
) - > Workflow :
workflow = await app . DATABASE . get_workflow_by_permanent_id (
workflow_permanent_id ,
organization_id = organization_id ,
version = version ,
2024-09-19 11:15:07 -07:00
exclude_deleted = exclude_deleted ,
2024-05-16 10:51:22 -07:00
)
if not workflow :
raise WorkflowNotFound ( workflow_permanent_id = workflow_permanent_id , version = version )
2025-05-29 06:15:04 -07:00
2024-03-01 10:09:30 -08:00
return workflow
2025-12-11 18:39:21 -08:00
async def set_template_status (
self ,
organization_id : str ,
workflow_permanent_id : str ,
is_template : bool ,
) - > dict [ str , Any ] :
"""
Set or unset a workflow as a template .
Template status is stored in a separate workflow_templates table keyed by
workflow_permanent_id , since template status is a property of the workflow
identity , not a specific version .
Returns a dict with the result since we ' re not updating the workflow itself.
"""
# Verify workflow exists and belongs to org
await self . get_workflow_by_permanent_id (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
)
if is_template :
await app . DATABASE . add_workflow_template (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
)
else :
await app . DATABASE . remove_workflow_template (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
)
return { " workflow_permanent_id " : workflow_permanent_id , " is_template " : is_template }
2025-09-21 02:48:27 -04:00
async def get_workflow_versions_by_permanent_id (
self ,
workflow_permanent_id : str ,
organization_id : str | None = None ,
exclude_deleted : bool = True ,
) - > list [ Workflow ] :
"""
Get all versions of a workflow by its permanent ID .
Returns an empty list if no workflow is found with that permanent ID .
"""
workflows = await app . DATABASE . get_workflow_versions_by_permanent_id (
workflow_permanent_id ,
organization_id = organization_id ,
exclude_deleted = exclude_deleted ,
)
return workflows
2025-11-04 18:30:17 -05:00
async def get_workflow_by_workflow_run_id (
self ,
workflow_run_id : str ,
organization_id : str | None = None ,
exclude_deleted : bool = True ,
) - > Workflow :
workflow = await app . DATABASE . get_workflow_for_workflow_run (
workflow_run_id ,
organization_id = organization_id ,
exclude_deleted = exclude_deleted ,
)
if not workflow :
raise WorkflowNotFoundForWorkflowRun ( workflow_run_id = workflow_run_id )
return workflow
2025-08-28 20:05:24 -04:00
async def get_block_outputs_for_debug_session (
self ,
workflow_permanent_id : str ,
user_id : str ,
organization_id : str ,
exclude_deleted : bool = True ,
version : int | None = None ,
) - > dict [ str , dict [ str , Any ] ] :
workflow = await app . DATABASE . get_workflow_by_permanent_id (
workflow_permanent_id ,
organization_id = organization_id ,
version = version ,
exclude_deleted = exclude_deleted ,
)
if not workflow :
raise WorkflowNotFound ( workflow_permanent_id = workflow_permanent_id , version = version )
labels_to_outputs : dict [ str , BlockOutputParameter ] = { }
for block in workflow . workflow_definition . blocks :
label = block . label
block_run = await app . DATABASE . get_latest_completed_block_run (
organization_id = organization_id ,
user_id = user_id ,
block_label = label ,
workflow_permanent_id = workflow_permanent_id ,
)
if not block_run :
continue
output_parameter = await app . DATABASE . get_workflow_run_output_parameter_by_id (
workflow_run_id = block_run . workflow_run_id , output_parameter_id = block_run . output_parameter_id
)
if not output_parameter :
continue
block_output_parameter = output_parameter . value
if not isinstance ( block_output_parameter , dict ) :
continue
block_output_parameter [ " created_at " ] = output_parameter . created_at
2025-12-19 13:37:03 -07:00
labels_to_outputs [ label ] = block_output_parameter # type: ignore[assignment]
2025-08-28 20:05:24 -04:00
2025-12-19 13:37:03 -07:00
return labels_to_outputs # type: ignore[return-value]
2025-08-28 20:05:24 -04:00
2025-01-28 15:04:18 +08:00
async def get_workflows_by_permanent_ids (
self ,
workflow_permanent_ids : list [ str ] ,
organization_id : str | None = None ,
page : int = 1 ,
page_size : int = 10 ,
2025-10-16 16:04:53 +03:00
search_key : str = " " ,
2025-01-28 15:04:18 +08:00
statuses : list [ WorkflowStatus ] | None = None ,
) - > list [ Workflow ] :
return await app . DATABASE . get_workflows_by_permanent_ids (
workflow_permanent_ids ,
organization_id = organization_id ,
page = page ,
page_size = page_size ,
2025-10-16 16:04:53 +03:00
title = search_key ,
2025-01-28 15:04:18 +08:00
statuses = statuses ,
)
2024-05-16 10:51:22 -07:00
async def get_workflows_by_organization_id (
self ,
organization_id : str ,
page : int = 1 ,
page_size : int = 10 ,
2024-06-27 12:53:08 -07:00
only_saved_tasks : bool = False ,
only_workflows : bool = False ,
2025-12-11 18:39:21 -08:00
only_templates : bool = False ,
2025-10-16 16:04:53 +03:00
search_key : str | None = None ,
2025-11-05 18:37:18 +03:00
folder_id : str | None = None ,
2025-01-25 04:08:51 +08:00
statuses : list [ WorkflowStatus ] | None = None ,
2024-05-16 10:51:22 -07:00
) - > list [ Workflow ] :
"""
Get all workflows with the latest version for the organization .
2025-10-16 16:04:53 +03:00
Args :
2025-11-05 18:37:18 +03:00
search_key : Unified search term for title , folder name , and parameter metadata .
folder_id : Filter workflows by folder ID .
2024-05-16 10:51:22 -07:00
"""
return await app . DATABASE . get_workflows_by_organization_id (
organization_id = organization_id ,
page = page ,
page_size = page_size ,
2024-06-27 12:53:08 -07:00
only_saved_tasks = only_saved_tasks ,
only_workflows = only_workflows ,
2025-12-11 18:39:21 -08:00
only_templates = only_templates ,
2025-10-16 16:04:53 +03:00
search_key = search_key ,
2025-11-05 18:37:18 +03:00
folder_id = folder_id ,
2025-01-25 04:08:51 +08:00
statuses = statuses ,
2024-05-16 10:51:22 -07:00
)
2025-10-07 16:56:53 -07:00
async def update_workflow_definition (
2024-03-01 10:09:30 -08:00
self ,
workflow_id : str ,
2024-05-15 08:43:36 -07:00
organization_id : str | None = None ,
2024-03-01 10:09:30 -08:00
title : str | None = None ,
description : str | None = None ,
workflow_definition : WorkflowDefinition | None = None ,
2024-03-24 22:55:38 -07:00
) - > Workflow :
2025-10-01 13:49:42 -07:00
updated_workflow = await app . DATABASE . update_workflow (
2024-03-01 10:09:30 -08:00
workflow_id = workflow_id ,
title = title ,
2024-05-16 10:51:22 -07:00
organization_id = organization_id ,
2024-03-01 10:09:30 -08:00
description = description ,
2024-05-16 18:20:11 -07:00
workflow_definition = ( workflow_definition . model_dump ( ) if workflow_definition else None ) ,
2024-03-01 10:09:30 -08:00
)
2025-10-07 16:56:53 -07:00
return updated_workflow
async def maybe_delete_cached_code (
self ,
workflow : Workflow ,
workflow_definition : WorkflowDefinition ,
organization_id : str ,
delete_script : bool = True ,
delete_code_cache_is_ok : bool = False ,
) - > None :
if workflow_definition :
workflow_definition . validate ( )
previous_valid_workflow = await app . DATABASE . get_workflow_by_permanent_id (
workflow_permanent_id = workflow . workflow_permanent_id ,
organization_id = organization_id ,
exclude_deleted = True ,
ignore_version = workflow . version ,
)
2025-11-05 15:26:11 +08:00
current_definition : dict [ str , Any ] = { }
new_definition : dict [ str , Any ] = { }
2025-10-07 16:56:53 -07:00
if previous_valid_workflow :
current_definition = _get_workflow_definition_core_data ( previous_valid_workflow . workflow_definition )
new_definition = _get_workflow_definition_core_data ( workflow_definition )
has_changes = current_definition != new_definition
else :
has_changes = False
if previous_valid_workflow and has_changes and delete_script :
2025-11-05 15:26:11 +08:00
plan = self . _determine_cache_invalidation (
previous_blocks = current_definition . get ( " blocks " , [ ] ) ,
new_blocks = new_definition . get ( " blocks " , [ ] ) ,
)
2025-10-10 11:52:08 -04:00
candidates = await app . DATABASE . get_workflow_scripts_by_permanent_id (
2025-10-01 13:49:42 -07:00
organization_id = organization_id ,
2025-10-07 16:56:53 -07:00
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
2025-10-01 13:49:42 -07:00
)
2025-11-05 15:26:11 +08:00
if plan . has_targets :
cached_groups , published_groups = await self . _partition_cached_blocks (
organization_id = organization_id ,
candidates = candidates ,
block_labels_to_disable = plan . block_labels_to_disable ,
)
if not cached_groups and not published_groups :
LOG . info (
" Workflow definition changed, no cached script blocks found after workflow block change " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
invalidate_reason = plan . reason ,
invalidate_label = plan . label ,
invalidate_index_prev = plan . previous_index ,
invalidate_index_new = plan . new_index ,
block_labels_to_disable = plan . block_labels_to_disable ,
)
return
if published_groups and not delete_code_cache_is_ok :
LOG . info (
" Workflow definition changed, asking user if clearing published cached blocks is ok " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
invalidate_reason = plan . reason ,
invalidate_label = plan . label ,
invalidate_index_prev = plan . previous_index ,
invalidate_index_new = plan . new_index ,
block_labels_to_disable = plan . block_labels_to_disable ,
to_clear_published_cnt = len ( published_groups ) ,
to_clear_non_published_cnt = len ( cached_groups ) ,
)
raise CannotUpdateWorkflowDueToCodeCache (
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
)
try :
groups_to_clear = [ * cached_groups , * published_groups ]
await self . _clear_cached_block_groups (
organization_id = organization_id ,
workflow = workflow ,
previous_workflow = previous_valid_workflow ,
plan = plan ,
groups = groups_to_clear ,
)
except Exception as e :
LOG . error (
" Failed to clear cached script blocks after workflow block change " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
invalidate_reason = plan . reason ,
invalidate_label = plan . label ,
invalidate_index_prev = plan . previous_index ,
invalidate_index_new = plan . new_index ,
error = str ( e ) ,
)
return
if plan . previous_index is not None :
LOG . info (
" Workflow definition changed, no cached script blocks exist to clear for workflow block change " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
invalidate_reason = plan . reason ,
invalidate_label = plan . label ,
invalidate_index_prev = plan . previous_index ,
invalidate_index_new = plan . new_index ,
)
return
2025-10-10 11:52:08 -04:00
to_delete_published = [ script for script in candidates if script . status == ScriptStatus . published ]
to_delete = [ script for script in candidates if script . status != ScriptStatus . published ]
if len ( to_delete_published ) > 0 :
2025-10-07 16:56:53 -07:00
if not delete_code_cache_is_ok :
2025-10-10 11:52:08 -04:00
LOG . info (
" Workflow definition changed, asking user if deleting published code is ok " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
to_delete_non_published_cnt = len ( to_delete ) ,
to_delete_published_cnt = len ( to_delete_published ) ,
)
2025-10-07 16:56:53 -07:00
raise CannotUpdateWorkflowDueToCodeCache (
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
2025-10-01 13:49:42 -07:00
)
2025-10-07 16:56:53 -07:00
else :
2025-10-10 11:52:08 -04:00
LOG . info (
" Workflow definition changed, user answered yes to deleting published code " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
to_delete_non_published_cnt = len ( to_delete ) ,
to_delete_published_cnt = len ( to_delete_published ) ,
)
to_delete . extend ( to_delete_published )
if len ( to_delete ) > 0 :
try :
await app . DATABASE . delete_workflow_scripts_by_permanent_id (
organization_id = organization_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
script_ids = [ s . script_id for s in to_delete ] ,
)
except Exception as e :
LOG . error (
" Failed to delete workflow scripts after workflow definition change " ,
workflow_id = workflow . workflow_id ,
workflow_permanent_id = previous_valid_workflow . workflow_permanent_id ,
organization_id = organization_id ,
previous_version = previous_valid_workflow . version ,
new_version = workflow . version ,
error = str ( e ) ,
to_delete_ids = [ script . script_id for script in to_delete ] ,
to_delete_cnt = len ( to_delete ) ,
)
2024-03-01 10:09:30 -08:00
2024-05-16 10:51:22 -07:00
async def delete_workflow_by_permanent_id (
self ,
workflow_permanent_id : str ,
organization_id : str | None = None ,
) - > None :
await app . DATABASE . soft_delete_workflow_by_permanent_id (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
)
2024-09-19 11:15:07 -07:00
async def delete_workflow_by_id (
self ,
workflow_id : str ,
organization_id : str ,
) - > None :
await app . DATABASE . soft_delete_workflow_by_id (
workflow_id = workflow_id ,
organization_id = organization_id ,
)
2025-01-24 23:31:26 +08:00
async def get_workflow_runs (
2025-05-17 14:26:18 -07:00
self ,
organization_id : str ,
page : int = 1 ,
page_size : int = 10 ,
status : list [ WorkflowRunStatus ] | None = None ,
ordering : tuple [ str , str ] | None = None ,
2025-01-24 23:31:26 +08:00
) - > list [ WorkflowRun ] :
return await app . DATABASE . get_workflow_runs (
2025-05-17 14:26:18 -07:00
organization_id = organization_id ,
page = page ,
page_size = page_size ,
status = status ,
ordering = ordering ,
2025-01-24 23:31:26 +08:00
)
2024-07-05 16:39:42 -07:00
2025-05-12 08:30:37 -07:00
async def get_workflow_runs_count (
self ,
organization_id : str ,
status : list [ WorkflowRunStatus ] | None = None ,
) - > int :
return await app . DATABASE . get_workflow_runs_count (
organization_id = organization_id ,
status = status ,
)
2024-07-05 16:39:42 -07:00
async def get_workflow_runs_for_workflow_permanent_id (
2025-01-24 23:31:26 +08:00
self ,
workflow_permanent_id : str ,
organization_id : str ,
page : int = 1 ,
page_size : int = 10 ,
status : list [ WorkflowRunStatus ] | None = None ,
2025-10-16 16:04:53 +03:00
search_key : str | None = None ,
2024-07-05 16:39:42 -07:00
) - > list [ WorkflowRun ] :
return await app . DATABASE . get_workflow_runs_for_workflow_permanent_id (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
page = page ,
page_size = page_size ,
2025-01-24 23:31:26 +08:00
status = status ,
2025-10-16 16:04:53 +03:00
search_key = search_key ,
2024-07-05 16:39:42 -07:00
)
2024-07-09 11:26:44 -07:00
async def create_workflow_run (
2025-01-28 16:59:54 +08:00
self ,
workflow_request : WorkflowRequestBody ,
workflow_permanent_id : str ,
workflow_id : str ,
organization_id : str ,
parent_workflow_run_id : str | None = None ,
2025-09-24 11:50:24 +08:00
sequential_key : str | None = None ,
2025-10-01 07:21:08 -04:00
debug_session_id : str | None = None ,
2025-10-02 16:06:54 -07:00
code_gen : bool | None = None ,
2024-07-09 11:26:44 -07:00
) - > WorkflowRun :
2025-11-06 01:24:39 -08:00
# validate the browser session or profile id
2025-07-04 01:49:51 -07:00
if workflow_request . browser_session_id :
browser_session = await app . DATABASE . get_persistent_browser_session (
session_id = workflow_request . browser_session_id ,
organization_id = organization_id ,
)
if not browser_session :
raise BrowserSessionNotFound ( browser_session_id = workflow_request . browser_session_id )
2025-11-06 01:24:39 -08:00
if workflow_request . browser_profile_id :
browser_profile = await app . DATABASE . get_browser_profile (
workflow_request . browser_profile_id ,
organization_id = organization_id ,
)
if not browser_profile :
raise BrowserProfileNotFound (
profile_id = workflow_request . browser_profile_id ,
organization_id = organization_id ,
)
2024-03-01 10:09:30 -08:00
return await app . DATABASE . create_workflow_run (
2024-07-09 11:26:44 -07:00
workflow_permanent_id = workflow_permanent_id ,
2024-03-01 10:09:30 -08:00
workflow_id = workflow_id ,
2024-07-09 11:26:44 -07:00
organization_id = organization_id ,
2025-07-03 18:45:04 -07:00
browser_session_id = workflow_request . browser_session_id ,
2025-11-06 01:24:39 -08:00
browser_profile_id = workflow_request . browser_profile_id ,
2024-03-01 10:09:30 -08:00
proxy_location = workflow_request . proxy_location ,
webhook_callback_url = workflow_request . webhook_callback_url ,
2024-07-11 21:34:00 -07:00
totp_verification_url = workflow_request . totp_verification_url ,
2024-09-08 15:07:03 -07:00
totp_identifier = workflow_request . totp_identifier ,
2025-01-28 16:59:54 +08:00
parent_workflow_run_id = parent_workflow_run_id ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolling_times = workflow_request . max_screenshot_scrolls ,
2025-06-19 00:42:34 -07:00
extra_http_headers = workflow_request . extra_http_headers ,
2025-08-21 11:16:22 +08:00
browser_address = workflow_request . browser_address ,
2025-09-24 11:50:24 +08:00
sequential_key = sequential_key ,
2025-09-27 11:18:17 -07:00
run_with = workflow_request . run_with ,
2025-10-01 07:21:08 -04:00
debug_session_id = debug_session_id ,
2025-10-01 14:13:56 -07:00
ai_fallback = workflow_request . ai_fallback ,
2025-10-02 16:06:54 -07:00
code_gen = code_gen ,
2024-03-01 10:09:30 -08:00
)
2025-09-13 15:57:48 +08:00
async def _update_workflow_run_status (
self ,
workflow_run_id : str ,
status : WorkflowRunStatus ,
failure_reason : str | None = None ,
2025-09-27 11:18:17 -07:00
run_with : str | None = None ,
2025-10-01 14:13:56 -07:00
ai_fallback : bool | None = None ,
2025-09-13 15:57:48 +08:00
) - > WorkflowRun :
workflow_run = await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run_id ,
status = status ,
failure_reason = failure_reason ,
2025-09-21 02:45:23 -04:00
run_with = run_with ,
2025-10-01 14:13:56 -07:00
ai_fallback = ai_fallback ,
2025-09-13 15:57:48 +08:00
)
if status in [ WorkflowRunStatus . completed , WorkflowRunStatus . failed , WorkflowRunStatus . terminated ] :
start_time = (
workflow_run . started_at . replace ( tzinfo = UTC )
if workflow_run . started_at
else workflow_run . created_at . replace ( tzinfo = UTC )
)
queued_seconds = ( start_time - workflow_run . created_at . replace ( tzinfo = UTC ) ) . total_seconds ( )
duration_seconds = ( datetime . now ( UTC ) - start_time ) . total_seconds ( )
LOG . info (
" Workflow run duration metrics " ,
workflow_run_id = workflow_run_id ,
workflow_id = workflow_run . workflow_id ,
queued_seconds = queued_seconds ,
duration_seconds = duration_seconds ,
workflow_run_status = workflow_run . status ,
organization_id = workflow_run . organization_id ,
2025-11-05 01:38:58 +08:00
run_with = workflow_run . run_with ,
ai_fallback = workflow_run . ai_fallback ,
2025-09-13 15:57:48 +08:00
)
return workflow_run
2025-09-27 11:18:17 -07:00
async def mark_workflow_run_as_completed ( self , workflow_run_id : str , run_with : str | None = None ) - > WorkflowRun :
2024-03-01 10:09:30 -08:00
LOG . info (
2024-05-16 13:44:53 -07:00
f " Marking workflow run { workflow_run_id } as completed " ,
workflow_run_id = workflow_run_id ,
workflow_status = " completed " ,
2024-03-01 10:09:30 -08:00
)
2025-10-08 14:58:50 -07:00
2025-10-29 21:42:27 -07:00
# Add workflow completion tag to trace
2025-10-08 14:58:50 -07:00
TraceManager . add_task_completion_tag ( WorkflowRunStatus . completed )
2025-09-13 15:57:48 +08:00
return await self . _update_workflow_run_status (
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . completed ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2024-03-01 10:09:30 -08:00
)
2026-01-13 16:56:06 -08:00
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
2025-09-13 15:57:48 +08:00
async def mark_workflow_run_as_failed (
2025-09-27 11:18:17 -07:00
self ,
workflow_run_id : str ,
failure_reason : str | None ,
run_with : str | None = None ,
2025-09-13 15:57:48 +08:00
) - > WorkflowRun :
2024-05-16 13:44:53 -07:00
LOG . info (
f " Marking workflow run { workflow_run_id } as failed " ,
workflow_run_id = workflow_run_id ,
workflow_status = " failed " ,
2024-11-15 11:07:44 +08:00
failure_reason = failure_reason ,
2024-05-16 13:44:53 -07:00
)
2025-10-08 14:58:50 -07:00
2025-10-29 21:42:27 -07:00
# Add workflow failure tag to trace
2025-10-08 14:58:50 -07:00
TraceManager . add_task_completion_tag ( WorkflowRunStatus . failed )
2025-09-13 15:57:48 +08:00
return await self . _update_workflow_run_status (
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . failed ,
2024-11-15 11:07:44 +08:00
failure_reason = failure_reason ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2024-03-01 10:09:30 -08:00
)
2025-09-27 11:18:17 -07:00
async def mark_workflow_run_as_running ( self , workflow_run_id : str , run_with : str | None = None ) - > WorkflowRun :
2025-11-26 10:46:13 -07:00
workflow_run = await self . _update_workflow_run_status (
2024-05-16 13:44:53 -07:00
workflow_run_id = workflow_run_id ,
2025-11-26 10:46:13 -07:00
status = WorkflowRunStatus . running ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2024-03-01 10:09:30 -08:00
)
2025-11-26 10:46:13 -07:00
start_time = (
workflow_run . started_at . replace ( tzinfo = UTC )
if workflow_run . started_at
else workflow_run . created_at . replace ( tzinfo = UTC )
)
queued_seconds = ( start_time - workflow_run . created_at . replace ( tzinfo = UTC ) ) . total_seconds ( )
LOG . info (
f " Marked workflow run { workflow_run_id } as running " ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run_id ,
2025-11-26 10:46:13 -07:00
workflow_status = " running " ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2025-11-26 10:46:13 -07:00
queued_seconds = queued_seconds ,
2024-03-01 10:09:30 -08:00
)
2025-11-26 10:46:13 -07:00
return workflow_run
2024-03-01 10:09:30 -08:00
2025-09-13 15:57:48 +08:00
async def mark_workflow_run_as_terminated (
2025-09-27 11:18:17 -07:00
self ,
workflow_run_id : str ,
failure_reason : str | None ,
run_with : str | None = None ,
2025-09-13 15:57:48 +08:00
) - > WorkflowRun :
2024-03-01 10:09:30 -08:00
LOG . info (
f " Marking workflow run { workflow_run_id } as terminated " ,
workflow_run_id = workflow_run_id ,
2024-05-16 13:44:53 -07:00
workflow_status = " terminated " ,
2024-11-15 11:07:44 +08:00
failure_reason = failure_reason ,
2024-03-01 10:09:30 -08:00
)
2025-10-08 14:58:50 -07:00
2025-10-29 21:42:27 -07:00
# Add workflow terminated tag to trace
2025-10-08 14:58:50 -07:00
TraceManager . add_task_completion_tag ( WorkflowRunStatus . terminated )
2025-09-13 15:57:48 +08:00
return await self . _update_workflow_run_status (
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . terminated ,
2024-11-15 11:07:44 +08:00
failure_reason = failure_reason ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2024-03-01 10:09:30 -08:00
)
2025-09-27 11:18:17 -07:00
async def mark_workflow_run_as_canceled ( self , workflow_run_id : str , run_with : str | None = None ) - > WorkflowRun :
2024-10-08 23:09:41 -07:00
LOG . info (
f " Marking workflow run { workflow_run_id } as canceled " ,
workflow_run_id = workflow_run_id ,
workflow_status = " canceled " ,
)
2025-10-08 14:58:50 -07:00
2025-10-29 21:42:27 -07:00
# Add workflow canceled tag to trace
2025-10-08 14:58:50 -07:00
TraceManager . add_task_completion_tag ( WorkflowRunStatus . canceled )
2025-09-13 15:57:48 +08:00
return await self . _update_workflow_run_status (
2024-10-08 23:09:41 -07:00
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . canceled ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2024-10-08 23:09:41 -07:00
)
2025-05-26 08:49:42 -07:00
async def mark_workflow_run_as_timed_out (
2025-09-27 11:18:17 -07:00
self ,
workflow_run_id : str ,
failure_reason : str | None = None ,
run_with : str | None = None ,
2025-05-26 08:49:42 -07:00
) - > WorkflowRun :
2025-04-07 11:54:39 -04:00
LOG . info (
f " Marking workflow run { workflow_run_id } as timed out " ,
workflow_run_id = workflow_run_id ,
workflow_status = " timed_out " ,
)
2025-10-08 14:58:50 -07:00
2025-10-29 21:42:27 -07:00
# Add workflow timed out tag to trace
2025-10-08 14:58:50 -07:00
TraceManager . add_task_completion_tag ( WorkflowRunStatus . timed_out )
2025-09-13 15:57:48 +08:00
return await self . _update_workflow_run_status (
2025-04-07 11:54:39 -04:00
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . timed_out ,
failure_reason = failure_reason ,
2025-09-27 11:18:17 -07:00
run_with = run_with ,
2025-04-07 11:54:39 -04:00
)
2024-12-22 17:49:33 -08:00
async def get_workflow_run ( self , workflow_run_id : str , organization_id : str | None = None ) - > WorkflowRun :
workflow_run = await app . DATABASE . get_workflow_run (
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
)
2024-03-01 10:09:30 -08:00
if not workflow_run :
raise WorkflowRunNotFound ( workflow_run_id )
return workflow_run
async def create_workflow_parameter (
self ,
workflow_id : str ,
workflow_parameter_type : WorkflowParameterType ,
key : str ,
default_value : bool | int | float | str | dict | list | None = None ,
description : str | None = None ,
) - > WorkflowParameter :
return await app . DATABASE . create_workflow_parameter (
workflow_id = workflow_id ,
workflow_parameter_type = workflow_parameter_type ,
key = key ,
description = description ,
default_value = default_value ,
)
async def create_aws_secret_parameter (
self , workflow_id : str , aws_key : str , key : str , description : str | None = None
) - > AWSSecretParameter :
return await app . DATABASE . create_aws_secret_parameter (
workflow_id = workflow_id , aws_key = aws_key , key = key , description = description
)
2024-03-21 17:16:56 -07:00
async def create_output_parameter (
self , workflow_id : str , key : str , description : str | None = None
) - > OutputParameter :
return await app . DATABASE . create_output_parameter ( workflow_id = workflow_id , key = key , description = description )
2024-03-01 10:09:30 -08:00
async def get_workflow_parameters ( self , workflow_id : str ) - > list [ WorkflowParameter ] :
return await app . DATABASE . get_workflow_parameters ( workflow_id = workflow_id )
async def create_workflow_run_parameter (
self ,
workflow_run_id : str ,
2024-10-22 17:36:25 -07:00
workflow_parameter : WorkflowParameter ,
value : Any ,
2024-03-01 10:09:30 -08:00
) - > WorkflowRunParameter :
2024-10-22 22:35:14 -07:00
value = json . dumps ( value ) if isinstance ( value , ( dict , list ) ) else value
2024-10-22 17:36:25 -07:00
# InvalidWorkflowParameter will be raised if the validation fails
workflow_parameter . workflow_parameter_type . convert_value ( value )
2024-03-01 10:09:30 -08:00
return await app . DATABASE . create_workflow_run_parameter (
workflow_run_id = workflow_run_id ,
2024-10-22 17:36:25 -07:00
workflow_parameter = workflow_parameter ,
2024-10-22 22:35:14 -07:00
value = value ,
2024-03-01 10:09:30 -08:00
)
async def get_workflow_run_parameter_tuples (
self , workflow_run_id : str
) - > list [ tuple [ WorkflowParameter , WorkflowRunParameter ] ] :
return await app . DATABASE . get_workflow_run_parameters ( workflow_run_id = workflow_run_id )
2024-03-21 17:16:56 -07:00
@staticmethod
async def get_workflow_output_parameters ( workflow_id : str ) - > list [ OutputParameter ] :
return await app . DATABASE . get_workflow_output_parameters ( workflow_id = workflow_id )
@staticmethod
async def get_workflow_run_output_parameters (
workflow_run_id : str ,
) - > list [ WorkflowRunOutputParameter ] :
return await app . DATABASE . get_workflow_run_output_parameters ( workflow_run_id = workflow_run_id )
@staticmethod
async def get_output_parameter_workflow_run_output_parameter_tuples (
workflow_id : str ,
workflow_run_id : str ,
) - > list [ tuple [ OutputParameter , WorkflowRunOutputParameter ] ] :
workflow_run_output_parameters = await app . DATABASE . get_workflow_run_output_parameters (
workflow_run_id = workflow_run_id
)
2025-03-17 16:22:44 -07:00
output_parameters = await app . DATABASE . get_workflow_output_parameters_by_ids (
output_parameter_ids = [
workflow_run_output_parameter . output_parameter_id
for workflow_run_output_parameter in workflow_run_output_parameters
]
)
2024-03-21 17:16:56 -07:00
return [
( output_parameter , workflow_run_output_parameter )
for workflow_run_output_parameter in workflow_run_output_parameters
2025-01-03 13:03:46 +08:00
for output_parameter in output_parameters
2024-03-21 17:16:56 -07:00
if output_parameter . output_parameter_id == workflow_run_output_parameter . output_parameter_id
]
2024-03-01 10:09:30 -08:00
async def get_last_task_for_workflow_run ( self , workflow_run_id : str ) - > Task | None :
return await app . DATABASE . get_last_task_for_workflow_run ( workflow_run_id = workflow_run_id )
async def get_tasks_by_workflow_run_id ( self , workflow_run_id : str ) - > list [ Task ] :
return await app . DATABASE . get_tasks_by_workflow_run_id ( workflow_run_id = workflow_run_id )
2025-12-05 12:30:05 -08:00
async def get_recent_task_screenshot_urls (
self ,
* ,
organization_id : str | None ,
task_id : str | None = None ,
task_v2_id : str | None = None ,
limit : int = 3 ,
) - > list [ str ] :
""" Return the latest action/final screenshot URLs for a task (v1 or v2). """
artifact_types = [ ArtifactType . SCREENSHOT_ACTION , ArtifactType . SCREENSHOT_FINAL ]
artifacts : list [ Artifact ] = [ ]
if task_id :
artifacts = (
await app . DATABASE . get_latest_n_artifacts (
task_id = task_id ,
artifact_types = artifact_types ,
organization_id = organization_id ,
n = limit ,
)
or [ ]
)
elif task_v2_id :
action_artifacts = await app . DATABASE . get_artifacts_by_entity_id (
organization_id = organization_id ,
artifact_type = ArtifactType . SCREENSHOT_ACTION ,
task_v2_id = task_v2_id ,
limit = limit ,
)
final_artifacts = await app . DATABASE . get_artifacts_by_entity_id (
organization_id = organization_id ,
artifact_type = ArtifactType . SCREENSHOT_FINAL ,
task_v2_id = task_v2_id ,
limit = limit ,
)
artifacts = sorted (
( action_artifacts or [ ] ) + ( final_artifacts or [ ] ) ,
key = lambda artifact : artifact . created_at ,
reverse = True ,
) [ : limit ]
if not artifacts :
return [ ]
return await app . ARTIFACT_MANAGER . get_share_links ( artifacts ) or [ ]
async def get_recent_workflow_screenshot_urls (
self ,
workflow_run_id : str ,
organization_id : str | None = None ,
limit : int = 3 ,
workflow_run_tasks : list [ Task ] | None = None ,
) - > list [ str ] :
""" Return latest screenshots across recent tasks in a workflow run. """
screenshot_artifacts : list [ Artifact ] = [ ]
seen_artifact_ids : set [ str ] = set ( )
if workflow_run_tasks is None :
workflow_run_tasks = await app . DATABASE . get_tasks_by_workflow_run_id ( workflow_run_id = workflow_run_id )
for task in workflow_run_tasks [ : : - 1 ] :
artifact = await app . DATABASE . get_latest_artifact (
task_id = task . task_id ,
artifact_types = [ ArtifactType . SCREENSHOT_ACTION , ArtifactType . SCREENSHOT_FINAL ] ,
organization_id = organization_id ,
)
if artifact :
screenshot_artifacts . append ( artifact )
seen_artifact_ids . add ( artifact . artifact_id )
if len ( screenshot_artifacts ) > = limit :
break
if len ( screenshot_artifacts ) < limit :
action_artifacts = await app . DATABASE . get_artifacts_by_entity_id (
organization_id = organization_id ,
artifact_type = ArtifactType . SCREENSHOT_ACTION ,
workflow_run_id = workflow_run_id ,
limit = limit ,
)
final_artifacts = await app . DATABASE . get_artifacts_by_entity_id (
organization_id = organization_id ,
artifact_type = ArtifactType . SCREENSHOT_FINAL ,
workflow_run_id = workflow_run_id ,
limit = limit ,
)
# Support runs that may not have Task rows (e.g., task_v2-only executions)
for artifact in sorted (
( action_artifacts or [ ] ) + ( final_artifacts or [ ] ) ,
key = lambda artifact : artifact . created_at ,
reverse = True ,
) :
if artifact . artifact_id in seen_artifact_ids :
continue
screenshot_artifacts . append ( artifact )
seen_artifact_ids . add ( artifact . artifact_id )
if len ( screenshot_artifacts ) > = limit :
break
if not screenshot_artifacts :
return [ ]
return await app . ARTIFACT_MANAGER . get_share_links ( screenshot_artifacts ) or [ ]
2024-10-15 06:26:16 -07:00
async def build_workflow_run_status_response_by_workflow_id (
self ,
workflow_run_id : str ,
2025-05-11 16:24:31 -07:00
organization_id : str | None = None ,
2024-12-31 11:24:09 -08:00
include_cost : bool = False ,
2026-01-07 11:41:57 -08:00
include_step_count : bool = False ,
2025-04-01 15:52:35 -04:00
) - > WorkflowRunResponseBase :
2024-12-22 17:49:33 -08:00
workflow_run = await self . get_workflow_run ( workflow_run_id = workflow_run_id , organization_id = organization_id )
2024-10-15 06:26:16 -07:00
if workflow_run is None :
LOG . error ( f " Workflow run { workflow_run_id } not found " )
raise WorkflowRunNotFound ( workflow_run_id = workflow_run_id )
workflow_permanent_id = workflow_run . workflow_permanent_id
return await self . build_workflow_run_status_response (
workflow_permanent_id = workflow_permanent_id ,
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
2024-12-31 11:24:09 -08:00
include_cost = include_cost ,
2026-01-07 11:41:57 -08:00
include_step_count = include_step_count ,
2024-10-15 06:26:16 -07:00
)
2024-03-01 10:09:30 -08:00
async def build_workflow_run_status_response (
2024-05-15 08:43:36 -07:00
self ,
2024-06-04 08:27:04 -07:00
workflow_permanent_id : str ,
2024-05-15 08:43:36 -07:00
workflow_run_id : str ,
2025-05-11 16:24:31 -07:00
organization_id : str | None = None ,
2024-12-31 11:24:09 -08:00
include_cost : bool = False ,
2026-01-07 11:41:57 -08:00
include_step_count : bool = False ,
2025-04-01 15:52:35 -04:00
) - > WorkflowRunResponseBase :
2025-01-28 15:04:18 +08:00
workflow = await self . get_workflow_by_permanent_id ( workflow_permanent_id )
2024-03-01 10:09:30 -08:00
if workflow is None :
2024-06-04 08:27:04 -07:00
LOG . error ( f " Workflow { workflow_permanent_id } not found " )
raise WorkflowNotFound ( workflow_permanent_id = workflow_permanent_id )
2024-03-01 10:09:30 -08:00
2024-12-22 17:49:33 -08:00
workflow_run = await self . get_workflow_run ( workflow_run_id = workflow_run_id , organization_id = organization_id )
2025-08-18 14:24:18 +08:00
task_v2 = await app . DATABASE . get_task_v2_by_workflow_run_id (
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
)
2024-03-01 10:09:30 -08:00
workflow_run_tasks = await app . DATABASE . get_tasks_by_workflow_run_id ( workflow_run_id = workflow_run_id )
2025-12-05 12:30:05 -08:00
screenshot_urls : list [ str ] | None = await self . get_recent_workflow_screenshot_urls (
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
workflow_run_tasks = workflow_run_tasks ,
)
screenshot_urls = screenshot_urls or None
2024-03-01 10:09:30 -08:00
recording_url = None
2025-12-02 14:29:00 +08:00
# Get recording url from browser session first,
# if not found, get the recording url from the artifacts
if workflow_run . browser_session_id :
try :
async with asyncio . timeout ( GET_DOWNLOADED_FILES_TIMEOUT ) :
recordings = await app . STORAGE . get_shared_recordings_in_browser_session (
organization_id = workflow_run . organization_id ,
browser_session_id = workflow_run . browser_session_id ,
)
# FIXME: we only support one recording for now
recording_url = recordings [ 0 ] . url if recordings else None
except asyncio . TimeoutError :
LOG . warning ( " Timeout getting recordings " , browser_session_id = workflow_run . browser_session_id )
if recording_url is None :
recording_artifact = await app . DATABASE . get_artifact_for_run (
run_id = task_v2 . observer_cruise_id if task_v2 else workflow_run_id ,
artifact_type = ArtifactType . RECORDING ,
organization_id = organization_id ,
)
if recording_artifact :
recording_url = await app . ARTIFACT_MANAGER . get_share_link ( recording_artifact )
2024-03-01 10:09:30 -08:00
2025-08-18 14:24:18 +08:00
downloaded_files : list [ FileInfo ] = [ ]
2024-11-29 16:05:44 +08:00
downloaded_file_urls : list [ str ] | None = None
try :
async with asyncio . timeout ( GET_DOWNLOADED_FILES_TIMEOUT ) :
2025-08-18 14:24:18 +08:00
context = skyvern_context . current ( )
2025-02-26 17:19:05 -08:00
downloaded_files = await app . STORAGE . get_downloaded_files (
2025-01-28 15:04:18 +08:00
organization_id = workflow_run . organization_id ,
2025-12-17 14:54:13 +08:00
run_id = context . run_id if context and context . run_id else workflow_run . workflow_run_id ,
2024-11-29 16:05:44 +08:00
)
2025-08-18 14:24:18 +08:00
if task_v2 :
task_v2_downloaded_files = await app . STORAGE . get_downloaded_files (
organization_id = workflow_run . organization_id ,
run_id = task_v2 . observer_cruise_id ,
)
if task_v2_downloaded_files :
downloaded_files . extend ( task_v2_downloaded_files )
2025-02-26 17:19:05 -08:00
if downloaded_files :
downloaded_file_urls = [ file_info . url for file_info in downloaded_files ]
2024-11-29 16:05:44 +08:00
except asyncio . TimeoutError :
LOG . warning (
" Timeout to get downloaded files " ,
workflow_run_id = workflow_run . workflow_run_id ,
)
except Exception :
LOG . warning (
" Failed to get downloaded files " ,
exc_info = True ,
workflow_run_id = workflow_run . workflow_run_id ,
)
2024-03-01 10:09:30 -08:00
workflow_parameter_tuples = await app . DATABASE . get_workflow_run_parameters ( workflow_run_id = workflow_run_id )
parameters_with_value = { wfp . key : wfrp . value for wfp , wfrp in workflow_parameter_tuples }
2024-05-16 18:20:11 -07:00
output_parameter_tuples : list [
tuple [ OutputParameter , WorkflowRunOutputParameter ]
] = await self . get_output_parameter_workflow_run_output_parameter_tuples (
2024-06-04 08:27:04 -07:00
workflow_id = workflow_run . workflow_id , workflow_run_id = workflow_run_id
2024-03-21 17:16:56 -07:00
)
2024-09-10 13:27:31 -07:00
outputs = None
2025-02-26 16:19:56 -08:00
EXTRACTED_INFORMATION_KEY = " extracted_information "
2024-03-21 17:16:56 -07:00
if output_parameter_tuples :
2024-05-16 13:44:53 -07:00
outputs = { output_parameter . key : output . value for output_parameter , output in output_parameter_tuples }
2025-06-12 05:04:26 -04:00
extracted_information : list [ Any ] = [ ]
for _ , output in output_parameter_tuples :
if output . value is not None :
extracted_information . extend ( WorkflowService . _collect_extracted_information ( output . value ) )
2025-02-26 16:19:56 -08:00
outputs [ EXTRACTED_INFORMATION_KEY ] = extracted_information
2024-09-10 13:27:31 -07:00
2025-09-24 11:39:35 +08:00
errors : list [ dict [ str , Any ] ] = [ ]
for task in workflow_run_tasks :
errors . extend ( task . errors )
2024-12-31 11:24:09 -08:00
total_steps = None
total_cost = None
2026-01-07 11:41:57 -08:00
if include_step_count or include_cost :
2024-12-31 11:24:09 -08:00
workflow_run_steps = await app . DATABASE . get_steps_by_task_ids (
task_ids = [ task . task_id for task in workflow_run_tasks ] , organization_id = organization_id
)
total_steps = len ( workflow_run_steps )
2026-01-07 11:41:57 -08:00
if include_cost :
workflow_run_blocks = await app . DATABASE . get_workflow_run_blocks (
workflow_run_id = workflow_run_id , organization_id = organization_id
)
text_prompt_blocks = [
block for block in workflow_run_blocks if block . block_type == BlockType . TEXT_PROMPT
]
# TODO: This is a temporary cost calculation. We need to implement a more accurate cost calculation.
# successful steps are the ones that have a status of completed and the total count of unique step.order
successful_steps = [ step for step in workflow_run_steps if step . status == StepStatus . completed ]
total_cost = 0.05 * ( len ( successful_steps ) + len ( text_prompt_blocks ) )
2025-04-01 15:52:35 -04:00
return WorkflowRunResponseBase (
2024-05-25 19:32:25 -07:00
workflow_id = workflow . workflow_permanent_id ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run_id ,
status = workflow_run . status ,
2024-11-15 13:20:30 +08:00
failure_reason = workflow_run . failure_reason ,
2024-03-01 10:09:30 -08:00
proxy_location = workflow_run . proxy_location ,
webhook_callback_url = workflow_run . webhook_callback_url ,
2025-07-29 00:12:44 +08:00
webhook_failure_reason = workflow_run . webhook_failure_reason ,
2024-07-11 21:34:00 -07:00
totp_verification_url = workflow_run . totp_verification_url ,
2024-09-08 15:07:03 -07:00
totp_identifier = workflow_run . totp_identifier ,
2025-06-21 08:50:19 +08:00
extra_http_headers = workflow_run . extra_http_headers ,
2025-06-11 23:36:49 -04:00
queued_at = workflow_run . queued_at ,
started_at = workflow_run . started_at ,
finished_at = workflow_run . finished_at ,
2024-03-01 10:09:30 -08:00
created_at = workflow_run . created_at ,
modified_at = workflow_run . modified_at ,
parameters = parameters_with_value ,
screenshot_urls = screenshot_urls ,
recording_url = recording_url ,
2025-02-26 17:19:05 -08:00
downloaded_files = downloaded_files ,
2024-11-29 16:05:44 +08:00
downloaded_file_urls = downloaded_file_urls ,
2024-05-16 13:44:53 -07:00
outputs = outputs ,
2024-12-31 11:24:09 -08:00
total_steps = total_steps ,
total_cost = total_cost ,
2025-02-06 03:10:17 +08:00
workflow_title = workflow . title ,
2025-07-16 13:32:43 -04:00
browser_session_id = workflow_run . browser_session_id ,
2025-11-06 01:24:39 -08:00
browser_profile_id = workflow_run . browser_profile_id ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolls = workflow_run . max_screenshot_scrolls ,
2025-08-18 14:24:18 +08:00
task_v2 = task_v2 ,
2025-08-21 11:16:22 +08:00
browser_address = workflow_run . browser_address ,
2025-09-14 22:53:52 -07:00
script_run = workflow_run . script_run ,
2025-09-24 11:39:35 +08:00
errors = errors ,
2024-03-01 10:09:30 -08:00
)
2024-11-01 15:13:41 -07:00
async def clean_up_workflow (
2024-03-01 10:09:30 -08:00
self ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
api_key : str | None = None ,
close_browser_on_completion : bool = True ,
2024-11-01 15:13:41 -07:00
need_call_webhook : bool = True ,
2025-01-09 22:04:53 +01:00
browser_session_id : str | None = None ,
2024-03-01 10:09:30 -08:00
) - > None :
2024-03-06 19:06:15 -08:00
analytics . capture ( " skyvern-oss-agent-workflow-status " , { " status " : workflow_run . status } )
2024-05-16 13:44:53 -07:00
tasks = await self . get_tasks_by_workflow_run_id ( workflow_run . workflow_run_id )
2024-03-12 22:28:16 -07:00
all_workflow_task_ids = [ task . task_id for task in tasks ]
2025-08-21 11:16:22 +08:00
close_browser_on_completion = (
close_browser_on_completion and browser_session_id is None and not workflow_run . browser_address
)
2024-03-01 10:09:30 -08:00
browser_state = await app . BROWSER_MANAGER . cleanup_for_workflow_run (
2024-05-16 18:20:11 -07:00
workflow_run . workflow_run_id ,
all_workflow_task_ids ,
2025-08-21 11:16:22 +08:00
close_browser_on_completion = close_browser_on_completion ,
2025-02-18 00:27:21 +08:00
browser_session_id = browser_session_id ,
2025-01-09 22:04:53 +01:00
organization_id = workflow_run . organization_id ,
2024-03-01 10:09:30 -08:00
)
if browser_state :
await self . persist_video_data ( browser_state , workflow , workflow_run )
2024-11-27 15:32:44 -08:00
if tasks :
await self . persist_debug_artifacts ( browser_state , tasks [ - 1 ] , workflow , workflow_run )
2025-12-17 13:45:29 -08:00
# Skip workflow-scoped session save when using browser_profile_id to avoid conflicts
# (profile persistence is handled separately via the profile storage)
if (
workflow . persist_browser_session
and browser_state . browser_artifacts . browser_session_dir
and not workflow_run . browser_profile_id
) :
2024-09-07 01:57:47 -07:00
await app . STORAGE . store_browser_session (
2025-01-28 15:04:18 +08:00
workflow_run . organization_id ,
2024-09-07 01:57:47 -07:00
workflow . workflow_permanent_id ,
browser_state . browser_artifacts . browser_session_dir ,
)
LOG . info ( " Persisted browser session for workflow run " , workflow_run_id = workflow_run . workflow_run_id )
2024-03-01 10:09:30 -08:00
2024-12-07 18:13:53 -08:00
await app . ARTIFACT_MANAGER . wait_for_upload_aiotasks ( all_workflow_task_ids )
2024-03-01 10:09:30 -08:00
2024-11-29 16:05:44 +08:00
try :
async with asyncio . timeout ( SAVE_DOWNLOADED_FILES_TIMEOUT ) :
2025-08-18 14:24:18 +08:00
context = skyvern_context . current ( )
2024-11-29 16:05:44 +08:00
await app . STORAGE . save_downloaded_files (
2025-08-18 14:24:18 +08:00
organization_id = workflow_run . organization_id ,
2025-12-17 14:54:13 +08:00
run_id = context . run_id if context and context . run_id else workflow_run . workflow_run_id ,
2024-11-29 16:05:44 +08:00
)
except asyncio . TimeoutError :
LOG . warning (
" Timeout to save downloaded files " ,
workflow_run_id = workflow_run . workflow_run_id ,
)
except Exception :
LOG . warning (
" Failed to save downloaded files " ,
exc_info = True ,
workflow_run_id = workflow_run . workflow_run_id ,
)
2024-11-01 15:13:41 -07:00
if not need_call_webhook :
return
2024-12-14 09:59:37 -08:00
await self . execute_workflow_webhook ( workflow_run , api_key )
async def execute_workflow_webhook (
self ,
workflow_run : WorkflowRun ,
api_key : str | None = None ,
) - > None :
workflow_id = workflow_run . workflow_id
2024-03-21 17:16:56 -07:00
workflow_run_status_response = await self . build_workflow_run_status_response (
2024-12-14 09:59:37 -08:00
workflow_permanent_id = workflow_run . workflow_permanent_id ,
2024-03-21 17:16:56 -07:00
workflow_run_id = workflow_run . workflow_run_id ,
2024-12-14 09:59:37 -08:00
organization_id = workflow_run . organization_id ,
2026-01-07 11:41:57 -08:00
include_step_count = True ,
2024-03-21 17:16:56 -07:00
)
2024-05-16 18:20:11 -07:00
LOG . info (
" Built workflow run status response " ,
workflow_run_status_response = workflow_run_status_response ,
)
2024-03-21 17:16:56 -07:00
2024-03-01 10:09:30 -08:00
if not workflow_run . webhook_callback_url :
LOG . warning (
" Workflow has no webhook callback url. Not sending workflow response " ,
2024-12-14 09:59:37 -08:00
workflow_id = workflow_id ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
)
return
if not api_key :
LOG . warning (
" Request has no api key. Not sending workflow response " ,
2024-12-14 09:59:37 -08:00
workflow_id = workflow_id ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
)
return
2025-05-24 13:01:53 -07:00
# build new schema for backward compatible webhook payload
2025-11-04 18:30:17 -05:00
app_url = f " { settings . SKYVERN_APP_URL . rstrip ( ' / ' ) } /runs/ { workflow_run . workflow_run_id } "
2025-05-27 03:00:14 -07:00
workflow_run_response = WorkflowRunResponse (
run_id = workflow_run . workflow_run_id ,
run_type = RunType . workflow_run ,
status = RunStatus ( workflow_run_status_response . status ) ,
output = workflow_run_status_response . outputs ,
downloaded_files = workflow_run_status_response . downloaded_files ,
recording_url = workflow_run_status_response . recording_url ,
screenshot_urls = workflow_run_status_response . screenshot_urls ,
failure_reason = workflow_run_status_response . failure_reason ,
app_url = app_url ,
2025-09-14 22:53:52 -07:00
script_run = workflow_run_status_response . script_run ,
2025-05-27 03:00:14 -07:00
created_at = workflow_run_status_response . created_at ,
modified_at = workflow_run_status_response . modified_at ,
run_request = WorkflowRunRequest (
workflow_id = workflow_run . workflow_permanent_id ,
title = workflow_run_status_response . workflow_title ,
parameters = workflow_run_status_response . parameters ,
proxy_location = workflow_run . proxy_location ,
webhook_url = workflow_run . webhook_callback_url or None ,
totp_url = workflow_run . totp_verification_url or None ,
totp_identifier = workflow_run . totp_identifier ,
) ,
2025-09-24 14:49:20 +08:00
errors = workflow_run_status_response . errors ,
2026-01-07 11:41:57 -08:00
step_count = workflow_run_status_response . total_steps ,
2025-05-27 03:00:14 -07:00
)
2025-11-04 11:29:14 +08:00
payload_dict : dict = json . loads ( workflow_run_status_response . model_dump_json ( ) )
2025-05-27 03:00:14 -07:00
workflow_run_response_dict = json . loads ( workflow_run_response . model_dump_json ( ) )
payload_dict . update ( workflow_run_response_dict )
2025-11-04 11:29:14 +08:00
signed_data = generate_skyvern_webhook_signature (
payload = payload_dict ,
2024-03-01 10:09:30 -08:00
api_key = api_key ,
)
LOG . info (
" Sending webhook run status to webhook callback url " ,
2024-12-14 09:59:37 -08:00
workflow_id = workflow_id ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
webhook_callback_url = workflow_run . webhook_callback_url ,
2025-11-04 11:29:14 +08:00
payload = signed_data . signed_payload ,
headers = signed_data . headers ,
2024-03-01 10:09:30 -08:00
)
try :
2025-01-14 08:59:53 -08:00
async with httpx . AsyncClient ( ) as client :
resp = await client . post (
2025-11-04 11:29:14 +08:00
url = workflow_run . webhook_callback_url ,
data = signed_data . signed_payload ,
headers = signed_data . headers ,
timeout = httpx . Timeout ( 30.0 ) ,
2025-01-14 08:59:53 -08:00
)
2025-05-27 03:21:50 -07:00
if resp . status_code > = 200 and resp . status_code < 300 :
2024-03-01 10:09:30 -08:00
LOG . info (
" Webhook sent successfully " ,
2024-12-14 09:59:37 -08:00
workflow_id = workflow_id ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
resp_code = resp . status_code ,
resp_text = resp . text ,
)
2025-07-29 00:12:44 +08:00
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run . workflow_run_id ,
webhook_failure_reason = " " ,
)
2024-03-01 10:09:30 -08:00
else :
LOG . info (
" Webhook failed " ,
2024-12-14 09:59:37 -08:00
workflow_id = workflow_id ,
2024-03-01 10:09:30 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
2025-11-04 11:29:14 +08:00
webhook_data = signed_data . signed_payload ,
2024-03-01 10:09:30 -08:00
resp = resp ,
resp_code = resp . status_code ,
resp_text = resp . text ,
)
2025-07-29 00:12:44 +08:00
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run . workflow_run_id ,
webhook_failure_reason = f " Webhook failed with status code { resp . status_code } , error message: { resp . text } " ,
)
2024-03-01 10:09:30 -08:00
except Exception as e :
raise FailedToSendWebhook (
2024-12-14 09:59:37 -08:00
workflow_id = workflow_id ,
2024-05-16 18:20:11 -07:00
workflow_run_id = workflow_run . workflow_run_id ,
2024-03-01 10:09:30 -08:00
) from e
async def persist_video_data (
self , browser_state : BrowserState , workflow : Workflow , workflow_run : WorkflowRun
) - > None :
# Create recording artifact after closing the browser, so we can get an accurate recording
2024-08-09 10:46:52 +08:00
video_artifacts = await app . BROWSER_MANAGER . get_video_artifacts (
2024-03-01 10:09:30 -08:00
workflow_id = workflow . workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
browser_state = browser_state ,
)
2024-08-09 10:46:52 +08:00
for video_artifact in video_artifacts :
2024-03-01 10:09:30 -08:00
await app . ARTIFACT_MANAGER . update_artifact_data (
2024-08-09 10:46:52 +08:00
artifact_id = video_artifact . video_artifact_id ,
2025-01-28 15:04:18 +08:00
organization_id = workflow_run . organization_id ,
2024-08-09 10:46:52 +08:00
data = video_artifact . video_data ,
2024-03-01 10:09:30 -08:00
)
async def persist_har_data (
2024-05-16 18:20:11 -07:00
self ,
browser_state : BrowserState ,
last_step : Step ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
2024-03-01 10:09:30 -08:00
) - > None :
har_data = await app . BROWSER_MANAGER . get_har_data (
2024-05-16 18:20:11 -07:00
workflow_id = workflow . workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
browser_state = browser_state ,
2024-03-01 10:09:30 -08:00
)
if har_data :
2024-03-12 22:28:16 -07:00
await app . ARTIFACT_MANAGER . create_artifact (
step = last_step ,
artifact_type = ArtifactType . HAR ,
data = har_data ,
2024-03-01 10:09:30 -08:00
)
2024-10-31 23:10:11 +08:00
async def persist_browser_console_log (
self ,
browser_state : BrowserState ,
last_step : Step ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
) - > None :
browser_log = await app . BROWSER_MANAGER . get_browser_console_log (
workflow_id = workflow . workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
browser_state = browser_state ,
)
if browser_log :
await app . ARTIFACT_MANAGER . create_artifact (
step = last_step ,
artifact_type = ArtifactType . BROWSER_CONSOLE_LOG ,
data = browser_log ,
)
2024-03-12 22:28:16 -07:00
async def persist_tracing_data (
self , browser_state : BrowserState , last_step : Step , workflow_run : WorkflowRun
) - > None :
if browser_state . browser_context is None or browser_state . browser_artifacts . traces_dir is None :
return
trace_path = f " { browser_state . browser_artifacts . traces_dir } / { workflow_run . workflow_run_id } .zip "
await app . ARTIFACT_MANAGER . create_artifact ( step = last_step , artifact_type = ArtifactType . TRACE , path = trace_path )
async def persist_debug_artifacts (
2024-05-16 18:20:11 -07:00
self ,
browser_state : BrowserState ,
last_task : Task ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
2024-03-12 22:28:16 -07:00
) - > None :
last_step = await app . DATABASE . get_latest_step (
task_id = last_task . task_id , organization_id = last_task . organization_id
)
if not last_step :
return
2024-11-01 02:17:22 +08:00
await self . persist_browser_console_log ( browser_state , last_step , workflow , workflow_run )
2024-03-12 22:28:16 -07:00
await self . persist_har_data ( browser_state , last_step , workflow , workflow_run )
await self . persist_tracing_data ( browser_state , last_step , workflow_run )
2024-03-24 22:55:38 -07:00
2025-10-07 16:56:53 -07:00
async def make_workflow_definition (
self ,
workflow_id : str ,
workflow_definition_yaml : WorkflowDefinitionYAML ,
) - > WorkflowDefinition :
2026-01-13 15:31:33 -07:00
workflow_definition = convert_workflow_definition (
workflow_definition_yaml = workflow_definition_yaml ,
2026-01-15 12:22:37 -07:00
workflow_id = workflow_id ,
2025-10-07 16:56:53 -07:00
)
2026-01-13 15:31:33 -07:00
await app . DATABASE . save_workflow_definition_parameters ( workflow_definition . parameters )
2025-10-07 16:56:53 -07:00
return workflow_definition
2024-05-16 10:51:22 -07:00
async def create_workflow_from_request (
self ,
2024-10-03 16:18:21 -07:00
organization : Organization ,
2024-05-16 10:51:22 -07:00
request : WorkflowCreateYAMLRequest ,
workflow_permanent_id : str | None = None ,
2025-10-01 17:00:35 -07:00
delete_script : bool = True ,
2025-10-07 16:56:53 -07:00
delete_code_cache_is_ok : bool = True ,
2024-05-16 10:51:22 -07:00
) - > Workflow :
2024-10-03 16:18:21 -07:00
organization_id = organization . organization_id
2024-05-16 18:20:11 -07:00
LOG . info (
" Creating workflow from request " ,
organization_id = organization_id ,
title = request . title ,
)
2024-09-19 11:15:07 -07:00
new_workflow_id : str | None = None
2025-10-07 16:56:53 -07:00
2025-12-01 20:02:52 -07:00
if workflow_permanent_id :
# Would return 404: WorkflowNotFound to the client if wpid does not match the organization
existing_latest_workflow = await self . get_workflow_by_permanent_id (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
exclude_deleted = False ,
)
else :
existing_latest_workflow = None
2024-03-24 22:55:38 -07:00
try :
2025-12-01 20:02:52 -07:00
if existing_latest_workflow :
2024-05-16 10:51:22 -07:00
existing_version = existing_latest_workflow . version
2025-10-07 16:56:53 -07:00
# NOTE: it's only potential, as it may be immediately deleted!
potential_workflow = await self . create_workflow (
2024-05-16 10:51:22 -07:00
title = request . title ,
workflow_definition = WorkflowDefinition ( parameters = [ ] , blocks = [ ] ) ,
description = request . description ,
organization_id = organization_id ,
proxy_location = request . proxy_location ,
webhook_callback_url = request . webhook_callback_url ,
2024-07-11 21:34:00 -07:00
totp_verification_url = request . totp_verification_url ,
2024-09-08 15:07:03 -07:00
totp_identifier = request . totp_identifier ,
2024-09-06 12:01:56 -07:00
persist_browser_session = request . persist_browser_session ,
2025-05-29 06:15:04 -07:00
model = request . model ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolling_times = request . max_screenshot_scrolls ,
2025-06-19 00:42:34 -07:00
extra_http_headers = request . extra_http_headers ,
2025-12-01 20:02:52 -07:00
workflow_permanent_id = existing_latest_workflow . workflow_permanent_id ,
2024-05-16 10:51:22 -07:00
version = existing_version + 1 ,
2024-06-27 12:53:08 -07:00
is_saved_task = request . is_saved_task ,
2025-01-25 04:08:51 +08:00
status = request . status ,
2025-09-29 15:14:15 -04:00
run_with = request . run_with ,
2025-08-06 08:32:14 -07:00
cache_key = request . cache_key ,
2025-08-29 16:18:22 -04:00
ai_fallback = request . ai_fallback ,
2025-09-18 13:32:55 +08:00
run_sequentially = request . run_sequentially ,
2025-09-24 11:50:24 +08:00
sequential_key = request . sequential_key ,
2025-11-05 18:37:18 +03:00
folder_id = existing_latest_workflow . folder_id ,
2024-05-16 10:51:22 -07:00
)
else :
2025-10-07 16:56:53 -07:00
# NOTE: it's only potential, as it may be immediately deleted!
potential_workflow = await self . create_workflow (
2024-05-16 10:51:22 -07:00
title = request . title ,
workflow_definition = WorkflowDefinition ( parameters = [ ] , blocks = [ ] ) ,
description = request . description ,
organization_id = organization_id ,
proxy_location = request . proxy_location ,
webhook_callback_url = request . webhook_callback_url ,
2024-07-11 21:34:00 -07:00
totp_verification_url = request . totp_verification_url ,
2024-09-08 15:07:03 -07:00
totp_identifier = request . totp_identifier ,
2024-09-06 12:01:56 -07:00
persist_browser_session = request . persist_browser_session ,
2025-05-29 06:15:04 -07:00
model = request . model ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolling_times = request . max_screenshot_scrolls ,
2025-06-19 00:42:34 -07:00
extra_http_headers = request . extra_http_headers ,
2024-06-27 12:53:08 -07:00
is_saved_task = request . is_saved_task ,
2025-01-25 04:08:51 +08:00
status = request . status ,
2025-09-29 15:14:15 -04:00
run_with = request . run_with ,
2025-08-06 08:32:14 -07:00
cache_key = request . cache_key ,
2025-09-04 12:20:22 -04:00
ai_fallback = request . ai_fallback ,
2025-09-18 13:32:55 +08:00
run_sequentially = request . run_sequentially ,
2025-09-24 11:50:24 +08:00
sequential_key = request . sequential_key ,
2025-11-06 20:09:26 +03:00
folder_id = request . folder_id ,
2024-05-16 10:51:22 -07:00
)
2024-09-19 11:15:07 -07:00
# Keeping track of the new workflow id to delete it if an error occurs during the creation process
2025-10-07 16:56:53 -07:00
new_workflow_id = potential_workflow . workflow_id
2024-05-16 13:08:24 -07:00
2025-10-07 16:56:53 -07:00
workflow_definition = await self . make_workflow_definition (
potential_workflow . workflow_id ,
request . workflow_definition ,
2024-05-16 13:08:24 -07:00
)
2024-04-16 15:41:44 -07:00
2025-10-07 16:56:53 -07:00
updated_workflow = await self . update_workflow_definition (
workflow_id = potential_workflow . workflow_id ,
2024-05-15 08:43:36 -07:00
organization_id = organization_id ,
2025-10-07 16:56:53 -07:00
title = request . title ,
description = request . description ,
2024-03-24 22:55:38 -07:00
workflow_definition = workflow_definition ,
)
2025-10-07 16:56:53 -07:00
await self . maybe_delete_cached_code (
updated_workflow ,
workflow_definition = workflow_definition ,
2024-03-24 22:55:38 -07:00
organization_id = organization_id ,
2025-10-07 16:56:53 -07:00
delete_script = delete_script ,
delete_code_cache_is_ok = delete_code_cache_is_ok ,
2024-03-24 22:55:38 -07:00
)
2025-10-07 16:56:53 -07:00
return updated_workflow
2024-03-24 22:55:38 -07:00
except Exception as e :
2024-09-19 11:15:07 -07:00
if new_workflow_id :
2024-10-07 10:22:23 -07:00
LOG . error (
f " Failed to create workflow from request, deleting workflow { new_workflow_id } " ,
organization_id = organization_id ,
)
2024-09-19 11:15:07 -07:00
await self . delete_workflow_by_id ( workflow_id = new_workflow_id , organization_id = organization_id )
else :
LOG . exception ( f " Failed to create workflow from request, title: { request . title } " )
2024-03-24 22:55:38 -07:00
raise e
@staticmethod
2024-11-25 11:19:35 -08:00
async def create_output_parameter_for_block ( workflow_id : str , block_yaml : BLOCK_YAML_TYPES ) - > OutputParameter :
2024-05-16 13:08:24 -07:00
output_parameter_key = f " { block_yaml . label } _output "
return await app . DATABASE . create_output_parameter (
workflow_id = workflow_id ,
key = output_parameter_key ,
description = f " Output parameter for block { block_yaml . label } " ,
)
2025-01-24 16:21:26 +08:00
async def create_empty_workflow (
2025-01-25 04:08:51 +08:00
self ,
organization : Organization ,
title : str ,
2025-11-28 14:24:44 -08:00
proxy_location : ProxyLocationInput = None ,
2025-06-19 00:42:34 -07:00
max_screenshot_scrolling_times : int | None = None ,
extra_http_headers : dict [ str , str ] | None = None ,
2025-09-29 15:14:15 -04:00
run_with : str | None = None ,
2025-01-25 04:08:51 +08:00
status : WorkflowStatus = WorkflowStatus . published ,
2025-01-24 16:21:26 +08:00
) - > Workflow :
2024-11-28 10:26:15 -08:00
"""
Create a blank workflow with no blocks
"""
# create a new workflow
workflow_create_request = WorkflowCreateYAMLRequest (
title = title ,
workflow_definition = WorkflowDefinitionYAML (
parameters = [ ] ,
blocks = [ ] ,
) ,
2025-01-24 16:21:26 +08:00
proxy_location = proxy_location ,
2025-01-25 04:08:51 +08:00
status = status ,
2025-07-03 02:03:01 -07:00
max_screenshot_scrolls = max_screenshot_scrolling_times ,
2025-06-19 00:42:34 -07:00
extra_http_headers = extra_http_headers ,
2025-09-29 15:14:15 -04:00
run_with = run_with ,
2024-11-28 10:26:15 -08:00
)
return await app . WORKFLOW_SERVICE . create_workflow_from_request (
organization = organization ,
request = workflow_create_request ,
)
2024-12-22 20:54:53 -08:00
async def get_workflow_run_timeline (
self ,
workflow_run_id : str ,
organization_id : str | None = None ,
) - > list [ WorkflowRunTimeline ] :
"""
build the tree structure of the workflow run timeline
"""
workflow_run_blocks = await app . DATABASE . get_workflow_run_blocks (
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
)
# get all the actions for all workflow run blocks
task_ids = [ block . task_id for block in workflow_run_blocks if block . task_id ]
task_id_to_block : dict [ str , WorkflowRunBlock ] = {
block . task_id : block for block in workflow_run_blocks if block . task_id
}
actions = await app . DATABASE . get_tasks_actions ( task_ids = task_ids , organization_id = organization_id )
for action in actions :
if not action . task_id :
continue
task_block = task_id_to_block [ action . task_id ]
task_block . actions . append ( action )
result = [ ]
block_map : dict [ str , WorkflowRunTimeline ] = { }
2024-12-23 13:37:30 -08:00
counter = 0
2024-12-22 20:54:53 -08:00
while workflow_run_blocks :
2024-12-23 13:37:30 -08:00
counter + = 1
2024-12-22 20:54:53 -08:00
block = workflow_run_blocks . pop ( 0 )
workflow_run_timeline = WorkflowRunTimeline (
type = WorkflowRunTimelineType . block ,
block = block ,
created_at = block . created_at ,
modified_at = block . modified_at ,
)
if block . parent_workflow_run_block_id :
if block . parent_workflow_run_block_id in block_map :
block_map [ block . parent_workflow_run_block_id ] . children . append ( workflow_run_timeline )
2025-03-26 13:35:40 -07:00
block_map [ block . workflow_run_block_id ] = workflow_run_timeline
2024-12-22 20:54:53 -08:00
else :
# put the block back to the queue
workflow_run_blocks . append ( block )
else :
result . append ( workflow_run_timeline )
2024-12-23 13:37:30 -08:00
block_map [ block . workflow_run_block_id ] = workflow_run_timeline
if counter > 1000 :
LOG . error ( " Too many blocks in the workflow run " , workflow_run_id = workflow_run_id )
break
2024-12-22 20:54:53 -08:00
return result
2025-08-09 13:11:16 -07:00
2025-08-30 03:33:42 +08:00
async def generate_script_if_needed (
2025-08-09 13:11:16 -07:00
self ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
2025-08-30 03:33:42 +08:00
block_labels : list [ str ] | None = None ,
2025-11-05 19:57:11 +08:00
blocks_to_update : set [ str ] | None = None ,
2025-08-09 13:11:16 -07:00
) - > None :
2025-10-02 16:06:54 -07:00
code_gen = workflow_run . code_gen
2025-11-05 19:57:11 +08:00
blocks_to_update = set ( blocks_to_update or [ ] )
2025-10-01 13:52:42 -04:00
LOG . info (
" Generate script? " ,
block_labels = block_labels ,
code_gen = code_gen ,
workflow_run_id = workflow_run . workflow_run_id ,
2025-11-05 19:57:11 +08:00
blocks_to_update = list ( blocks_to_update ) ,
2025-10-01 13:52:42 -04:00
)
if block_labels and not code_gen :
# Do not generate script if block_labels is provided, and an explicit code_gen
# request is not made
2025-08-30 03:33:42 +08:00
return None
2025-08-09 13:11:16 -07:00
2025-09-19 08:50:21 -07:00
existing_script , rendered_cache_key_value = await workflow_script_service . get_workflow_script (
2025-08-30 03:33:42 +08:00
workflow ,
workflow_run ,
block_labels ,
2025-08-09 13:11:16 -07:00
)
2025-11-05 19:57:11 +08:00
2025-08-30 03:33:42 +08:00
if existing_script :
2025-11-05 19:57:11 +08:00
cached_block_labels : set [ str ] = set ( )
script_blocks = await app . DATABASE . get_script_blocks_by_script_revision_id (
2025-08-30 03:33:42 +08:00
script_revision_id = existing_script . script_revision_id ,
2025-11-05 19:57:11 +08:00
organization_id = workflow . organization_id ,
)
for script_block in script_blocks :
if script_block . script_block_label :
cached_block_labels . add ( script_block . script_block_label )
2025-11-21 20:18:31 -08:00
should_cache_block_labels = {
block . label
for block in workflow . workflow_definition . blocks
if block . label and block . block_type in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
}
should_cache_block_labels . add ( settings . WORKFLOW_START_BLOCK_LABEL )
2025-11-05 19:57:11 +08:00
cached_block_labels . add ( settings . WORKFLOW_START_BLOCK_LABEL )
2025-11-21 20:18:31 -08:00
if cached_block_labels != should_cache_block_labels :
missing_labels = should_cache_block_labels - cached_block_labels
2025-11-05 19:57:11 +08:00
if missing_labels :
blocks_to_update . update ( missing_labels )
# Always rebuild the orchestrator if the definition changed
blocks_to_update . add ( settings . WORKFLOW_START_BLOCK_LABEL )
should_regenerate = bool ( blocks_to_update ) or bool ( code_gen )
if not should_regenerate :
LOG . info (
" Workflow script already up to date; skipping regeneration " ,
workflow_id = workflow . workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
cache_key_value = rendered_cache_key_value ,
script_id = existing_script . script_id ,
script_revision_id = existing_script . script_revision_id ,
run_with = workflow_run . run_with ,
)
return
2025-11-21 20:18:31 -08:00
LOG . info (
" deleting old workflow script and generating new script " ,
workflow_id = workflow . workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
cache_key_value = rendered_cache_key_value ,
script_id = existing_script . script_id ,
script_revision_id = existing_script . script_revision_id ,
run_with = workflow_run . run_with ,
blocks_to_update = list ( blocks_to_update ) ,
code_gen = code_gen ,
)
2025-11-05 19:57:11 +08:00
# delete the existing workflow scripts if any
await app . DATABASE . delete_workflow_scripts_by_permanent_id (
organization_id = workflow . organization_id ,
workflow_permanent_id = workflow . workflow_permanent_id ,
script_ids = [ existing_script . script_id ] ,
2025-08-09 13:11:16 -07:00
)
2025-11-05 19:57:11 +08:00
# create a new script
regenerated_script = await app . DATABASE . create_script (
organization_id = workflow . organization_id ,
run_id = workflow_run . workflow_run_id ,
)
await workflow_script_service . generate_workflow_script (
workflow_run = workflow_run ,
workflow = workflow ,
script = regenerated_script ,
rendered_cache_key_value = rendered_cache_key_value ,
cached_script = existing_script ,
updated_block_labels = blocks_to_update ,
)
aio_task_primary_key = f " { regenerated_script . script_id } _ { regenerated_script . version } "
if aio_task_primary_key in app . ARTIFACT_MANAGER . upload_aiotasks_map :
aio_tasks = app . ARTIFACT_MANAGER . upload_aiotasks_map [ aio_task_primary_key ]
if aio_tasks :
await asyncio . gather ( * aio_tasks )
else :
LOG . warning (
" No upload aio tasks found for regenerated script " ,
script_id = regenerated_script . script_id ,
version = regenerated_script . version ,
)
2025-08-09 13:11:16 -07:00
return
2025-08-10 22:57:00 -07:00
created_script = await app . DATABASE . create_script (
organization_id = workflow . organization_id ,
run_id = workflow_run . workflow_run_id ,
)
2025-09-19 08:50:21 -07:00
await workflow_script_service . generate_workflow_script (
workflow_run = workflow_run ,
workflow = workflow ,
script = created_script ,
rendered_cache_key_value = rendered_cache_key_value ,
2025-11-05 19:57:11 +08:00
cached_script = None ,
updated_block_labels = None ,
2025-08-09 13:11:16 -07:00
)
2025-10-24 12:28:06 -07:00
aio_task_primary_key = f " { created_script . script_id } _ { created_script . version } "
if aio_task_primary_key in app . ARTIFACT_MANAGER . upload_aiotasks_map :
aio_tasks = app . ARTIFACT_MANAGER . upload_aiotasks_map [ aio_task_primary_key ]
if aio_tasks :
await asyncio . gather ( * aio_tasks )
else :
LOG . warning (
" No upload aio tasks found for script " ,
script_id = created_script . script_id ,
version = created_script . version ,
)
2025-09-21 02:45:23 -04:00
def should_run_script (
self ,
workflow : Workflow ,
workflow_run : WorkflowRun ,
) - > bool :
if workflow_run . run_with == " code " :
return True
2025-10-14 16:17:03 -07:00
if workflow_run . run_with == " agent " :
return False
2025-09-29 15:14:15 -04:00
if workflow . run_with == " code " :
2025-09-21 02:45:23 -04:00
return True
return False