mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-30 04:30:19 +00:00
🔄 synced local 'docs/' with remote 'docs/'
This commit is contained in:
parent
9fcfffd85f
commit
8a30ef8a42
71 changed files with 934 additions and 340 deletions
921
docs/developers/optimization/browser-profiles.mdx
Normal file
921
docs/developers/optimization/browser-profiles.mdx
Normal file
|
|
@ -0,0 +1,921 @@
|
|||
---
|
||||
title: Browser Profiles
|
||||
subtitle: Save and reuse authenticated browser state across runs
|
||||
description: Save browser state snapshots including cookies, localStorage, and session files as reusable profiles. Skip login steps and restore authenticated state instantly across workflow runs.
|
||||
slug: developers/optimization/browser-profiles
|
||||
keywords:
|
||||
- create_browser_profile
|
||||
- state snapshot
|
||||
- skip login
|
||||
- cookies
|
||||
- localStorage
|
||||
- session files
|
||||
- persist_browser_session
|
||||
---
|
||||
|
||||
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](/developers/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="wpid_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: "wpid_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": "wpid_daily_metrics",
|
||||
"browser_profile_id": "bp_490705123456789012"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"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 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="/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>
|
||||
726
docs/developers/optimization/browser-sessions.mdx
Normal file
726
docs/developers/optimization/browser-sessions.mdx
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
---
|
||||
title: Browser Sessions
|
||||
subtitle: Persist live browser state across multiple tasks and workflows
|
||||
description: Create and manage live browser sessions that persist cookies, local storage, and page state between task or workflow runs. Use sessions for back-to-back tasks, human-in-the-loop approval, and real-time agents.
|
||||
slug: developers/optimization/browser-sessions
|
||||
keywords:
|
||||
- create_browser_session
|
||||
- persistent state
|
||||
- cookies
|
||||
- local storage
|
||||
- timeout
|
||||
- session chaining
|
||||
- human-in-the-loop
|
||||
- real-time agent
|
||||
---
|
||||
|
||||
A **Browser Session** is a live browser instance that persists cookies, local storage, and page state between task or workflow runs. Think of it as keeping a browser tab open. Use sessions when you need back-to-back tasks to share state, human-in-the-loop approval, or real-time agents.
|
||||
|
||||
---
|
||||
|
||||
## Create a session
|
||||
|
||||
Start a session with optional configuration for timeout, proxy, browser type, and extensions.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import asyncio
|
||||
from skyvern import Skyvern
|
||||
|
||||
async def main():
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
session = await client.create_browser_session(
|
||||
timeout=60, # Max 60 minutes
|
||||
proxy_location="RESIDENTIAL", # US residential proxy
|
||||
browser_type="chrome", # Chrome or Edge
|
||||
extensions=["ad-blocker"], # Optional extensions
|
||||
)
|
||||
|
||||
print(f"Session ID: {session.browser_session_id}")
|
||||
print(f"Status: {session.status}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
async function main() {
|
||||
const client = new Skyvern({
|
||||
apiKey: process.env.SKYVERN_API_KEY!,
|
||||
});
|
||||
|
||||
const session = await client.createBrowserSession({
|
||||
timeout: 60,
|
||||
proxy_location: "RESIDENTIAL",
|
||||
browser_type: "chrome",
|
||||
extensions: ["ad-blocker"],
|
||||
});
|
||||
|
||||
console.log(`Session ID: ${session.browser_session_id}`);
|
||||
console.log(`Status: ${session.status}`);
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"timeout": 60,
|
||||
"proxy_location": "RESIDENTIAL",
|
||||
"browser_type": "chrome",
|
||||
"extensions": ["ad-blocker"]
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `timeout` | integer | Session lifetime in minutes. Min: 5, Max: 1440 (24 hours). Default: `60` |
|
||||
| `proxy_location` | string | Geographic proxy location (Cloud only). See [Proxy & Geolocation](/developers/going-to-production/proxy-geolocation) for available options |
|
||||
| `browser_type` | string | Browser type: `chrome` or `msedge` |
|
||||
| `extensions` | array | Extensions to install: `ad-blocker`, `captcha-solver` |
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"browser_session_id": "pbs_490705123456789012",
|
||||
"organization_id": "o_485917350850524254",
|
||||
"status": "running",
|
||||
"timeout": 60,
|
||||
"browser_type": "chrome",
|
||||
"extensions": ["ad-blocker"],
|
||||
"vnc_streaming_supported": true,
|
||||
"app_url": "https://app.skyvern.com/browser-session/pbs_490705123456789012",
|
||||
"started_at": "2026-02-01T10:30:03.110Z",
|
||||
"created_at": "2026-02-01T10:30:00.000Z",
|
||||
"modified_at": "2026-02-01T10:30:03.251Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Session statuses:**
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `running` | Browser is live and accepting tasks. This is the status returned on creation. The browser launches within seconds. |
|
||||
| `closed` | Session was closed manually or by timeout. No further tasks can run. |
|
||||
|
||||
<Warning>
|
||||
Sessions close automatically when the timeout expires, **even if a task is still running**. The timeout countdown begins when the browser launches. Set timeouts with enough margin for your longest expected task.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## Run tasks with a session
|
||||
|
||||
Pass `browser_session_id` to `run_task` to execute tasks in an existing session. Each task continues from where the previous one left off: same page, same cookies, same form data.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import asyncio
|
||||
from skyvern import Skyvern
|
||||
|
||||
async def main():
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
# Create session
|
||||
session = await client.create_browser_session(timeout=30)
|
||||
session_id = session.browser_session_id
|
||||
|
||||
try:
|
||||
# Task 1: Login (wait for completion before continuing)
|
||||
await client.run_task(
|
||||
prompt="Login with username 'support@company.com'",
|
||||
url="https://dashboard.example.com/login",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
# Task 2: Search (already logged in from Task 1)
|
||||
result = await client.run_task(
|
||||
prompt="Find customer with email 'customer@example.com'",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
print(f"Customer: {result.output}")
|
||||
|
||||
finally:
|
||||
# Always close when done
|
||||
await client.close_browser_session(browser_session_id=session_id)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
async function main() {
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
const session = await client.createBrowserSession({ timeout: 30 });
|
||||
const sessionId = session.browser_session_id;
|
||||
|
||||
try {
|
||||
// Task 1: Login (wait for completion)
|
||||
await client.runTask({
|
||||
body: {
|
||||
prompt: "Login with username 'support@company.com'",
|
||||
url: "https://dashboard.example.com/login",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
// Task 2: Search (reuses login state)
|
||||
const result = await client.runTask({
|
||||
body: {
|
||||
prompt: "Find customer with email 'customer@example.com'",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
console.log(`Customer: ${JSON.stringify(result.output)}`);
|
||||
|
||||
} finally {
|
||||
await client.closeBrowserSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Create session
|
||||
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 '{"timeout": 30}' | jq -r '.browser_session_id')
|
||||
|
||||
echo "Session: $SESSION_ID"
|
||||
|
||||
# Task 1: Login
|
||||
RUN_ID=$(curl -s -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"prompt\": \"Login with username 'support@company.com'\",
|
||||
\"url\": \"https://dashboard.example.com/login\",
|
||||
\"browser_session_id\": \"$SESSION_ID\"
|
||||
}" | 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
|
||||
|
||||
# Task 2: Search (reuses login state)
|
||||
curl -s -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"prompt\": \"Find customer with email 'customer@example.com'\",
|
||||
\"browser_session_id\": \"$SESSION_ID\"
|
||||
}"
|
||||
|
||||
# Close session when done
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions/$SESSION_ID/close" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
---
|
||||
|
||||
## Run workflows with a session
|
||||
|
||||
Pass `browser_session_id` to `run_workflow` to execute a workflow in an existing session. This is useful when you need to run a predefined workflow but want it to continue from your current browser state.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import asyncio
|
||||
from skyvern import Skyvern
|
||||
|
||||
async def main():
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
# Create session
|
||||
session = await client.create_browser_session(timeout=60)
|
||||
session_id = session.browser_session_id
|
||||
|
||||
try:
|
||||
# First, login manually via a task
|
||||
await client.run_task(
|
||||
prompt="Login with username 'admin@company.com'",
|
||||
url="https://app.example.com/login",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
# Then run a workflow in the same session (already logged in)
|
||||
result = await client.run_workflow(
|
||||
workflow_id="wpid_export_monthly_report",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
print(f"Workflow completed: {result.status}")
|
||||
|
||||
finally:
|
||||
await client.close_browser_session(browser_session_id=session_id)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
async function main() {
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
const session = await client.createBrowserSession({ timeout: 60 });
|
||||
const sessionId = session.browser_session_id;
|
||||
|
||||
try {
|
||||
// First, login manually via a task
|
||||
await client.runTask({
|
||||
body: {
|
||||
prompt: "Login with username 'admin@company.com'",
|
||||
url: "https://app.example.com/login",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
// Then run a workflow in the same session (already logged in)
|
||||
const result = await client.runWorkflow({
|
||||
body: {
|
||||
workflow_id: "wpid_export_monthly_report",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
console.log(`Workflow completed: ${result.status}`);
|
||||
|
||||
} finally {
|
||||
await client.closeBrowserSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Create session
|
||||
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 '{"timeout": 60}' | jq -r '.browser_session_id')
|
||||
|
||||
# Login via a task
|
||||
RUN_ID=$(curl -s -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"prompt\": \"Login with username 'admin@company.com'\",
|
||||
\"url\": \"https://app.example.com/login\",
|
||||
\"browser_session_id\": \"$SESSION_ID\"
|
||||
}" | jq -r '.run_id')
|
||||
|
||||
# Poll until login completes
|
||||
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
|
||||
|
||||
# Run workflow in the same session (already logged in)
|
||||
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\": \"wpid_export_monthly_report\",
|
||||
\"browser_session_id\": \"$SESSION_ID\"
|
||||
}"
|
||||
|
||||
# Close session when done
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions/$SESSION_ID/close" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
You cannot use both `browser_session_id` and `browser_profile_id` in the same request. Choose one or the other.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Close a session
|
||||
|
||||
Close a session to release resources and stop billing. The browser shuts down immediately.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
await client.close_browser_session(
|
||||
browser_session_id="pbs_490705123456789012"
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
await client.closeBrowserSession("pbs_490705123456789012");
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.skyvern.com/v1/browser_sessions/pbs_490705123456789012/close" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Warning>
|
||||
**Always close sessions when done.** Active sessions continue billing even when idle. Use try/finally blocks to ensure cleanup.
|
||||
</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
try:
|
||||
session = await client.create_browser_session(timeout=30)
|
||||
session_id = session.browser_session_id
|
||||
|
||||
# Do work...
|
||||
await client.run_task(
|
||||
prompt="...",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
finally:
|
||||
# Always close, even if task fails
|
||||
await client.close_browser_session(browser_session_id=session_id)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
const session = await client.createBrowserSession({ timeout: 30 });
|
||||
const sessionId = session.browser_session_id;
|
||||
|
||||
try {
|
||||
await client.runTask({
|
||||
body: {
|
||||
prompt: "...",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
} finally {
|
||||
// Always close, even if task fails
|
||||
await client.closeBrowserSession(sessionId);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
---
|
||||
|
||||
## Example: Human-in-the-loop
|
||||
|
||||
A shopping bot that pauses for human approval before completing a purchase.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import asyncio
|
||||
from skyvern import Skyvern
|
||||
|
||||
async def shopping_with_approval():
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
session = await client.create_browser_session(timeout=15)
|
||||
session_id = session.browser_session_id
|
||||
|
||||
try:
|
||||
# Step 1: Add to cart
|
||||
await client.run_task(
|
||||
prompt="Find wireless headphones under $100, add top result to cart",
|
||||
url="https://shop.example.com",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
# Step 2: Wait for human approval
|
||||
approval = input("Approve purchase? (yes/no): ")
|
||||
|
||||
if approval.lower() == "yes":
|
||||
# Step 3: Checkout (cart persists from Step 1)
|
||||
result = await client.run_task(
|
||||
prompt="Complete checkout and confirm order",
|
||||
browser_session_id=session_id,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
print(f"Order placed: {result.output}")
|
||||
else:
|
||||
print("Purchase cancelled")
|
||||
|
||||
finally:
|
||||
await client.close_browser_session(browser_session_id=session_id)
|
||||
|
||||
asyncio.run(shopping_with_approval())
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
import * as readline from "readline";
|
||||
|
||||
async function shoppingWithApproval() {
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
const session = await client.createBrowserSession({ timeout: 15 });
|
||||
const sessionId = session.browser_session_id;
|
||||
|
||||
try {
|
||||
// Step 1: Add to cart
|
||||
await client.runTask({
|
||||
body: {
|
||||
prompt: "Find wireless headphones under $100, add top result to cart",
|
||||
url: "https://shop.example.com",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
// Step 2: Wait for human approval
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const approval = await new Promise<string>((resolve) =>
|
||||
rl.question("Approve purchase? (yes/no): ", resolve)
|
||||
);
|
||||
rl.close();
|
||||
|
||||
if (approval.toLowerCase() === "yes") {
|
||||
// Step 3: Checkout (cart persists from Step 1)
|
||||
const result = await client.runTask({
|
||||
body: {
|
||||
prompt: "Complete checkout and confirm order",
|
||||
browser_session_id: sessionId,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
console.log(`Order placed: ${JSON.stringify(result.output)}`);
|
||||
} else {
|
||||
console.log("Purchase cancelled");
|
||||
}
|
||||
|
||||
} finally {
|
||||
await client.closeBrowserSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
shoppingWithApproval();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The browser maintains the cart contents during the approval pause. No state is lost.
|
||||
|
||||
---
|
||||
|
||||
## Best practices
|
||||
|
||||
### Set appropriate timeouts
|
||||
|
||||
Sessions bill while open, so match the timeout to your use case. A task typically completes in 30 to 90 seconds, so a 10-minute timeout covers most multi-step sequences with margin. Human-in-the-loop flows need longer timeouts to account for wait time.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Quick multi-step task (2-3 tasks back to back)
|
||||
session = await client.create_browser_session(timeout=10)
|
||||
|
||||
# Human-in-the-loop with wait time for approval
|
||||
session = await client.create_browser_session(timeout=60)
|
||||
|
||||
# Long-running agent that monitors a dashboard
|
||||
session = await client.create_browser_session(timeout=480) # 8 hours
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Quick multi-step task (2-3 tasks back to back)
|
||||
const session1 = await client.createBrowserSession({ timeout: 10 });
|
||||
|
||||
// Human-in-the-loop with wait time for approval
|
||||
const session2 = await client.createBrowserSession({ timeout: 60 });
|
||||
|
||||
// Long-running agent that monitors a dashboard
|
||||
const session3 = await client.createBrowserSession({ timeout: 480 }); // 8 hours
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Quick multi-step task (2-3 tasks back to back)
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timeout": 10}'
|
||||
|
||||
# Human-in-the-loop with wait time for approval
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timeout": 60}'
|
||||
|
||||
# Long-running agent that monitors a dashboard
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timeout": 480}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Use workflows for predetermined sequences
|
||||
|
||||
If your steps don't need pauses between them, a workflow runs them in a single browser instance without the overhead of creating and managing a session. Each task in a session incurs its own startup cost, while workflow blocks share one browser.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Less efficient: multiple tasks in a session (each task has startup overhead)
|
||||
session = await client.create_browser_session()
|
||||
await client.run_task(prompt="Step 1", browser_session_id=session.browser_session_id, wait_for_completion=True)
|
||||
await client.run_task(prompt="Step 2", browser_session_id=session.browser_session_id, wait_for_completion=True)
|
||||
|
||||
# More efficient: single workflow (blocks share one browser, no inter-task overhead)
|
||||
await client.run_workflow(workflow_id="wpid_abc", wait_for_completion=True)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Less efficient: multiple tasks in a session (each task has startup overhead)
|
||||
const session = await client.createBrowserSession({});
|
||||
await client.runTask({ body: { prompt: "Step 1", browser_session_id: session.browser_session_id }, waitForCompletion: true });
|
||||
await client.runTask({ body: { prompt: "Step 2", browser_session_id: session.browser_session_id }, waitForCompletion: true });
|
||||
|
||||
// More efficient: single workflow (blocks share one browser, no inter-task overhead)
|
||||
await client.runWorkflow({ body: { workflow_id: "wpid_abc" }, waitForCompletion: true });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Choose the right browser type
|
||||
|
||||
Chrome has the widest compatibility. Use Edge only when a site requires or detects it specifically.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Chrome (default) - widest compatibility
|
||||
session = await client.create_browser_session(browser_type="chrome")
|
||||
|
||||
# Edge - for sites that require or fingerprint Edge
|
||||
session = await client.create_browser_session(browser_type="msedge")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Chrome (default) - widest compatibility
|
||||
const chromeSession = await client.createBrowserSession({ browser_type: "chrome" });
|
||||
|
||||
// Edge - for sites that require or fingerprint Edge
|
||||
const edgeSession = await client.createBrowserSession({ browser_type: "msedge" });
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Chrome (default) - widest compatibility
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"browser_type": "chrome"}'
|
||||
|
||||
# Edge - for sites that require or fingerprint Edge
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"browser_type": "msedge"}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Use extensions strategically
|
||||
|
||||
Extensions add startup time, so only enable them when needed. The ad-blocker removes overlay ads that can interfere with automation. The captcha-solver handles CAPTCHAs automatically but is only available on Cloud.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Block ads that overlay content and interfere with clicks
|
||||
session = await client.create_browser_session(extensions=["ad-blocker"])
|
||||
|
||||
# Auto-solve captchas (Cloud only)
|
||||
session = await client.create_browser_session(extensions=["captcha-solver"])
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Block ads that overlay content and interfere with clicks
|
||||
const adBlockSession = await client.createBrowserSession({ extensions: ["ad-blocker"] });
|
||||
|
||||
// Auto-solve captchas (Cloud only)
|
||||
const captchaSession = await client.createBrowserSession({ extensions: ["captcha-solver"] });
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Block ads that overlay content and interfere with clicks
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"extensions": ["ad-blocker"]}'
|
||||
|
||||
# Auto-solve captchas (Cloud only)
|
||||
curl -s -X POST "https://api.skyvern.com/v1/browser_sessions" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"extensions": ["captcha-solver"]}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
---
|
||||
|
||||
## Sessions vs Profiles
|
||||
|
||||
Skyvern also offers [Browser Profiles](/developers/optimization/browser-profiles), saved snapshots of browser state (cookies, storage, session files) that you can reuse across days or weeks. Choose based on your use case:
|
||||
|
||||
| Aspect | Browser Session | Browser Profile |
|
||||
|--------|----------------|-----------------|
|
||||
| **What it is** | Live browser instance | Saved snapshot of browser state |
|
||||
| **Lifetime** | Minutes to hours | Days to months |
|
||||
| **State** | Current page, cookies, open connections | Cookies, storage, session files |
|
||||
| **Billing** | Charged while open | No cost when not in use |
|
||||
| **Best for** | Back-to-back tasks, human-in-the-loop, real-time agents | Repeated logins, scheduled workflows, shared auth state |
|
||||
|
||||
<Tip>
|
||||
You can create a [Browser Profile](/developers/optimization/browser-profiles) from a completed session to save its authenticated state for future reuse.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Connect Your Local Browser"
|
||||
icon="laptop"
|
||||
href="/developers/optimization/browser-tunneling"
|
||||
>
|
||||
Let Skyvern Cloud use your local browser with your existing logins
|
||||
</Card>
|
||||
<Card
|
||||
title="Browser Profiles"
|
||||
icon="floppy-disk"
|
||||
href="/developers/optimization/browser-profiles"
|
||||
>
|
||||
Save session state for reuse across days
|
||||
</Card>
|
||||
<Card
|
||||
title="Cost Control"
|
||||
icon="dollar-sign"
|
||||
href="/developers/optimization/cost-control"
|
||||
>
|
||||
Optimize costs with max_steps and efficient prompts
|
||||
</Card>
|
||||
</CardGroup>
|
||||
267
docs/developers/optimization/browser-tunneling.mdx
Normal file
267
docs/developers/optimization/browser-tunneling.mdx
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
---
|
||||
title: Connect Skyvern to Your Local Browser
|
||||
subtitle: Run automations using your existing cookies, logins, and extensions
|
||||
description: Use skyvern browser serve to let Skyvern Cloud control a Chrome browser on your local machine, reusing your existing cookies, sessions, extensions, and saved passwords through a secure tunnel.
|
||||
slug: developers/optimization/browser-tunneling
|
||||
keywords:
|
||||
- browser session
|
||||
- task
|
||||
- CLI
|
||||
- authentication
|
||||
- login
|
||||
- proxy
|
||||
- Playwright
|
||||
---
|
||||
|
||||
Want Skyvern Cloud to automate a site where you're already logged in? With `skyvern browser serve`, Skyvern Cloud can control a Chrome browser running on your machine, using your existing cookies, sessions, extensions, and saved passwords.
|
||||
|
||||
**Common use cases:**
|
||||
- Automate sites where you're already authenticated (no need to re-login)
|
||||
- Reach internal tools behind a VPN or firewall
|
||||
- Use browser extensions you've already installed (ad blockers, auth tools, etc.)
|
||||
- Keep all browser data on your own machine
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
One command starts Chrome on your machine and creates a tunnel so Skyvern Cloud can connect:
|
||||
|
||||
```bash
|
||||
skyvern browser serve --tunnel
|
||||
```
|
||||
|
||||
That's it. The CLI launches Chrome, starts a local proxy server, and opens an ngrok tunnel. You'll see a tunnel URL in the output; pass it as `browser_address` when running tasks.
|
||||
|
||||
<Note>
|
||||
The `--tunnel` flag requires [ngrok](https://ngrok.com/download) to be installed and authenticated. Run `ngrok authtoken <your-token>` once to set it up.
|
||||
</Note>
|
||||
|
||||
### Step-by-step (manual tunnel)
|
||||
|
||||
If you prefer to manage the tunnel yourself:
|
||||
|
||||
```bash
|
||||
# 1. Start the browser server
|
||||
skyvern browser serve
|
||||
|
||||
# 2. In a separate terminal, create a tunnel
|
||||
ngrok http 9222
|
||||
|
||||
# 3. Copy the ngrok URL and use it as browser_address in your task
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Run a task on your local browser
|
||||
|
||||
Once the tunnel is running, pass the tunnel URL as `browser_address`:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import asyncio
|
||||
from skyvern import Skyvern
|
||||
|
||||
async def main():
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
result = await client.run_task(
|
||||
prompt="Download the latest invoice from my account",
|
||||
browser_address="wss://abc123.ngrok-free.dev",
|
||||
)
|
||||
|
||||
print(f"Status: {result.status}")
|
||||
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! });
|
||||
|
||||
const result = await client.runTask({
|
||||
body: {
|
||||
prompt: "Download the latest invoice from my account",
|
||||
browser_address: "wss://abc123.ngrok-free.dev",
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
console.log(`Status: ${result.status}`);
|
||||
console.log(`Output: ${JSON.stringify(result.output)}`);
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Download the latest invoice from my account",
|
||||
"browser_address": "wss://abc123.ngrok-free.dev"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Your machine │
|
||||
│ │
|
||||
│ ┌────────────┐ CDP (internal) ┌──────────────┐ │
|
||||
│ │ Chrome │◄─────────────────►│ Unified │ │
|
||||
│ │ (port │ port 10222 │ Server │ │
|
||||
│ │ 10222) │ │ (port 9222) │ │
|
||||
│ └────────────┘ └──────┬───────┘ │
|
||||
│ │ │
|
||||
└──────────────────────────────────────────┼───────────┘
|
||||
│
|
||||
ngrok / tunnel│
|
||||
│
|
||||
┌──────▼───────┐
|
||||
│ Skyvern Cloud│
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
`skyvern browser serve`:
|
||||
|
||||
1. **Launches Chrome** with CDP (Chrome DevTools Protocol) enabled on an internal port
|
||||
2. **Starts a proxy server** on the exposed port (default `9222`) that forwards CDP traffic to Chrome
|
||||
3. **Optionally creates a tunnel** (`--tunnel`) so Skyvern Cloud can reach it from the internet
|
||||
|
||||
---
|
||||
|
||||
## CLI options
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--port` | `9222` | Port for the proxy server. Chrome uses `port + 1000` internally. |
|
||||
| `--profile-dir` | `~/.skyvern/chrome-profile` | Chrome user data directory. Point this at an existing profile to reuse cookies and logins. |
|
||||
| `--download-dir` | `~/.skyvern/downloads/{browser_id}` | Directory for browser downloads. |
|
||||
| `--api-key` | (none) | API key for authenticating incoming requests. See [Security](#security). |
|
||||
| `--headless` | `false` | Run Chrome in headless mode (no visible window). |
|
||||
| `--chrome-path` | (auto-detect) | Path to Chrome/Chromium executable. |
|
||||
| `--tunnel` | `false` | Automatically start an ngrok tunnel. Requires `ngrok` installed. |
|
||||
| `--json` | `false` | Output connection info as JSON (for scripting). Cannot combine with `--tunnel`. |
|
||||
|
||||
The `--api-key` option can also be set via the `SKYVERN_BROWSER_SERVE_API_KEY` environment variable.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Use a specific Chrome profile (reuse your existing logins)
|
||||
skyvern browser serve --profile-dir ~/Library/Application\ Support/Google/Chrome/Default
|
||||
|
||||
# Run headless with auto-tunnel
|
||||
skyvern browser serve --headless --tunnel
|
||||
|
||||
# Custom port with API key authentication
|
||||
skyvern browser serve --port 8222 --api-key "my-secret-key"
|
||||
|
||||
# Output connection info as JSON (useful for scripting)
|
||||
skyvern browser serve --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
<Warning>
|
||||
**Read this section before exposing your browser to the internet.**
|
||||
|
||||
When you run `skyvern browser serve` without `--api-key` and expose it via a tunnel, **anyone with the tunnel URL has full remote control of your Chrome browser**. This includes access to all logged-in sessions, cookies, saved passwords, and anything visible in the browser.
|
||||
</Warning>
|
||||
|
||||
### API key authentication
|
||||
|
||||
Use your Skyvern API key (from [Settings](https://app.skyvern.com/settings)) with the `--api-key` flag to require authentication on every incoming request:
|
||||
|
||||
```bash
|
||||
skyvern browser serve --api-key "your-skyvern-api-key"
|
||||
```
|
||||
|
||||
Or set it via environment variable:
|
||||
|
||||
```bash
|
||||
export SKYVERN_BROWSER_SERVE_API_KEY="your-skyvern-api-key"
|
||||
skyvern browser serve
|
||||
```
|
||||
|
||||
When enabled, all requests without a valid `x-api-key` header receive a `401 Unauthorized` response. Skyvern Cloud automatically sends the correct API key when connecting.
|
||||
|
||||
### Additional security measures
|
||||
|
||||
The built-in API key provides basic protection. For additional security such as IP allowlisting, mTLS, or VPN-based access, contact **support@skyvern.com**.
|
||||
|
||||
---
|
||||
|
||||
## When to use this vs other options
|
||||
|
||||
| Approach | When to use |
|
||||
|----------|-------------|
|
||||
| **Connect to your local browser** (`skyvern browser serve`) | You want Skyvern Cloud to use your local browser with your existing sessions |
|
||||
| **[Browser Sessions](/developers/optimization/browser-sessions)** | You want Skyvern Cloud to manage the browser entirely in the cloud |
|
||||
| **[Browser Profiles](/developers/optimization/browser-profiles)** | You want to save and reuse cookies/storage state across cloud sessions |
|
||||
| **[CDP Connect](/developers/self-hosted/browser#cdp-connect-external-chrome)** (self-hosted) | You're running Skyvern locally and want to connect to an existing Chrome instance |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="ngrok not found">
|
||||
Install ngrok from [ngrok.com/download](https://ngrok.com/download) and authenticate it:
|
||||
|
||||
```bash
|
||||
ngrok authtoken <your-token>
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Chrome not found">
|
||||
Use `--chrome-path` to specify the path to your Chrome executable:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
skyvern browser serve --chrome-path "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
|
||||
# Linux
|
||||
skyvern browser serve --chrome-path /usr/bin/google-chrome
|
||||
|
||||
# Windows
|
||||
skyvern browser serve --chrome-path "C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Port already in use">
|
||||
Another process is using port 9222. Either stop that process or use a different port:
|
||||
|
||||
```bash
|
||||
skyvern browser serve --port 8222
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tunnel connection drops">
|
||||
ngrok free tier tunnels have connection limits. For production use, consider an ngrok paid plan or contact support@skyvern.com for alternative tunnel options.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Browser Sessions" icon="window" href="/developers/optimization/browser-sessions">
|
||||
Use cloud-managed browser sessions for multi-step tasks
|
||||
</Card>
|
||||
<Card title="Browser Profiles" icon="floppy-disk" href="/developers/optimization/browser-profiles">
|
||||
Save and reuse authenticated browser state
|
||||
</Card>
|
||||
</CardGroup>
|
||||
301
docs/developers/optimization/cost-control.mdx
Normal file
301
docs/developers/optimization/cost-control.mdx
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
---
|
||||
title: Cost Control
|
||||
subtitle: Limit steps and optimize prompts to manage costs
|
||||
description: Control Skyvern costs using max_steps limits, code generation for repeatable tasks, cheaper engine tiers, and prompt optimization techniques to reduce step count and credit usage.
|
||||
slug: developers/optimization/cost-control
|
||||
keywords:
|
||||
- max_steps
|
||||
- credits
|
||||
- code generation
|
||||
- engine tier
|
||||
- prompt optimization
|
||||
- step count
|
||||
- budget
|
||||
---
|
||||
|
||||
Skyvern Cloud uses a **credit-based billing model**. Each plan includes a monthly credit allowance that determines how many actions you can run.
|
||||
|
||||
| Plan | Price | Actions Included |
|
||||
|------|-------|------------------|
|
||||
| Free | $0 | ~170 |
|
||||
| Hobby | $29 | ~1,200 |
|
||||
| Pro | $149 | ~6,200 |
|
||||
| Enterprise | Custom | Unlimited |
|
||||
|
||||
Use `max_steps` to limit steps per run and prevent runaway costs.
|
||||
|
||||
---
|
||||
|
||||
## Limit steps with max_steps
|
||||
|
||||
Set `max_steps` to cap the worst-case cost of any run. If a task gets stuck in a loop or hits repeated failures, `max_steps` stops it before it burns through your budget. The run terminates with `status: "timed_out"` when it hits the limit.
|
||||
|
||||
### For tasks
|
||||
|
||||
Pass `max_steps` in the request body.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from skyvern import Skyvern
|
||||
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
result = await client.run_task(
|
||||
prompt="Extract the top 3 products",
|
||||
url="https://example.com/products",
|
||||
max_steps=15,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
print(f"Steps taken: {result.step_count}")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
const result = await client.runTask({
|
||||
body: {
|
||||
prompt: "Extract the top 3 products",
|
||||
url: "https://example.com/products",
|
||||
max_steps: 15,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
console.log(`Steps taken: ${result.step_count}`);
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Extract the top 3 products",
|
||||
"url": "https://example.com/products",
|
||||
"max_steps": 15
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### For workflows
|
||||
|
||||
Pass `max_steps_override` as a parameter (Python) or `x-max-steps-override` header (TypeScript/cURL). This limits total steps across all blocks.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from skyvern import Skyvern
|
||||
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
result = await client.run_workflow(
|
||||
workflow_id="wf_data_extraction",
|
||||
max_steps_override=30,
|
||||
wait_for_completion=True,
|
||||
)
|
||||
print(f"Steps taken: {result.step_count}")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
const result = await client.runWorkflow({
|
||||
"x-max-steps-override": 30,
|
||||
body: { workflow_id: "wf_data_extraction" },
|
||||
waitForCompletion: true,
|
||||
});
|
||||
console.log(`Steps taken: ${result.step_count}`);
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "x-max-steps-override: 30" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"workflow_id": "wf_data_extraction"}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
If runs consistently time out, increase `max_steps` or simplify the task. If `step_count` is much lower than `max_steps`, reduce the limit.
|
||||
</Info>
|
||||
|
||||
---
|
||||
|
||||
## Use code generation for repeatable tasks
|
||||
|
||||
On Skyvern Cloud, the default **Skyvern 2.0 with Code** engine records the actions the AI takes and generates reusable code from them. Subsequent runs execute the generated code instead of the AI agent, skipping LLM inference and screenshot analysis entirely. This makes them faster, deterministic, and significantly cheaper.
|
||||
|
||||
1. Run your task with the default engine. Skyvern generates code from the recorded actions.
|
||||
2. Subsequent runs execute the cached code directly, no AI reasoning required.
|
||||
3. If the code doesn't handle an edge case, adjust your prompt and re-run to regenerate. Skyvern also falls back to the AI agent automatically if the cached code fails.
|
||||
|
||||
You can control this with the `run_with` parameter. Set it to `"code"` to use cached code, or `"agent"` to force AI reasoning.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from skyvern import Skyvern
|
||||
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
# First run: AI agent executes and generates code
|
||||
result = await client.run_task(
|
||||
prompt="Extract the top 3 products",
|
||||
url="https://example.com/products",
|
||||
wait_for_completion=True,
|
||||
)
|
||||
|
||||
# Subsequent runs: execute cached code instead of AI
|
||||
result = await client.run_task(
|
||||
prompt="Extract the top 3 products",
|
||||
url="https://example.com/products",
|
||||
run_with="code",
|
||||
wait_for_completion=True,
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
// First run: AI agent executes and generates code
|
||||
const result = await client.runTask({
|
||||
body: {
|
||||
prompt: "Extract the top 3 products",
|
||||
url: "https://example.com/products",
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
|
||||
// Subsequent runs: execute cached code instead of AI
|
||||
const codeResult = await client.runTask({
|
||||
body: {
|
||||
prompt: "Extract the top 3 products",
|
||||
url: "https://example.com/products",
|
||||
run_with: "code",
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# First run: AI agent executes and generates code
|
||||
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Extract the top 3 products",
|
||||
"url": "https://example.com/products"
|
||||
}'
|
||||
|
||||
# Subsequent runs: execute cached code instead of AI
|
||||
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Extract the top 3 products",
|
||||
"url": "https://example.com/products",
|
||||
"run_with": "code"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
Set `publish_workflow: true` to save the generated workflow so you can re-trigger it later or [schedule it on a recurring cron](/cloud/building-workflows/scheduling).
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Choose a cheaper engine
|
||||
|
||||
Not every task needs the most powerful engine. Use a lighter engine for simple, single-objective work.
|
||||
|
||||
| Engine | Cost | Best for |
|
||||
|--------|------|----------|
|
||||
| `skyvern-2.0` | Highest | Complex, multi-step tasks that require flexibility |
|
||||
| `skyvern-1.0` | Lower | Single-objective tasks like form fills or single-page extraction |
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from skyvern import Skyvern
|
||||
|
||||
client = Skyvern(api_key="YOUR_API_KEY")
|
||||
|
||||
result = await client.run_task(
|
||||
prompt="Fill out the contact form",
|
||||
url="https://example.com/contact",
|
||||
engine="skyvern-1.0",
|
||||
wait_for_completion=True,
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Skyvern } from "@skyvern/client";
|
||||
|
||||
const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
|
||||
|
||||
const result = await client.runTask({
|
||||
body: {
|
||||
prompt: "Fill out the contact form",
|
||||
url: "https://example.com/contact",
|
||||
engine: "skyvern-1.0",
|
||||
},
|
||||
waitForCompletion: true,
|
||||
});
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
|
||||
-H "x-api-key: $SKYVERN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Fill out the contact form",
|
||||
"url": "https://example.com/contact",
|
||||
"engine": "skyvern-1.0"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For self-hosted deployments, you can also swap the underlying LLM to a cheaper model (e.g., Gemini 2.5 Flash instead of a pro-tier model) via the `LLM_KEY` environment variable. See [LLM configuration](/developers/self-hosted/llm-configuration) for details.
|
||||
|
||||
---
|
||||
|
||||
## Write better prompts
|
||||
|
||||
Small prompt changes can cut step count significantly.
|
||||
|
||||
- **Be specific about the goal and completion criteria.** "Extract the price, title, and rating of the first 3 products" finishes faster than "look at the products page."
|
||||
- **Avoid open-ended exploration.** Prompts like "find interesting data" or "look around" cause the agent to wander.
|
||||
- **Use `data_extraction_schema`** to constrain what fields the AI extracts. This prevents it from spending steps parsing irrelevant content.
|
||||
- **Provide `url`** to start on the correct page instead of making the agent search for it.
|
||||
- **Use [browser profiles](/developers/optimization/browser-profiles)** to skip login steps on repeated runs.
|
||||
|
||||
---
|
||||
|
||||
## Monitor usage
|
||||
|
||||
- Check `step_count` in run responses to understand actual consumption per task.
|
||||
- Use `get_run_timeline()` to inspect individual steps and identify waste (loops, unnecessary navigation, retries).
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Browser Sessions"
|
||||
icon="browser"
|
||||
href="/developers/optimization/browser-sessions"
|
||||
>
|
||||
Maintain live browser state between calls
|
||||
</Card>
|
||||
<Card
|
||||
title="Browser Profiles"
|
||||
icon="floppy-disk"
|
||||
href="/developers/optimization/browser-profiles"
|
||||
>
|
||||
Save authenticated state for reuse across days
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Loading…
Add table
Add a link
Reference in a new issue