diff --git a/.dockerignore b/.dockerignore index 8fc469dd1..bc05d817a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,9 @@ **/traces **/inputs **/har +**/downloads +**/browser_sessions +**/credential_vault **/.git **/.github **/.mypy_cache diff --git a/.env.example b/.env.example index d95ee4f41..8de4fcc0b 100644 --- a/.env.example +++ b/.env.example @@ -86,8 +86,9 @@ YUTORI_TOOL_SET="" # LLM_KEY: The chosen language model to use. This should be one of the models # provided by the enabled LLM providers (e.g., OPENAI_GPT5_5, OPENAI_GPT5_4, -# ANTHROPIC_CLAUDE4.7_OPUS, ANTHROPIC_CLAUDE4.6_SONNET, GEMINI_3_PRO, -# BEDROCK_ANTHROPIC_CLAUDE4.7_OPUS_INFERENCE_PROFILE). +# ANTHROPIC_CLAUDE5_FABLE, ANTHROPIC_CLAUDE4.7_OPUS, ANTHROPIC_CLAUDE4.6_SONNET, +# GEMINI_3_PRO, BEDROCK_ANTHROPIC_CLAUDE5_FABLE_INFERENCE_PROFILE, +# BEDROCK_ANTHROPIC_CLAUDE5_FABLE_WITH_FALLBACK). # See docs: https://www.skyvern.com/docs/self-hosted/llm-configuration LLM_KEY="" # a cheaper LLM providers to help finishing some small tasks, like custom selection or svg conversion. If empty, it will be the same as LLM_KEY @@ -138,12 +139,29 @@ ANALYTICS_ID="anonymous" # OP_SERVICE_ACCOUNT_TOKEN: API token for 1Password integration OP_SERVICE_ACCOUNT_TOKEN="" +# ============================================================================= +# SKYVERN CREDENTIAL VAULT CONFIGURATION +# ============================================================================= +# Built-in encrypted local vault used by default by the OSS Docker Compose setup. +# Docker also sets ENABLE_LOCAL_CREDENTIAL_VAULT=true and persists this under /data/credential_vault. +CREDENTIAL_VAULT_TYPE=skyvern +ENABLE_LOCAL_CREDENTIAL_VAULT=true +# LOCAL_CREDENTIAL_VAULT_PATH=/data/credential_vault +# Production self-hosters should set LOCAL_CREDENTIAL_VAULT_KEY from an external secret store. +# If unset, Skyvern generates .fernet_key inside LOCAL_CREDENTIAL_VAULT_PATH; backups or host-dir +# exposure of that directory include both encrypted items and the key needed to decrypt them. +# LOCAL_CREDENTIAL_VAULT_KEY= +# The OSS Docker volumes for browser_sessions/ and downloads/ can contain plaintext cookies, +# session tokens, and downloaded files. Treat them as sensitive and exclude them from casual backups. + # Enable recording skyvern logs as artifacts ENABLE_LOG_ARTIFACTS=false # ============================================================================= # SKYVERN BITWARDEN CONFIGURATION # ============================================================================= +# Set CREDENTIAL_VAULT_TYPE=bitwarden to use Bitwarden or vaultwarden instead +# of the built-in Skyvern credential vault. # Your organization ID in official Bitwarden server or vaultwarden (if using organizations) SKYVERN_AUTH_BITWARDEN_ORGANIZATION_ID=your-org-id-here @@ -178,3 +196,27 @@ SKYVERN_AUTH_BITWARDEN_CLIENT_SECRET=your-client-secret-here # Optional: override Redis URL specifically for notifications (falls back to REDIS_URL) # NOTIFICATION_REDIS_URL= # REDIS_URL=redis://localhost:6379/0 + +# ============================================================================= +# OPTIONAL: GCP SELF-HOSTED DEPLOYMENT +# ============================================================================= +# Uncomment to run Skyvern on Google Cloud with GCS storage and Secret Manager +# credentials. Auth uses Application Default Credentials (Workload Identity on +# GKE, or GOOGLE_APPLICATION_CREDENTIALS pointing at a service-account key). + +# Store artifacts/screenshots/browser sessions/uploads in Google Cloud Storage +# SKYVERN_STORAGE_TYPE=gcs +# GCS_PROJECT_ID=your-gcp-project-id +# GCS_BUCKET_ARTIFACTS=skyvern-artifacts +# GCS_BUCKET_SCREENSHOTS=skyvern-screenshots +# GCS_BUCKET_BROWSER_SESSIONS=skyvern-browser-sessions +# GCS_BUCKET_UPLOADS=skyvern-uploads +# Service account that signs GCS download URLs; required only under Workload +# Identity, where the runtime SA has no signing key of its own. +# GCS_SIGNER_SA_EMAIL=skyvern@your-gcp-project-id.iam.gserviceaccount.com + +# Store workflow credentials in GCP Secret Manager instead of Bitwarden +# CREDENTIAL_VAULT_TYPE=gcp +# GCP_CREDENTIAL_VAULT_PROJECT_ID=your-gcp-project-id +# Secret-id prefix; must be unique per Skyvern deployment sharing a project +# GCP_CREDENTIAL_VAULT_PREFIX=skyvern-cred- diff --git a/.github/workflows/sdk-release.yml b/.github/workflows/sdk-release.yml index b483f839a..bb9160810 100644 --- a/.github/workflows/sdk-release.yml +++ b/.github/workflows/sdk-release.yml @@ -12,37 +12,53 @@ jobs: check-version-change: runs-on: ubuntu-latest outputs: - version_changed: ${{ steps.check.outputs.version_changed }} + should_publish: ${{ steps.check.outputs.should_publish }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 2 persist-credentials: false - - name: Check if version changed + - name: Decide whether to publish id: check run: | - # Get version from current pyproject.toml + # Compare current vs previous commit, AND check PyPI for the new version. + # Without the PyPI check, reverts (1.0.37 -> 1.0.36) and re-bumps after a + # revert (1.0.36 -> 1.0.37, where 1.0.37 was already published) both look + # like "version changed" and crash the publish step with HTTP 400. CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') - # Get version from previous commit git checkout HEAD^1 PREVIOUS_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') - if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then - echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION" - echo "version_changed=true" >> $GITHUB_OUTPUT + if [ "$CURRENT_VERSION" = "$PREVIOUS_VERSION" ]; then + echo "Version unchanged at $CURRENT_VERSION; skipping publish." + echo "should_publish=false" >> $GITHUB_OUTPUT + exit 0 + fi + echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION." + + # Treat both skyvern and skyvern-ui as already-published only if both + # are present on PyPI at the new version. If either is missing (e.g. + # the first release of a new sibling package), proceed. + PYPI_INDEX="https://pypi.org/pypi" + SKYVERN_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PYPI_INDEX/skyvern/$CURRENT_VERSION/json") + SKYVERN_UI_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PYPI_INDEX/skyvern-ui/$CURRENT_VERSION/json") + echo "PyPI lookup: skyvern=$SKYVERN_STATUS skyvern-ui=$SKYVERN_UI_STATUS" + + if [ "$SKYVERN_STATUS" = "200" ] && [ "$SKYVERN_UI_STATUS" = "200" ]; then + echo "Both packages already at $CURRENT_VERSION on PyPI; skipping publish." + echo "should_publish=false" >> $GITHUB_OUTPUT else - echo "Version remained at $CURRENT_VERSION" - echo "version_changed=false" >> $GITHUB_OUTPUT + echo "should_publish=true" >> $GITHUB_OUTPUT fi run-ci: needs: check-version-change - if: needs.check-version-change.outputs.version_changed == 'true' + if: needs.check-version-change.outputs.should_publish == 'true' uses: ./.github/workflows/ci.yml build-sdk: runs-on: ubuntu-latest needs: [check-version-change, run-ci] - if: needs.check-version-change.outputs.version_changed == 'true' + if: needs.check-version-change.outputs.should_publish == 'true' steps: - name: Check out Git repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -123,7 +139,12 @@ jobs: uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: packages-dir: dist/skyvern-ui/ + # Defense-in-depth: if check-version-change ever misses an edge case + # (e.g. half-published release where one sibling landed), skip files + # already on PyPI rather than failing the whole publish step. + skip-existing: true - name: Publish skyvern to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: packages-dir: dist/skyvern/ + skip-existing: true diff --git a/.gitignore b/.gitignore index b3f08ff9e..86172c6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -162,6 +162,7 @@ ig_* # Skyvern ignores browser_sessions/ +credential_vault/ videos/ skyvern/artifacts/ artifacts/ @@ -189,6 +190,7 @@ tags.temp.tmp # copy of Chrome user profile from Chrome >= 136 tmp/user_data_dir +tmp/ # Claude .claude/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c5da55e0..db865b331 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -132,3 +132,6 @@ repos: rev: v0.17.2 hooks: - id: yamlfmt + # Helm chart templates contain Go templating ({{ ... }}) and are not + # valid standalone YAML, so yamlfmt cannot parse them. + exclude: ^k8s/charts/.*/templates/ diff --git a/Dockerfile b/Dockerfile index 351c3d5a5..d954a1565 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,7 @@ RUN uv pip compile pyproject.toml --extra server --python-version 3.11 -o requir FROM python:3.11-slim-bookworm WORKDIR /app COPY --from=requirements-stage /tmp/requirements.txt /app/requirements.txt +COPY ./skyvern/forge/sdk/utils/tesseract_languages.py /tmp/tesseract_languages.py RUN pip install --upgrade pip setuptools wheel # --no-deps: requirements.txt is fully resolved by uv, including the # pyproject overrides that loosen litellm's jsonschema==4.23.0 pin. @@ -18,7 +19,13 @@ RUN pip install --upgrade pip setuptools wheel RUN pip install --no-cache-dir --no-deps -r requirements.txt RUN playwright install-deps RUN playwright install -RUN apt-get install -y xauth x11-apps netpbm gpg ca-certificates x11vnc && apt-get clean +RUN apt-get update && \ + apt-get install -y xauth x11-apps netpbm gpg ca-certificates x11vnc tesseract-ocr $(python /tmp/tesseract_languages.py --apt-packages) && \ + tesseract --version && \ + rm /tmp/tesseract_languages.py && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + RUN pip install --no-cache-dir websockify COPY .nvmrc /app/.nvmrc @@ -47,6 +54,9 @@ ENV VIDEO_PATH=/data/videos ENV HAR_PATH=/data/har ENV LOG_PATH=/data/log ENV ARTIFACT_STORAGE_PATH=/data/artifacts +ENV DOWNLOAD_PATH=/data/downloads +ENV BROWSER_SESSION_BASE_PATH=/data/browser_sessions +ENV LOCAL_CREDENTIAL_VAULT_PATH=/data/credential_vault # cache tiktoken RUN python /app/scripts/load_tiktoken.py diff --git a/alembic/versions/2026_06_14_2221-218048b68412_add_workflow_copilot_completion_criteria_sets_table.py b/alembic/versions/2026_06_14_2221-218048b68412_add_workflow_copilot_completion_criteria_sets_table.py new file mode 100644 index 000000000..0f44122f9 --- /dev/null +++ b/alembic/versions/2026_06_14_2221-218048b68412_add_workflow_copilot_completion_criteria_sets_table.py @@ -0,0 +1,65 @@ +"""add workflow_copilot_completion_criteria_sets table + +Revision ID: 218048b68412 +Revises: a8c4e2f9d6b1 +Create Date: 2026-06-14T22:21:36.243898+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "218048b68412" +down_revision: Union[str, None] = "a8c4e2f9d6b1" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +_TABLE = "workflow_copilot_completion_criteria_sets" + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + if inspector.has_table(_TABLE): + return + + if conn.dialect.name == "postgresql": + op.execute(sa.text("SET lock_timeout = '2s'")) + + op.create_table( + _TABLE, + sa.Column("completion_criteria_set_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("workflow_copilot_chat_id", sa.String(), nullable=False), + sa.Column("goal_epoch", sa.Integer(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("criteria", sa.JSON(), nullable=False), + sa.Column("source_turn_id", sa.String(), nullable=True), + sa.Column("source_goal_text", sa.UnicodeText(), nullable=True), + sa.Column("consecutive_all_no_evidence", sa.Integer(), nullable=False), + sa.Column("tripwire_fired", sa.Boolean(), nullable=False), + sa.Column("last_fully_satisfied_workflow_yaml", sa.UnicodeText(), nullable=True), + sa.Column("superseded_by_set_id", sa.String(), nullable=True), + sa.Column("superseded_at", sa.DateTime(), nullable=True), + sa.Column("supersede_reason", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("completion_criteria_set_id"), + ) + op.create_index("wcccs_org_chat_index", _TABLE, ["organization_id", "workflow_copilot_chat_id"]) + + if conn.dialect.name == "postgresql": + op.execute(sa.text("SET lock_timeout = '0'")) + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + if not inspector.has_table(_TABLE): + return + op.drop_index("wcccs_org_chat_index", table_name=_TABLE) + op.drop_table(_TABLE) diff --git a/alembic/versions/2026_06_16_0803-cfc2318acdf9_add_sequential_key_lookup_index_on_workflow_runs.py b/alembic/versions/2026_06_16_0803-cfc2318acdf9_add_sequential_key_lookup_index_on_workflow_runs.py new file mode 100644 index 000000000..cf7b44631 --- /dev/null +++ b/alembic/versions/2026_06_16_0803-cfc2318acdf9_add_sequential_key_lookup_index_on_workflow_runs.py @@ -0,0 +1,33 @@ +"""add sequential_key lookup index on workflow_runs + +Revision ID: cfc2318acdf9 +Revises: 218048b68412 +Create Date: 2026-06-16T08:03:01.419039+00:00 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "cfc2318acdf9" +down_revision: Union[str, None] = "218048b68412" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.get_context().autocommit_block(): + op.execute("SET statement_timeout = '24h';") + op.execute(""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_workflow_runs_sequential_key_lookup + ON workflow_runs (workflow_permanent_id, sequential_key, queued_at) + WHERE status IN ('queued', 'running', 'paused') AND browser_session_id IS NULL + """) + op.execute("RESET statement_timeout;") + + +def downgrade() -> None: + with op.get_context().autocommit_block(): + op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_workflow_runs_sequential_key_lookup") diff --git a/alembic/versions/2026_06_16_1253-1fee32b3d7c6_add_browser_profile_loaded_to_persistent_browser_sessions.py b/alembic/versions/2026_06_16_1253-1fee32b3d7c6_add_browser_profile_loaded_to_persistent_browser_sessions.py new file mode 100644 index 000000000..8f50865b6 --- /dev/null +++ b/alembic/versions/2026_06_16_1253-1fee32b3d7c6_add_browser_profile_loaded_to_persistent_browser_sessions.py @@ -0,0 +1,30 @@ +"""add browser_profile_loaded to persistent_browser_sessions + +Revision ID: 1fee32b3d7c6 +Revises: cfc2318acdf9 +Create Date: 2026-06-16T12:53:28.144752+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "1fee32b3d7c6" +down_revision: Union[str, None] = "cfc2318acdf9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "persistent_browser_sessions", + sa.Column("browser_profile_loaded", sa.Boolean(), server_default=sa.true(), nullable=False), + ) + + +def downgrade() -> None: + op.drop_column("persistent_browser_sessions", "browser_profile_loaded") diff --git a/alembic/versions/2026_06_19_2044-58b0ced36529_add_downloaded_file_count_to_workflow_run_blocks.py b/alembic/versions/2026_06_19_2044-58b0ced36529_add_downloaded_file_count_to_workflow_run_blocks.py new file mode 100644 index 000000000..4373544af --- /dev/null +++ b/alembic/versions/2026_06_19_2044-58b0ced36529_add_downloaded_file_count_to_workflow_run_blocks.py @@ -0,0 +1,28 @@ +"""add downloaded_file_count to workflow_run_blocks + +Revision ID: 58b0ced36529 +Revises: 1fee32b3d7c6 +Create Date: 2026-06-19T20:44:46.548990+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "58b0ced36529" +down_revision: Union[str, None] = "1fee32b3d7c6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("SET LOCAL lock_timeout = '5s'") + op.add_column("workflow_run_blocks", sa.Column("downloaded_file_count", sa.Integer(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("workflow_run_blocks", "downloaded_file_count") diff --git a/alembic/versions/2026_06_22_1122-61c9acf9f3ba_add_copilot_chat_history_indexes.py b/alembic/versions/2026_06_22_1122-61c9acf9f3ba_add_copilot_chat_history_indexes.py new file mode 100644 index 000000000..2ff72e342 --- /dev/null +++ b/alembic/versions/2026_06_22_1122-61c9acf9f3ba_add_copilot_chat_history_indexes.py @@ -0,0 +1,37 @@ +"""add copilot chat history indexes + +Revision ID: 61c9acf9f3ba +Revises: 58b0ced36529 +Create Date: 2026-06-22 11:22:49.851792+00:00 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "61c9acf9f3ba" +down_revision: Union[str, None] = "58b0ced36529" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.get_context().autocommit_block(): + op.execute("SET statement_timeout = '1h';") + op.execute(""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS wccm_org_chat_index + ON workflow_copilot_chat_messages (organization_id, workflow_copilot_chat_id); + """) + op.execute(""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS wcc_org_created_at_index + ON workflow_copilot_chats (organization_id, created_at); + """) + op.execute("RESET statement_timeout;") + + +def downgrade() -> None: + with op.get_context().autocommit_block(): + op.execute("DROP INDEX CONCURRENTLY IF EXISTS wccm_org_chat_index;") + op.execute("DROP INDEX CONCURRENTLY IF EXISTS wcc_org_created_at_index;") diff --git a/alembic/versions/2026_06_24_1630-2c76348b4e7a_add_tag_values_table.py b/alembic/versions/2026_06_24_1630-2c76348b4e7a_add_tag_values_table.py new file mode 100644 index 000000000..27c19e1d3 --- /dev/null +++ b/alembic/versions/2026_06_24_1630-2c76348b4e7a_add_tag_values_table.py @@ -0,0 +1,54 @@ +"""add tag values table + +Revision ID: 2c76348b4e7a +Revises: 61c9acf9f3ba +Create Date: 2026-06-24T16:30:09.366617+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "2c76348b4e7a" +down_revision: Union[str, None] = "61c9acf9f3ba" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "tag_values", + sa.Column("tag_value_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("color", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.Column("deleted_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.organization_id"], + ), + sa.PrimaryKeyConstraint("tag_value_id"), + ) + op.create_index( + "ix_tag_values_org_key_value_active", + "tag_values", + ["organization_id", "key", "value"], + unique=True, + postgresql_where=sa.text("deleted_at IS NULL"), + ) + + +def downgrade() -> None: + op.drop_index( + "ix_tag_values_org_key_value_active", + table_name="tag_values", + postgresql_where=sa.text("deleted_at IS NULL"), + ) + op.drop_table("tag_values") diff --git a/alembic/versions/2026_06_25_0017-45128d67bc1a_add_browser_profile_key_to_workflows.py b/alembic/versions/2026_06_25_0017-45128d67bc1a_add_browser_profile_key_to_workflows.py new file mode 100644 index 000000000..f0db01467 --- /dev/null +++ b/alembic/versions/2026_06_25_0017-45128d67bc1a_add_browser_profile_key_to_workflows.py @@ -0,0 +1,27 @@ +"""add browser_profile_key to workflows + +Revision ID: 45128d67bc1a +Revises: 2c76348b4e7a +Create Date: 2026-06-25T00:17:17.827852+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "45128d67bc1a" +down_revision: Union[str, None] = "2c76348b4e7a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("workflows", sa.Column("browser_profile_key", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("workflows", "browser_profile_key") diff --git a/alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py b/alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py new file mode 100644 index 000000000..8a2a443bb --- /dev/null +++ b/alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py @@ -0,0 +1,36 @@ +"""add proxy pin fields + +Revision ID: a1cad008154a +Revises: 45128d67bc1a +Create Date: 2026-06-26T19:26:11.232122+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a1cad008154a" +down_revision: Union[str, None] = "45128d67bc1a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("SET LOCAL lock_timeout = '5s'") + op.add_column("credentials", sa.Column("proxy_location", sa.String(), nullable=True)) + op.add_column("credentials", sa.Column("proxy_session_id", sa.String(), nullable=True)) + op.add_column("browser_profiles", sa.Column("proxy_location", sa.String(), nullable=True)) + op.add_column("browser_profiles", sa.Column("proxy_session_id", sa.String(), nullable=True)) + op.add_column("persistent_browser_sessions", sa.Column("proxy_session_id", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("persistent_browser_sessions", "proxy_session_id") + op.drop_column("browser_profiles", "proxy_session_id") + op.drop_column("browser_profiles", "proxy_location") + op.drop_column("credentials", "proxy_session_id") + op.drop_column("credentials", "proxy_location") diff --git a/alembic/versions/2026_07_02_2317-ecf365563e98_managed_browser_profiles_for_save_and_reuse_session.py b/alembic/versions/2026_07_02_2317-ecf365563e98_managed_browser_profiles_for_save_and_reuse_session.py new file mode 100644 index 000000000..de4304f7d --- /dev/null +++ b/alembic/versions/2026_07_02_2317-ecf365563e98_managed_browser_profiles_for_save_and_reuse_session.py @@ -0,0 +1,67 @@ +"""managed browser profiles for save and reuse session + +Revision ID: ecf365563e98 +Revises: a1cad008154a +Create Date: 2026-07-02T23:17:33.611632+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "ecf365563e98" +down_revision: Union[str, None] = "a1cad008154a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "browser_profiles", + sa.Column("is_managed", sa.Boolean(), server_default=sa.text("false"), nullable=False), + ) + op.add_column("browser_profiles", sa.Column("workflow_permanent_id", sa.String(), nullable=True)) + op.add_column("browser_profiles", sa.Column("browser_profile_key_digest", sa.String(), nullable=True)) + op.create_index( + "uq_browser_profiles_org_name_user", + "browser_profiles", + ["organization_id", "name"], + unique=True, + postgresql_where=sa.text("is_managed = false"), + sqlite_where=sa.text("is_managed = false"), + ) + op.create_index( + "uq_browser_profiles_managed_segment", + "browser_profiles", + ["organization_id", "workflow_permanent_id", "browser_profile_key_digest"], + unique=True, + # Exclude soft-deleted rows so deleting a managed profile doesn't tombstone the + # segment — the next run re-creates it instead of colliding on the unique key. + postgresql_where=sa.text("is_managed = true AND deleted_at IS NULL"), + sqlite_where=sa.text("is_managed = true AND deleted_at IS NULL"), + ) + op.create_index("idx_browser_profiles_wpid", "browser_profiles", ["workflow_permanent_id"], unique=False) + op.drop_constraint("uc_org_browser_profile_name", "browser_profiles", type_="unique") + + +def downgrade() -> None: + op.execute("DELETE FROM browser_profiles WHERE is_managed = true") + op.create_unique_constraint("uc_org_browser_profile_name", "browser_profiles", ["organization_id", "name"]) + op.drop_index("idx_browser_profiles_wpid", table_name="browser_profiles") + op.drop_index( + "uq_browser_profiles_managed_segment", + table_name="browser_profiles", + postgresql_where=sa.text("is_managed = true AND deleted_at IS NULL"), + ) + op.drop_index( + "uq_browser_profiles_org_name_user", + table_name="browser_profiles", + postgresql_where=sa.text("is_managed = false"), + ) + op.drop_column("browser_profiles", "browser_profile_key_digest") + op.drop_column("browser_profiles", "workflow_permanent_id") + op.drop_column("browser_profiles", "is_managed") diff --git a/alembic/versions/2026_07_03_0333-2ac47bc1c075_add_enable_self_healing_to_workflows.py b/alembic/versions/2026_07_03_0333-2ac47bc1c075_add_enable_self_healing_to_workflows.py new file mode 100644 index 000000000..3217cbd8b --- /dev/null +++ b/alembic/versions/2026_07_03_0333-2ac47bc1c075_add_enable_self_healing_to_workflows.py @@ -0,0 +1,30 @@ +"""add enable_self_healing to workflows + +Revision ID: 2ac47bc1c075 +Revises: ecf365563e98 +Create Date: 2026-07-03T03:33:20.863940+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "2ac47bc1c075" +down_revision: Union[str, None] = "ecf365563e98" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "workflows", + sa.Column("enable_self_healing", sa.Boolean(), nullable=False, server_default=sa.false()), + ) + + +def downgrade() -> None: + op.drop_column("workflows", "enable_self_healing") diff --git a/alembic/versions/2026_07_03_1853-aff1632dc377_add_workflow_run_credential_selections.py b/alembic/versions/2026_07_03_1853-aff1632dc377_add_workflow_run_credential_selections.py new file mode 100644 index 000000000..814f1a24d --- /dev/null +++ b/alembic/versions/2026_07_03_1853-aff1632dc377_add_workflow_run_credential_selections.py @@ -0,0 +1,49 @@ +"""add workflow run credential selections + +Revision ID: aff1632dc377 +Revises: 2ac47bc1c075 +Create Date: 2026-07-03T18:53:23.324057+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "aff1632dc377" +down_revision: Union[str, None] = "2ac47bc1c075" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("credential_parameters", sa.Column("credential_ids", sa.JSON(), nullable=True)) + op.add_column("credential_parameters", sa.Column("selection_strategy", sa.String(), nullable=True)) + op.create_table( + "workflow_run_credential_selections", + sa.Column("selection_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("workflow_run_id", sa.String(), nullable=False), + sa.Column("workflow_permanent_id", sa.String(), nullable=False), + sa.Column("parameter_key", sa.String(), nullable=False), + sa.Column("credential_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("selection_id"), + sa.UniqueConstraint("workflow_run_id", "parameter_key", name="uq_wrcs_workflow_run_parameter_key"), + ) + op.create_index( + "idx_wrcs_lru_lookup", + "workflow_run_credential_selections", + ["organization_id", "workflow_permanent_id", "parameter_key", "credential_id", "created_at"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("idx_wrcs_lru_lookup", table_name="workflow_run_credential_selections") + op.drop_table("workflow_run_credential_selections") + op.drop_column("credential_parameters", "selection_strategy") + op.drop_column("credential_parameters", "credential_ids") diff --git a/alembic/versions/2026_07_07_1535-e2d87251fee8_browser_profile_name_unique_partial_on_deleted_at.py b/alembic/versions/2026_07_07_1535-e2d87251fee8_browser_profile_name_unique_partial_on_deleted_at.py new file mode 100644 index 000000000..0ac0a82ca --- /dev/null +++ b/alembic/versions/2026_07_07_1535-e2d87251fee8_browser_profile_name_unique_partial_on_deleted_at.py @@ -0,0 +1,51 @@ +"""browser profile name unique partial on deleted_at + +Revision ID: e2d87251fee8 +Revises: aff1632dc377 +Create Date: 2026-07-07T15:35:51.042177+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e2d87251fee8" +down_revision: Union[str, None] = "aff1632dc377" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.drop_index( + "uq_browser_profiles_org_name_user", + table_name="browser_profiles", + postgresql_where=sa.text("is_managed = false"), + ) + op.create_index( + "uq_browser_profiles_org_name_user", + "browser_profiles", + ["organization_id", "name"], + unique=True, + postgresql_where=sa.text("is_managed = false AND deleted_at IS NULL"), + sqlite_where=sa.text("is_managed = false AND deleted_at IS NULL"), + ) + + +def downgrade() -> None: + op.drop_index( + "uq_browser_profiles_org_name_user", + table_name="browser_profiles", + postgresql_where=sa.text("is_managed = false AND deleted_at IS NULL"), + ) + op.create_index( + "uq_browser_profiles_org_name_user", + "browser_profiles", + ["organization_id", "name"], + unique=True, + postgresql_where=sa.text("is_managed = false"), + sqlite_where=sa.text("is_managed = false"), + ) diff --git a/alembic/versions/2026_07_07_2121-5b618ec6dce6_add_pin_saved_session_ip_to_workflows.py b/alembic/versions/2026_07_07_2121-5b618ec6dce6_add_pin_saved_session_ip_to_workflows.py new file mode 100644 index 000000000..4ae5beb91 --- /dev/null +++ b/alembic/versions/2026_07_07_2121-5b618ec6dce6_add_pin_saved_session_ip_to_workflows.py @@ -0,0 +1,31 @@ +"""add pin_saved_session_ip to workflows + +Revision ID: 5b618ec6dce6 +Revises: e2d87251fee8 +Create Date: 2026-07-07T21:21:37.964227+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5b618ec6dce6" +down_revision: Union[str, None] = "e2d87251fee8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("SET LOCAL lock_timeout = '5s'") + op.add_column( + "workflows", + sa.Column("pin_saved_session_ip", sa.Boolean(), nullable=False, server_default=sa.false()), + ) + + +def downgrade() -> None: + op.drop_column("workflows", "pin_saved_session_ip") diff --git a/alembic/versions/2026_07_08_1754-bec06d149264_add_workflow_run_tag_events_table.py b/alembic/versions/2026_07_08_1754-bec06d149264_add_workflow_run_tag_events_table.py new file mode 100644 index 000000000..06fc42350 --- /dev/null +++ b/alembic/versions/2026_07_08_1754-bec06d149264_add_workflow_run_tag_events_table.py @@ -0,0 +1,133 @@ +"""add workflow run tag events table + +Revision ID: bec06d149264 +Revises: 5b618ec6dce6 +Create Date: 2026-07-08T17:54:31.712270+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "bec06d149264" +down_revision: Union[str, None] = "5b618ec6dce6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "workflow_run_tag_events", + sa.Column("tag_event_id", sa.String(), nullable=False), + sa.Column("workflow_run_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=True), + sa.Column("value", sa.String(), nullable=True), + sa.Column("event_type", sa.String(), nullable=False), + sa.Column("set_at", sa.DateTime(), nullable=False), + sa.Column("set_by", sa.String(), nullable=False), + sa.Column("source", sa.String(), nullable=False), + sa.Column("caller_type", sa.String(), nullable=True), + sa.Column("superseded_at", sa.DateTime(), nullable=True), + sa.Column("inherited_from_tag_event_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.CheckConstraint("event_type IN ('set', 'delete')", name="ck_workflow_run_tag_events_event_type"), + sa.CheckConstraint( + "source IN ('manual', 'bulk_apply', 'backfill', 'inherited', 'import', 'system')", + name="ck_workflow_run_tag_events_source", + ), + sa.CheckConstraint( + "caller_type IS NULL OR caller_type IN ('user', 'api_key', 'system')", + name="ck_workflow_run_tag_events_caller_type", + ), + sa.CheckConstraint( + "event_type != 'set' OR value IS NOT NULL", + name="ck_workflow_run_tag_events_set_has_value", + ), + sa.ForeignKeyConstraint( + ["workflow_run_id"], + ["workflow_runs.workflow_run_id"], + ), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.organization_id"], + ), + sa.PrimaryKeyConstraint("tag_event_id"), + ) + op.create_index( + "workflow_run_tag_events_org_wr_set_at_idx", + "workflow_run_tag_events", + ["organization_id", "workflow_run_id", "set_at"], + unique=False, + ) + op.create_index( + "workflow_run_tag_events_org_key_value_active_idx", + "workflow_run_tag_events", + ["organization_id", "key", "value"], + unique=False, + postgresql_include=["workflow_run_id"], + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set'"), + ) + op.create_index( + "workflow_run_tag_events_org_value_active_idx", + "workflow_run_tag_events", + ["organization_id", "value"], + unique=False, + postgresql_include=["workflow_run_id"], + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set'"), + ) + op.create_index( + "workflow_run_tag_events_org_set_at_idx", + "workflow_run_tag_events", + ["organization_id", "set_at"], + unique=False, + ) + op.create_index( + "workflow_run_tag_events_active_grouped_unique", + "workflow_run_tag_events", + ["organization_id", "workflow_run_id", "key"], + unique=True, + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set' AND key IS NOT NULL"), + sqlite_where=sa.text("superseded_at IS NULL AND event_type = 'set' AND key IS NOT NULL"), + ) + op.create_index( + "workflow_run_tag_events_active_label_unique", + "workflow_run_tag_events", + ["organization_id", "workflow_run_id", "value"], + unique=True, + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set' AND key IS NULL"), + sqlite_where=sa.text("superseded_at IS NULL AND event_type = 'set' AND key IS NULL"), + ) + + +def downgrade() -> None: + op.drop_index( + "workflow_run_tag_events_active_label_unique", + table_name="workflow_run_tag_events", + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set' AND key IS NULL"), + ) + op.drop_index( + "workflow_run_tag_events_active_grouped_unique", + table_name="workflow_run_tag_events", + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set' AND key IS NOT NULL"), + ) + op.drop_index("workflow_run_tag_events_org_set_at_idx", table_name="workflow_run_tag_events") + op.drop_index( + "workflow_run_tag_events_org_value_active_idx", + table_name="workflow_run_tag_events", + postgresql_include=["workflow_run_id"], + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set'"), + ) + op.drop_index( + "workflow_run_tag_events_org_key_value_active_idx", + table_name="workflow_run_tag_events", + postgresql_include=["workflow_run_id"], + postgresql_where=sa.text("superseded_at IS NULL AND event_type = 'set'"), + ) + op.drop_index("workflow_run_tag_events_org_wr_set_at_idx", table_name="workflow_run_tag_events") + op.drop_table("workflow_run_tag_events") diff --git a/alembic/versions/2026_07_09_2247-e1227914ecfe_add_heal_episodes_and_proposals.py b/alembic/versions/2026_07_09_2247-e1227914ecfe_add_heal_episodes_and_proposals.py new file mode 100644 index 000000000..25504ba4b --- /dev/null +++ b/alembic/versions/2026_07_09_2247-e1227914ecfe_add_heal_episodes_and_proposals.py @@ -0,0 +1,108 @@ +"""add heal episodes and proposals + +Revision ID: e1227914ecfe +Revises: bec06d149264 +Create Date: 2026-07-09T22:47:11.311011+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e1227914ecfe" +down_revision: Union[str, None] = "bec06d149264" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "heal_episodes", + sa.Column("heal_episode_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("workflow_permanent_id", sa.String(), nullable=False), + sa.Column("workflow_id", sa.String(), nullable=False), + sa.Column("workflow_run_id", sa.String(), nullable=False), + sa.Column("workflow_run_block_id", sa.String(), nullable=False), + sa.Column("block_label", sa.String(), nullable=False), + sa.Column("engine", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("skip_reason", sa.String(), nullable=True), + sa.Column("block_prompt", sa.UnicodeText(), nullable=True), + sa.Column("block_code", sa.UnicodeText(), nullable=True), + sa.Column("block_steps", sa.JSON(), nullable=True), + sa.Column("snapshot_available", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("convergence_eligible", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("parameter_binding_keys", sa.JSON(), nullable=True), + sa.Column("exception_class", sa.String(), nullable=True), + sa.Column("failing_line", sa.Integer(), nullable=True), + sa.Column("matched_step_index", sa.Integer(), nullable=True), + sa.Column("failure_message", sa.UnicodeText(), nullable=True), + sa.Column("escalation_task_id", sa.String(), nullable=True), + sa.Column("wall_clock_ms", sa.Integer(), nullable=True), + sa.Column("action_count", sa.Integer(), nullable=True), + sa.Column("output_obligation", sa.String(), nullable=True), + sa.Column("dom_snapshot_artifact_id", sa.String(), nullable=True), + sa.Column("scout_transcript_artifact_id", sa.String(), nullable=True), + sa.Column("screenshot_artifact_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("heal_episode_id"), + ) + op.create_index( + "he_org_wpid_index", + "heal_episodes", + ["organization_id", "workflow_permanent_id", "created_at"], + unique=False, + ) + op.create_index("he_org_created_at_index", "heal_episodes", ["organization_id", "created_at"], unique=False) + op.create_index("he_org_wrid_index", "heal_episodes", ["organization_id", "workflow_run_id"], unique=False) + + op.create_table( + "workflow_heal_proposals", + sa.Column("heal_proposal_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("workflow_permanent_id", sa.String(), nullable=False), + sa.Column("block_label", sa.String(), nullable=False), + sa.Column("candidate_definition", sa.JSON(), nullable=False), + sa.Column("provenance", sa.JSON(), nullable=True), + sa.Column("episode_ids", sa.JSON(), nullable=False), + sa.Column("rendered_diff", sa.UnicodeText(), nullable=True), + sa.Column("base_version", sa.Integer(), nullable=False), + sa.Column("base_definition_hash", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False, server_default=sa.text("'proposed'")), + sa.Column("adopted_workflow_id", sa.String(), nullable=True), + sa.Column("episode_window", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("heal_proposal_id"), + ) + op.create_index( + "hp_org_wpid_index", + "workflow_heal_proposals", + ["organization_id", "workflow_permanent_id"], + unique=False, + ) + + op.add_column( + "organizations", + sa.Column("selfheal_screenshot_capture_enabled", sa.Boolean(), nullable=False, server_default=sa.false()), + ) + op.add_column("organizations", sa.Column("selfheal_artifact_retention_days", sa.Integer(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("organizations", "selfheal_artifact_retention_days") + op.drop_column("organizations", "selfheal_screenshot_capture_enabled") + + op.drop_index("hp_org_wpid_index", table_name="workflow_heal_proposals") + op.drop_table("workflow_heal_proposals") + + op.drop_index("he_org_wrid_index", table_name="heal_episodes") + op.drop_index("he_org_created_at_index", table_name="heal_episodes") + op.drop_index("he_org_wpid_index", table_name="heal_episodes") + op.drop_table("heal_episodes") diff --git a/docker-compose.yml b/docker-compose.yml index 5614266da..fb8999bdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,9 @@ services: - ./videos:/data/videos - ./har:/data/har - ./log:/data/log + - ./downloads:/data/downloads + - ./browser_sessions:/data/browser_sessions + - ./credential_vault:/data/credential_vault # Generated credentials allow the UI to pick up the local API key on first startup. - ./.skyvern:/app/.skyvern # Uncomment the following two lines if you want to connect to any local changes @@ -62,6 +65,11 @@ services: - BROWSER_REMOTE_DEBUGGING_URL=${BROWSER_REMOTE_DEBUGGING_URL:-http://127.0.0.1:9222} - BROWSER_REMOTE_DEBUGGING_HOST_HEADER=${BROWSER_REMOTE_DEBUGGING_HOST_HEADER:-} - BROWSER_CDP_CONNECT_TIMEOUT_MS=${BROWSER_CDP_CONNECT_TIMEOUT_MS:-120000} + - DOWNLOAD_PATH=/data/downloads + - BROWSER_SESSION_BASE_PATH=/data/browser_sessions + - CREDENTIAL_VAULT_TYPE=${CREDENTIAL_VAULT_TYPE:-skyvern} + - ENABLE_LOCAL_CREDENTIAL_VAULT=${ENABLE_LOCAL_CREDENTIAL_VAULT:-true} + - LOCAL_CREDENTIAL_VAULT_PATH=/data/credential_vault - ENABLE_CODE_BLOCK=true # --- Control your own browser (Chrome/Chromium) --- # Prefer Chrome's chrome://inspect/#remote-debugging flow for an existing profile. diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index aea2cf7cd..3f216317b 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -1519,6 +1519,24 @@ "title": "Key" }, "description": "Tag key to delete" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." } ], "responses": { @@ -1549,6 +1567,290 @@ "x-fern-sdk-method-name": "delete_tag_key" } }, + "/v1/tag-values": { + "get": { + "tags": [ + "Tags" + ], + "summary": "List tag values", + "description": "List the palette color and current workflow usage count for each grouped tag (key, value) for the organization. The frontend joins these onto tags by (key, value); workflow_count is the number of non-deleted workflows carrying the label and powers the per-label usage and delete blast-radius warnings.", + "operationId": "list_tag_values_v1_tag_values_get", + "parameters": [ + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "responses": { + "200": { + "description": "Successfully retrieved tag values", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagValue" + }, + "title": "Response List Tag Values V1 Tag Values Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "list_tag_values" + } + }, + "/v1/tag-values/{key}": { + "patch": { + "tags": [ + "Tags" + ], + "summary": "Update tag value color", + "description": "Recolor a grouped tag (key, value). The value is supplied in the body so values containing '/' stay addressable. The new color must be a palette name.", + "operationId": "update_tag_value_v1_tag_values__key__patch", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Tag key (group)", + "examples": [ + "env" + ], + "title": "Key" + }, + "description": "Tag key (group)" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagValueUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully recolored tag value", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagValue" + } + } + } + }, + "404": { + "description": "Tag value not found" + }, + "422": { + "description": "Invalid palette color" + } + }, + "x-fern-sdk-method-name": "update_tag_value" + }, + "delete": { + "tags": [ + "Tags" + ], + "summary": "Delete tag value", + "description": "Soft-delete a grouped tag (key, value) and remove that label from every workflow carrying it (cascade). The value rides in the body so values containing '/' stay addressable. Returns how many workflows the label was removed from.", + "operationId": "delete_tag_value_v1_tag_values__key__delete", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Tag key (group)", + "examples": [ + "env" + ], + "title": "Key" + }, + "description": "Tag key (group)" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagValueDelete" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully deleted tag value", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagValueDeleteResponse" + } + } + } + }, + "404": { + "description": "Tag value not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "delete_tag_value" + } + }, + "/v1/tag-values/{key}/rename": { + "patch": { + "tags": [ + "Tags" + ], + "summary": "Rename tag value", + "description": "Rename a grouped tag (key, value) to (key, new_value). The cascade re-tags every workflow carrying the old label; the new label inherits the old color. Both values ride in the body so values containing '/' stay addressable. Rejects with 409 when the new value already exists for the key.", + "operationId": "rename_tag_value_v1_tag_values__key__rename_patch", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Tag key (group)", + "examples": [ + "env" + ], + "title": "Key" + }, + "description": "Tag key (group)" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagValueRename" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully renamed tag value", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagValueRenameResponse" + } + } + } + }, + "404": { + "description": "Tag value not found" + }, + "409": { + "description": "Target value already exists for this key" + }, + "422": { + "description": "Invalid value" + } + }, + "x-fern-sdk-method-name": "rename_tag_value" + } + }, "/v1/workflow-tags": { "get": { "tags": [ @@ -2241,7 +2543,7 @@ "Browser Profiles" ], "summary": "Create a browser profile", - "description": "Create a browser profile from a persistent browser session or workflow run.", + "description": "Create a blank browser profile, or create one from a persistent browser session or workflow run.", "operationId": "create_browser_profile_v1_browser_profiles_post", "parameters": [ { @@ -2285,7 +2587,7 @@ } }, "400": { - "description": "Invalid request - missing source or source not found" + "description": "Invalid request - source not found or source archive unavailable" }, "409": { "description": "Browser profile name already exists" @@ -2307,11 +2609,11 @@ "code-samples": [ { "sdk": "python", - "code": "from skyvern import Skyvern\n\nskyvern = Skyvern(api_key=\"YOUR_API_KEY\")\n# Create a browser profile from a persistent browser session\nbrowser_profile = await skyvern.browser_profiles.create_browser_profile(\n name=\"My Profile\",\n browser_session_id=\"pbs_123\",\n)\nprint(browser_profile)\n\n# Or create from a workflow run with persist_browser_session=True\nbrowser_profile = await skyvern.browser_profiles.create_browser_profile(\n name=\"My Profile\",\n workflow_run_id=\"wr_123\",\n)\nprint(browser_profile)\n" + "code": "from skyvern import Skyvern\n\nskyvern = Skyvern(api_key=\"YOUR_API_KEY\")\n# Create a blank browser profile for future runs\nblank_profile = await skyvern.browser_profiles.create_browser_profile(\n name=\"Fresh Profile\",\n)\nprint(blank_profile)\n\n# Create a browser profile from a persistent browser session\nbrowser_profile = await skyvern.browser_profiles.create_browser_profile(\n name=\"My Profile\",\n browser_session_id=\"pbs_123\",\n)\nprint(browser_profile)\n\n# Or create from a workflow run with persist_browser_session=True\nbrowser_profile = await skyvern.browser_profiles.create_browser_profile(\n name=\"My Profile\",\n workflow_run_id=\"wr_123\",\n)\nprint(browser_profile)\n" }, { "sdk": "typescript", - "code": "import { SkyvernClient } from \"@skyvern/client\";\n\nconst skyvern = new SkyvernClient({ apiKey: \"YOUR_API_KEY\" });\n// Create a browser profile from a persistent browser session\nconst browserProfile = await skyvern.browserProfiles.createBrowserProfile({\n name: \"My Profile\",\n browser_session_id: \"pbs_123\",\n});\nconsole.log(browserProfile);\n\n// Or create from a workflow run with persist_browser_session=True\nconst browserProfile2 = await skyvern.browserProfiles.createBrowserProfile({\n name: \"My Profile\",\n workflow_run_id: \"wr_123\",\n});\nconsole.log(browserProfile2);\n" + "code": "import { SkyvernClient } from \"@skyvern/client\";\n\nconst skyvern = new SkyvernClient({ apiKey: \"YOUR_API_KEY\" });\n// Create a blank browser profile for future runs\nconst blankProfile = await skyvern.browserProfiles.createBrowserProfile({\n name: \"Fresh Profile\",\n});\nconsole.log(blankProfile);\n\n// Create a browser profile from a persistent browser session\nconst browserProfile = await skyvern.browserProfiles.createBrowserProfile({\n name: \"My Profile\",\n browser_session_id: \"pbs_123\",\n});\nconsole.log(browserProfile);\n\n// Or create from a workflow run with persist_browser_session=True\nconst browserProfile2 = await skyvern.browserProfiles.createBrowserProfile({\n name: \"My Profile\",\n workflow_run_id: \"wr_123\",\n});\nconsole.log(browserProfile2);\n" } ] } @@ -2381,6 +2683,24 @@ }, "description": "Case-insensitive substring search across: browser profile name and description. A profile is returned if either field matches." }, + { + "name": "managed", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Omit to return all profiles; false returns only user-created profiles; true returns only auto-managed profiles.", + "title": "Managed" + }, + "description": "Omit to return all profiles; false returns only user-created profiles; true returns only auto-managed profiles." + }, { "name": "x-api-key", "in": "header", @@ -2731,7 +3051,8 @@ "schema": { "$ref": "#/components/schemas/CreateBrowserSessionRequest", "default": { - "timeout": 60 + "timeout": 60, + "generate_browser_profile": false } } } @@ -2937,6 +3258,90 @@ } }, "/v1/browser_sessions/{browser_session_id}": { + "patch": { + "tags": [ + "Browser Sessions" + ], + "summary": "Update a session", + "description": "Update a live browser session. Currently supports toggling generate_browser_profile, which is read when the session ends to decide whether to save its browser profile.", + "operationId": "update_browser_session_v1_browser_sessions__browser_session_id__patch", + "parameters": [ + { + "name": "browser_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the browser session. browser_session_id starts with `pbs_`", + "examples": [ + "pbs_123456" + ], + "title": "Browser Session Id" + }, + "description": "The ID of the browser session. browser_session_id starts with `pbs_`" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBrowserSessionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully updated browser session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowserSessionResponse" + } + } + } + }, + "404": { + "description": "Browser session not found" + }, + "403": { + "description": "Unauthorized - Invalid or missing authentication" + }, + "409": { + "description": "Conflict - browser session has already ended and can no longer be updated" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "update_browser_session" + }, "get": { "tags": [ "Browser Sessions" @@ -3185,7 +3590,7 @@ }, { "sdk": "python", - "code": "from skyvern import Skyvern\n\nskyvern = Skyvern(api_key=\"YOUR_API_KEY\")\nawait skyvern.create_credential(\n name=\"My Credit Card\",\n credential_type=\"credit_card\",\n credential={\n \"card_number\": \"4242424242424242\",\n \"card_cvv\": \"424\",\n \"card_exp_month\": \"12\",\n \"card_exp_year\": \"2028\",\n \"card_brand\": \"visa\",\n \"card_holder_name\": \"John Doe\",\n },\n)\n" + "code": "from skyvern import Skyvern\n\nskyvern = Skyvern(api_key=\"YOUR_API_KEY\")\nawait skyvern.create_credential(\n name=\"My Credit Card\",\n credential_type=\"credit_card\",\n credential={\n \"card_number\": \"4242424242424242\",\n \"card_cvv\": \"424\",\n \"card_exp_month\": \"12\",\n \"card_exp_year\": \"2028\",\n \"card_brand\": \"visa\",\n \"card_holder_name\": \"John Doe\",\n \"billing_address\": {\n \"line1\": \"123 Main St\",\n \"city\": \"San Francisco\",\n \"state_code\": \"CA\",\n \"postal_code\": \"94105\",\n \"country_code\": \"US\",\n },\n \"billing_email\": \"billing@example.com\",\n \"metadata\": {\"checkout_profile\": \"default\"},\n },\n)\n" }, { "sdk": "typescript", @@ -3193,7 +3598,7 @@ }, { "sdk": "typescript", - "code": "import { SkyvernClient } from \"@skyvern/client\";\n\nconst skyvern = new SkyvernClient({ apiKey: \"YOUR_API_KEY\" });\nawait skyvern.createCredential({\n name: \"My Credit Card\",\n credential_type: \"credit_card\",\n credential: {\n card_number: \"4242424242424242\",\n card_cvv: \"424\",\n card_exp_month: \"12\",\n card_exp_year: \"2028\",\n card_brand: \"visa\",\n card_holder_name: \"John Doe\"\n }\n});\n" + "code": "import { SkyvernClient } from \"@skyvern/client\";\n\nconst skyvern = new SkyvernClient({ apiKey: \"YOUR_API_KEY\" });\nawait skyvern.createCredential({\n name: \"My Credit Card\",\n credential_type: \"credit_card\",\n credential: {\n card_number: \"4242424242424242\",\n card_cvv: \"424\",\n card_exp_month: \"12\",\n card_exp_year: \"2028\",\n card_brand: \"visa\",\n card_holder_name: \"John Doe\",\n billing_address: {\n line1: \"123 Main St\",\n city: \"San Francisco\",\n state_code: \"CA\",\n postal_code: \"94105\",\n country_code: \"US\",\n },\n billing_email: \"billing@example.com\",\n metadata: { checkout_profile: \"default\" }\n }\n});\n" } ] } @@ -3258,10 +3663,10 @@ "type": "null" } ], - "description": "Filter credentials by vault type (e.g. 'custom', 'bitwarden', 'azure_vault')", + "description": "Filter credentials by vault type (e.g. 'skyvern', 'custom', 'bitwarden', 'azure_vault')", "title": "Vault Type" }, - "description": "Filter credentials by vault type (e.g. 'custom', 'bitwarden', 'azure_vault')" + "description": "Filter credentials by vault type (e.g. 'skyvern', 'custom', 'bitwarden', 'azure_vault')" }, { "name": "credential_type", @@ -3631,6 +4036,256 @@ ] } }, + "/v1/custom-llms": { + "get": { + "tags": [ + "Custom LLMs" + ], + "summary": "List Custom LLMs", + "description": "List the custom LLM configurations available to the current organization.", + "operationId": "list_custom_llms_v1_custom_llms_get", + "parameters": [ + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomLLMListResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "list_custom_llms" + }, + "post": { + "tags": [ + "Custom LLMs" + ], + "summary": "Create Custom LLM", + "description": "Create a custom LLM configuration for the current organization.", + "operationId": "create_custom_llm_v1_custom_llms_post", + "parameters": [ + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomLLMCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomLLMResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "create_custom_llm" + } + }, + "/v1/custom-llms/{custom_llm_id}": { + "put": { + "tags": [ + "Custom LLMs" + ], + "summary": "Update Custom LLM", + "description": "Update a custom LLM configuration for the current organization.", + "operationId": "update_custom_llm_v1_custom_llms__custom_llm_id__put", + "parameters": [ + { + "name": "custom_llm_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The custom LLM id.", + "title": "Custom Llm Id" + }, + "description": "The custom LLM id." + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomLLMUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomLLMResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "update_custom_llm" + }, + "delete": { + "tags": [ + "Custom LLMs" + ], + "summary": "Delete Custom LLM", + "description": "Delete a custom LLM configuration for the current organization.", + "operationId": "delete_custom_llm_v1_custom_llms__custom_llm_id__delete", + "parameters": [ + { + "name": "custom_llm_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The custom LLM id.", + "title": "Custom Llm Id" + }, + "description": "The custom LLM id." + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearOrganizationAuthTokenResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-fern-sdk-method-name": "delete_custom_llm" + } + }, "/v1/run/tasks/login": { "post": { "tags": [ @@ -4478,6 +5133,24 @@ "title": "Workflow Permanent Id" }, "description": "Workflow permanent ID" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." } ], "requestBody": { @@ -4615,6 +5288,24 @@ "title": "Key" }, "description": "Tag key to delete" + }, + { + "name": "x-api-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings.", + "title": "X-Api-Key" + }, + "description": "Skyvern API key for authentication. API key can be found at https://app.skyvern.com/settings." } ], "responses": { @@ -5136,6 +5827,44 @@ }, "description": "Exact-match filter on the error_code field inside each task's errors JSON array. A run matches if any of its tasks contains an error with a matching error_code. Error codes are user-defined strings set during workflow execution." }, + { + "name": "created_at_start", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "description": "Only include runs created at or after this UTC timestamp (ISO 8601).", + "title": "Created At Start" + }, + "description": "Only include runs created at or after this UTC timestamp (ISO 8601)." + }, + { + "name": "created_at_end", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "description": "Only include runs created strictly before this UTC timestamp (ISO 8601).", + "title": "Created At End" + }, + "description": "Only include runs created strictly before this UTC timestamp (ISO 8601)." + }, { "name": "x-api-key", "in": "header", @@ -7038,6 +7767,8 @@ "complete", "reload_page", "close_page", + "new_tab", + "switch_tab", "extract", "verification_code", "goto_url", @@ -7249,6 +7980,7 @@ "type": "string", "enum": [ "recording", + "audio", "session_replay", "browser_console_log", "skyvern_log", @@ -8020,7 +8752,8 @@ "print_page", "workflow_trigger", "google_sheets_read", - "google_sheets_write" + "google_sheets_write", + "pdf_fill" ], "title": "BlockType" }, @@ -8213,6 +8946,62 @@ ], "title": "Source Browser Type" }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location" + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id" + }, + "is_managed": { + "type": "boolean", + "title": "Is Managed", + "default": false + }, + "workflow_permanent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workflow Permanent Id" + }, + "browser_profile_key_digest": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Browser Profile Key Digest" + }, "created_at": { "type": "string", "format": "date-time", @@ -8397,6 +9186,12 @@ "title": "Browser Profile Id", "description": "ID of the browser profile loaded into this session, if any. browser_profile_id starts with `bp_`." }, + "generate_browser_profile": { + "type": "boolean", + "title": "Generate Browser Profile", + "description": "Whether this session's browser profile will be saved when it ends so it can become a reusable browser profile.", + "default": false + }, "vnc_streaming_supported": { "type": "boolean", "title": "Vnc Streaming Supported", @@ -8551,6 +9346,21 @@ ], "title": "BulkCancelRunsResponse" }, + "ClearOrganizationAuthTokenResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether the token was cleared successfully" + } + }, + "type": "object", + "required": [ + "success" + ], + "title": "ClearOrganizationAuthTokenResponse", + "description": "Response model for clearing an organization auth token." + }, "ClickAction": { "properties": { "type": { @@ -8754,6 +9564,31 @@ "type": "array", "title": "Parameters", "default": [] + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt" + }, + "steps": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/CodeBlockStep" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Steps" } }, "type": "object", @@ -8764,6 +9599,115 @@ ], "title": "CodeBlock" }, + "CodeBlockStep": { + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "action_type": { + "$ref": "#/components/schemas/ActionType", + "default": "null_action" + }, + "line_start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Line Start" + }, + "line_end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Line End" + } + }, + "type": "object", + "title": "CodeBlockStep" + }, + "CodeBlockStepYAML": { + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "action_type": { + "type": "string", + "title": "Action Type", + "default": "null_action" + }, + "line_start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Line Start" + }, + "line_end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Line End" + } + }, + "type": "object", + "title": "CodeBlockStepYAML" + }, "CodeBlockYAML": { "properties": { "block_type": { @@ -8833,6 +9777,31 @@ } ], "title": "Parameter Keys" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt" + }, + "steps": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/CodeBlockStepYAML" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Steps" } }, "type": "object", @@ -9139,16 +10108,49 @@ "browser_session_id": { "anyOf": [ { - "type": "string" + "type": "string", + "minLength": 1 }, { "type": "null" } ], "title": "Browser Session Id", - "description": "Persistent browser session to convert into a profile" + "description": "Persistent browser session to convert into a profile. Omit for a blank profile." }, "workflow_run_id": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Workflow Run Id", + "description": "Workflow run whose persisted session should be captured. Omit for a blank profile." + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location for this profile's pinned proxy identity." + }, + "proxy_session_id": { "anyOf": [ { "type": "string" @@ -9157,8 +10159,8 @@ "type": "null" } ], - "title": "Workflow Run Id", - "description": "Workflow run whose persisted session should be captured" + "title": "Proxy Session Id", + "description": "Explicit sticky-session id for this profile's pinned proxy identity." } }, "type": "object", @@ -9203,6 +10205,18 @@ "title": "Proxy Location", "description": "\nGeographic Proxy location to route the browser traffic through. This is only available in Skyvern Cloud.\n\nAvailable geotargeting options:\n- RESIDENTIAL: the default value. Skyvern Cloud uses a random US residential proxy.\n- RESIDENTIAL_ES: Spain\n- RESIDENTIAL_IE: Ireland\n- RESIDENTIAL_GB: United Kingdom\n- RESIDENTIAL_IN: India\n- RESIDENTIAL_JP: Japan\n- RESIDENTIAL_FR: France\n- RESIDENTIAL_DE: Germany\n- RESIDENTIAL_NZ: New Zealand\n- RESIDENTIAL_PH: Philippines\n- RESIDENTIAL_KR: South Korea\n- RESIDENTIAL_SA: Saudi Arabia\n- RESIDENTIAL_ZA: South Africa\n- RESIDENTIAL_AR: Argentina\n- RESIDENTIAL_AU: Australia\n- RESIDENTIAL_BR: Brazil\n- RESIDENTIAL_TR: Turkey\n- RESIDENTIAL_CA: Canada\n- RESIDENTIAL_MX: Mexico\n- RESIDENTIAL_IT: Italy\n- RESIDENTIAL_NL: Netherlands\n- RESIDENTIAL_ISP: ISP proxy\n- US-CA: California (deprecated, routes through RESIDENTIAL_ISP)\n- US-NY: New York (deprecated, routes through RESIDENTIAL_ISP)\n- US-TX: Texas (deprecated, routes through RESIDENTIAL_ISP)\n- US-FL: Florida (deprecated, routes through RESIDENTIAL_ISP)\n- US-WA: Washington (deprecated, routes through RESIDENTIAL_ISP)\n- NONE: No proxy\n\nFor self-hosted deployments, you can pass a custom proxy URL as a dict: {\"url\": \"http://user:password@proxy.example.com:8080\"}. This routes the browser through your own proxy server and takes precedence over any globally configured proxy pool.\n Can also be a GeoTarget object for granular city/state targeting: {\"country\": \"US\", \"subdivision\": \"CA\", \"city\": \"San Francisco\"}, or a custom proxy URL dict for self-hosted deployments: {\"url\": \"http://user:password@proxy.example.com:8080\"}" }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions." + }, "extensions": { "anyOf": [ { @@ -9241,6 +10255,12 @@ ], "title": "Browser Profile Id", "description": "ID of a browser profile to load into this session (restores cookies, localStorage, etc.). browser_profile_id starts with `bp_`." + }, + "generate_browser_profile": { + "type": "boolean", + "title": "Generate Browser Profile", + "description": "When true, the session's browser profile (cookies, localStorage, etc.) is saved to storage when the session ends so it can be turned into a reusable browser profile. Defaults to false to avoid storing profiles for sessions that never need them. Sessions started with a browser_profile_id always persist their profile regardless of this flag.", + "default": false } }, "type": "object", @@ -9292,10 +10312,48 @@ ], "description": "Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization.", "examples": [ + "skyvern", "custom", "azure_vault", "bitwarden" ] + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location for this credential's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Optional advanced reuse key for this credential's pinned proxy identity." + }, + "rotate_proxy_session_id": { + "type": "boolean", + "title": "Rotate Proxy Session Id", + "description": "Rotate the Skyvern-managed proxy sticky-session id when updating this credential.", + "default": false } }, "type": "object", @@ -9461,6 +10519,31 @@ "type": "string", "title": "Credential Id" }, + "credential_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Credential Ids" + }, + "selection_strategy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Selection Strategy" + }, "created_at": { "type": "string", "format": "date-time", @@ -9521,6 +10604,31 @@ "credential_id": { "type": "string", "title": "Credential Id" + }, + "credential_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Credential Ids" + }, + "selection_strategy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Selection Strategy" } }, "type": "object", @@ -9576,7 +10684,7 @@ "type": "null" } ], - "description": "Which vault stores this credential (e.g., 'bitwarden', 'azure_vault', 'custom')" + "description": "Which vault stores this credential (e.g., 'skyvern', 'bitwarden', 'azure_vault', 'custom')" }, "browser_profile_id": { "anyOf": [ @@ -9640,6 +10748,37 @@ "examples": [ "cfld_1234567890" ] + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location used for the credential's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Opaque Skyvern-managed proxy sticky-session id." } }, "type": "object", @@ -9665,12 +10804,141 @@ "CredentialVaultType": { "type": "string", "enum": [ + "skyvern", "bitwarden", "azure_vault", + "gcp", "custom" ], "title": "CredentialVaultType" }, + "CreditCardBillingAddress": { + "properties": { + "line1": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Line1", + "description": "Billing address line 1", + "examples": [ + "123 Main St" + ] + }, + "line2": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Line2", + "description": "Billing address line 2", + "examples": [ + "Apt 4B" + ] + }, + "city": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "City", + "description": "Billing city", + "examples": [ + "San Francisco" + ] + }, + "state": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "State", + "description": "Billing state or region", + "examples": [ + "California" + ] + }, + "state_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "State Code", + "description": "Billing state or region code", + "examples": [ + "CA" + ] + }, + "postal_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Postal Code", + "description": "Billing postal code", + "examples": [ + "94105" + ] + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Country", + "description": "Billing country", + "examples": [ + "United States" + ] + }, + "country_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Country Code", + "description": "ISO 3166-1 alpha-2 billing country code", + "examples": [ + "US" + ] + } + }, + "type": "object", + "title": "CreditCardBillingAddress", + "description": "Optional billing address fields associated with a credit card credential." + }, "CreditCardCredentialResponse": { "properties": { "last_four": { @@ -9696,7 +10964,215 @@ "brand" ], "title": "CreditCardCredentialResponse", - "description": "Response model for credit card credentials — non-sensitive fields only.\n\nSECURITY: Must NEVER include full card number, CVV, expiration date, or card holder name." + "description": "Response model for credit card credentials — non-sensitive fields only.\n\nSECURITY: Must NEVER include full card number, CVV, expiration date, card holder name,\nbilling fields, or metadata." + }, + "CustomLLM": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "config": { + "$ref": "#/components/schemas/CustomLLMConfig" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "modified_at": { + "type": "string", + "title": "Modified At" + }, + "valid": { + "type": "boolean", + "title": "Valid" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "config", + "created_at", + "modified_at", + "valid" + ], + "title": "CustomLLM" + }, + "CustomLLMConfig": { + "properties": { + "display_name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Display Name" + }, + "provider": { + "$ref": "#/components/schemas/CustomLLMProvider" + }, + "model_name": { + "type": "string", + "maxLength": 250, + "minLength": 1, + "title": "Model Name" + }, + "api_base": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Api Base" + }, + "api_key": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ], + "title": "Api Key" + }, + "api_version": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Api Version" + }, + "supports_vision": { + "type": "boolean", + "title": "Supports Vision", + "default": true + }, + "add_assistant_prefix": { + "type": "boolean", + "title": "Add Assistant Prefix", + "default": false + }, + "max_completion_tokens": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Completion Tokens" + }, + "temperature": { + "anyOf": [ + { + "type": "number", + "maximum": 2.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Temperature" + }, + "reasoning_effort": { + "anyOf": [ + { + "type": "string", + "maxLength": 50 + }, + { + "type": "null" + } + ], + "title": "Reasoning Effort" + } + }, + "type": "object", + "required": [ + "display_name", + "provider", + "model_name" + ], + "title": "CustomLLMConfig" + }, + "CustomLLMCreateRequest": { + "properties": { + "config": { + "$ref": "#/components/schemas/CustomLLMConfig" + } + }, + "type": "object", + "required": [ + "config" + ], + "title": "CustomLLMCreateRequest" + }, + "CustomLLMListResponse": { + "properties": { + "custom_llms": { + "items": { + "$ref": "#/components/schemas/CustomLLM" + }, + "type": "array", + "title": "Custom Llms" + } + }, + "type": "object", + "required": [ + "custom_llms" + ], + "title": "CustomLLMListResponse" + }, + "CustomLLMProvider": { + "type": "string", + "enum": [ + "openai_compatible", + "ollama", + "openrouter" + ], + "title": "CustomLLMProvider" + }, + "CustomLLMResponse": { + "properties": { + "custom_llm": { + "$ref": "#/components/schemas/CustomLLM" + } + }, + "type": "object", + "required": [ + "custom_llm" + ], + "title": "CustomLLMResponse" + }, + "CustomLLMUpdateRequest": { + "properties": { + "config": { + "$ref": "#/components/schemas/CustomLLMConfig" + } + }, + "type": "object", + "required": [ + "config" + ], + "title": "CustomLLMUpdateRequest" }, "DeleteScheduleResponse": { "properties": { @@ -11414,7 +12890,8 @@ "type": "string", "enum": [ "s3", - "azure" + "azure", + "google_drive" ], "title": "FileStorageType" }, @@ -11426,7 +12903,8 @@ "excel", "pdf", "image", - "docx" + "docx", + "zip" ], "title": "FileType" }, @@ -11571,6 +13049,28 @@ ], "title": "Azure Blob Container Name" }, + "google_credential_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Google Credential Id" + }, + "google_drive_folder_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Google Drive Folder Id" + }, "path": { "anyOf": [ { @@ -11581,6 +13081,12 @@ } ], "title": "Path" + }, + "continue_on_empty": { + "type": "boolean", + "title": "Continue On Empty", + "description": "When the run download directory has no files, allow the empty upload only after confirming no registered, browser-session, or alternate candidate downloads exist (False, default). Set True to always allow an empty upload.", + "default": false } }, "type": "object", @@ -11734,6 +13240,28 @@ ], "title": "Azure Folder Path" }, + "google_credential_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Google Credential Id" + }, + "google_drive_folder_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Google Drive Folder Id" + }, "path": { "anyOf": [ { @@ -12014,6 +13542,9 @@ }, { "$ref": "#/components/schemas/GoogleSheetsWriteBlock" + }, + { + "$ref": "#/components/schemas/PdfFillBlock" } ], "discriminator": { @@ -12035,6 +13566,7 @@ "human_interaction": "#/components/schemas/HumanInteractionBlock", "login": "#/components/schemas/LoginBlock", "navigation": "#/components/schemas/NavigationBlock", + "pdf_fill": "#/components/schemas/PdfFillBlock", "pdf_parser": "#/components/schemas/PDFParserBlock", "print_page": "#/components/schemas/PrintPageBlock", "send_email": "#/components/schemas/SendEmailBlock", @@ -12280,6 +13812,9 @@ { "$ref": "#/components/schemas/PrintPageBlockYAML" }, + { + "$ref": "#/components/schemas/PdfFillBlockYAML" + }, { "$ref": "#/components/schemas/WorkflowTriggerBlockYAML" }, @@ -13180,6 +14715,20 @@ "title": "Save Response As File", "default": false }, + "secret_response_paths": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Secret Response Paths" + }, "parameters": { "items": { "oneOf": [ @@ -13380,6 +14929,20 @@ "title": "Save Response As File", "default": false }, + "secret_response_paths": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Secret Response Paths" + }, "parameter_keys": { "anyOf": [ { @@ -15436,6 +16999,56 @@ "examples": [ "John Doe" ] + }, + "billing_address": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreditCardBillingAddress" + }, + { + "type": "null" + } + ], + "description": "Optional billing address associated with the card" + }, + "billing_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Billing Email", + "description": "Optional billing email address" + }, + "billing_phone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Billing Phone", + "description": "Optional billing phone number" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Optional additional credit card metadata fields" } }, "type": "object", @@ -16081,6 +17694,285 @@ "title": "PasswordCredentialResponse", "description": "Response model for password credentials — non-sensitive fields only.\n\nSECURITY: Must NEVER include password or TOTP secret." }, + "PdfFillBlock": { + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "Author-facing identifier for a block; unique within a workflow." + }, + "next_block_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Block Label", + "description": "Optional pointer to the next block label when constructing a DAG. Defaults to sequential order when omitted." + }, + "block_type": { + "type": "string", + "const": "pdf_fill", + "title": "Block Type", + "default": "pdf_fill" + }, + "output_parameter": { + "$ref": "#/components/schemas/OutputParameter" + }, + "continue_on_failure": { + "type": "boolean", + "title": "Continue On Failure", + "default": false + }, + "model": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "disable_cache": { + "type": "boolean", + "title": "Disable Cache", + "default": false + }, + "ignore_workflow_system_prompt": { + "type": "boolean", + "title": "Ignore Workflow System Prompt", + "default": false + }, + "next_loop_on_failure": { + "type": "boolean", + "title": "Next Loop On Failure", + "default": false + }, + "file_url": { + "type": "string", + "title": "File Url" + }, + "prompt": { + "type": "string", + "title": "Prompt" + }, + "payload": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "items": {}, + "type": "array" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Payload" + }, + "llm_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Llm Key" + }, + "parameters": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/WorkflowParameter" + }, + { + "$ref": "#/components/schemas/ContextParameter" + }, + { + "$ref": "#/components/schemas/AWSSecretParameter" + }, + { + "$ref": "#/components/schemas/AzureSecretParameter" + }, + { + "$ref": "#/components/schemas/BitwardenLoginCredentialParameter" + }, + { + "$ref": "#/components/schemas/BitwardenSensitiveInformationParameter" + }, + { + "$ref": "#/components/schemas/BitwardenCreditCardDataParameter" + }, + { + "$ref": "#/components/schemas/OnePasswordCredentialParameter" + }, + { + "$ref": "#/components/schemas/AzureVaultCredentialParameter" + }, + { + "$ref": "#/components/schemas/OutputParameter" + }, + { + "$ref": "#/components/schemas/CredentialParameter" + } + ], + "discriminator": { + "propertyName": "parameter_type", + "mapping": { + "aws_secret": "#/components/schemas/AWSSecretParameter", + "azure_secret": "#/components/schemas/AzureSecretParameter", + "azure_vault_credential": "#/components/schemas/AzureVaultCredentialParameter", + "bitwarden_credit_card_data": "#/components/schemas/BitwardenCreditCardDataParameter", + "bitwarden_login_credential": "#/components/schemas/BitwardenLoginCredentialParameter", + "bitwarden_sensitive_information": "#/components/schemas/BitwardenSensitiveInformationParameter", + "context": "#/components/schemas/ContextParameter", + "credential": "#/components/schemas/CredentialParameter", + "onepassword": "#/components/schemas/OnePasswordCredentialParameter", + "output": "#/components/schemas/OutputParameter", + "workflow": "#/components/schemas/WorkflowParameter" + } + } + }, + "type": "array", + "title": "Parameters", + "default": [] + } + }, + "type": "object", + "required": [ + "label", + "output_parameter", + "file_url", + "prompt" + ], + "title": "PdfFillBlock" + }, + "PdfFillBlockYAML": { + "properties": { + "block_type": { + "type": "string", + "const": "pdf_fill", + "title": "Block Type", + "default": "pdf_fill" + }, + "label": { + "type": "string", + "title": "Label", + "description": "Author-facing identifier; must be unique per workflow." + }, + "next_block_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Block Label", + "description": "Optional pointer to the label of the next block. When omitted, it will default to sequential order. See [[s-4bnl]]." + }, + "continue_on_failure": { + "type": "boolean", + "title": "Continue On Failure", + "default": false + }, + "model": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "ignore_workflow_system_prompt": { + "type": "boolean", + "title": "Ignore Workflow System Prompt", + "default": false + }, + "next_loop_on_failure": { + "type": "boolean", + "title": "Next Loop On Failure", + "default": false + }, + "file_url": { + "type": "string", + "title": "File Url" + }, + "prompt": { + "type": "string", + "title": "Prompt" + }, + "payload": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "items": {}, + "type": "array" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Payload" + }, + "llm_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Llm Key" + }, + "parameter_keys": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Parameter Keys" + } + }, + "type": "object", + "required": [ + "label", + "file_url", + "prompt" + ], + "title": "PdfFillBlockYAML" + }, "PersistentBrowserType": { "type": "string", "enum": [ @@ -17571,6 +19463,21 @@ "type": "array", "title": "Tags To Delete", "description": "Tags to soft-delete. List of {key?, value?} targets." + }, + "colors": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Colors", + "description": "Optional map of grouped tag key to palette color name for the value being set. Keys absent from this map keep their existing color or receive a random palette color." } }, "type": "object", @@ -17861,6 +19768,146 @@ "title": "TagResponse", "description": "Current state of one tag (``GET /v1/workflows/{wpid}/tags`` row).\n``key`` is null for a standalone label." }, + "TagValue": { + "properties": { + "key": { + "type": "string", + "title": "Key" + }, + "value": { + "type": "string", + "title": "Value" + }, + "color": { + "type": "string", + "title": "Color" + }, + "workflow_count": { + "type": "integer", + "title": "Workflow Count", + "description": "Number of non-deleted workflows currently carrying this (key, value) label.", + "default": 0 + } + }, + "type": "object", + "required": [ + "key", + "value", + "color" + ], + "title": "TagValue", + "description": "Tag-value color registry entry: the palette color assigned to a grouped\n(key, value) pair." + }, + "TagValueDelete": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Tag value (label) under the key to soft-delete." + } + }, + "type": "object", + "required": [ + "value" + ], + "title": "TagValueDelete", + "description": "Body for ``DELETE /v1/tag-values/{key}``. The value rides in the body, not the\npath, so values containing ``/`` stay addressable." + }, + "TagValueDeleteResponse": { + "properties": { + "key": { + "type": "string", + "title": "Key" + }, + "value": { + "type": "string", + "title": "Value" + }, + "removed_from_workflow_count": { + "type": "integer", + "title": "Removed From Workflow Count" + } + }, + "type": "object", + "required": [ + "key", + "value", + "removed_from_workflow_count" + ], + "title": "TagValueDeleteResponse", + "description": "Response for ``DELETE /v1/tag-values/{key}``." + }, + "TagValueRename": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Current tag value (label) under the key to rename." + }, + "new_value": { + "type": "string", + "title": "New Value", + "description": "New tag value (label) to rename it to." + } + }, + "type": "object", + "required": [ + "value", + "new_value" + ], + "title": "TagValueRename", + "description": "Body for ``PATCH /v1/tag-values/{key}/rename``. Both the current and the new\nvalue ride in the body so values containing ``/`` stay addressable." + }, + "TagValueRenameResponse": { + "properties": { + "key": { + "type": "string", + "title": "Key" + }, + "value": { + "type": "string", + "title": "Value" + }, + "color": { + "type": "string", + "title": "Color" + }, + "renamed_workflow_count": { + "type": "integer", + "title": "Renamed Workflow Count" + } + }, + "type": "object", + "required": [ + "key", + "value", + "color", + "renamed_workflow_count" + ], + "title": "TagValueRenameResponse", + "description": "Response for ``PATCH /v1/tag-values/{key}/rename``: the renamed label with its\ncarried-over color and the number of workflows re-tagged." + }, + "TagValueUpdate": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Tag value (label) under the key to recolor." + }, + "color": { + "type": "string", + "title": "Color", + "description": "Palette color name to assign to this (key, value)." + } + }, + "type": "object", + "required": [ + "value", + "color" + ], + "title": "TagValueUpdate", + "description": "Body for ``PATCH /v1/tag-values/{key}``. The value rides in the body, not the\npath, so values containing ``/`` stay addressable." + }, "TagsResponse": { "properties": { "workflow_permanent_id": { @@ -19520,7 +21567,7 @@ "max_iterations": { "type": "integer", "title": "Max Iterations", - "default": 10 + "default": 50 }, "max_steps": { "type": "integer", @@ -19628,7 +21675,7 @@ "max_iterations": { "type": "integer", "title": "Max Iterations", - "default": 10 + "default": 50 }, "max_steps": { "type": "integer", @@ -20191,11 +22238,62 @@ ], "title": "Description", "description": "New description for the browser profile" + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location for this profile's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Opaque Skyvern-managed proxy sticky-session id." + }, + "rotate_proxy_session_id": { + "type": "boolean", + "title": "Rotate Proxy Session Id", + "description": "Rotate the Skyvern-managed proxy sticky-session id for this profile.", + "default": false } }, "type": "object", "title": "UpdateBrowserProfileRequest" }, + "UpdateBrowserSessionRequest": { + "properties": { + "generate_browser_profile": { + "type": "boolean", + "title": "Generate Browser Profile", + "description": "Enable or disable saving this session's browser profile when it ends. Can be toggled while the session is still alive; the value is read at session teardown." + } + }, + "type": "object", + "required": [ + "generate_browser_profile" + ], + "title": "UpdateBrowserSessionRequest" + }, "UpdateWorkflowFolderRequest": { "properties": { "folder_id": { @@ -21682,6 +23780,9 @@ }, { "$ref": "#/components/schemas/GoogleSheetsWriteBlock" + }, + { + "$ref": "#/components/schemas/PdfFillBlock" } ], "discriminator": { @@ -21703,6 +23804,7 @@ "human_interaction": "#/components/schemas/HumanInteractionBlock", "login": "#/components/schemas/LoginBlock", "navigation": "#/components/schemas/NavigationBlock", + "pdf_fill": "#/components/schemas/PdfFillBlock", "pdf_parser": "#/components/schemas/PDFParserBlock", "print_page": "#/components/schemas/PrintPageBlock", "send_email": "#/components/schemas/SendEmailBlock", @@ -21876,6 +23978,9 @@ { "$ref": "#/components/schemas/PrintPageBlockYAML" }, + { + "$ref": "#/components/schemas/PdfFillBlockYAML" + }, { "$ref": "#/components/schemas/WorkflowTriggerBlockYAML" }, @@ -22003,6 +24108,11 @@ "title": "Persist Browser Session", "default": false }, + "pin_saved_session_ip": { + "type": "boolean", + "title": "Pin Saved Session Ip", + "default": false + }, "browser_profile_id": { "anyOf": [ { @@ -22014,6 +24124,17 @@ ], "title": "Browser Profile Id" }, + "browser_profile_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Browser Profile Key" + }, "model": { "anyOf": [ { @@ -22106,6 +24227,11 @@ "title": "Adaptive Caching", "default": false }, + "enable_self_healing": { + "type": "boolean", + "title": "Enable Self Healing", + "default": false + }, "code_version": { "anyOf": [ { @@ -22188,6 +24314,11 @@ ], "title": "Edited By" }, + "copilot_authored": { + "type": "boolean", + "title": "Copilot Authored", + "default": false + }, "created_at": { "type": "string", "format": "date-time", @@ -22305,6 +24436,11 @@ "title": "Persist Browser Session", "default": false }, + "pin_saved_session_ip": { + "type": "boolean", + "title": "Pin Saved Session Ip", + "default": false + }, "browser_profile_id": { "anyOf": [ { @@ -22316,6 +24452,17 @@ ], "title": "Browser Profile Id" }, + "browser_profile_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Browser Profile Key" + }, "model": { "anyOf": [ { @@ -22351,7 +24498,7 @@ "anyOf": [ { "type": "integer", - "maximum": 240.0, + "maximum": 480.0, "minimum": 1.0 }, { @@ -22419,6 +24566,17 @@ "title": "Adaptive Caching", "default": false }, + "enable_self_healing": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Self Healing" + }, "code_version": { "anyOf": [ { @@ -22619,6 +24777,9 @@ }, { "$ref": "#/components/schemas/GoogleSheetsWriteBlock" + }, + { + "$ref": "#/components/schemas/PdfFillBlock" } ], "discriminator": { @@ -22640,6 +24801,7 @@ "human_interaction": "#/components/schemas/HumanInteractionBlock", "login": "#/components/schemas/LoginBlock", "navigation": "#/components/schemas/NavigationBlock", + "pdf_fill": "#/components/schemas/PdfFillBlock", "pdf_parser": "#/components/schemas/PDFParserBlock", "print_page": "#/components/schemas/PrintPageBlock", "send_email": "#/components/schemas/SendEmailBlock", @@ -22704,9 +24866,15 @@ "WorkflowDefinitionYAML": { "properties": { "version": { - "type": "integer", - "title": "Version", - "default": 1 + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Version" }, "parameters": { "items": { @@ -22836,6 +25004,9 @@ { "$ref": "#/components/schemas/PrintPageBlockYAML" }, + { + "$ref": "#/components/schemas/PdfFillBlockYAML" + }, { "$ref": "#/components/schemas/WorkflowTriggerBlockYAML" }, @@ -22865,6 +25036,7 @@ "human_interaction": "#/components/schemas/HumanInteractionBlockYAML", "login": "#/components/schemas/LoginBlockYAML", "navigation": "#/components/schemas/NavigationBlockYAML", + "pdf_fill": "#/components/schemas/PdfFillBlockYAML", "pdf_parser": "#/components/schemas/PDFParserBlockYAML", "print_page": "#/components/schemas/PrintPageBlockYAML", "send_email": "#/components/schemas/SendEmailBlockYAML", @@ -23892,6 +26064,17 @@ ], "title": "Body" }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt" + }, "instructions": { "anyOf": [ { @@ -23978,6 +26161,17 @@ "type": "null" } ] + }, + "downloaded_file_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Downloaded File Count" } }, "type": "object", @@ -24130,7 +26324,7 @@ "anyOf": [ { "type": "integer", - "maximum": 240.0, + "maximum": 480.0, "minimum": 1.0 }, { @@ -24138,7 +26332,7 @@ } ], "title": "Max Elapsed Time Minutes", - "description": "Timeout this workflow run after the configured elapsed runtime in minutes. Maximum runtime is 4 hours." + "description": "Timeout this workflow run after the configured elapsed runtime in minutes. When omitted, the platform default is 240 minutes. The maximum configurable value is 480 minutes." }, "extra_http_headers": { "anyOf": [ @@ -24375,7 +26569,7 @@ "anyOf": [ { "type": "integer", - "maximum": 240.0, + "maximum": 480.0, "minimum": 1.0 }, { @@ -24383,7 +26577,7 @@ } ], "title": "Max Elapsed Time Minutes", - "description": "Timeout this workflow run after the configured elapsed runtime in minutes. Maximum runtime is 4 hours." + "description": "Timeout this workflow run after the configured elapsed runtime in minutes. When omitted, the platform default is 240 minutes. The maximum configurable value is 480 minutes." }, "extra_http_headers": { "anyOf": [ diff --git a/docs/changelog-emails/2026-06-june.md b/docs/changelog-emails/2026-06-june.md new file mode 100644 index 000000000..bc960d3a0 --- /dev/null +++ b/docs/changelog-emails/2026-06-june.md @@ -0,0 +1,114 @@ + + +# Skyvern Changelog — June 2026 + +*Everything we shipped in June 2026 — weeks of June 1, 8, 15, and 22.* + +--- + +## Workflows Are Now Agents + +We renamed **Workflows to Agents** across the product, the public API, and the docs. The API now lives under **`/agents`** (e.g. `POST /v1/run/agents`) and accepts an **`agent_id`** alias for `workflow_id` — same `wpid_` value, echoed back in responses. It's fully backwards-compatible: your existing `workflow_id` inputs, `/workflows` endpoints, SDK methods, and bookmarked doc URLs all keep working. + + + +--- + +## Workflow Studio (Preview) + +Meet the next-generation editor. **Workflow Studio** is an opt-in preview that reimagines how you build: a refreshed **build canvas**, an **integrated live browser view**, and a built-in **run viewer** — all in one place. Take control of the live browser, center the viewport, and watch a run unfold without leaving the editor. Flip it on from Feature Preview in Settings. + + + +--- + +## Code-First Code Blocks + +For when you want to drop to code. Code blocks now have a **dedicated code editor** with **syntax highlighting**, **Jinja parameter hints**, and a **run timeline** that records every action — including `page.evaluate(...)` calls. A **Build ↔ Code toggle** flips between visual and code editing, block labels and extraction output render correctly, and Copilot-generated code blocks support a **typed extraction schema** and fill logins with your stored credentials. + + + +--- + +## Copilot: Ask, Build, Code — With Memory + +The Copilot got sharper about intent and context. It now **routes every request by mode** — **Ask** answers without touching your workflow, **Build** creates or edits it, **Code** generates executable code blocks. It **remembers the full chat history per workflow**, so prior context and decisions carry forward as you keep building. And its **repair loop is honest now**: when consecutive fixes make no verified progress, it stops after three tries and escalates cleanly — draft preserved — instead of spinning. + + + +--- + +## A Bigger Model Menu + +More choice for every run. New this month: **DeepSeek**, **Xiaomi MiMo** (and **MiMo-V2.5** via OpenRouter), **Gemini 3.5 Flash**, **GPT-5-mini** (your own OpenAI key or via OpenRouter), and **Claude Opus 4.8** (Anthropic + Bedrock). OpenRouter runs now track token counts and cost like every other provider. (Gemini 2.5 Pro/Flash are deprecated; CUA and the Claude Opus family are now enterprise-only.) + + + +--- + +## Credentials & OTP, Everywhere + +Logins that just work. Pull credentials straight from **1Password** (with an item picker) or **Bitwarden**, look up one-time passcodes from **Gmail** via OAuth, and **fill OTP at runtime** from your stored TOTP secrets — even inside code blocks. Expired credential sessions **auto re-save**, and you can **clear a saved credential** without deleting and recreating it. + + + +--- + +## Multi-Tab Browser Control + +Automations that span tabs. The autonomous agent loop can now **open and work across multiple browser tabs** at once — following new-tab links and handling pop-ups — so flows that jump between tabs no longer dead-end. + + + +--- + +## Self-Host on Google Cloud + +Full GCP-native deployments. Self-hosted Skyvern now supports **Google Cloud Storage** for artifacts and **GCP Secret Manager** as a vault backend — so you can run the whole stack on Google Cloud with your own storage and secrets. + + + +--- + +## Quick List — June + +**New features** +- **Analytics drill-down & tag filters** — period-aware per-agent drill-down (avg duration, credits/run, period-over-period trends) plus filtering runs and metrics by workflow tag +- **Agent Directory Tree** — the agents/folders list is now a navigable tree for nested hierarchies +- **Self-serve editor onboarding** — a get-started modal, a 4-stop guided tour, and smart empty states for faster time-to-first-automation +- **Bulk actions on the agents list** — select multiple agents and act on them at once +- **Label management in Settings** — rename, recolor, and soft-delete tags +- **Browser profiles in MCP sessions** — associate a browser profile with MCP-driven runs for credential/state reuse +- **Feature Preview panel in Settings** — opt into upcoming features before they're fully released +- **Workflow schedules in open source** — self-hosted deployments now support schedules +- **Uncapped workflow runtime** — configure long-running automations with no maximum time limit + +**Improvements** +- Structured output is verified against its schema before a run is marked complete +- Tasks return a best-effort **partial deliverable** when they exhaust their credit budget +- The task planner prefers **loop-based tasks** for repeated same-shaped work, and can now emit EXTRACT_INFORMATION / GOTO_URL actions +- The agent **acts immediately on stalled pages** and **halts reliably on terminal blockers** instead of spinning +- Remote browser sessions capped at 4 hours; debug sessions stay open after a run so you can inspect the final page +- End-state screenshots captured for **every open tab** at completion; per-run recordings clipped to the run window and stored as downloadable artifacts +- Run app + recording URLs surfaced in MCP and CLI responses; run-ID search fallback on the runs list +- Batched cache invalidation on workflow save (removed a per-block N+1) for faster saves on large workflows +- OpenRouter cost tracking; analytics tag filters accept labels, groups, or `group:label` +- Improved authenticator-2FA credential setup; PDF field mapping by positional labels +- Copilot data-write blocks default to `continue_on_failure=false`; code-repair progress shown as quiet indicators + +**Bug fixes** +- Fixed the workflow builder freezing on an infinite FlowRenderer layout loop +- Fixed the code editor crashing (stack overflow) on large or deeply-nested JSON +- Fixed the app failing to load after a deploy from stale chunks — it now auto-recovers with a cache-busting reload +- Fixed login credentials not reattaching after editing a workflow via MCP +- Fixed conditional routing not being preserved when loading a v1-format workflow +- Fixed the active run context being lost when switching organizations +- Fixed multi-step logins not advancing when credentials filled JavaScript-gated fields +- Fixed scanned PDFs being parsed as a single page instead of page-by-page +- Fixed shadow-DOM text nodes being missed during element text extraction +- Fixed runs not stopping when a configured time limit was exceeded +- Fixed credits not recorded when data was captured inside a Navigate block +- Fixed telephone inputs dropping digits on bare NANP-format numbers +- Fixed persistent sessions reporting a stale cached session as live +- Fixed FileUpload blocks silently skipping when no file was provided (now a clean no-op) +- Fixed the block library drawer overflowing its container in the workflow builder diff --git a/docs/changelog-emails/images/2026-06-00-header.png b/docs/changelog-emails/images/2026-06-00-header.png new file mode 100644 index 000000000..64edf83c0 Binary files /dev/null and b/docs/changelog-emails/images/2026-06-00-header.png differ diff --git a/docs/changelog-emails/images/2026-06-01-agents-rename.png b/docs/changelog-emails/images/2026-06-01-agents-rename.png new file mode 100644 index 000000000..b59997963 Binary files /dev/null and b/docs/changelog-emails/images/2026-06-01-agents-rename.png differ diff --git a/docs/changelog-emails/images/2026-06-02-workflow-studio.png b/docs/changelog-emails/images/2026-06-02-workflow-studio.png new file mode 100644 index 000000000..c5bf24849 Binary files /dev/null and b/docs/changelog-emails/images/2026-06-02-workflow-studio.png differ diff --git a/docs/changelog-emails/images/2026-06-03-code-first.png b/docs/changelog-emails/images/2026-06-03-code-first.png new file mode 100644 index 000000000..9bf2eac49 Binary files /dev/null and b/docs/changelog-emails/images/2026-06-03-code-first.png differ diff --git a/docs/changelog-emails/images/2026-06-04-copilot.png b/docs/changelog-emails/images/2026-06-04-copilot.png new file mode 100644 index 000000000..4d6c1298a Binary files /dev/null and b/docs/changelog-emails/images/2026-06-04-copilot.png differ diff --git a/docs/changelog-emails/images/2026-06-05-models.png b/docs/changelog-emails/images/2026-06-05-models.png new file mode 100644 index 000000000..8266e172b Binary files /dev/null and b/docs/changelog-emails/images/2026-06-05-models.png differ diff --git a/docs/changelog-emails/images/2026-06-06-credentials.png b/docs/changelog-emails/images/2026-06-06-credentials.png new file mode 100644 index 000000000..a50398a5d Binary files /dev/null and b/docs/changelog-emails/images/2026-06-06-credentials.png differ diff --git a/docs/changelog-emails/images/2026-06-07-multi-tab.png b/docs/changelog-emails/images/2026-06-07-multi-tab.png new file mode 100644 index 000000000..038d99f6e Binary files /dev/null and b/docs/changelog-emails/images/2026-06-07-multi-tab.png differ diff --git a/docs/changelog-emails/images/2026-06-08-gcp-self-host.png b/docs/changelog-emails/images/2026-06-08-gcp-self-host.png new file mode 100644 index 000000000..15338f88b Binary files /dev/null and b/docs/changelog-emails/images/2026-06-08-gcp-self-host.png differ diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 3a7f0bd69..343b06341 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -13,6 +13,9 @@ keywords:
TIMELINE
+ Week of Jun 30, 2026 + Week of Jun 22, 2026 + Week of Jun 15, 2026 Week of Jun 8, 2026 Week of Jun 1, 2026 Week of May 25, 2026 @@ -20,31 +23,168 @@ keywords: Week of May 11, 2026 Week of May 4, 2026 Week of Apr 27, 2026 - v1.0.24 — Apr 21, 2026 - v1.0.23 — Apr 14, 2026 - v1.0.22 — Feb 26, 2026 - v1.0.21 — Feb 24, 2026 - v1.0.20 — Feb 21, 2026 - v1.0.19 — Feb 20, 2026 - v1.0.18 — Feb 19, 2026 - v1.0.17 — Feb 19, 2026 - v1.0.16 — Feb 19, 2026 - v1.0.15 — Feb 17, 2026 - v1.0.14 — Feb 17, 2026 - v1.0.13 — Feb 10, 2026 - v1.0.12 — Feb 3, 2026 - v1.0.11 — Jan 29, 2026 - v1.0.10 — Jan 22, 2026 - v1.0.9 — Jan 20, 2026 - v1.0.8 — Jan 9, 2026 + Week of Apr 20, 2026 + Week of Apr 13, 2026 + Week of Feb 23, 2026 + Week of Feb 16, 2026 + Week of Feb 9, 2026 + Week of Feb 2, 2026 + Week of Jan 26, 2026 + Week of Jan 19, 2026 + Week of Jan 5, 2026New features, improvements, and fixes in Skyvern
+ +
-| Plan | Price | Credits/month | Estimated actions | Concurrent runs |
-|------|-------|---------------|-------------------|-----------------|
-| **Free** | $0 | 1,000 | ~170 | 1 |
-| **Hobby** | $29/month | 30,000 | ~1,200 | 10 |
-| **Pro** | $149/month | 150,000 | ~6,200 | 25 |
+| Plan | Price | Credits | Estimated actions | Concurrent runs |
+|------|-------|---------|-------------------|-----------------|
+| **Free** | $0 | 5,000 one-time | ~200 | 1 |
+| **Hobby** | $29/month | 30,000/month | ~1,200 | 10 |
+| **Pro** | $149/month | 150,000/month | ~6,200 | 25 |
| **Enterprise** | Custom | Unlimited | Unlimited | Unlimited |
-**Free** is available to all new accounts with zero commitment. **Hobby** and **Pro** are monthly subscriptions. Click **Switch to Hobby** or **Switch to Pro** to change plans. Your remaining dollar balance is applied as a credit toward your first subscription payment.
+**Free** is available to all new accounts with a one-time credit grant and zero commitment. **Hobby** and **Pro** are monthly subscriptions. Click **Switch to Hobby** or **Switch to Pro** to change plans. Your remaining dollar balance is applied as a credit toward your first subscription payment.
+ Helps avoid extra 2FA, captchas, or temporary locks caused by + signing in from a new location. +
+ {pinResidentialIspProxy && existingProxyIdentity && ( ++ Consistent IP active: identity {existingProxyIdentity} +
++ Skyvern will create an IP identity for this credential when you + save. +
+ )} ++ Skyvern can find verification codes and magic links in a + connected Gmail inbox without manual forwarding. +
+
@@ -119,8 +192,8 @@ function CredentialsTotpTab() {
1Password
++ Read-only — managed in your 1Password account +
++ {item.title} +
++ {item.vault_name} +
++ Website +
+ )} ++ Vault ID +
++ Item ID +
++ {getHostname(item.url) ?? item.url} +
+ )} ++ {item.vault_id} +
++ {item.item_id} +
++ {enterpriseApps.description} +
+ )} +- - Contact us to set up two-factor authentication in - workflows - {" "} - or{" "} + {enterpriseContactUrl && ( + <> + + Contact us to set up two-factor authentication in + workflows + {" "} + or{" "} + > + )} - see our documentation on how to set up two-factor - authentication in workflows + {enterpriseContactUrl ? "see" : "See"} our documentation + on how to set up two-factor authentication in workflows {" "} to get started.
@@ -395,10 +784,12 @@ function PasswordCredentialContent({ )} {totpMethod === "authenticator" && ({message}
+ ), + )} +- You need to find the authenticator secret from the website + Works with Google Authenticator, Authy, 1Password, and any + standard TOTP authenticator app. +
+
+ You need to find the authenticator key from the website
where you are using the credential. Here are some guides
- from popular authenticator apps:{" "}
+ from popular password managers:{" "}
{
+ it("maps the structured no-code-secret code to actionable code-based setup copy", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail({
+ error_code: "authenticator_no_code_secret",
+ message: "Push enrollment payloads are not supported.",
+ }),
+ );
+
+ expect(result?.code).toBe("no_code_secret");
+ expect(result?.message).toMatch(/code-based setup key/i);
+ expect(result?.message).toMatch(/push-approval/i);
+ expect(result?.message).toMatch(/authenticator app or one-time code/i);
+ });
+
+ it("maps the structured enterprise-required code to enterprise access copy", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail({
+ error_code: "authenticator_feature_restricted",
+ message: "Enterprise plan required.",
+ }),
+ );
+
+ expect(result?.code).toBe("enterprise_required");
+ expect(result?.message).toMatch(/enterprise plan/i);
+ });
+
+ it("preserves the detected vendor id for enterprise-required copy", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail({
+ error_code: "authenticator_feature_restricted",
+ message: "Enterprise plan required.",
+ vendor: "example",
+ }),
+ );
+
+ expect(result?.code).toBe("enterprise_required");
+ expect(result?.message).toBe(
+ "This authenticator requires a Skyvern enterprise plan.",
+ );
+ expect(result?.vendor).toBe("example");
+ });
+
+ it("maps unsupported TOTP config to the backend message", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail({
+ error_code: "authenticator_totp_config_unsupported",
+ message: "The authenticator setup code is malformed.",
+ }),
+ );
+
+ expect(result?.code).toBe("unsupported_totp_config");
+ expect(result?.message).toBe("The authenticator setup code is malformed.");
+ });
+
+ it("falls back to the backend message for an invalid-key code", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail({
+ error_code: "invalid_authenticator_key",
+ message: "That is not a valid Base32 secret.",
+ }),
+ );
+
+ expect(result?.code).toBe("invalid_authenticator_key");
+ expect(result?.message).toBe("That is not a valid Base32 secret.");
+ });
+
+ it("does not infer unsupported push enrollment from unknown future codes", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail({
+ error_code: "batch_enrollment_failed",
+ message: "The authenticator setup could not be saved.",
+ }),
+ );
+
+ expect(result?.code).toBe("unknown");
+ expect(result?.message).toBe("The authenticator setup could not be saved.");
+ });
+
+ it("classifies a legacy string detail that mentions the authenticator key", () => {
+ const result = getAuthenticatorSaveError(
+ axiosErrorWithDetail(
+ "Invalid authenticator key. Paste the raw Base32 setup key.",
+ ),
+ );
+
+ expect(result?.code).toBe("invalid_authenticator_key");
+ expect(result?.message).toContain("Invalid authenticator key");
+ });
+
+ it("returns null for a legacy string detail unrelated to the authenticator", () => {
+ expect(
+ getAuthenticatorSaveError(
+ axiosErrorWithDetail("Username and password are required"),
+ ),
+ ).toBeNull();
+ });
+
+ it("returns null when there is no response detail", () => {
+ expect(getAuthenticatorSaveError(new Error("boom"))).toBeNull();
+ });
+});
+
+describe("getCredentialErrorMessage", () => {
+ it("returns a legacy string detail verbatim", () => {
+ expect(
+ getCredentialErrorMessage(axiosErrorWithDetail("Something went wrong")),
+ ).toBe("Something went wrong");
+ });
+
+ it("returns the message field from a structured detail", () => {
+ expect(
+ getCredentialErrorMessage(
+ axiosErrorWithDetail({
+ error_code: "enterprise_required",
+ message: "Enterprise plan required.",
+ }),
+ ),
+ ).toBe("Enterprise plan required.");
+ });
+
+ it("returns null when no usable detail is present", () => {
+ expect(getCredentialErrorMessage(new Error("boom"))).toBeNull();
+ });
+});
diff --git a/skyvern-frontend/src/routes/credentials/authenticatorSaveError.ts b/skyvern-frontend/src/routes/credentials/authenticatorSaveError.ts
new file mode 100644
index 000000000..36e76293e
--- /dev/null
+++ b/skyvern-frontend/src/routes/credentials/authenticatorSaveError.ts
@@ -0,0 +1,146 @@
+import { isAxiosError } from "axios";
+
+type AuthenticatorErrorCode =
+ | "no_code_secret"
+ | "unsupported_totp_config"
+ | "enterprise_required"
+ | "invalid_authenticator_key"
+ | "unknown";
+
+type AuthenticatorSaveError = {
+ code: AuthenticatorErrorCode;
+ message: string;
+ vendor?: string;
+};
+
+type StructuredErrorDetail = {
+ error_code?: unknown;
+ message?: unknown;
+ vendor?: unknown;
+};
+
+const EXACT_ERROR_CODE_MAP: Record
{humanReadable}
@@ -310,6 +315,11 @@ function CreateOrgScheduleDialog({ open, onOpenChange }: Readonly+ Schedule runs must be at least 5 minutes apart. +
+ )} {/* Timezone Selector */} @@ -383,7 +393,7 @@ function CreateOrgScheduleDialog({ open, onOpenChange }: Readonly+ Schedule runs must be at least 5 minutes apart. +
+ )} {/* Timezone */} @@ -431,7 +448,7 @@ function ScheduleDetailPage() {{error}
: null} ++ No labels yet. Grouped labels you add to workflows (group:label) + appear here for management. +
+ ) : ( +
{getAppVersion() !== "development" && (
diff --git a/skyvern-frontend/src/routes/streaming/InteractiveStreamView.tsx b/skyvern-frontend/src/routes/streaming/InteractiveStreamView.tsx
index b99bf31eb..6f98a149f 100644
--- a/skyvern-frontend/src/routes/streaming/InteractiveStreamView.tsx
+++ b/skyvern-frontend/src/routes/streaming/InteractiveStreamView.tsx
@@ -1,5 +1,5 @@
import type { RefObject } from "react";
-import { GlobeIcon } from "@radix-ui/react-icons";
+import { ExitIcon, GlobeIcon, HandIcon } from "@radix-ui/react-icons";
import { ZoomableImage } from "@/components/ZoomableImage";
import { Button } from "@/components/ui/button";
import { cn } from "@/util/utils";
@@ -21,6 +21,7 @@ interface InteractiveStreamViewProps {
handleKeyUp: (e: React.KeyboardEvent) => void;
};
currentUrl?: string;
+ centered?: boolean;
}
function UrlBar({ url }: { url: string }) {
@@ -43,6 +44,7 @@ function InteractiveStreamView({
showControlButtons,
handlers,
currentUrl,
+ centered,
}: InteractiveStreamViewProps) {
const imgDataUrl = `data:image/${streamFormat};base64,${streamImgSrc}`;
@@ -58,16 +60,23 @@ function InteractiveStreamView({
{currentUrl &&