> ## Documentation Index
> Fetch the complete documentation index at: https://skyvern.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser Profiles

> Save browser state snapshots including cookies, localStorage, and session files as reusable profiles. Skip login steps and restore authenticated state instantly across agent runs.

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 agent repeatedly with the same account (daily data extraction, scheduled reports)
* Want multiple agents to share the same authenticated state
* Need to avoid repeated authentication to save time and steps

***

## How profiles work

You can create a blank profile with only `name` and optional `description`, then pass that profile to future agent runs. Blank profiles are seeded from the configured default browser profile directory when available, with a minimal loadable profile skeleton as a fallback.

When an agent 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 agent runs to restore the saved state.

For [browser sessions](/developers/optimization/browser-sessions), profile saving is opt-in: a session archives its state when it ends only if it has `generate_browser_profile` enabled or was started from a saved profile. See [Save a session's profile](/developers/optimization/browser-sessions#save-a-sessions-profile).

***

## Create a Browser Profile

Create a blank profile when you want a reusable profile ID before any browsing has happened. Create a sourced profile when you want to capture cookies, localStorage, and session files from a workflow run or browser session.

### Blank profile

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  from skyvern import Skyvern

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

      profile = await client.create_browser_profile(
          name="fresh-profile",
          description="Blank profile for future runs",
      )
      print(f"Profile created: {profile.browser_profile_id}")

  asyncio.run(main())
  ```

  ```typescript TypeScript theme={null}
  import { Skyvern } from "@skyvern/client";

  async function main() {
    const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

    const profile = await client.createBrowserProfile({
      name: "fresh-profile",
      description: "Blank profile for future runs",
    });

    console.log(`Profile created: ${profile.browser_profile_id}`);
  }

  main();
  ```

  ```bash cURL theme={null}
  curl -s -X POST "https://api.skyvern.com/v1/browser_profiles" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "fresh-profile",
      "description": "Blank profile for future runs"
    }'
  ```
</CodeGroup>

**Parameters:**

| Parameter            | Type             | Description                                                                                                                 |
| -------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `name`               | string           | Required unique display name                                                                                                |
| `description`        | string           | Optional profile description                                                                                                |
| `browser_session_id` | string           | Omit for a blank profile                                                                                                    |
| `workflow_run_id`    | string           | Omit for a blank profile                                                                                                    |
| `proxy_location`     | string \| object | Optional pinned proxy location for the profile's residential ISP identity (e.g. `RESIDENTIAL_ISP`, or a `GeoTarget` object) |
| `proxy_session_id`   | string           | Optional advanced reuse key for the profile's pinned proxy identity                                                         |

### From an agent run

Create an agent with `persist_browser_session=true` in the agent 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.

<Note>
  `persist_browser_session` must be set when **creating the agent**, not when running it. It is an agent definition property, not a runtime parameter.
</Note>

<CodeGroup>
  ```python Python theme={null}
  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())
  ```

  ```typescript TypeScript theme={null}
  import { Skyvern } from "@skyvern/client";

  async function main() {
    const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

    // Step 1: Create a workflow with persist_browser_session=true
    const workflow = await client.createWorkflow({
      body: {
        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"
              }
            ]
          }
        }
      }
    });
    console.log(`Created workflow: ${workflow.workflow_permanent_id}`);

    // Step 2: Run the workflow and wait for completion
    const workflowRun = await client.runWorkflow({
      body: { workflow_id: workflow.workflow_permanent_id },
      waitForCompletion: true,
    });
    console.log(`Workflow completed: ${workflowRun.status}`);

    // Step 3: Create profile from the completed run
    let profile;
    for (let attempt = 0; attempt < 10; attempt++) {
      try {
        profile = await client.createBrowserProfile({
          name: "analytics-dashboard-login",
          workflow_run_id: workflowRun.run_id,
          description: "Authenticated state for analytics dashboard",
        });
        break;
      } catch (e) {
        if (String(e).toLowerCase().includes("persisted") && attempt < 9) {
          await new Promise((r) => setTimeout(r, 1000));
          continue;
        }
        throw e;
      }
    }

    console.log(`Profile created: ${profile.browser_profile_id}`);
  }

  main();
  ```

  ```bash cURL theme={null}
  # Step 1: Create a workflow with persist_browser_session=true
  WORKFLOW_RESPONSE=$(curl -s -X POST "https://api.skyvern.com/v1/workflows" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "json_definition": {
        "title": "Login to Dashboard",
        "persist_browser_session": true,
        "workflow_definition": {
          "parameters": [],
          "blocks": [
            {
              "block_type": "navigation",
              "label": "login",
              "url": "https://dashboard.example.com/login",
              "navigation_goal": "Login with the provided credentials"
            }
          ]
        }
      }
    }')

  WORKFLOW_ID=$(echo "$WORKFLOW_RESPONSE" | jq -r '.workflow_permanent_id')
  echo "Created workflow: $WORKFLOW_ID"

  # Step 2: Run the workflow
  RUN_RESPONSE=$(curl -s -X POST "https://api.skyvern.com/v1/run/workflows" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"workflow_id\": \"$WORKFLOW_ID\"}")

  RUN_ID=$(echo "$RUN_RESPONSE" | jq -r '.run_id')

  # Wait for completion
  while true; do
    STATUS=$(curl -s "https://api.skyvern.com/v1/runs/$RUN_ID" \
      -H "x-api-key: $SKYVERN_API_KEY" | jq -r '.status')
    [ "$STATUS" != "created" ] && [ "$STATUS" != "queued" ] && [ "$STATUS" != "running" ] && break
    sleep 5
  done

  # Step 3: Create profile (retry while session archives)
  for i in {1..10}; do
    PROFILE_RESPONSE=$(curl -s -X POST "https://api.skyvern.com/v1/browser_profiles" \
      -H "x-api-key: $SKYVERN_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{
        \"name\": \"analytics-dashboard-login\",
        \"workflow_run_id\": \"$RUN_ID\",
        \"description\": \"Authenticated state for analytics dashboard\"
      }")
    if echo "$PROFILE_RESPONSE" | jq -e '.browser_profile_id' > /dev/null 2>&1; then
      echo "$PROFILE_RESPONSE"
      break
    fi
    sleep 1
  done
  ```
</CodeGroup>

**Parameters:**

| Parameter         | Type   | Description                                                                     |
| ----------------- | ------ | ------------------------------------------------------------------------------- |
| `name`            | string | Required. Display name for the profile. Must be unique within your organization |
| `workflow_run_id` | string | ID of the completed workflow run to create the profile from                     |
| `description`     | string | Optional description of the profile's purpose                                   |

### From a browser session

You can also create a profile from a closed [Browser Session](/developers/optimization/browser-sessions) that was set to save its profile. Create the session with `generate_browser_profile` enabled (or [turn it on while the session is alive](/developers/optimization/browser-sessions#save-a-sessions-profile)), close the session, then pass the session ID instead of the workflow run ID.

<Warning>
  Sessions do not save their profile by default. If the session did not have `generate_browser_profile` enabled and was not started from a saved profile, this call fails with a `400` error — retrying does not help. Update API scripts and integrations that create profiles from closed sessions; n8n users may need updated Skyvern nodes. Profiles created from workflow runs (`persist_browser_session=true`) are unaffected.
</Warning>

<Note>
  For opted-in sessions, the archive uploads asynchronously after the session closes, so keep brief retry logic for the transient "not persisted yet" window.
</Note>

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  from skyvern import Skyvern

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

      # browser_session_id from a closed session created with generate_browser_profile enabled
      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())
  ```

  ```typescript TypeScript theme={null}
  import { Skyvern } from "@skyvern/client";

  async function main() {
    const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

    // browser_session_id from a closed session created with generate_browser_profile enabled
    const sessionId = "pbs_your_session_id";

    // Create profile from the closed session (retry while archive uploads)
    let profile;
    for (let attempt = 0; attempt < 10; attempt++) {
      try {
        profile = await client.createBrowserProfile({
          name: "dashboard-admin-login",
          browser_session_id: sessionId,
          description: "Admin account for dashboard access",
        });
        break;
      } catch (e) {
        if (String(e).toLowerCase().includes("persisted") && attempt < 9) {
          await new Promise((r) => setTimeout(r, 2000));
          continue;
        }
        throw e;
      }
    }

    console.log(`Profile created: ${profile.browser_profile_id}`);
  }

  main();
  ```

  ```bash cURL theme={null}
  # Create a session that saves its profile when it ends
  SESSION_ID=$(curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"generate_browser_profile": true}' | jq -r '.browser_session_id')

  # ... run your tasks against $SESSION_ID here ...

  # Close the session so the profile uploads
  curl -s -X POST "https://api.skyvern.com/v1/browser_sessions/$SESSION_ID/close" \
    -H "x-api-key: $SKYVERN_API_KEY"

  # Create profile (retry while session archives)
  for i in {1..10}; do
    PROFILE_RESPONSE=$(curl -s -X POST "https://api.skyvern.com/v1/browser_profiles" \
      -H "x-api-key: $SKYVERN_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{
        \"name\": \"dashboard-admin-login\",
        \"browser_session_id\": \"$SESSION_ID\",
        \"description\": \"Admin account for dashboard access\"
      }")
    if echo "$PROFILE_RESPONSE" | jq -e '.browser_profile_id' > /dev/null 2>&1; then
      echo "$PROFILE_RESPONSE"
      break
    fi
    sleep 2
  done
  ```
