Files
Dorod-Sky/docs/optimization/browser-profiles.mdx

913 lines
26 KiB
Plaintext
Raw Permalink Normal View History

---
title: Browser Profiles
subtitle: Save and reuse authenticated browser state across runs
slug: optimization/browser-profiles
---
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.
<Note>
`persist_browser_session` must be set when **creating the workflow**, not when running it. It is a workflow definition property, not a runtime parameter.
</Note>
### From a workflow run
<CodeGroup>
```python Python
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
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
# 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 [Browser Session](/optimization/browser-sessions) 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.
<Warning>
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.
</Warning>
<CodeGroup>
```python Python
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())
```
```typescript TypeScript
import { Skyvern } from "@skyvern/client";
async function main() {
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
// browser_session_id from a workflow run with persist_browser_session=true
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
# browser_session_id from a workflow run with persist_browser_session=true
SESSION_ID="pbs_your_session_id"
# 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 been part of a workflow with `persist_browser_session=true` |
| `description` | string | Optional 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.
<CodeGroup>
```python Python
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="wf_daily_metrics",
browser_profile_id="bp_490705123456789012",
wait_for_completion=True,
)
print(f"Output: {result.output}")
asyncio.run(main())
```
```typescript TypeScript
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: "wf_daily_metrics",
browser_profile_id: "bp_490705123456789012",
},
waitForCompletion: true,
});
console.log(`Output: ${JSON.stringify(result.output)}`);
}
main();
```
```bash cURL
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": "wf_daily_metrics",
"browser_profile_id": "bp_490705123456789012"
}'
```
</CodeGroup>
**Example response:**
```json
{
"run_id": "wr_494469342201718946",
"status": "created",
"run_request": {
"workflow_id": "wf_daily_metrics",
"browser_profile_id": "bp_490705123456789012",
"proxy_location": "RESIDENTIAL"
}
}
```
<Note>
`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.
</Note>
---
## 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.
<Steps>
<Step title="Create a workflow with persist_browser_session">
The workflow must have `persist_browser_session=true` so Skyvern archives the browser state after the run.
<CodeGroup>
```python Python
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
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
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
{
"workflow_permanent_id": "wpid_494674198088536840",
"persist_browser_session": true,
"title": "Visit Hacker News"
}
```
</Step>
<Step title="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.
<CodeGroup>
```python Python
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
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
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
{
"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
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
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
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
{
"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
# 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
// 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
# 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
[
{
"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 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.
<CodeGroup>
```python Python
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
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
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
{
"run_id": "wr_494674434311738148",
"status": "created"
}
```
</Step>
<Step title="Delete the profile">
Clean up profiles you no longer need.
<CodeGroup>
```python Python
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
await client.deleteBrowserProfile(profile.browser_profile_id);
// Confirm deletion
const remaining = await client.listBrowserProfiles({});
console.log(remaining.length); // 0
```
```bash cURL
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 workflow that authenticates with a site. The saved profile then lets all future workflows skip the login step entirely.
</Tip>
---
## 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
# 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
// 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 workflow 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
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
// 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
# 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 workflow must have `persist_browser_session=true` in its definition. This lets you create a fresh profile from each completed run.
<CodeGroup>
```python Python
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
// 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="/optimization/browser-sessions"
>
Maintain live browser state for real-time interactions
</Card>
<Card
title="Cost Control"
icon="dollar-sign"
href="/optimization/cost-control"
>
Optimize costs with max_steps and efficient prompts
</Card>
</CardGroup>