docs: add optimization section (sessions, profiles, cost control) (#4712)
This commit is contained in:
@@ -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": [
|
||||
|
||||
912
docs/optimization/browser-profiles.mdx
Normal file
912
docs/optimization/browser-profiles.mdx
Normal 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>
|
||||
709
docs/optimization/browser-sessions.mdx
Normal file
709
docs/optimization/browser-sessions.mdx
Normal 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>
|
||||
292
docs/optimization/cost-control.mdx
Normal file
292
docs/optimization/cost-control.mdx
Normal 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>
|
||||
Reference in New Issue
Block a user