</CodeGroup>

**Parameters:**

| Parameter            | Type   | Description                                                                                                                                              |
| -------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`               | string | Required. Display name for the profile. Must be unique within your organization                                                                          |
| `browser_session_id` | string | ID of the closed browser session (starts with `pbs_`). The session must have had `generate_browser_profile` enabled or been started from a saved profile |
| `description`        | string | Optional description of the profile's purpose                                                                                                            |

***

## Use a Browser Profile

Pass `browser_profile_id` when running an agent to restore the saved state. Skyvern restores cookies, localStorage, and session files before the first step runs.

<CodeGroup>
  ```python Python theme={null}
  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())
  ```

  ```typescript TypeScript theme={null}
  import { Skyvern } from "@skyvern/client";

  async function main() {
    const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

    // Run workflow with saved profile, no login needed
    const result = await client.runWorkflow({
      body: {
        workflow_id: "wpid_daily_metrics",
        browser_profile_id: "bp_490705123456789012",
      },
      waitForCompletion: true,
    });

    console.log(`Output: ${JSON.stringify(result.output)}`);
  }

  main();
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.skyvern.com/v1/run/workflows" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "workflow_id": "wpid_daily_metrics",
      "browser_profile_id": "bp_490705123456789012"
    }'
  ```
</CodeGroup>

**Example response:**

```json theme={null}
{
  "run_id": "wr_494469342201718946",
  "status": "created",
  "run_request": {
    "workflow_id": "wpid_daily_metrics",
    "browser_profile_id": "bp_490705123456789012",
    "proxy_location": "RESIDENTIAL"
  }
}
```

<Note>
  `browser_profile_id` is supported for agents 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.
</Note>

***

## Tutorial: save and reuse browsing state

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

<Steps>
  <Step title="Create an agent with persist_browser_session">
    The agent must have `persist_browser_session=true` so Skyvern archives the browser state after the run.

    <CodeGroup>
      ```python Python theme={null}
      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
      ```

      ```typescript TypeScript theme={null}
      const workflow = await client.createWorkflow({
        body: {
          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"
                }
              ]
            }
          }
        }
      });
      console.log(workflow.workflow_permanent_id); // wpid_494674198088536840
      ```

      ```bash cURL theme={null}
      curl -s -X POST "https://api.skyvern.com/v1/workflows" \
        -H "x-api-key: $SKYVERN_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "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"
                }
              ]
            }
          }
        }'
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "workflow_permanent_id": "wpid_494674198088536840",
      "persist_browser_session": true,
      "title": "Visit Hacker News"
    }
    ```
  </Step>

  <Step title="Run the agent">
    Run the agent and wait for it to complete. Skyvern opens a browser, executes the navigation block, then archives the browser state in the background.

    <CodeGroup>
      ```python Python theme={null}
      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
      ```

      ```typescript TypeScript theme={null}
      const run = await client.runWorkflow({
        body: { workflow_id: workflow.workflow_permanent_id },
        waitForCompletion: true,
      });
      console.log(run.run_id); // wr_494674202383504144
      console.log(run.status); // completed
      ```

      ```bash cURL theme={null}
      RUN=$(curl -s -X POST "https://api.skyvern.com/v1/run/workflows" \
        -H "x-api-key: $SKYVERN_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"workflow_id\": \"$WORKFLOW_ID\"}")
      RUN_ID=$(echo "$RUN" | jq -r '.run_id')

      # Poll until complete
      while true; do
        STATUS=$(curl -s "https://api.skyvern.com/v1/runs/$RUN_ID" \
          -H "x-api-key: $SKYVERN_API_KEY" | jq -r '.status')
        [ "$STATUS" != "created" ] && [ "$STATUS" != "queued" ] && [ "$STATUS" != "running" ] && break
        sleep 5
      done
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "run_id": "wr_494674202383504144",
      "status": "completed"
    }
    ```
  </Step>

  <Step title="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.

    <CodeGroup>
      ```python Python theme={null}
      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
      ```

      ```typescript TypeScript theme={null}
      let profile;
      for (let attempt = 0; attempt < 10; attempt++) {
        try {
          profile = await client.createBrowserProfile({
            name: "hn-browsing-state",
            workflow_run_id: run.run_id,
            description: "Hacker News cookies and browsing state",
          });
          console.log(profile.browser_profile_id); // bp_494674399951999772
          break;
        } catch (e) {
          if (String(e).toLowerCase().includes("persisted") && attempt < 9) {
            await new Promise((r) => setTimeout(r, 2000));
            continue;
          }
          throw e;
        }
      }
      ```

      ```bash cURL theme={null}
      for i in {1..10}; do
        PROFILE=$(curl -s -X POST "https://api.skyvern.com/v1/browser_profiles" \
          -H "x-api-key: $SKYVERN_API_KEY" \
          -H "Content-Type: application/json" \
          -d "{
            \"name\": \"hn-browsing-state\",
            \"workflow_run_id\": \"$RUN_ID\",
            \"description\": \"Hacker News cookies and browsing state\"
          }")
        PROFILE_ID=$(echo "$PROFILE" | jq -r '.browser_profile_id // empty')
        if [ -n "$PROFILE_ID" ]; then
          echo "$PROFILE" | jq .
          break
        fi
        sleep 2
      done
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "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
    }
    ```
  </Step>

  <Step title="Verify the profile exists">
    List all profiles or fetch one by ID to confirm it was saved.

    <CodeGroup>
      ```python Python theme={null}
      # 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
      ```

      ```typescript TypeScript theme={null}
      // List all profiles
      const profiles = await client.listBrowserProfiles({});
      console.log(profiles.length); // 1

      // Get a single profile
      const fetched = await client.getBrowserProfile(profile.browser_profile_id);
      console.log(fetched.name); // hn-browsing-state
      ```

      ```bash cURL theme={null}
      # List all profiles
      curl -s "https://api.skyvern.com/v1/browser_profiles" \
        -H "x-api-key: $SKYVERN_API_KEY" | jq '.[].name'

      # Get a single profile
      curl -s "https://api.skyvern.com/v1/browser_profiles/$PROFILE_ID" \
        -H "x-api-key: $SKYVERN_API_KEY" | jq .
      ```
    </CodeGroup>

    ```json List response theme={null}
    [
      {
        "browser_profile_id": "bp_494674399951999772",
        "name": "hn-browsing-state",
        "created_at": "2026-02-12T01:09:18.048208"
      }
    ]
    ```
  </Step>

  <Step title="Reuse the profile in a second agent">
    Pass `browser_profile_id` when running an agent. Skyvern restores the saved cookies, localStorage, and session files before the first block runs. The second agent starts with the browser state from step 2, no repeat navigation needed.

    <CodeGroup>
      ```python Python theme={null}
      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
      ```

      ```typescript TypeScript theme={null}
      const result = await client.runWorkflow({
        body: {
          workflow_id: dataWorkflow.workflow_permanent_id,
          browser_profile_id: profile.browser_profile_id,
        },
        waitForCompletion: true,
      });
      console.log(result.status); // completed
      ```

      ```bash cURL theme={null}
      curl -s -X POST "https://api.skyvern.com/v1/run/workflows" \
        -H "x-api-key: $SKYVERN_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{
          \"workflow_id\": \"$WORKFLOW2_ID\",
          \"browser_profile_id\": \"$PROFILE_ID\"
        }"
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "run_id": "wr_494674434311738148",
      "status": "created"
    }
    ```
  </Step>

  <Step title="Delete the profile">
    Clean up profiles you no longer need.

    <CodeGroup>
      ```python Python theme={null}
      await client.delete_browser_profile(profile_id=profile.browser_profile_id)

      # Confirm deletion
      remaining = await client.list_browser_profiles()
      print(len(remaining)) # 0
      ```

      ```typescript TypeScript theme={null}
      await client.deleteBrowserProfile(profile.browser_profile_id);

      // Confirm deletion
      const remaining = await client.listBrowserProfiles({});
      console.log(remaining.length); // 0
      ```

      ```bash cURL theme={null}
      curl -s -X DELETE "https://api.skyvern.com/v1/browser_profiles/$PROFILE_ID" \
        -H "x-api-key: $SKYVERN_API_KEY"
      ```
    </CodeGroup>
  </Step>
</Steps>

<Tip>
  In a real scenario, step 1 would be a login agent that authenticates with a site. The saved profile then lets all future agents skip the login step entirely.
</Tip>

***

## Pin a proxy identity

A browser profile can carry a pinned residential ISP proxy identity so that every run reusing the profile reaches the target site from the same IP and locale. Pinning is opt-in and uses two fields:

* `proxy_location` — set to `RESIDENTIAL_ISP` (or a `GeoTarget` object) to opt the profile into a static ISP identity.
* `proxy_session_id` — opaque Skyvern-managed sticky-session key. Omit it to let Skyvern generate one automatically when `proxy_location` is `RESIDENTIAL_ISP`.

When a run uses a profile that has a pinned proxy, Skyvern routes traffic through the same residential ISP identity the profile was last associated with, even if the run request does not set `proxy_location`.

### Set a pin at create time

<CodeGroup>
  ```python Python theme={null}
  profile = await client.create_browser_profile(
      name="prod-salesforce-admin",
      description="Pinned residential ISP identity",
      proxy_location="RESIDENTIAL_ISP",
  )
  ```

  ```bash cURL theme={null}
  curl -s -X POST "https://api.skyvern.com/v1/browser_profiles" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "prod-salesforce-admin",
      "description": "Pinned residential ISP identity",
      "proxy_location": "RESIDENTIAL_ISP"
    }'
  ```
</CodeGroup>

### Rotate the pinned identity

Update the profile with `rotate_proxy_session_id: true` to ask Skyvern to mint a fresh sticky-session id while keeping the same `proxy_location`. Use this when the existing IP gets flagged or when you want to start a new identity for the same profile.

```bash cURL theme={null}
curl -s -X PATCH "https://api.skyvern.com/v1/browser_profiles/$PROFILE_ID" \
  -H "x-api-key: $SKYVERN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rotate_proxy_session_id": true }'
