Skip to main content
A Browser Profile is a saved snapshot of browser state (cookies, localStorage, and session files) that you can reuse across multiple runs. Profiles let you skip login steps and restore authenticated state instantly. Profiles are ideal when you:
  • Run the same workflow repeatedly with the same account (daily data extraction, scheduled reports)
  • Want multiple workflows to share the same authenticated state
  • Need to avoid repeated authentication to save time and steps

How profiles work

When a workflow runs with persist_browser_session=true, Skyvern archives the browser state (cookies, storage, session files) after the run completes. This archiving happens asynchronously in the background. Once the archive is ready, you can create a profile from it, then pass that profile to future workflow runs to restore the saved state.

Create a Browser Profile

Create a workflow with persist_browser_session=true in the workflow definition, run it, wait for completion, then create a profile from the run. Session archiving happens asynchronously, so add brief retry logic when creating the profile.
persist_browser_session must be set when creating the workflow, not when running it. It is a workflow definition property, not a runtime parameter.

From a workflow run

import asyncio
from skyvern import Skyvern

async def main():
    client = Skyvern(api_key="YOUR_API_KEY")

    # Step 1: Create a workflow with persist_browser_session=true
    workflow = await client.create_workflow(
        json_definition={
            "title": "Login to Dashboard",
            "persist_browser_session": True,  # Set here in workflow definition
            "workflow_definition": {
                "parameters": [],
                "blocks": [
                    {
                        "block_type": "navigation",
                        "label": "login",
                        "url": "https://dashboard.example.com/login",
                        "navigation_goal": "Login with the provided credentials"
                    }
                ]
            }
        }
    )
    print(f"Created workflow: {workflow.workflow_permanent_id}")

    # Step 2: Run the workflow
    workflow_run = await client.run_workflow(
        workflow_id=workflow.workflow_permanent_id,
        wait_for_completion=True,
    )
    print(f"Workflow completed: {workflow_run.status}")

    # Step 3: Create profile from the completed run
    # Retry briefly while session archives asynchronously
    for attempt in range(10):
        try:
            profile = await client.create_browser_profile(
                name="analytics-dashboard-login",
                workflow_run_id=workflow_run.run_id,
                description="Authenticated state for analytics dashboard",
            )
            print(f"Profile created: {profile.browser_profile_id}")
            break
        except Exception as e:
            if "persisted" in str(e).lower() and attempt < 9:
                await asyncio.sleep(1)
                continue
            raise

asyncio.run(main())
Parameters:
ParameterTypeDescription
namestringRequired. Display name for the profile. Must be unique within your organization
workflow_run_idstringID of the completed workflow run to create the profile from
descriptionstringOptional description of the profile’s purpose

From a browser session

You can also create a profile from a Browser Session that was used inside a workflow with persist_browser_session=true. After the workflow run completes and the session is closed, pass the session ID instead of the workflow run ID.
Only sessions that were part of a workflow with persist_browser_session=true produce an archive. A session created with create_browser_session() alone does not archive its state. Archiving happens asynchronously after the session closes, so add retry logic.
import asyncio
from skyvern import Skyvern

async def main():
    client = Skyvern(api_key="YOUR_API_KEY")

    # browser_session_id from a workflow run with persist_browser_session=true
    session_id = "pbs_your_session_id"

    # Create profile from the closed session (retry while archive uploads)
    for attempt in range(10):
        try:
            profile = await client.create_browser_profile(
                name="dashboard-admin-login",
                browser_session_id=session_id,
                description="Admin account for dashboard access",
            )
            print(f"Profile created: {profile.browser_profile_id}")
            break
        except Exception as e:
            if "persisted" in str(e).lower() and attempt < 9:
                await asyncio.sleep(2)
                continue
            raise

asyncio.run(main())
Parameters:
ParameterTypeDescription
namestringRequired. Display name for the profile. Must be unique within your organization
browser_session_idstringID of the closed browser session (starts with pbs_). The session must have been part of a workflow with persist_browser_session=true
descriptionstringOptional description of the profile’s purpose

Use a Browser Profile

Pass browser_profile_id when running a workflow to restore the saved state. Skyvern restores cookies, localStorage, and session files before the first step runs.
import asyncio
from skyvern import Skyvern

async def main():
    client = Skyvern(api_key="YOUR_API_KEY")

    # Run workflow with saved profile, no login needed
    result = await client.run_workflow(
        workflow_id="wpid_daily_metrics",
        browser_profile_id="bp_490705123456789012",
        wait_for_completion=True,
    )

    print(f"Output: {result.output}")

asyncio.run(main())
Example response:
{
  "run_id": "wr_494469342201718946",
  "status": "created",
  "run_request": {
    "workflow_id": "wpid_daily_metrics",
    "browser_profile_id": "bp_490705123456789012",
    "proxy_location": "RESIDENTIAL"
  }
}
browser_profile_id is supported for workflows only. It is not available for standalone tasks via run_task. You also cannot use both browser_profile_id and browser_session_id in the same request.

Tutorial: save and reuse browsing state

This walkthrough demonstrates the full profile lifecycle: create a workflow that saves browser state, capture that state as a profile, then reuse it in a second workflow. Each step shows the code and the actual API response.
1

Create a workflow with persist_browser_session

