2024-11-29 16:05:44 +08:00
import asyncio
2024-03-01 10:09:30 -08:00
import json
2025-02-02 03:10:38 +08:00
from datetime import UTC , datetime
2024-10-22 17:36:25 -07:00
from typing import Any
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
2024-03-06 19:06:15 -08:00
from skyvern import analytics
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 (
FailedToSendWebhook ,
MissingValueForParameter ,
SkyvernException ,
WorkflowNotFound ,
WorkflowRunNotFound ,
)
2024-03-01 10:09:30 -08:00
from skyvern . forge import app
from skyvern . forge . sdk . artifact . models import ArtifactType
from skyvern . forge . sdk . core import skyvern_context
2025-01-13 19:28:14 -08:00
from skyvern . forge . sdk . core . security import generate_skyvern_webhook_headers
2024-03-01 10:09:30 -08:00
from skyvern . forge . sdk . core . skyvern_context import SkyvernContext
2024-11-26 11:29:33 +08:00
from skyvern . forge . sdk . db . enums import TaskType
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-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
2024-04-16 15:41:44 -07:00
from skyvern . forge . sdk . workflow . exceptions import (
ContextParameterSourceNotDefined ,
2024-11-25 10:42:34 +08:00
InvalidWaitBlockTime ,
2024-05-16 13:08:24 -07:00
InvalidWorkflowDefinition ,
2024-04-16 15:41:44 -07:00
WorkflowDefinitionHasDuplicateParameterKeys ,
2024-05-16 13:08:24 -07:00
WorkflowDefinitionHasReservedParameterKeys ,
2024-10-23 23:20:51 -07:00
WorkflowParameterMissingRequiredValue ,
2024-04-16 15:41:44 -07:00
)
2024-03-25 00:57:37 -07:00
from skyvern . forge . sdk . workflow . models . block import (
2024-11-21 15:12:26 +08:00
ActionBlock ,
2024-10-08 23:09:41 -07:00
BlockStatus ,
2024-03-25 00:57:37 -07:00
BlockType ,
BlockTypeVar ,
CodeBlock ,
2024-03-28 16:46:54 -07:00
DownloadToS3Block ,
2024-11-22 14:44:22 +08:00
ExtractionBlock ,
2024-11-26 23:36:22 +08:00
FileDownloadBlock ,
2024-07-05 17:08:20 -07:00
FileParserBlock ,
2025-03-23 15:37:20 -07:00
FileUploadBlock ,
2024-03-25 00:57:37 -07:00
ForLoopBlock ,
2024-11-22 14:44:22 +08:00
LoginBlock ,
NavigationBlock ,
2025-01-20 12:33:54 -08:00
PDFParserBlock ,
2024-03-31 01:58:11 -07:00
SendEmailBlock ,
2024-03-25 00:57:37 -07:00
TaskBlock ,
2025-01-28 16:59:54 +08:00
TaskV2Block ,
2024-03-25 00:57:37 -07:00
TextPromptBlock ,
2024-04-04 19:09:19 -07:00
UploadToS3Block ,
2025-02-01 04:13:00 +08:00
UrlBlock ,
2024-11-21 15:12:26 +08:00
ValidationBlock ,
2024-11-25 10:42:34 +08:00
WaitBlock ,
2024-03-25 00:57:37 -07:00
)
2024-03-21 17:16:56 -07:00
from skyvern . forge . sdk . workflow . models . parameter import (
2024-04-16 15:41:44 -07:00
PARAMETER_TYPE ,
2024-03-21 17:16:56 -07:00
AWSSecretParameter ,
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 ,
2024-03-21 17:16:56 -07:00
OutputParameter ,
2024-03-24 22:55:38 -07:00
Parameter ,
ParameterType ,
2024-03-21 17:16:56 -07:00
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-03-17 15:19:18 -07:00
WorkflowRunResponse ,
2024-03-01 10:09:30 -08:00
WorkflowRunStatus ,
2025-01-25 04:08:51 +08:00
WorkflowStatus ,
2024-03-01 10:09:30 -08:00
)
2024-11-28 10:26:15 -08:00
from skyvern . forge . sdk . workflow . models . yaml import (
BLOCK_YAML_TYPES ,
ForLoopBlockYAML ,
WorkflowCreateYAMLRequest ,
WorkflowDefinitionYAML ,
)
2025-03-24 15:15:21 -07:00
from skyvern . schemas . runs import ProxyLocation
2024-03-01 10:09:30 -08:00
from skyvern . webeye . browser_factory import BrowserState
LOG = structlog . get_logger ( )
class WorkflowService :
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 ,
2024-03-01 10:09:30 -08:00
organization_id : str ,
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 ,
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-01-28 15:04:18 +08:00
organization_id = None if is_template_workflow else 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
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-01-28 15:04:18 +08:00
organization_id = organization_id ,
2025-01-28 16:59:54 +08:00
parent_workflow_run_id = parent_workflow_run_id ,
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 ,
2024-03-01 10:09:30 -08:00
)
skyvern_context . set (
SkyvernContext (
organization_id = organization_id ,
request_id = request_id ,
workflow_id = workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
max_steps_override = max_steps_override ,
)
)
# 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 :
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 ]
await self . create_workflow_run_parameter (
workflow_run_id = workflow_run . workflow_run_id ,
workflow_parameter = workflow_parameter ,
value = request_body_value ,
)
elif workflow_parameter . default_value is not None :
await self . create_workflow_run_parameter (
workflow_run_id = workflow_run . workflow_run_id ,
workflow_parameter = workflow_parameter ,
value = workflow_parameter . default_value ,
)
else :
raise MissingValueForParameter (
parameter_key = workflow_parameter . key ,
workflow_id = workflow . workflow_id ,
workflow_run_id = workflow_run . workflow_run_id ,
)
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
await self . mark_workflow_run_as_failed (
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
async def execute_workflow (
self ,
workflow_run_id : str ,
api_key : str ,
2024-10-02 15:16:08 -07:00
organization : Organization ,
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-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 ,
)
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 )
2024-03-01 10:09:30 -08:00
2024-03-12 22:28:16 -07:00
# Set workflow run status to running, create workflow run parameters
await self . mark_workflow_run_as_running ( workflow_run_id = workflow_run . 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-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
wp_wps_tuples = await self . get_workflow_run_parameter_tuples ( workflow_run_id = workflow_run . 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 ,
wp_wps_tuples ,
workflow_output_parameters ,
context_parameters ,
secret_parameters ,
)
except Exception as e :
LOG . exception (
f " Error while initializing workflow run context for workflow run { workflow_run . workflow_run_id } " ,
workflow_run_id = workflow_run . workflow_run_id ,
)
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 } "
await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . 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 = browser_session_id is None ,
)
return workflow_run
2024-03-01 10:09:30 -08:00
# Execute workflow blocks
blocks = workflow . workflow_definition . 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 ) :
try :
2024-11-14 01:32:53 -08:00
refreshed_workflow_run = await app . DATABASE . get_workflow_run (
2024-12-22 17:49:33 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
organization_id = organization_id ,
2024-11-14 01:32:53 -08:00
)
if refreshed_workflow_run and refreshed_workflow_run . status == WorkflowRunStatus . canceled :
LOG . info (
" Workflow run is canceled, stopping execution inside workflow execution loop " ,
workflow_run_id = workflow_run . workflow_run_id ,
block_idx = block_idx ,
block_type = block . block_type ,
block_label = block . label ,
)
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
2024-12-14 09:59:37 -08:00
need_call_webhook = True ,
2025-01-09 22:04:53 +01:00
close_browser_on_completion = browser_session_id is None ,
browser_session_id = browser_session_id ,
2024-11-14 01:32:53 -08:00
)
return workflow_run
2025-01-22 13:23:10 +08:00
if refreshed_workflow_run and refreshed_workflow_run . status == WorkflowRunStatus . timed_out :
LOG . info (
" Workflow run is timed out, stopping execution inside workflow execution loop " ,
workflow_run_id = workflow_run . workflow_run_id ,
block_idx = block_idx ,
block_type = block . block_type ,
block_label = block . label ,
)
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
need_call_webhook = True ,
close_browser_on_completion = browser_session_id is None ,
browser_session_id = browser_session_id ,
)
return workflow_run
2024-04-04 19:09:19 -07:00
parameters = block . get_all_parameters ( workflow_run_id )
2024-03-12 22:28:16 -07:00
await app . WORKFLOW_CONTEXT_MANAGER . register_block_parameters_for_workflow_run (
2024-10-02 15:16:08 -07:00
workflow_run_id , parameters , organization
2024-03-12 22:28:16 -07:00
)
LOG . info (
2025-01-14 14:06:43 -08:00
f " Executing root block { block . block_type } at index { block_idx } / { blocks_cnt - 1 } for workflow run { workflow_run_id } " ,
2024-03-12 22:28:16 -07:00
block_type = block . block_type ,
workflow_run_id = workflow_run . workflow_run_id ,
block_idx = block_idx ,
2024-11-14 01:32:53 -08:00
block_type_var = block . block_type ,
block_label = block . label ,
2024-03-12 22:28:16 -07:00
)
2024-12-22 11:16:23 -08:00
block_result = await block . execute_safe (
workflow_run_id = workflow_run_id ,
organization_id = organization_id ,
2025-01-09 22:04:53 +01:00
browser_session_id = browser_session_id ,
2024-12-22 11:16:23 -08:00
)
2024-10-08 23:09:41 -07:00
if block_result . status == BlockStatus . canceled :
LOG . info (
2025-01-14 14:06:43 -08:00
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 " ,
2024-10-08 23:09:41 -07:00
block_type = block . block_type ,
workflow_run_id = workflow_run . workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
2024-11-14 01:32:53 -08:00
block_type_var = block . block_type ,
block_label = block . label ,
2024-10-08 23:09:41 -07:00
)
await self . mark_workflow_run_as_canceled ( workflow_run_id = workflow_run . workflow_run_id )
# We're not sending a webhook here because the workflow run is manually marked as canceled.
2024-11-01 15:13:41 -07:00
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
need_call_webhook = False ,
2025-01-09 22:04:53 +01:00
close_browser_on_completion = browser_session_id is None ,
browser_session_id = browser_session_id ,
2024-11-01 15:13:41 -07:00
)
2024-10-08 23:09:41 -07:00
return workflow_run
elif block_result . status == BlockStatus . failed :
2024-04-04 19:09:19 -07:00
LOG . error (
2025-01-14 14:06:43 -08:00
f " Block with type { block . block_type } at index { block_idx } / { blocks_cnt - 1 } failed for workflow run { workflow_run_id } " ,
2024-04-04 19:09:19 -07:00
block_type = block . block_type ,
workflow_run_id = workflow_run . workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
2024-11-14 01:32:53 -08:00
block_type_var = block . block_type ,
block_label = block . label ,
2024-04-04 19:09:19 -07:00
)
2024-12-02 15:45:16 +08:00
if not block . continue_on_failure :
2025-03-17 21:26:01 -07:00
failure_reason = (
f " { block . block_type } block failed. failure reason: { block_result . failure_reason } "
)
2024-11-15 11:07:44 +08:00
await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id , failure_reason = failure_reason
)
2024-11-01 15:13:41 -07:00
await self . clean_up_workflow (
2024-05-16 18:20:11 -07:00
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
2025-01-09 22:04:53 +01:00
close_browser_on_completion = browser_session_id is None ,
browser_session_id = browser_session_id ,
2024-05-16 18:20:11 -07:00
)
2024-05-16 13:44:53 -07:00
return workflow_run
2024-12-02 15:45:16 +08:00
LOG . warning (
2025-01-14 14:06:43 -08:00
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 } " ,
2024-12-02 15:45:16 +08:00
block_type = block . block_type ,
workflow_run_id = workflow_run . 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 ,
)
2024-10-08 23:09:41 -07:00
elif block_result . status == BlockStatus . terminated :
LOG . info (
2025-01-14 14:06:43 -08:00
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 " ,
2024-10-08 23:09:41 -07:00
block_type = block . block_type ,
workflow_run_id = workflow_run . workflow_run_id ,
block_idx = block_idx ,
block_result = block_result ,
2024-11-14 01:32:53 -08:00
block_type_var = block . block_type ,
block_label = block . label ,
2024-10-08 23:09:41 -07:00
)
2024-12-02 15:45:16 +08:00
if not block . continue_on_failure :
2025-03-17 21:26:01 -07:00
failure_reason = f " { block . block_type } block terminated. Reason: { block_result . failure_reason } "
2024-11-15 11:07:44 +08:00
await self . mark_workflow_run_as_terminated (
workflow_run_id = workflow_run . workflow_run_id , failure_reason = failure_reason
)
2024-11-01 15:13:41 -07:00
await self . clean_up_workflow (
2024-10-08 23:09:41 -07:00
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
2025-01-09 22:04:53 +01:00
close_browser_on_completion = browser_session_id is None ,
browser_session_id = browser_session_id ,
2024-10-08 23:09:41 -07:00
)
return workflow_run
2024-12-02 15:45:16 +08:00
LOG . warning (
2025-01-14 14:06:43 -08:00
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 " ,
2024-12-02 15:45:16 +08:00
block_type = block . block_type ,
workflow_run_id = workflow_run . 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 ,
)
2025-01-22 13:23:10 +08:00
elif 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 . 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 :
2025-03-17 21:26:01 -07:00
failure_reason = f " { block . block_type } block timed out. Reason: { block_result . failure_reason } "
2025-01-22 13:23:10 +08:00
await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id , failure_reason = failure_reason
)
await self . clean_up_workflow (
workflow = workflow ,
workflow_run = workflow_run ,
api_key = api_key ,
close_browser_on_completion = browser_session_id is None ,
browser_session_id = browser_session_id ,
)
return workflow_run
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 . 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 ,
)
2024-11-15 11:07:44 +08:00
except Exception as e :
2024-04-04 19:09:19 -07:00
LOG . exception (
f " Error while executing workflow run { workflow_run . workflow_run_id } " ,
2024-03-21 17:16:56 -07:00
workflow_run_id = workflow_run . workflow_run_id ,
2024-11-14 01:32:53 -08:00
block_idx = block_idx ,
block_type = block . block_type ,
block_label = block . label ,
2024-03-21 17:16:56 -07:00
)
2024-11-15 11:07:44 +08:00
2025-01-09 12:34:25 -08:00
exception_message = f " Unexpected error: { str ( e ) } "
2024-11-15 11:07:44 +08:00
if isinstance ( e , SkyvernException ) :
2025-01-05 22:58:24 -08:00
exception_message = f " unexpected SkyvernException( { e . __class__ . __name__ } ): { str ( e ) } "
2024-11-15 11:07:44 +08:00
2025-03-17 21:26:01 -07:00
failure_reason = f " { block . block_type } block failed. failure reason: { exception_message } "
2024-11-15 11:07:44 +08:00
await self . mark_workflow_run_as_failed (
workflow_run_id = workflow_run . workflow_run_id , failure_reason = failure_reason
)
2025-01-09 22:04:53 +01: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 = browser_session_id is None ,
)
2024-05-16 13:44:53 -07:00
return workflow_run
2024-04-04 19:09:19 -07:00
2024-12-22 17:49:33 -08:00
refreshed_workflow_run = await app . DATABASE . get_workflow_run (
workflow_run_id = workflow_run . workflow_run_id ,
organization_id = organization_id ,
)
2024-11-14 01:32:53 -08:00
if refreshed_workflow_run and refreshed_workflow_run . status not in (
WorkflowRunStatus . canceled ,
WorkflowRunStatus . failed ,
WorkflowRunStatus . terminated ,
2025-01-22 13:23:10 +08:00
WorkflowRunStatus . timed_out ,
2024-11-14 01:32:53 -08:00
) :
await self . mark_workflow_run_as_completed ( workflow_run_id = workflow_run . workflow_run_id )
else :
LOG . info (
2025-01-22 13:23:10 +08:00
" Workflow run is already timed_out, canceled, failed, or terminated, not marking as completed " ,
2024-11-14 01:32:53 -08:00
workflow_run_id = workflow_run . workflow_run_id ,
workflow_run_status = refreshed_workflow_run . status if refreshed_workflow_run else None ,
)
2025-01-09 22:04:53 +01: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 = browser_session_id is None ,
)
2025-02-02 03:10:38 +08:00
# Track workflow run duration when completed
2025-02-04 20:40:23 +08:00
duration_seconds = ( datetime . now ( UTC ) - workflow_run . created_at . replace ( tzinfo = UTC ) ) . total_seconds ( )
2025-02-02 03:10:38 +08:00
LOG . info (
" Workflow run duration metrics " ,
workflow_run_id = workflow_run_id ,
workflow_id = workflow_run . workflow_id ,
duration_seconds = duration_seconds ,
2025-02-18 15:26:30 +08:00
workflow_run_status = WorkflowRunStatus . completed ,
2025-02-02 03:10:38 +08:00
organization_id = organization_id ,
)
2024-03-01 10:09:30 -08:00
return workflow_run
async def create_workflow (
self ,
organization_id : str ,
title : str ,
workflow_definition : WorkflowDefinition ,
description : str | None = None ,
2024-05-16 10:51:22 -07:00
proxy_location : ProxyLocation | None = None ,
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 ,
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 ,
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 ,
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 ,
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 ,
2024-03-01 10:09:30 -08:00
)
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 )
2024-03-01 10:09:30 -08:00
return workflow
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 ,
title : str = " " ,
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 ,
title = title ,
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-01-25 02:59:02 +08:00
title : str = " " ,
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 .
"""
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-01-25 02:59:02 +08:00
title = title ,
2025-01-25 04:08:51 +08:00
statuses = statuses ,
2024-05-16 10:51:22 -07:00
)
2024-03-01 10:09:30 -08:00
async def update_workflow (
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 :
2024-03-21 17:16:56 -07:00
if workflow_definition :
workflow_definition . validate ( )
2024-05-16 10:51:22 -07:00
2024-03-01 10:09:30 -08:00
return await app . DATABASE . update_workflow (
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
)
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 (
self , organization_id : str , page : int = 1 , page_size : int = 10 , status : list [ WorkflowRunStatus ] | None = None
) - > list [ WorkflowRun ] :
return await app . DATABASE . get_workflow_runs (
organization_id = organization_id , page = page , page_size = page_size , 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 ,
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 ,
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 ,
2024-07-09 11:26:44 -07:00
) - > WorkflowRun :
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 ,
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 ,
2024-03-01 10:09:30 -08:00
)
async def mark_workflow_run_as_completed ( self , workflow_run_id : str ) - > None :
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
)
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . completed ,
)
2024-11-15 11:07:44 +08:00
async def mark_workflow_run_as_failed ( self , workflow_run_id : str , failure_reason : str | None ) - > None :
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
)
2024-03-01 10:09:30 -08:00
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . failed ,
2024-11-15 11:07:44 +08:00
failure_reason = failure_reason ,
2024-03-01 10:09:30 -08:00
)
async def mark_workflow_run_as_running ( self , workflow_run_id : str ) - > None :
LOG . info (
2024-05-16 13:44:53 -07:00
f " Marking workflow run { workflow_run_id } as running " ,
workflow_run_id = workflow_run_id ,
workflow_status = " running " ,
2024-03-01 10:09:30 -08:00
)
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . running ,
)
2024-11-15 11:07:44 +08:00
async def mark_workflow_run_as_terminated ( self , workflow_run_id : str , failure_reason : str | None ) - > None :
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
)
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . terminated ,
2024-11-15 11:07:44 +08:00
failure_reason = failure_reason ,
2024-03-01 10:09:30 -08:00
)
2024-10-08 23:09:41 -07:00
async def mark_workflow_run_as_canceled ( self , workflow_run_id : str ) - > None :
LOG . info (
f " Marking workflow run { workflow_run_id } as canceled " ,
workflow_run_id = workflow_run_id ,
workflow_status = " canceled " ,
)
await app . DATABASE . update_workflow_run (
workflow_run_id = workflow_run_id ,
status = WorkflowRunStatus . canceled ,
)
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-04-03 16:01:03 -07:00
async def create_bitwarden_login_credential_parameter (
self ,
workflow_id : str ,
bitwarden_client_id_aws_secret_key : str ,
bitwarden_client_secret_aws_secret_key : str ,
bitwarden_master_password_aws_secret_key : str ,
key : str ,
2025-03-03 11:45:50 -05:00
url_parameter_key : str | None = None ,
2024-04-03 16:01:03 -07:00
description : str | None = None ,
2024-06-10 22:06:58 -07:00
bitwarden_collection_id : str | None = None ,
2025-03-03 11:45:50 -05:00
bitwarden_item_id : str | None = None ,
2024-04-03 16:01:03 -07:00
) - > Parameter :
return await app . DATABASE . create_bitwarden_login_credential_parameter (
workflow_id = workflow_id ,
bitwarden_client_id_aws_secret_key = bitwarden_client_id_aws_secret_key ,
bitwarden_client_secret_aws_secret_key = bitwarden_client_secret_aws_secret_key ,
bitwarden_master_password_aws_secret_key = bitwarden_master_password_aws_secret_key ,
key = key ,
2025-03-03 11:45:50 -05:00
url_parameter_key = url_parameter_key ,
2024-04-03 16:01:03 -07:00
description = description ,
2024-06-10 22:06:58 -07:00
bitwarden_collection_id = bitwarden_collection_id ,
2025-03-03 11:45:50 -05:00
bitwarden_item_id = bitwarden_item_id ,
2024-04-03 16:01:03 -07:00
)
2025-02-14 00:00:19 +08:00
async def create_credential_parameter (
self ,
workflow_id : str ,
key : str ,
credential_id : str ,
description : str | None = None ,
) - > CredentialParameter :
return await app . DATABASE . create_credential_parameter (
workflow_id = workflow_id ,
key = key ,
credential_id = credential_id ,
description = description ,
)
2024-07-11 09:48:14 -07:00
async def create_bitwarden_sensitive_information_parameter (
self ,
workflow_id : str ,
bitwarden_client_id_aws_secret_key : str ,
bitwarden_client_secret_aws_secret_key : str ,
bitwarden_master_password_aws_secret_key : str ,
bitwarden_collection_id : str ,
bitwarden_identity_key : str ,
bitwarden_identity_fields : list [ str ] ,
key : str ,
description : str | None = None ,
) - > Parameter :
return await app . DATABASE . create_bitwarden_sensitive_information_parameter (
workflow_id = workflow_id ,
bitwarden_client_id_aws_secret_key = bitwarden_client_id_aws_secret_key ,
bitwarden_client_secret_aws_secret_key = bitwarden_client_secret_aws_secret_key ,
bitwarden_master_password_aws_secret_key = bitwarden_master_password_aws_secret_key ,
bitwarden_collection_id = bitwarden_collection_id ,
bitwarden_identity_key = bitwarden_identity_key ,
bitwarden_identity_fields = bitwarden_identity_fields ,
key = key ,
description = description ,
)
2024-10-03 16:18:21 -07:00
async def create_bitwarden_credit_card_data_parameter (
self ,
workflow_id : str ,
bitwarden_client_id_aws_secret_key : str ,
bitwarden_client_secret_aws_secret_key : str ,
bitwarden_master_password_aws_secret_key : str ,
bitwarden_collection_id : str ,
bitwarden_item_id : str ,
key : str ,
description : str | None = None ,
) - > Parameter :
return await app . DATABASE . create_bitwarden_credit_card_data_parameter (
workflow_id = workflow_id ,
bitwarden_client_id_aws_secret_key = bitwarden_client_id_aws_secret_key ,
bitwarden_client_secret_aws_secret_key = bitwarden_client_secret_aws_secret_key ,
bitwarden_master_password_aws_secret_key = bitwarden_master_password_aws_secret_key ,
bitwarden_collection_id = bitwarden_collection_id ,
bitwarden_item_id = bitwarden_item_id ,
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 )
2024-10-15 06:26:16 -07:00
async def build_workflow_run_status_response_by_workflow_id (
self ,
workflow_run_id : str ,
organization_id : str ,
2024-12-31 11:24:09 -08:00
include_cost : bool = False ,
2025-03-17 15:19:18 -07:00
) - > WorkflowRunResponse :
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 ,
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 ,
organization_id : str ,
2024-12-31 11:24:09 -08:00
include_cost : bool = False ,
2025-03-17 15:19:18 -07:00
) - > WorkflowRunResponse :
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 )
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 )
2024-05-13 00:03:31 -07:00
screenshot_artifacts = [ ]
screenshot_urls : list [ str ] | None = None
2024-03-01 10:09:30 -08:00
# get the last screenshot for the last 3 tasks of the workflow run
for task in workflow_run_tasks [ : : - 1 ] :
screenshot_artifact = await app . DATABASE . get_latest_artifact (
task_id = task . task_id ,
2024-05-16 18:20:11 -07:00
artifact_types = [
ArtifactType . SCREENSHOT_ACTION ,
ArtifactType . SCREENSHOT_FINAL ,
] ,
2024-03-01 10:09:30 -08:00
organization_id = organization_id ,
)
if screenshot_artifact :
2024-05-13 00:03:31 -07:00
screenshot_artifacts . append ( screenshot_artifact )
if len ( screenshot_artifacts ) > = 3 :
2024-03-01 10:09:30 -08:00
break
2024-05-13 00:03:31 -07:00
if screenshot_artifacts :
screenshot_urls = await app . ARTIFACT_MANAGER . get_share_links ( screenshot_artifacts )
2024-03-01 10:09:30 -08:00
recording_url = None
recording_artifact = await app . DATABASE . get_artifact_for_workflow_run (
2024-05-16 18:20:11 -07:00
workflow_run_id = workflow_run_id ,
artifact_type = ArtifactType . RECORDING ,
organization_id = organization_id ,
2024-03-01 10:09:30 -08:00
)
if recording_artifact :
recording_url = await app . ARTIFACT_MANAGER . get_share_link ( recording_artifact )
2025-02-26 17:19:05 -08:00
downloaded_files : list [ FileInfo ] | None = None
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-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 ,
task_id = None ,
workflow_run_id = workflow_run . workflow_run_id ,
2024-11-29 16:05:44 +08:00
)
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-02-25 11:59:22 -08:00
extracted_information = {
2025-02-26 16:19:56 -08:00
output_parameter . key : output . value [ EXTRACTED_INFORMATION_KEY ]
2025-02-25 11:59:22 -08:00
for output_parameter , output in output_parameter_tuples
2025-02-26 16:19:56 -08:00
if output . value is not None
and isinstance ( output . value , dict )
2025-02-26 16:27:47 -08:00
and EXTRACTED_INFORMATION_KEY in output . value
2025-02-26 16:19:56 -08:00
and output . value [ EXTRACTED_INFORMATION_KEY ] is not None
2025-02-25 11:59:22 -08:00
}
2025-02-26 16:19:56 -08:00
outputs [ EXTRACTED_INFORMATION_KEY ] = extracted_information
2024-09-10 13:27:31 -07:00
2024-12-31 11:24:09 -08:00
total_steps = None
total_cost = None
if include_cost :
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
)
2025-01-17 09:17:31 -08:00
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 ]
2024-12-31 11:24:09 -08:00
total_steps = len ( workflow_run_steps )
# 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
2024-12-31 12:27:35 -08:00
successful_steps = [ step for step in workflow_run_steps if step . status == StepStatus . completed ]
2025-01-17 09:17:31 -08:00
total_cost = 0.1 * ( len ( successful_steps ) + len ( text_prompt_blocks ) )
2025-03-17 15:19:18 -07:00
return WorkflowRunResponse (
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 ,
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 ,
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 ,
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 ]
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-02-18 00:27:21 +08:00
close_browser_on_completion = close_browser_on_completion and browser_session_id is None ,
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 )
2024-09-07 01:57:47 -07:00
if workflow . persist_browser_session and browser_state . browser_artifacts . browser_session_dir :
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 ) :
await app . STORAGE . save_downloaded_files (
2025-01-28 15:04:18 +08:00
workflow_run . organization_id , task_id = None , workflow_run_id = 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 ,
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
2024-05-16 13:44:53 -07:00
# send webhook to the webhook callback url
2024-03-01 10:09:30 -08:00
payload = workflow_run_status_response . model_dump_json ( )
2025-01-13 19:28:14 -08:00
headers = generate_skyvern_webhook_headers (
2024-03-01 10:09:30 -08:00
payload = payload ,
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 ,
payload = payload ,
headers = headers ,
)
try :
2025-01-14 08:59:53 -08:00
async with httpx . AsyncClient ( ) as client :
resp = await client . post (
url = workflow_run . webhook_callback_url , data = payload , headers = headers , timeout = httpx . Timeout ( 30.0 )
)
2024-11-01 15:13:41 -07:00
if resp . status_code == 200 :
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 ,
)
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 ,
2024-08-08 20:52:24 +03:00
webhook_data = payload ,
2024-03-01 10:09:30 -08:00
resp = resp ,
resp_code = resp . status_code ,
resp_text = resp . text ,
)
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
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 ,
) - > 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
2024-03-24 22:55:38 -07:00
try :
2024-05-16 10:51:22 -07:00
if workflow_permanent_id :
existing_latest_workflow = await self . get_workflow_by_permanent_id (
workflow_permanent_id = workflow_permanent_id ,
organization_id = organization_id ,
2024-09-19 11:15:07 -07:00
exclude_deleted = False ,
2024-05-16 10:51:22 -07:00
)
existing_version = existing_latest_workflow . version
workflow = await self . create_workflow (
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 ,
2024-05-16 10:51:22 -07:00
workflow_permanent_id = workflow_permanent_id ,
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 ,
2024-05-16 10:51:22 -07:00
)
else :
workflow = await self . create_workflow (
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 ,
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 ,
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
new_workflow_id = workflow . workflow_id
2024-03-24 22:55:38 -07:00
# Create parameters from the request
2024-04-16 15:41:44 -07:00
parameters : dict [ str , PARAMETER_TYPE ] = { }
2024-03-24 22:55:38 -07:00
duplicate_parameter_keys = set ( )
2024-04-09 00:39:12 -07:00
2024-05-16 13:08:24 -07:00
# Check if user's trying to manually create an output parameter
if any (
parameter . parameter_type == ParameterType . OUTPUT for parameter in request . workflow_definition . parameters
) :
raise InvalidWorkflowDefinition ( message = " Cannot manually create output parameters " )
# Check if any parameter keys collide with automatically created output parameter keys
block_labels = [ block . label for block in request . workflow_definition . blocks ]
# TODO (kerem): Check if block labels are unique
output_parameter_keys = [ f " { block_label } _output " for block_label in block_labels ]
parameter_keys = [ parameter . key for parameter in request . workflow_definition . parameters ]
if any ( key in output_parameter_keys for key in parameter_keys ) :
raise WorkflowDefinitionHasReservedParameterKeys (
reserved_keys = output_parameter_keys , parameter_keys = parameter_keys
)
# Create output parameters for all blocks
block_output_parameters = await WorkflowService . _create_all_output_parameters_for_workflow (
2024-05-16 18:20:11 -07:00
workflow_id = workflow . workflow_id ,
block_yamls = request . workflow_definition . blocks ,
2024-05-16 13:08:24 -07:00
)
for block_output_parameter in block_output_parameters . values ( ) :
parameters [ block_output_parameter . key ] = block_output_parameter
2024-04-09 00:39:12 -07:00
# We're going to process context parameters after other parameters since they depend on the other parameters
context_parameter_yamls = [ ]
2024-03-24 22:55:38 -07:00
for parameter in request . workflow_definition . parameters :
if parameter . key in parameters :
LOG . error ( f " Duplicate parameter key { parameter . key } " )
duplicate_parameter_keys . add ( parameter . key )
continue
if parameter . parameter_type == ParameterType . AWS_SECRET :
parameters [ parameter . key ] = await self . create_aws_secret_parameter (
workflow_id = workflow . workflow_id ,
aws_key = parameter . aws_key ,
key = parameter . key ,
description = parameter . description ,
)
2025-02-14 00:00:19 +08:00
elif parameter . parameter_type == ParameterType . CREDENTIAL :
parameters [ parameter . key ] = await self . create_credential_parameter (
workflow_id = workflow . workflow_id ,
key = parameter . key ,
description = parameter . description ,
credential_id = parameter . credential_id ,
)
2024-04-03 16:01:03 -07:00
elif parameter . parameter_type == ParameterType . BITWARDEN_LOGIN_CREDENTIAL :
2025-03-03 11:45:50 -05:00
if not parameter . bitwarden_collection_id and not parameter . bitwarden_item_id :
2024-10-23 23:20:51 -07:00
raise WorkflowParameterMissingRequiredValue (
workflow_parameter_type = ParameterType . BITWARDEN_LOGIN_CREDENTIAL ,
workflow_parameter_key = parameter . key ,
2025-03-03 11:45:50 -05:00
required_value = " bitwarden_collection_id or bitwarden_item_id " ,
)
if parameter . bitwarden_collection_id and not parameter . url_parameter_key :
raise WorkflowParameterMissingRequiredValue (
workflow_parameter_type = ParameterType . BITWARDEN_LOGIN_CREDENTIAL ,
workflow_parameter_key = parameter . key ,
required_value = " url_parameter_key " ,
2024-10-23 23:20:51 -07:00
)
2024-04-03 16:01:03 -07:00
parameters [ parameter . key ] = await self . create_bitwarden_login_credential_parameter (
workflow_id = workflow . workflow_id ,
bitwarden_client_id_aws_secret_key = parameter . bitwarden_client_id_aws_secret_key ,
bitwarden_client_secret_aws_secret_key = parameter . bitwarden_client_secret_aws_secret_key ,
bitwarden_master_password_aws_secret_key = parameter . bitwarden_master_password_aws_secret_key ,
url_parameter_key = parameter . url_parameter_key ,
key = parameter . key ,
description = parameter . description ,
2024-06-10 22:06:58 -07:00
bitwarden_collection_id = parameter . bitwarden_collection_id ,
2025-03-03 11:45:50 -05:00
bitwarden_item_id = parameter . bitwarden_item_id ,
2024-04-03 16:01:03 -07:00
)
2024-07-11 09:48:14 -07:00
elif parameter . parameter_type == ParameterType . BITWARDEN_SENSITIVE_INFORMATION :
parameters [ parameter . key ] = await self . create_bitwarden_sensitive_information_parameter (
workflow_id = workflow . workflow_id ,
bitwarden_client_id_aws_secret_key = parameter . bitwarden_client_id_aws_secret_key ,
bitwarden_client_secret_aws_secret_key = parameter . bitwarden_client_secret_aws_secret_key ,
bitwarden_master_password_aws_secret_key = parameter . bitwarden_master_password_aws_secret_key ,
2024-09-08 15:07:03 -07:00
# TODO: remove "# type: ignore" after ensuring bitwarden_collection_id is always set
bitwarden_collection_id = parameter . bitwarden_collection_id , # type: ignore
2024-07-11 09:48:14 -07:00
bitwarden_identity_key = parameter . bitwarden_identity_key ,
bitwarden_identity_fields = parameter . bitwarden_identity_fields ,
key = parameter . key ,
description = parameter . description ,
)
2024-10-03 16:18:21 -07:00
elif parameter . parameter_type == ParameterType . BITWARDEN_CREDIT_CARD_DATA :
parameters [ parameter . key ] = await self . create_bitwarden_credit_card_data_parameter (
workflow_id = workflow . workflow_id ,
bitwarden_client_id_aws_secret_key = parameter . bitwarden_client_id_aws_secret_key ,
bitwarden_client_secret_aws_secret_key = parameter . bitwarden_client_secret_aws_secret_key ,
bitwarden_master_password_aws_secret_key = parameter . bitwarden_master_password_aws_secret_key ,
2024-10-15 12:06:50 -07:00
# TODO: remove "# type: ignore" after ensuring bitwarden_collection_id is always set
bitwarden_collection_id = parameter . bitwarden_collection_id , # type: ignore
2025-03-03 11:45:50 -05:00
bitwarden_item_id = parameter . bitwarden_item_id , # type: ignore
2024-10-03 16:18:21 -07:00
key = parameter . key ,
description = parameter . description ,
)
2024-03-24 22:55:38 -07:00
elif parameter . parameter_type == ParameterType . WORKFLOW :
parameters [ parameter . key ] = await self . create_workflow_parameter (
workflow_id = workflow . workflow_id ,
workflow_parameter_type = parameter . workflow_parameter_type ,
key = parameter . key ,
default_value = parameter . default_value ,
description = parameter . description ,
)
elif parameter . parameter_type == ParameterType . OUTPUT :
parameters [ parameter . key ] = await self . create_output_parameter (
workflow_id = workflow . workflow_id ,
key = parameter . key ,
description = parameter . description ,
)
2024-04-09 00:39:12 -07:00
elif parameter . parameter_type == ParameterType . CONTEXT :
context_parameter_yamls . append ( parameter )
else :
LOG . error ( f " Invalid parameter type { parameter . parameter_type } " )
# Now we can process the context parameters since all other parameters have been created
for context_parameter in context_parameter_yamls :
2024-04-16 15:41:44 -07:00
if context_parameter . source_parameter_key not in parameters :
raise ContextParameterSourceNotDefined (
2024-05-16 18:20:11 -07:00
context_parameter_key = context_parameter . key ,
source_key = context_parameter . source_parameter_key ,
2024-04-16 15:41:44 -07:00
)
if context_parameter . key in parameters :
LOG . error ( f " Duplicate parameter key { context_parameter . key } " )
duplicate_parameter_keys . add ( context_parameter . key )
continue
# We're only adding the context parameter to the parameters dict, we're not creating it in the database
# It'll only be stored in the `workflow.workflow_definition`
# todo (kerem): should we have a database table for context parameters?
2024-04-09 00:39:12 -07:00
parameters [ context_parameter . key ] = ContextParameter (
key = context_parameter . key ,
description = context_parameter . description ,
2024-04-16 15:41:44 -07:00
source = parameters [ context_parameter . source_parameter_key ] ,
2024-04-09 00:39:12 -07:00
# Context parameters don't have a default value, the value always depends on the source parameter
value = None ,
)
2024-03-24 22:55:38 -07:00
if duplicate_parameter_keys :
raise WorkflowDefinitionHasDuplicateParameterKeys ( duplicate_keys = duplicate_parameter_keys )
# Create blocks from the request
block_label_mapping = { }
blocks = [ ]
for block_yaml in request . workflow_definition . blocks :
2024-05-16 13:08:24 -07:00
block = await self . block_yaml_to_block ( workflow , block_yaml , parameters )
2024-03-24 22:55:38 -07:00
blocks . append ( block )
block_label_mapping [ block . label ] = block
# Set the blocks for the workflow definition
2024-04-16 15:41:44 -07:00
workflow_definition = WorkflowDefinition ( parameters = parameters . values ( ) , blocks = blocks )
2024-03-24 22:55:38 -07:00
workflow = await self . update_workflow (
workflow_id = workflow . workflow_id ,
2024-05-15 08:43:36 -07:00
organization_id = organization_id ,
2024-03-24 22:55:38 -07:00
workflow_definition = workflow_definition ,
)
LOG . info (
f " Created workflow from request, title: { request . title } " ,
parameter_keys = [ parameter . key for parameter in parameters . values ( ) ] ,
block_labels = [ block . label for block in blocks ] ,
organization_id = organization_id ,
title = request . title ,
workflow_id = workflow . workflow_id ,
)
return workflow
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 } " ,
)
@staticmethod
async def _create_all_output_parameters_for_workflow (
workflow_id : str , block_yamls : list [ BLOCK_YAML_TYPES ]
) - > dict [ str , OutputParameter ] :
output_parameters = { }
for block_yaml in block_yamls :
2024-11-25 11:19:35 -08:00
output_parameter = await WorkflowService . create_output_parameter_for_block (
2024-05-16 13:08:24 -07:00
workflow_id = workflow_id , block_yaml = block_yaml
)
output_parameters [ block_yaml . label ] = output_parameter
# Recursively create output parameters for for loop blocks
if isinstance ( block_yaml , ForLoopBlockYAML ) :
output_parameters . update (
await WorkflowService . _create_all_output_parameters_for_workflow (
workflow_id = workflow_id , block_yamls = block_yaml . loop_blocks
)
)
return output_parameters
@staticmethod
async def block_yaml_to_block (
2024-05-16 18:20:11 -07:00
workflow : Workflow ,
block_yaml : BLOCK_YAML_TYPES ,
parameters : dict [ str , Parameter ] ,
2024-05-16 13:08:24 -07:00
) - > BlockTypeVar :
output_parameter = parameters [ f " { block_yaml . label } _output " ]
2024-03-24 22:55:38 -07:00
if block_yaml . block_type == BlockType . TASK :
task_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
return TaskBlock (
label = block_yaml . label ,
url = block_yaml . url ,
title = block_yaml . title ,
parameters = task_block_parameters ,
output_parameter = output_parameter ,
navigation_goal = block_yaml . navigation_goal ,
data_extraction_goal = block_yaml . data_extraction_goal ,
data_schema = block_yaml . data_schema ,
error_code_mapping = block_yaml . error_code_mapping ,
2024-05-30 11:22:28 -07:00
max_steps_per_run = block_yaml . max_steps_per_run ,
2024-03-24 22:55:38 -07:00
max_retries = block_yaml . max_retries ,
2024-06-02 23:24:30 -07:00
complete_on_download = block_yaml . complete_on_download ,
2024-09-11 21:56:38 -07:00
download_suffix = block_yaml . download_suffix ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2024-09-08 15:07:03 -07:00
totp_verification_url = block_yaml . totp_verification_url ,
totp_identifier = block_yaml . totp_identifier ,
2024-10-15 12:06:50 -07:00
cache_actions = block_yaml . cache_actions ,
2024-12-13 16:54:48 +08:00
complete_criterion = block_yaml . complete_criterion ,
terminate_criterion = block_yaml . terminate_criterion ,
2025-03-19 18:16:55 -07:00
complete_verification = block_yaml . complete_verification ,
2024-03-24 22:55:38 -07:00
)
elif block_yaml . block_type == BlockType . FOR_LOOP :
2024-04-10 13:47:25 -07:00
loop_blocks = [
2024-05-16 13:08:24 -07:00
await WorkflowService . block_yaml_to_block ( workflow , loop_block , parameters )
2024-04-10 13:47:25 -07:00
for loop_block in block_yaml . loop_blocks
]
2024-12-13 02:37:37 +08:00
loop_over_parameter : Parameter | None = None
if block_yaml . loop_over_parameter_key :
loop_over_parameter = parameters [ block_yaml . loop_over_parameter_key ]
if block_yaml . loop_variable_reference :
# it's backaward compatible with jinja style parameter and context paramter
# we trim the format like {{ loop_key }} into loop_key to initialize the context parater,
# otherwise it might break the context parameter initialization chain, blow up the worklofw parameters
# TODO: consider remove this if we totally give up context parameter
trimmed_key = block_yaml . loop_variable_reference . strip ( " {} " )
if trimmed_key in parameters :
loop_over_parameter = parameters [ trimmed_key ]
if loop_over_parameter is None and not block_yaml . loop_variable_reference :
2025-01-05 22:58:24 -08:00
raise Exception ( " Loop value parameter is required for for loop block " )
2024-12-13 02:37:37 +08:00
2024-03-24 22:55:38 -07:00
return ForLoopBlock (
label = block_yaml . label ,
2024-04-03 16:01:03 -07:00
loop_over = loop_over_parameter ,
2024-12-13 02:37:37 +08:00
loop_variable_reference = block_yaml . loop_variable_reference ,
2024-04-10 13:47:25 -07:00
loop_blocks = loop_blocks ,
2024-03-24 22:55:38 -07:00
output_parameter = output_parameter ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2025-01-30 03:40:25 +08:00
complete_if_empty = block_yaml . complete_if_empty ,
2024-03-24 22:55:38 -07:00
)
elif block_yaml . block_type == BlockType . CODE :
return CodeBlock (
label = block_yaml . label ,
code = block_yaml . code ,
2024-05-16 17:11:49 -07:00
parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
) ,
2024-03-24 22:55:38 -07:00
output_parameter = output_parameter ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2024-03-24 22:55:38 -07:00
)
2024-03-25 00:57:37 -07:00
elif block_yaml . block_type == BlockType . TEXT_PROMPT :
return TextPromptBlock (
label = block_yaml . label ,
llm_key = block_yaml . llm_key ,
prompt = block_yaml . prompt ,
2024-05-16 17:11:49 -07:00
parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
) ,
2024-03-25 00:57:37 -07:00
json_schema = block_yaml . json_schema ,
output_parameter = output_parameter ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2024-03-25 00:57:37 -07:00
)
2024-03-28 16:46:54 -07:00
elif block_yaml . block_type == BlockType . DOWNLOAD_TO_S3 :
return DownloadToS3Block (
label = block_yaml . label ,
output_parameter = output_parameter ,
url = block_yaml . url ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2024-03-28 16:46:54 -07:00
)
2024-04-04 19:09:19 -07:00
elif block_yaml . block_type == BlockType . UPLOAD_TO_S3 :
return UploadToS3Block (
label = block_yaml . label ,
output_parameter = output_parameter ,
path = block_yaml . path ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2024-04-04 19:09:19 -07:00
)
2025-03-23 15:37:20 -07:00
elif block_yaml . block_type == BlockType . FILE_UPLOAD :
return FileUploadBlock (
label = block_yaml . label ,
output_parameter = output_parameter ,
storage_type = block_yaml . storage_type ,
s3_bucket = block_yaml . s3_bucket ,
aws_access_key_id = block_yaml . aws_access_key_id ,
aws_secret_access_key = block_yaml . aws_secret_access_key ,
region_name = block_yaml . region_name ,
path = block_yaml . path ,
continue_on_failure = block_yaml . continue_on_failure ,
)
2024-03-31 01:58:11 -07:00
elif block_yaml . block_type == BlockType . SEND_EMAIL :
return SendEmailBlock (
label = block_yaml . label ,
output_parameter = output_parameter ,
smtp_host = parameters [ block_yaml . smtp_host_secret_parameter_key ] ,
smtp_port = parameters [ block_yaml . smtp_port_secret_parameter_key ] ,
smtp_username = parameters [ block_yaml . smtp_username_secret_parameter_key ] ,
smtp_password = parameters [ block_yaml . smtp_password_secret_parameter_key ] ,
sender = block_yaml . sender ,
recipients = block_yaml . recipients ,
subject = block_yaml . subject ,
body = block_yaml . body ,
file_attachments = block_yaml . file_attachments or [ ] ,
2024-06-03 13:48:42 -07:00
continue_on_failure = block_yaml . continue_on_failure ,
2024-03-31 01:58:11 -07:00
)
2024-07-05 17:08:20 -07:00
elif block_yaml . block_type == BlockType . FILE_URL_PARSER :
return FileParserBlock (
label = block_yaml . label ,
output_parameter = output_parameter ,
file_url = block_yaml . file_url ,
file_type = block_yaml . file_type ,
continue_on_failure = block_yaml . continue_on_failure ,
2025-01-20 12:33:54 -08:00
)
elif block_yaml . block_type == BlockType . PDF_PARSER :
return PDFParserBlock (
label = block_yaml . label ,
output_parameter = output_parameter ,
file_url = block_yaml . file_url ,
json_schema = block_yaml . json_schema ,
continue_on_failure = block_yaml . continue_on_failure ,
2024-07-05 17:08:20 -07:00
)
2024-11-21 15:12:26 +08:00
elif block_yaml . block_type == BlockType . VALIDATION :
validation_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
if not block_yaml . complete_criterion and not block_yaml . terminate_criterion :
raise Exception ( " Both complete criterion and terminate criterion are empty " )
return ValidationBlock (
label = block_yaml . label ,
2024-11-26 11:29:33 +08:00
task_type = TaskType . validation ,
2024-11-21 15:12:26 +08:00
parameters = validation_block_parameters ,
output_parameter = output_parameter ,
complete_criterion = block_yaml . complete_criterion ,
terminate_criterion = block_yaml . terminate_criterion ,
error_code_mapping = block_yaml . error_code_mapping ,
continue_on_failure = block_yaml . continue_on_failure ,
# only need one step for validation block
max_steps_per_run = 1 ,
)
elif block_yaml . block_type == BlockType . ACTION :
action_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
2024-11-21 17:38:42 +08:00
if not block_yaml . navigation_goal :
raise Exception ( " empty action instruction " )
2024-11-21 15:12:26 +08:00
return ActionBlock (
label = block_yaml . label ,
url = block_yaml . url ,
title = block_yaml . title ,
2024-11-26 11:29:33 +08:00
task_type = TaskType . action ,
2024-11-21 15:12:26 +08:00
parameters = action_block_parameters ,
output_parameter = output_parameter ,
navigation_goal = block_yaml . navigation_goal ,
error_code_mapping = block_yaml . error_code_mapping ,
max_retries = block_yaml . max_retries ,
complete_on_download = block_yaml . complete_on_download ,
download_suffix = block_yaml . download_suffix ,
continue_on_failure = block_yaml . continue_on_failure ,
totp_verification_url = block_yaml . totp_verification_url ,
totp_identifier = block_yaml . totp_identifier ,
cache_actions = block_yaml . cache_actions ,
2025-03-19 18:16:55 -07:00
# DO NOT run complete verification for action block
complete_verification = False ,
2024-11-21 15:12:26 +08:00
max_steps_per_run = 1 ,
)
2024-11-22 14:44:22 +08:00
elif block_yaml . block_type == BlockType . NAVIGATION :
navigation_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
return NavigationBlock (
label = block_yaml . label ,
url = block_yaml . url ,
title = block_yaml . title ,
parameters = navigation_block_parameters ,
output_parameter = output_parameter ,
navigation_goal = block_yaml . navigation_goal ,
error_code_mapping = block_yaml . error_code_mapping ,
max_steps_per_run = block_yaml . max_steps_per_run ,
max_retries = block_yaml . max_retries ,
complete_on_download = block_yaml . complete_on_download ,
download_suffix = block_yaml . download_suffix ,
continue_on_failure = block_yaml . continue_on_failure ,
totp_verification_url = block_yaml . totp_verification_url ,
totp_identifier = block_yaml . totp_identifier ,
cache_actions = block_yaml . cache_actions ,
2024-12-13 16:54:48 +08:00
complete_criterion = block_yaml . complete_criterion ,
terminate_criterion = block_yaml . terminate_criterion ,
2025-03-19 18:16:55 -07:00
complete_verification = block_yaml . complete_verification ,
2024-11-22 14:44:22 +08:00
)
elif block_yaml . block_type == BlockType . EXTRACTION :
extraction_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
return ExtractionBlock (
label = block_yaml . label ,
url = block_yaml . url ,
title = block_yaml . title ,
parameters = extraction_block_parameters ,
output_parameter = output_parameter ,
data_extraction_goal = block_yaml . data_extraction_goal ,
data_schema = block_yaml . data_schema ,
max_steps_per_run = block_yaml . max_steps_per_run ,
max_retries = block_yaml . max_retries ,
continue_on_failure = block_yaml . continue_on_failure ,
cache_actions = block_yaml . cache_actions ,
2025-03-19 18:16:55 -07:00
complete_verification = False ,
2024-11-22 14:44:22 +08:00
)
elif block_yaml . block_type == BlockType . LOGIN :
login_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
return LoginBlock (
label = block_yaml . label ,
url = block_yaml . url ,
title = block_yaml . title ,
parameters = login_block_parameters ,
output_parameter = output_parameter ,
navigation_goal = block_yaml . navigation_goal ,
error_code_mapping = block_yaml . error_code_mapping ,
max_steps_per_run = block_yaml . max_steps_per_run ,
max_retries = block_yaml . max_retries ,
continue_on_failure = block_yaml . continue_on_failure ,
totp_verification_url = block_yaml . totp_verification_url ,
totp_identifier = block_yaml . totp_identifier ,
cache_actions = block_yaml . cache_actions ,
2024-12-13 16:54:48 +08:00
complete_criterion = block_yaml . complete_criterion ,
terminate_criterion = block_yaml . terminate_criterion ,
2025-03-19 18:16:55 -07:00
complete_verification = block_yaml . complete_verification ,
2024-11-22 14:44:22 +08:00
)
2024-11-25 10:42:34 +08:00
elif block_yaml . block_type == BlockType . WAIT :
2024-12-02 15:01:22 -08:00
if block_yaml . wait_sec < = 0 or block_yaml . wait_sec > settings . WORKFLOW_WAIT_BLOCK_MAX_SEC :
raise InvalidWaitBlockTime ( settings . WORKFLOW_WAIT_BLOCK_MAX_SEC )
2024-11-25 10:42:34 +08:00
return WaitBlock (
label = block_yaml . label ,
wait_sec = block_yaml . wait_sec ,
continue_on_failure = block_yaml . continue_on_failure ,
output_parameter = output_parameter ,
)
2024-11-26 23:36:22 +08:00
elif block_yaml . block_type == BlockType . FILE_DOWNLOAD :
file_download_block_parameters = (
[ parameters [ parameter_key ] for parameter_key in block_yaml . parameter_keys ]
if block_yaml . parameter_keys
else [ ]
)
return FileDownloadBlock (
label = block_yaml . label ,
url = block_yaml . url ,
title = block_yaml . title ,
parameters = file_download_block_parameters ,
output_parameter = output_parameter ,
navigation_goal = block_yaml . navigation_goal ,
error_code_mapping = block_yaml . error_code_mapping ,
max_steps_per_run = block_yaml . max_steps_per_run ,
max_retries = block_yaml . max_retries ,
download_suffix = block_yaml . download_suffix ,
continue_on_failure = block_yaml . continue_on_failure ,
totp_verification_url = block_yaml . totp_verification_url ,
totp_identifier = block_yaml . totp_identifier ,
cache_actions = block_yaml . cache_actions ,
complete_on_download = True ,
2025-03-19 20:01:13 -07:00
complete_verification = True ,
2024-11-26 23:36:22 +08:00
)
2025-01-28 16:59:54 +08:00
elif block_yaml . block_type == BlockType . TaskV2 :
return TaskV2Block (
label = block_yaml . label ,
prompt = block_yaml . prompt ,
url = block_yaml . url ,
totp_verification_url = block_yaml . totp_verification_url ,
totp_identifier = block_yaml . totp_identifier ,
max_iterations = block_yaml . max_iterations ,
2025-03-04 01:07:07 -05:00
max_steps = block_yaml . max_steps ,
2025-01-28 16:59:54 +08:00
output_parameter = output_parameter ,
2025-02-01 04:13:00 +08:00
)
elif block_yaml . block_type == BlockType . GOTO_URL :
return UrlBlock (
label = block_yaml . label ,
url = block_yaml . url ,
output_parameter = output_parameter ,
2025-03-19 18:16:55 -07:00
complete_verification = False ,
2025-01-28 16:59:54 +08:00
)
2024-11-26 23:36:22 +08:00
2024-03-24 22:55:38 -07:00
raise ValueError ( f " Invalid block type { block_yaml . block_type } " )
2024-11-28 10:26:15 -08:00
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 ,
proxy_location : ProxyLocation | None = None ,
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 ,
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