docs: add optimization section (sessions, profiles, cost control) (#4712)

This commit is contained in:
Naman
2026-02-12 17:51:52 +05:30
committed by GitHub
parent 7bcc703076
commit 03bf353e0e
4 changed files with 1921 additions and 0 deletions

View File

@@ -42,6 +42,14 @@
"multi-step-automations/workflow-parameters"
]
},
{
"group": "Optimization",
"pages": [
"optimization/browser-sessions",
"optimization/browser-profiles",
"optimization/cost-control"
]
},
{
"group": "Going to Production",
"pages": [

View File

@@ -0,0 +1,912 @@
---
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>

View File

@@ -0,0 +1,709 @@
---
title: Browser Sessions
subtitle: Persist live browser state across multiple tasks and workflows
slug: optimization/browser-sessions
---
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](/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="wf_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: "wf_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\": \"wf_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="wf_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: "wf_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](/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](/optimization/browser-profiles) from a completed session to save its authenticated state for future reuse.
</Tip>
---
## Next steps
<CardGroup cols={2}>
<Card
title="Browser Profiles"
icon="floppy-disk"
href="/optimization/browser-profiles"
>
Save session state for reuse across days
</Card>
<Card
title="Cost Control"
icon="dollar-sign"
href="/optimization/cost-control"
>
Optimize costs with max_steps and efficient prompts
</Card>
</CardGroup>

View File

@@ -0,0 +1,292 @@
---
title: Cost Control
subtitle: Limit steps and optimize prompts to manage costs
slug: optimization/cost-control
---
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 cron.
</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](/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](/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="/optimization/browser-sessions"
>
Maintain live browser state between calls
</Card>
<Card
title="Browser Profiles"
icon="floppy-disk"
href="/optimization/browser-profiles"
>
Save authenticated state for reuse across days
</Card>
</CardGroup>