```

To clear the pin entirely, set `proxy_location` to `null` in the same update call.

<Note>
  `proxy_session_id` is only valid alongside `RESIDENTIAL_ISP`. Requests that supply `proxy_session_id` with any other `proxy_location` are rejected with a 400.
</Note>

***

## Best practices

### Use descriptive names

Include the account, site, and purpose in the profile name so it is easy to identify later.

<CodeGroup>
  ```python Python theme={null}
  # 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,
  )
  ```

  ```typescript TypeScript theme={null}
  // Good: identifies account, site, and purpose
  const profile = await client.createBrowserProfile({
    name: "prod-salesforce-admin",
    description: "Admin login for daily opportunity sync",
    workflow_run_id: runId,
  });

  // Bad: unclear what this is for
  const badProfile = await client.createBrowserProfile({
    name: "profile1",
    workflow_run_id: runId,
  });
  ```
</CodeGroup>

### Refresh profiles periodically

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

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```typescript TypeScript theme={null}
  // Create dated profile after each successful login
  const profile = await client.createBrowserProfile({
    name: `crm-login-${new Date().toISOString().split("T")[0]}`,
    workflow_run_id: newLoginRun.run_id,
  });

  // Delete old profile
  await client.deleteBrowserProfile(oldProfileId);
  ```

  ```bash cURL theme={null}
  # Create dated profile after a successful login run
  curl -X POST "https://api.skyvern.com/v1/browser_profiles" \
    -H "x-api-key: $SKYVERN_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"name\": \"crm-login-$(date +%Y-%m-%d)\",
      \"workflow_run_id\": \"$NEW_RUN_ID\"
    }"

  # Delete old profile
  curl -X DELETE "https://api.skyvern.com/v1/browser_profiles/$OLD_PROFILE_ID" \
    -H "x-api-key: $SKYVERN_API_KEY"
  ```
</CodeGroup>

### Capture updated state after each run

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

<CodeGroup>
  ```python Python theme={null}
  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,
      )
  ```

  ```typescript TypeScript theme={null}
  // Step 1: Create workflow with persist_browser_session in the definition
  const workflow = await client.createWorkflow({
    body: {
      json_definition: {
        title: "Daily Sync",
        persist_browser_session: true,  // Set here, not in runWorkflow
        workflow_definition: {
          parameters: [],
          blocks: [/* ... */]
        }
      }
    }
  });

  // Step 2: Run with an existing profile
  const result = await client.runWorkflow({
    body: {
      workflow_id: workflow.workflow_permanent_id,
      browser_profile_id: "bp_current",
    },
    waitForCompletion: true,
  });

  // Step 3: Create updated profile from the completed run
  if (shouldRefreshProfile) {
    const newProfile = await client.createBrowserProfile({
      name: `daily-sync-${new Date().toISOString().split("T")[0]}`,
      workflow_run_id: result.run_id,
    });
  }
  ```
</CodeGroup>

***

## Next steps

<CardGroup cols={2}>
  <Card title="Browser Sessions" icon="browser" href="/developers/optimization/browser-sessions">
    Maintain live browser state for real-time interactions
  </Card>

  <Card title="Cost Control" icon="dollar-sign" href="/developers/optimization/cost-control">
    Optimize costs with max\_steps and efficient prompts
  </Card>
</CardGroup>
