Files
Dorod-Sky/skyvern/schemas/workflows.py

1013 lines
38 KiB
Python
Raw Normal View History

import abc
from dataclasses import dataclass
from enum import StrEnum
from typing import Annotated, Any, Literal
import structlog
from pydantic import BaseModel, Field, field_validator, model_validator
from skyvern.config import settings
from skyvern.forge.sdk.workflow.models.parameter import OutputParameter, ParameterType, WorkflowParameterType
from skyvern.schemas.runs import GeoTarget, ProxyLocation, RunEngine
from skyvern.utils.strings import sanitize_identifier
from skyvern.utils.templating import replace_jinja_reference
LOG = structlog.get_logger()
def sanitize_block_label(value: str) -> str:
"""Sanitizes a block label to be a valid Python/Jinja2 identifier.
Block labels are used to create output parameter keys (e.g., '{label}_output')
which are then used as Jinja2 template variable names.
Args:
value: The raw label value to sanitize
Returns:
A sanitized label that is a valid Python identifier
"""
return sanitize_identifier(value, default="block")
def sanitize_parameter_key(value: str) -> str:
"""Sanitizes a parameter key to be a valid Python/Jinja2 identifier.
Parameter keys are used as Jinja2 template variable names.
Args:
value: The raw key value to sanitize
Returns:
A sanitized key that is a valid Python identifier
"""
return sanitize_identifier(value, default="parameter")
def _replace_references_in_value(value: Any, old_key: str, new_key: str) -> Any:
"""Recursively replaces Jinja references in a value (string, dict, or list)."""
if isinstance(value, str):
return replace_jinja_reference(value, old_key, new_key)
elif isinstance(value, dict):
return {k: _replace_references_in_value(v, old_key, new_key) for k, v in value.items()}
elif isinstance(value, list):
return [_replace_references_in_value(item, old_key, new_key) for item in value]
return value
def _replace_direct_string_in_value(value: Any, old_key: str, new_key: str) -> Any:
"""Recursively replaces exact string matches in a value (for fields like source_parameter_key)."""
if isinstance(value, str):
return new_key if value == old_key else value
elif isinstance(value, dict):
return {k: _replace_direct_string_in_value(v, old_key, new_key) for k, v in value.items()}
elif isinstance(value, list):
return [_replace_direct_string_in_value(item, old_key, new_key) for item in value]
return value
def _make_unique(candidate: str, seen: set[str]) -> str:
"""Appends a numeric suffix to make candidate unique within the seen set.
Args:
candidate: The candidate identifier
seen: Set of already-used identifiers (mutated -- the chosen name is added)
Returns:
A unique identifier, either candidate itself or candidate_N
"""
if candidate not in seen:
seen.add(candidate)
return candidate
counter = 2
while f"{candidate}_{counter}" in seen:
counter += 1
unique = f"{candidate}_{counter}"
seen.add(unique)
return unique
def _sanitize_blocks_recursive(
blocks: list[dict[str, Any]],
label_mapping: dict[str, str],
seen_labels: set[str],
) -> list[dict[str, Any]]:
"""Recursively sanitizes block labels and collects the label mapping.
Args:
blocks: List of block dictionaries
label_mapping: Dictionary to store old_label -> new_label mappings (mutated)
seen_labels: Set of already-used labels for collision avoidance (mutated)
Returns:
List of blocks with sanitized labels
"""
sanitized_blocks = []
for block in blocks:
block = dict(block) # Make a copy to avoid mutating the original
# Sanitize the block's label
if "label" in block and isinstance(block["label"], str):
old_label = block["label"]
new_label = sanitize_block_label(old_label)
new_label = _make_unique(new_label, seen_labels)
if old_label != new_label:
label_mapping[old_label] = new_label
block["label"] = new_label
else:
# Even if unchanged, track it as seen for collision avoidance
seen_labels.add(old_label)
# Handle nested blocks in for_loop
if "loop_blocks" in block and isinstance(block["loop_blocks"], list):
block["loop_blocks"] = _sanitize_blocks_recursive(block["loop_blocks"], label_mapping, seen_labels)
sanitized_blocks.append(block)
return sanitized_blocks
def _sanitize_parameters(
parameters: list[dict[str, Any]],
key_mapping: dict[str, str],
seen_keys: set[str],
) -> list[dict[str, Any]]:
"""Sanitizes parameter keys and collects the key mapping.
Args:
parameters: List of parameter dictionaries
key_mapping: Dictionary to store old_key -> new_key mappings (mutated)
seen_keys: Set of already-used keys for collision avoidance (mutated)
Returns:
List of parameters with sanitized keys
"""
sanitized_params = []
for param in parameters:
param = dict(param) # Make a copy
if "key" in param and isinstance(param["key"], str):
old_key = param["key"]
new_key = sanitize_parameter_key(old_key)
new_key = _make_unique(new_key, seen_keys)
if old_key != new_key:
key_mapping[old_key] = new_key
param["key"] = new_key
else:
# Even if unchanged, track it as seen for collision avoidance
seen_keys.add(old_key)
sanitized_params.append(param)
return sanitized_params
def _update_parameter_keys_in_blocks(
blocks: list[dict[str, Any]],
key_mapping: dict[str, str],
) -> list[dict[str, Any]]:
"""Updates parameter_keys arrays in blocks to use new parameter key names.
Args:
blocks: List of block dictionaries
key_mapping: Dictionary of old_key -> new_key mappings
Returns:
List of blocks with updated parameter_keys
"""
updated_blocks = []
for block in blocks:
block = dict(block)
# Update parameter_keys array if present
if "parameter_keys" in block and isinstance(block["parameter_keys"], list):
block["parameter_keys"] = [
key_mapping.get(key, key) if isinstance(key, str) else key for key in block["parameter_keys"]
]
# Handle nested blocks in for_loop
if "loop_blocks" in block and isinstance(block["loop_blocks"], list):
block["loop_blocks"] = _update_parameter_keys_in_blocks(block["loop_blocks"], key_mapping)
updated_blocks.append(block)
return updated_blocks
def sanitize_workflow_yaml_with_references(workflow_yaml: dict[str, Any]) -> dict[str, Any]:
"""Sanitizes block labels and parameter keys, and updates all references throughout the workflow.
This function:
1. Sanitizes all block labels to be valid Python identifiers
2. Sanitizes all parameter keys to be valid Python identifiers
3. Updates all Jinja references from {old_key} to {new_key}
4. Updates next_block_label if it references an old label
5. Updates finally_block_label if it references an old label
6. Updates parameter_keys arrays in blocks
7. Updates source_parameter_key and other direct references
Args:
workflow_yaml: The parsed workflow YAML dictionary
Returns:
The workflow YAML with sanitized identifiers and updated references
"""
workflow_yaml = dict(workflow_yaml) # Make a copy
workflow_definition = workflow_yaml.get("workflow_definition")
if not workflow_definition or not isinstance(workflow_definition, dict):
return workflow_yaml
workflow_definition = dict(workflow_definition) # Make a copy
workflow_yaml["workflow_definition"] = workflow_definition
# Step 1: Sanitize all block labels and collect the mapping
label_mapping: dict[str, str] = {} # old_label -> new_label
seen_labels: set[str] = set()
blocks = workflow_definition.get("blocks")
if blocks and isinstance(blocks, list):
workflow_definition["blocks"] = _sanitize_blocks_recursive(blocks, label_mapping, seen_labels)
# Step 2: Sanitize all parameter keys and collect the mapping
param_key_mapping: dict[str, str] = {} # old_key -> new_key
seen_keys: set[str] = set()
parameters = workflow_definition.get("parameters")
if parameters and isinstance(parameters, list):
workflow_definition["parameters"] = _sanitize_parameters(parameters, param_key_mapping, seen_keys)
# If nothing was changed, return early
if not label_mapping and not param_key_mapping:
return workflow_yaml
LOG.info(
"Auto-sanitized workflow identifiers during import",
sanitized_labels=label_mapping if label_mapping else None,
sanitized_parameter_keys=param_key_mapping if param_key_mapping else None,
)
# Step 3: Update all block label references
for old_label, new_label in label_mapping.items():
old_output_key = f"{old_label}_output"
new_output_key = f"{new_label}_output"
# Update Jinja references in blocks for {label}_output pattern
if "blocks" in workflow_definition:
workflow_definition["blocks"] = _replace_references_in_value(
workflow_definition["blocks"], old_output_key, new_output_key
)
# Also update shorthand {{ label }} references (must be done after _output to avoid partial matches)
workflow_definition["blocks"] = _replace_references_in_value(
workflow_definition["blocks"], old_label, new_label
)
# Update Jinja references in parameters for {label}_output pattern
if "parameters" in workflow_definition:
workflow_definition["parameters"] = _replace_references_in_value(
workflow_definition["parameters"], old_output_key, new_output_key
)
# Also update shorthand {{ label }} references
workflow_definition["parameters"] = _replace_references_in_value(
workflow_definition["parameters"], old_label, new_label
)
# Also update direct string references (e.g., source_parameter_key)
workflow_definition["parameters"] = _replace_direct_string_in_value(
workflow_definition["parameters"], old_output_key, new_output_key
)
# Step 4: Update all parameter key references
for old_key, new_key in param_key_mapping.items():
# Update Jinja references in blocks (e.g., {{ old_key }})
if "blocks" in workflow_definition:
workflow_definition["blocks"] = _replace_references_in_value(
workflow_definition["blocks"], old_key, new_key
)
# Update Jinja references in parameters (e.g., default values that reference other params)
if "parameters" in workflow_definition:
workflow_definition["parameters"] = _replace_references_in_value(
workflow_definition["parameters"], old_key, new_key
)
# Also update direct string references (e.g., source_parameter_key)
workflow_definition["parameters"] = _replace_direct_string_in_value(
workflow_definition["parameters"], old_key, new_key
)
# Step 5: Update parameter_keys arrays in blocks
if param_key_mapping and "blocks" in workflow_definition:
workflow_definition["blocks"] = _update_parameter_keys_in_blocks(
workflow_definition["blocks"], param_key_mapping
)
# Step 6: Update finally_block_label if it references an old label
if "finally_block_label" in workflow_definition:
finally_label = workflow_definition["finally_block_label"]
if finally_label in label_mapping:
workflow_definition["finally_block_label"] = label_mapping[finally_label]
# Step 7: Update next_block_label in all blocks
def update_next_block_label(blocks: list[dict[str, Any]]) -> list[dict[str, Any]]:
updated_blocks = []
for block in blocks:
block = dict(block)
if "next_block_label" in block:
next_label = block["next_block_label"]
if next_label in label_mapping:
block["next_block_label"] = label_mapping[next_label]
if "loop_blocks" in block and isinstance(block["loop_blocks"], list):
block["loop_blocks"] = update_next_block_label(block["loop_blocks"])
updated_blocks.append(block)
return updated_blocks
if label_mapping and "blocks" in workflow_definition:
workflow_definition["blocks"] = update_next_block_label(workflow_definition["blocks"])
return workflow_yaml
class WorkflowStatus(StrEnum):
published = "published"
draft = "draft"
auto_generated = "auto_generated"
importing = "importing"
import_failed = "import_failed"
class BlockType(StrEnum):
TASK = "task"
TaskV2 = "task_v2"
FOR_LOOP = "for_loop"
CONDITIONAL = "conditional"
CODE = "code"
TEXT_PROMPT = "text_prompt"
DOWNLOAD_TO_S3 = "download_to_s3"
UPLOAD_TO_S3 = "upload_to_s3"
FILE_UPLOAD = "file_upload"
SEND_EMAIL = "send_email"
FILE_URL_PARSER = "file_url_parser"
VALIDATION = "validation"
ACTION = "action"
NAVIGATION = "navigation"
EXTRACTION = "extraction"
LOGIN = "login"
WAIT = "wait"
FILE_DOWNLOAD = "file_download"
GOTO_URL = "goto_url"
PDF_PARSER = "pdf_parser"
HTTP_REQUEST = "http_request"
HUMAN_INTERACTION = "human_interaction"
2026-01-14 15:46:49 -08:00
PRINT_PAGE = "print_page"
class BlockStatus(StrEnum):
running = "running"
completed = "completed"
failed = "failed"
terminated = "terminated"
canceled = "canceled"
timed_out = "timed_out"
@dataclass(frozen=True)
class BlockResult:
success: bool
output_parameter: OutputParameter
output_parameter_value: dict[str, Any] | list | str | None = None
status: BlockStatus | None = None
failure_reason: str | None = None
workflow_run_block_id: str | None = None
class FileType(StrEnum):
CSV = "csv"
EXCEL = "excel"
PDF = "pdf"
IMAGE = "image"
DOCX = "docx"
2026-01-14 15:46:49 -08:00
class PDFFormat(StrEnum):
A4 = "A4"
LETTER = "Letter"
LEGAL = "Legal"
TABLOID = "Tabloid"
class FileStorageType(StrEnum):
S3 = "s3"
AZURE = "azure"
class ParameterYAML(BaseModel, abc.ABC):
parameter_type: ParameterType
key: str
description: str | None = None
@field_validator("key")
@classmethod
def validate_key_is_valid_identifier(cls, v: str) -> str:
"""Validate that parameter key is a valid Jinja2/Python identifier.
Parameter keys are used as Jinja2 template variable names. Jinja2 variable names
must be valid Python identifiers (letters, digits, underscores; cannot start with digit).
Characters like '/', '-', '.', etc. are NOT allowed because they are interpreted as
operators in Jinja2 templates, causing parsing errors like "'State_' is undefined"
when using a key like "State_/_Province".
"""
if any(char in v for char in [" ", "\t", "\n", "\r"]):
raise ValueError("Key cannot contain whitespace characters")
if not v.isidentifier():
raise ValueError(
f"Key '{v}' is not a valid parameter name. "
"Parameter keys must be valid Python identifiers "
"(only letters, digits, and underscores; cannot start with a digit). "
"Characters like '/', '-', '.', etc. are not allowed because they conflict with Jinja2 template syntax."
)
return v
class AWSSecretParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal[ParameterType.AWS_SECRET] = ParameterType.AWS_SECRET # type: ignore
aws_key: str
class BitwardenLoginCredentialParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal[ParameterType.BITWARDEN_LOGIN_CREDENTIAL] = ParameterType.BITWARDEN_LOGIN_CREDENTIAL # type: ignore
# bitwarden cli required fields
bitwarden_client_id_aws_secret_key: str
bitwarden_client_secret_aws_secret_key: str
bitwarden_master_password_aws_secret_key: str
# parameter key for the url to request the login credentials from bitwarden
url_parameter_key: str | None = None
# bitwarden collection id to filter the login credentials from,
# if not provided, no filtering will be done
bitwarden_collection_id: str | None = None
# bitwarden item id to request the login credential
bitwarden_item_id: str | None = None
class CredentialParameterYAML(ParameterYAML):
parameter_type: Literal[ParameterType.CREDENTIAL] = ParameterType.CREDENTIAL # type: ignore
credential_id: str
class BitwardenSensitiveInformationParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal["bitwarden_sensitive_information"] = ParameterType.BITWARDEN_SENSITIVE_INFORMATION # type: ignore
# bitwarden cli required fields
bitwarden_client_id_aws_secret_key: str
bitwarden_client_secret_aws_secret_key: str
bitwarden_master_password_aws_secret_key: str
# bitwarden collection id to filter the Bitwarden Identity from
bitwarden_collection_id: str
# unique key to identify the Bitwarden Identity in the collection
# this has to be in the identity's name
bitwarden_identity_key: str
# fields to extract from the Bitwarden Identity. Custom fields are prioritized over default identity fields
bitwarden_identity_fields: list[str]
class BitwardenCreditCardDataParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal[ParameterType.BITWARDEN_CREDIT_CARD_DATA] = ParameterType.BITWARDEN_CREDIT_CARD_DATA # type: ignore
# bitwarden cli required fields
bitwarden_client_id_aws_secret_key: str
bitwarden_client_secret_aws_secret_key: str
bitwarden_master_password_aws_secret_key: str
# bitwarden ids for the credit card item
bitwarden_collection_id: str
bitwarden_item_id: str
class OnePasswordCredentialParameterYAML(ParameterYAML):
parameter_type: Literal[ParameterType.ONEPASSWORD] = ParameterType.ONEPASSWORD # type: ignore
vault_id: str
item_id: str
2025-09-12 11:01:57 -06:00
class AzureVaultCredentialParameterYAML(ParameterYAML):
parameter_type: Literal[ParameterType.AZURE_VAULT_CREDENTIAL] = ParameterType.AZURE_VAULT_CREDENTIAL # type: ignore
vault_name: str
username_key: str
password_key: str
totp_secret_key: str | None = None
class WorkflowParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal[ParameterType.WORKFLOW] = ParameterType.WORKFLOW # type: ignore
workflow_parameter_type: WorkflowParameterType
default_value: str | int | float | bool | dict | list | None = None
class ContextParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal[ParameterType.CONTEXT] = ParameterType.CONTEXT # type: ignore
source_parameter_key: str
class OutputParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
parameter_type: Literal[ParameterType.OUTPUT] = ParameterType.OUTPUT # type: ignore
class BlockYAML(BaseModel, abc.ABC):
block_type: BlockType
label: str = Field(description="Author-facing identifier; must be unique per workflow.")
next_block_label: str | None = Field(
default=None,
description="Optional pointer to the label of the next block. "
"When omitted, it will default to sequential order. See [[s-4bnl]].",
)
continue_on_failure: bool = False
model: dict[str, Any] | None = None
2025-12-04 14:51:44 +08:00
# Only valid for blocks inside a for loop block
# Whether to continue to the next iteration when the block fails
2025-12-04 16:33:58 +08:00
next_loop_on_failure: bool = False
@field_validator("label")
@classmethod
def validate_label(cls, value: str) -> str:
"""Validate that block label is a valid Python identifier.
Block labels are used to create output parameter keys (e.g., '{label}_output')
which are then used as Jinja2 template variable names. Therefore, block labels
must be valid Python identifiers.
"""
if not value or not value.strip():
raise ValueError("Block labels cannot be empty.")
if not value.isidentifier():
raise ValueError(
f"Block label '{value}' is not a valid label. "
"Block labels must be valid Python identifiers "
"(only letters, digits, and underscores; cannot start with a digit). "
"Characters like '/', '-', '.', etc. are not allowed because they conflict with Jinja2 template syntax."
)
return value
class TaskBlockYAML(BlockYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the BlockType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
block_type: Literal[BlockType.TASK] = BlockType.TASK # type: ignore
url: str | None = None
title: str = ""
engine: RunEngine = RunEngine.skyvern_v1
navigation_goal: str | None = None
data_extraction_goal: str | None = None
data_schema: dict[str, Any] | list | str | None = None
error_code_mapping: dict[str, str] | None = None
max_retries: int = 0
max_steps_per_run: int | None = None
parameter_keys: list[str] | None = None
complete_on_download: bool = False
download_suffix: str | None = (
None # DEPRECATED: This field now sets the complete filename instead of appending to a random name
)
totp_verification_url: str | None = None
totp_identifier: str | None = None
2025-10-14 17:09:19 -07:00
disable_cache: bool = False
complete_criterion: str | None = None
terminate_criterion: str | None = None
complete_verification: bool = True
include_action_history_in_verification: bool = False
class ForLoopBlockYAML(BlockYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the BlockType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
block_type: Literal[BlockType.FOR_LOOP] = BlockType.FOR_LOOP # type: ignore
loop_blocks: list["BLOCK_YAML_SUBCLASSES"]
loop_over_parameter_key: str = ""
loop_variable_reference: str | None = None
complete_if_empty: bool = False
class BranchCriteriaYAML(BaseModel):
2025-12-07 12:37:00 -08:00
criteria_type: Literal["jinja2_template", "prompt"] = "jinja2_template"
expression: str
description: str | None = None
class BranchConditionYAML(BaseModel):
criteria: BranchCriteriaYAML | None = None
next_block_label: str | None = None
description: str | None = None
is_default: bool = False
@model_validator(mode="after")
def validate_condition(cls, condition: "BranchConditionYAML") -> "BranchConditionYAML":
if condition.criteria is None and not condition.is_default:
raise ValueError("Branches without criteria must be marked as default.")
if condition.criteria is not None and condition.is_default:
raise ValueError("Default branches may not define criteria.")
return condition
class ConditionalBlockYAML(BlockYAML):
block_type: Literal[BlockType.CONDITIONAL] = BlockType.CONDITIONAL # type: ignore
branch_conditions: list[BranchConditionYAML] = Field(default_factory=list)
@model_validator(mode="after")
def validate_branches(cls, block: "ConditionalBlockYAML") -> "ConditionalBlockYAML":
if not block.branch_conditions:
raise ValueError("Conditional blocks require at least one branch.")
default_branches = [branch for branch in block.branch_conditions if branch.is_default]
if len(default_branches) > 1:
raise ValueError("Only one default branch is permitted per conditional block.")
return block
class CodeBlockYAML(BlockYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the BlockType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
block_type: Literal[BlockType.CODE] = BlockType.CODE # type: ignore
code: str
parameter_keys: list[str] | None = None
class TextPromptBlockYAML(BlockYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the BlockType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
block_type: Literal[BlockType.TEXT_PROMPT] = BlockType.TEXT_PROMPT # type: ignore
llm_key: str | None = None
prompt: str
parameter_keys: list[str] | None = None
json_schema: dict[str, Any] | None = None
class DownloadToS3BlockYAML(BlockYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the BlockType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
block_type: Literal[BlockType.DOWNLOAD_TO_S3] = BlockType.DOWNLOAD_TO_S3 # type: ignore
url: str
class UploadToS3BlockYAML(BlockYAML):
block_type: Literal[BlockType.UPLOAD_TO_S3] = BlockType.UPLOAD_TO_S3 # type: ignore
path: str | None = None
class FileUploadBlockYAML(BlockYAML):
block_type: Literal[BlockType.FILE_UPLOAD] = BlockType.FILE_UPLOAD # type: ignore
storage_type: FileStorageType = FileStorageType.S3
s3_bucket: str | None = None
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
region_name: str | None = None
azure_storage_account_name: str | None = None
azure_storage_account_key: str | None = None
azure_blob_container_name: str | None = None
azure_folder_path: str | None = None
path: str | None = None
class SendEmailBlockYAML(BlockYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
# This pattern already works in block.py but since the BlockType is not defined in this file, mypy is not able
# to infer the type of the parameter_type attribute.
block_type: Literal[BlockType.SEND_EMAIL] = BlockType.SEND_EMAIL # type: ignore
smtp_host_secret_parameter_key: str
smtp_port_secret_parameter_key: str
smtp_username_secret_parameter_key: str
smtp_password_secret_parameter_key: str
sender: str
recipients: list[str]
subject: str
body: str
file_attachments: list[str] | None = None
class FileParserBlockYAML(BlockYAML):
block_type: Literal[BlockType.FILE_URL_PARSER] = BlockType.FILE_URL_PARSER # type: ignore
file_url: str
file_type: FileType
json_schema: dict[str, Any] | None = None
class PDFParserBlockYAML(BlockYAML):
block_type: Literal[BlockType.PDF_PARSER] = BlockType.PDF_PARSER # type: ignore
file_url: str
json_schema: dict[str, Any] | None = None
class ValidationBlockYAML(BlockYAML):
block_type: Literal[BlockType.VALIDATION] = BlockType.VALIDATION # type: ignore
complete_criterion: str | None = None
terminate_criterion: str | None = None
error_code_mapping: dict[str, str] | None = None
parameter_keys: list[str] | None = None
disable_cache: bool = False
class ActionBlockYAML(BlockYAML):
block_type: Literal[BlockType.ACTION] = BlockType.ACTION # type: ignore
url: str | None = None
title: str = ""
engine: RunEngine = RunEngine.skyvern_v1
navigation_goal: str | None = None
error_code_mapping: dict[str, str] | None = None
max_retries: int = 0
parameter_keys: list[str] | None = None
complete_on_download: bool = False
download_suffix: str | None = (
None # DEPRECATED: This field now sets the complete filename instead of appending to a random name
)
totp_verification_url: str | None = None
totp_identifier: str | None = None
2025-10-14 17:09:19 -07:00
disable_cache: bool = False
class NavigationBlockYAML(BlockYAML):
block_type: Literal[BlockType.NAVIGATION] = BlockType.NAVIGATION # type: ignore
navigation_goal: str
url: str | None = None
title: str = ""
engine: RunEngine = RunEngine.skyvern_v1
error_code_mapping: dict[str, str] | None = None
max_retries: int = 0
max_steps_per_run: int | None = None
parameter_keys: list[str] | None = None
complete_on_download: bool = False
download_suffix: str | None = (
None # DEPRECATED: This field now sets the complete filename instead of appending to a random name
)
totp_verification_url: str | None = None
totp_identifier: str | None = None
2025-10-14 17:09:19 -07:00
disable_cache: bool = False
complete_criterion: str | None = None
terminate_criterion: str | None = None
complete_verification: bool = True
include_action_history_in_verification: bool = False
class ExtractionBlockYAML(BlockYAML):
block_type: Literal[BlockType.EXTRACTION] = BlockType.EXTRACTION # type: ignore
data_extraction_goal: str
url: str | None = None
title: str = ""
engine: RunEngine = RunEngine.skyvern_v1
data_schema: dict[str, Any] | list | str | None = None
max_retries: int = 0
max_steps_per_run: int | None = None
parameter_keys: list[str] | None = None
2025-10-14 17:09:19 -07:00
disable_cache: bool = False
class LoginBlockYAML(BlockYAML):
block_type: Literal[BlockType.LOGIN] = BlockType.LOGIN # type: ignore
url: str | None = None
title: str = ""
engine: RunEngine = RunEngine.skyvern_v1
navigation_goal: str | None = None
error_code_mapping: dict[str, str] | None = None
max_retries: int = 0
max_steps_per_run: int | None = None
parameter_keys: list[str] | None = None
totp_verification_url: str | None = None
totp_identifier: str | None = None
2025-10-14 17:09:19 -07:00
disable_cache: bool = False
complete_criterion: str | None = None
terminate_criterion: str | None = None
complete_verification: bool = True
class WaitBlockYAML(BlockYAML):
block_type: Literal[BlockType.WAIT] = BlockType.WAIT # type: ignore
wait_sec: int = 0
class HumanInteractionBlockYAML(BlockYAML):
block_type: Literal[BlockType.HUMAN_INTERACTION] = BlockType.HUMAN_INTERACTION # type: ignore
instructions: str = "Please review and approve or reject to continue the workflow."
positive_descriptor: str = "Approve"
negative_descriptor: str = "Reject"
timeout_seconds: int
sender: str
recipients: list[str]
subject: str
body: str
class FileDownloadBlockYAML(BlockYAML):
block_type: Literal[BlockType.FILE_DOWNLOAD] = BlockType.FILE_DOWNLOAD # type: ignore
navigation_goal: str
url: str | None = None
title: str = ""
engine: RunEngine = RunEngine.skyvern_v1
error_code_mapping: dict[str, str] | None = None
max_retries: int = 0
max_steps_per_run: int | None = None
parameter_keys: list[str] | None = None
download_suffix: str | None = (
None # DEPRECATED: This field now sets the complete filename instead of appending to a random name
)
totp_verification_url: str | None = None
totp_identifier: str | None = None
2025-10-14 17:09:19 -07:00
disable_cache: bool = False
download_timeout: float | None = None
class UrlBlockYAML(BlockYAML):
block_type: Literal[BlockType.GOTO_URL] = BlockType.GOTO_URL # type: ignore
url: str
class TaskV2BlockYAML(BlockYAML):
block_type: Literal[BlockType.TaskV2] = BlockType.TaskV2 # type: ignore
prompt: str
url: str | None = None
totp_verification_url: str | None = None
totp_identifier: str | None = None
max_iterations: int = settings.MAX_ITERATIONS_PER_TASK_V2
max_steps: int = settings.MAX_STEPS_PER_TASK_V2
disable_cache: bool = False
class HttpRequestBlockYAML(BlockYAML):
block_type: Literal[BlockType.HTTP_REQUEST] = BlockType.HTTP_REQUEST # type: ignore
# Individual HTTP parameters
method: str = "GET"
url: str | None = None
headers: dict[str, str] | None = None
body: dict[str, Any] | None = None # Changed to consistently be dict only
2025-12-11 01:03:11 +08:00
files: dict[str, str] | None = None # Dictionary mapping field names to file paths/URLs for multipart file uploads
timeout: int = 30
follow_redirects: bool = True
download_filename: str | None = None
save_response_as_file: bool = False
# Parameter keys for templating
parameter_keys: list[str] | None = None
2026-01-14 15:46:49 -08:00
class PrintPageBlockYAML(BlockYAML):
block_type: Literal[BlockType.PRINT_PAGE] = BlockType.PRINT_PAGE # type: ignore
include_timestamp: bool = True
custom_filename: str | None = None
format: PDFFormat = PDFFormat.A4
landscape: bool = False
print_background: bool = True
parameter_keys: list[str] | None = None
2026-01-14 15:46:49 -08:00
PARAMETER_YAML_SUBCLASSES = (
AWSSecretParameterYAML
| BitwardenLoginCredentialParameterYAML
| BitwardenSensitiveInformationParameterYAML
| BitwardenCreditCardDataParameterYAML
| OnePasswordCredentialParameterYAML
2025-09-12 11:01:57 -06:00
| AzureVaultCredentialParameterYAML
| WorkflowParameterYAML
| ContextParameterYAML
| OutputParameterYAML
| CredentialParameterYAML
)
PARAMETER_YAML_TYPES = Annotated[PARAMETER_YAML_SUBCLASSES, Field(discriminator="parameter_type")]
BLOCK_YAML_SUBCLASSES = (
TaskBlockYAML
| ForLoopBlockYAML
| CodeBlockYAML
| TextPromptBlockYAML
| DownloadToS3BlockYAML
| UploadToS3BlockYAML
| FileUploadBlockYAML
| SendEmailBlockYAML
| FileParserBlockYAML
| ValidationBlockYAML
| ActionBlockYAML
| NavigationBlockYAML
| ExtractionBlockYAML
| LoginBlockYAML
| WaitBlockYAML
| HumanInteractionBlockYAML
| FileDownloadBlockYAML
| UrlBlockYAML
| PDFParserBlockYAML
| TaskV2BlockYAML
| HttpRequestBlockYAML
| ConditionalBlockYAML
2026-01-14 15:46:49 -08:00
| PrintPageBlockYAML
)
BLOCK_YAML_TYPES = Annotated[BLOCK_YAML_SUBCLASSES, Field(discriminator="block_type")]
class WorkflowDefinitionYAML(BaseModel):
version: int = 1
parameters: list[PARAMETER_YAML_TYPES]
blocks: list[BLOCK_YAML_TYPES]
finally_block_label: str | None = None
@model_validator(mode="after")
def validate_unique_block_labels(cls, workflow: "WorkflowDefinitionYAML") -> "WorkflowDefinitionYAML":
labels = [block.label for block in workflow.blocks]
duplicates = [label for label in labels if labels.count(label) > 1]
if duplicates:
unique_duplicates = sorted(set(duplicates))
raise ValueError(
f"Block labels must be unique within a workflow. "
f"Found duplicate label(s): {', '.join(unique_duplicates)}"
)
if workflow.finally_block_label and workflow.finally_block_label not in labels:
raise ValueError(
f"finally_block_label '{workflow.finally_block_label}' does not reference a valid block. "
f"Available labels: {', '.join(labels) if labels else '(none)'}"
)
return workflow
class WorkflowCreateYAMLRequest(BaseModel):
title: str
description: str | None = None
proxy_location: ProxyLocation | GeoTarget | dict | None = None
webhook_callback_url: str | None = None
totp_verification_url: str | None = None
totp_identifier: str | None = None
persist_browser_session: bool = False
model: dict[str, Any] | None = None
workflow_definition: WorkflowDefinitionYAML
is_saved_task: bool = False
max_screenshot_scrolls: int | None = None
extra_http_headers: dict[str, str] | None = None
status: WorkflowStatus = WorkflowStatus.published
run_with: str | None = None
2025-08-29 05:24:17 +08:00
ai_fallback: bool = False
cache_key: str | None = "default"
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
folder_id: str | None = None
class WorkflowRequest(BaseModel):
json_definition: WorkflowCreateYAMLRequest | None = Field(
default=None,
description="Workflow definition in JSON format",
)
yaml_definition: str | None = Field(
default=None,
description="Workflow definition in YAML format",
)