From 03bf353e0e6d5c0aba69c15725aef47618bc4f89 Mon Sep 17 00:00:00 2001 From: Naman Date: Thu, 12 Feb 2026 17:51:52 +0530 Subject: [PATCH] docs: add optimization section (sessions, profiles, cost control) (#4712) --- docs/docs.json | 8 + docs/optimization/browser-profiles.mdx | 912 +++++++++++++++++++++++++ docs/optimization/browser-sessions.mdx | 709 +++++++++++++++++++ docs/optimization/cost-control.mdx | 292 ++++++++ 4 files changed, 1921 insertions(+) create mode 100644 docs/optimization/browser-profiles.mdx create mode 100644 docs/optimization/browser-sessions.mdx create mode 100644 docs/optimization/cost-control.mdx diff --git a/docs/docs.json b/docs/docs.json index f9cf5278..c41e6f7f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -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": [ diff --git a/docs/optimization/browser-profiles.mdx b/docs/optimization/browser-profiles.mdx new file mode 100644 index 00000000..2e49366c --- /dev/null +++ b/docs/optimization/browser-profiles.mdx @@ -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. + + +`persist_browser_session` must be set when **creating the workflow**, not when running it. It is a workflow definition property, not a runtime parameter. + + +### From a workflow run + + +```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 +``` + + +**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. + + +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. + + + +```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 +``` + + +**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. + + +```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" + }' +``` + + +**Example response:** + +```json +{ + "run_id": "wr_494469342201718946", + "status": "created", + "run_request": { + "workflow_id": "wf_daily_metrics", + "browser_profile_id": "bp_490705123456789012", + "proxy_location": "RESIDENTIAL" + } +} +``` + + +`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. + + +--- + +## 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. + + + + + The workflow must have `persist_browser_session=true` so Skyvern archives the browser state after the run. + + + ```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" + } + ] + } + } + }' + ``` + + + ```json Response + { + "workflow_permanent_id": "wpid_494674198088536840", + "persist_browser_session": true, + "title": "Visit Hacker News" + } + ``` + + + + + + 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. + + + ```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 + ``` + + + ```json Response + { + "run_id": "wr_494674202383504144", + "status": "completed" + } + ``` + + + + + + Archiving happens asynchronously after the run completes, so add retry logic. In practice the archive is usually ready within a few seconds. + + + ```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 + ``` + + + ```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 + } + ``` + + + + + + List all profiles or fetch one by ID to confirm it was saved. + + + ```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 . + ``` + + + ```json List response + [ + { + "browser_profile_id": "bp_494674399951999772", + "name": "hn-browsing-state", + "created_at": "2026-02-12T01:09:18.048208" + } + ] + ``` + + + + + + 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. + + + ```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\" + }" + ``` + + + ```json Response + { + "run_id": "wr_494674434311738148", + "status": "created" + } + ``` + + + + + + Clean up profiles you no longer need. + + + ```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" + ``` + + + + + + +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. + + +--- + +## Best practices + +### Use descriptive names + +Include the account, site, and purpose in the profile name so it is easy to identify later. + + +```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, +}); +``` + + +### 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. + + +```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" +``` + + +### 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. + + +```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, + }); +} +``` + + +--- + +## Next steps + + + + Maintain live browser state for real-time interactions + + + Optimize costs with max_steps and efficient prompts + + diff --git a/docs/optimization/browser-sessions.mdx b/docs/optimization/browser-sessions.mdx new file mode 100644 index 00000000..f3c9293c --- /dev/null +++ b/docs/optimization/browser-sessions.mdx @@ -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. + + +```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"] + }' +``` + + +**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. | + + +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. + + +--- + +## 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. + + +```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" +``` + + +--- + +## 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. + + +```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" +``` + + + +You cannot use both `browser_session_id` and `browser_profile_id` in the same request. Choose one or the other. + + +--- + +## Close a session + +Close a session to release resources and stop billing. The browser shuts down immediately. + + +```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" +``` + + + +**Always close sessions when done.** Active sessions continue billing even when idle. Use try/finally blocks to ensure cleanup. + + + +```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); +} +``` + + +--- + +## Example: Human-in-the-loop + +A shopping bot that pauses for human approval before completing a purchase. + + +```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((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(); +``` + + +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. + + +```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}' +``` + + +### 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. + + +```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 }); +``` + + +### Choose the right browser type + +Chrome has the widest compatibility. Use Edge only when a site requires or detects it specifically. + + +```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"}' +``` + + +### 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. + + +```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"]}' +``` + + +--- + +## 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 | + + +You can create a [Browser Profile](/optimization/browser-profiles) from a completed session to save its authenticated state for future reuse. + + +--- + +## Next steps + + + + Save session state for reuse across days + + + Optimize costs with max_steps and efficient prompts + + diff --git a/docs/optimization/cost-control.mdx b/docs/optimization/cost-control.mdx new file mode 100644 index 00000000..f0d8ef3a --- /dev/null +++ b/docs/optimization/cost-control.mdx @@ -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. + + +```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 + }' +``` + + +### 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. + + +```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"}' +``` + + + +If runs consistently time out, increase `max_steps` or simplify the task. If `step_count` is much lower than `max_steps`, reduce the limit. + + +--- + +## 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. + + +```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" + }' +``` + + + +Set `publish_workflow: true` to save the generated workflow so you can re-trigger it later or schedule it on a cron. + + +--- + +## 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 | + + +```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" + }' +``` + + +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 + + + + Maintain live browser state between calls + + + Save authenticated state for reuse across days + +