The workflow must have persist_browser_session=true so Skyvern archives the browser state after the run.
workflow = await client.create_workflow(
    json_definition={
        "title": "Visit Hacker News",
        "persist_browser_session": True,
        "workflow_definition": {
            "parameters": [],
            "blocks": [
                {
                    "block_type": "navigation",
                    "label": "visit_hn",
                    "url": "https://news.ycombinator.com",
                    "navigation_goal": "Navigate to the Hacker News homepage and confirm it loaded"
                }
            ]
        }
    }
)
print(workflow.workflow_permanent_id)   # wpid_494674198088536840
print(workflow.persist_browser_session) # True
Response
{
  "workflow_permanent_id": "wpid_494674198088536840",
  "persist_browser_session": true,
  "title": "Visit Hacker News"
}
2

Run the workflow

Run the workflow and wait for it to complete. Skyvern opens a browser, executes the navigation block, then archives the browser state in the background.
run = await client.run_workflow(
    workflow_id=workflow.workflow_permanent_id,
    wait_for_completion=True,
)
print(run.run_id) # wr_494674202383504144
print(run.status)  # completed
Response
{
  "run_id": "wr_494674202383504144",
  "status": "completed"
}
3

Create a profile from the completed run

Archiving happens asynchronously after the run completes, so add retry logic. In practice the archive is usually ready within a few seconds.
for attempt in range(10):
    try:
        profile = await client.create_browser_profile(
            name="hn-browsing-state",
            workflow_run_id=run.run_id,
            description="Hacker News cookies and browsing state",
        )
        print(profile.browser_profile_id) # bp_494674399951999772
        break
    except Exception as e:
        if "persisted" in str(e).lower() and attempt < 9:
            await asyncio.sleep(2)
            continue
        raise
Response
{
  "browser_profile_id": "bp_494674399951999772",
  "organization_id": "o_475582633898688888",
  "name": "hn-browsing-state",
  "description": "Hacker News cookies and browsing state",
  "created_at": "2026-02-12T01:09:18.048208",
  "modified_at": "2026-02-12T01:09:18.048212",
  "deleted_at": null
}
4

Verify the profile exists

List all profiles or fetch one by ID to confirm it was saved.
# List all profiles
profiles = await client.list_browser_profiles()
print(len(profiles)) # 1

# Get a single profile
fetched = await client.get_browser_profile(profile_id=profile.browser_profile_id)
print(fetched.name) # hn-browsing-state
List response
[
  {
    "browser_profile_id": "bp_494674399951999772",
    "name": "hn-browsing-state",
    "created_at": "2026-02-12T01:09:18.048208"
  }
]
5

Reuse the profile in a second workflow

Pass browser_profile_id when running a workflow. Skyvern restores the saved cookies, localStorage, and session files before the first block runs. The second workflow starts with the browser state from step 2, no repeat navigation needed.
result = await client.run_workflow(
    workflow_id=data_workflow.workflow_permanent_id,
    browser_profile_id=profile.browser_profile_id,
    wait_for_completion=True,
)
print(result.status) # completed
Response
{
  "run_id": "wr_494674434311738148",
  "status": "created"
}
6

Delete the profile

Clean up profiles you no longer need.
await client.delete_browser_profile(profile_id=profile.browser_profile_id)

# Confirm deletion
remaining = await client.list_browser_profiles()
print(len(remaining)) # 0
In a real scenario, step 1 would be a login workflow that authenticates with a site. The saved profile then lets all future workflows skip the login step entirely.

Best practices

Use descriptive names

Include the account, site, and purpose in the profile name so it is easy to identify later.
# Good: identifies account, site, and purpose
profile = await client.create_browser_profile(
    name="prod-salesforce-admin",
    description="Admin login for daily opportunity sync",
    workflow_run_id=run_id,
)

# Bad: unclear what this is for
profile = await client.create_browser_profile(
    name="profile1",
    workflow_run_id=run_id,
)

Refresh profiles periodically

Session tokens and cookies expire. Re-run your login workflow and create fresh profiles before they go stale. Adding the date to the name makes it easy to track which profile is current.
from datetime import date

# Create dated profile after each successful login
profile = await client.create_browser_profile(
    name=f"crm-login-{date.today()}",
    workflow_run_id=new_login_run.run_id,
)

# Delete old profile
await client.delete_browser_profile(old_profile_id)

Capture updated state after each run

To capture state changes during a run (like token refreshes), the workflow must have persist_browser_session=true in its definition. This lets you create a fresh profile from each completed run.
from datetime import date

# Step 1: Create workflow with persist_browser_session in the definition
workflow = await client.create_workflow(
    json_definition={
        "title": "Daily Sync",
        "persist_browser_session": True,  # Set here, not in run_workflow
        "workflow_definition": {
            "parameters": [],
            "blocks": [...]
        }
    }
)

# Step 2: Run with an existing profile
result = await client.run_workflow(
    workflow_id=workflow.workflow_permanent_id,
    browser_profile_id="bp_current",
    wait_for_completion=True,
)

# Step 3: Create updated profile from the completed run
if should_refresh_profile:
    new_profile = await client.create_browser_profile(
        name=f"daily-sync-{date.today()}",
        workflow_run_id=result.run_id,
    )

Next steps

Browser Sessions

Maintain live browser state for real-time interactions

Cost Control

Optimize costs with max_steps and efficient prompts