diff --git a/docs/cloud/account-settings/api-keys.mdx b/docs/cloud/account-settings/api-keys.mdx
new file mode 100644
index 00000000..5f889238
--- /dev/null
+++ b/docs/cloud/account-settings/api-keys.mdx
@@ -0,0 +1,57 @@
+---
+title: API Key
+subtitle: Authenticate programmatic access to Skyvern
+slug: cloud/account-settings/api-keys
+---
+
+Every API call to Skyvern requires an API key. The key identifies your organization and determines billing.
+
+## Finding your API key
+
+Open **Settings** in the sidebar. Your API key is shown in a masked field. Click the **eye icon** to reveal the full value, or the **copy icon** to copy it to your clipboard.
+
+
+
+
+Treat your API key like a password. Anyone with this key can create runs, read results, and manage workflows on behalf of your organization.
+
+
+## Using your key
+
+Every Skyvern API request requires the `x-api-key` header:
+
+```bash
+curl -X POST "https://api.skyvern.com/v1/runs" \
+ -H "x-api-key: YOUR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "url": "https://example.com", "goal": "Extract the pricing table" }'
+```
+
+With the Python SDK:
+
+```python
+from skyvern import Skyvern
+
+client = Skyvern(api_key="YOUR_API_KEY")
+```
+
+
+Store the key in an environment variable (`SKYVERN_API_KEY`) rather than hardcoding it. In production, use your platform's secrets management (AWS Secrets Manager, Vault, etc.).
+
+
+
+
+ Full endpoint documentation with interactive playground
+
+
+ Understand how runs are metered and billed
+
+
diff --git a/docs/cloud/account-settings/billing-usage.mdx b/docs/cloud/account-settings/billing-usage.mdx
new file mode 100644
index 00000000..164cb5c8
--- /dev/null
+++ b/docs/cloud/account-settings/billing-usage.mdx
@@ -0,0 +1,75 @@
+---
+title: Billing & Usage
+subtitle: Understand what you're paying for and manage your plan
+slug: cloud/account-settings/billing-usage
+---
+
+Skyvern bills per **credit** — each browser action the AI takes during a run consumes credits. You pay for real work, not for workflows created or credentials stored.
+
+## Plans
+
+Open **Billing** in the sidebar to see available plans and your current balance.
+
+
+
+| Plan | Price | Credits | Includes |
+|------|-------|---------|----------|
+| **Hobby** | $29/month | 30,000 credits/month | Email support, priority queue |
+| **Pro** | $149/month | 150,000 credits/month | Priority support, higher rate limits |
+
+Click **Switch to Hobby** or **Switch to Pro** to change plans. Your remaining dollar balance is applied as a credit toward your first subscription payment.
+
+
+Need custom volumes or a dedicated plan? Use the **Book a call** link at the bottom of the Billing page, or visit [View full pricing details](https://skyvern.com) for a complete comparison.
+
+
+## How credits are consumed
+
+A credit maps to one discrete browser action: clicking a button, typing into a field, selecting a dropdown, or extracting data from a page. Navigation counts too.
+
+| Example | Approximate credits |
+|---------|---------------------|
+| Login flow (email, password, submit) | ~3 |
+| Form fill (10 fields + submit) | ~11 |
+| Multi-page extraction (5 pages) | ~15–25 |
+
+Credits consumed by a run are visible on the [Run Details](/cloud/viewing-results/run-details) page — the **Actions** counter at the top of the Overview timeline shows exactly how many actions the AI took.
+
+
+Reduce credit usage by being specific in your prompts. "Find information about pricing" causes the AI to explore multiple pages. "Extract the monthly price from the pricing table on /pricing" gets there directly.
+
+
+## Your current balance
+
+The **Your Current Balance** section shows your remaining dollar balance. Credits are deducted as runs execute in real time. If your balance reaches zero, new runs won't start until you add credits or switch to a subscription plan.
+
+## FAQ
+
+
+Each browser action the AI takes — clicking, typing, navigating, extracting — costs one credit. Credits are deducted in real time as runs execute. Your plan includes a monthly credit allowance that resets each billing cycle. Unused credits don't roll over.
+
+
+
+Yes. If you exhaust your monthly allowance, you can purchase additional credits from the Billing page. You can also upgrade to a higher plan for a larger monthly allowance.
+
+
+
+Yes. Downgrade or cancel your subscription from the Billing page. The change takes effect at the end of your current billing cycle, and you keep access to your credits until then.
+
+
+
+
+ Manage API keys for programmatic access
+
+
+ Manage your team and organization details
+
+
diff --git a/docs/cloud/account-settings/organization-settings.mdx b/docs/cloud/account-settings/organization-settings.mdx
new file mode 100644
index 00000000..0006027b
--- /dev/null
+++ b/docs/cloud/account-settings/organization-settings.mdx
@@ -0,0 +1,50 @@
+---
+title: Organization Settings
+subtitle: Manage your organization name and team access
+slug: cloud/account-settings/organization-settings
+---
+
+Everything in Skyvern — workflows, credentials, runs, API keys — belongs to an **organization**. One is created automatically when you sign up.
+
+## Organization details
+
+Open **Settings** in the sidebar. The Settings page shows your organization name and current plan. The organization name appears throughout the UI and in API responses.
+
+To rename your organization, update the **Organization Name** field and click **Save**.
+
+## Team members
+
+Plans that support multiple users (Pro and Enterprise) show a **Members** section below the organization details.
+
+### Inviting members
+
+Click **Invite Member**, enter their email address, and send the invitation. They'll receive an email with a link to join your organization. Once they accept, they appear in the members list with full access to workflows, runs, credentials, and API keys.
+
+### Removing members
+
+Click the **remove icon** next to a member's name to revoke their access. They lose access to all organization resources immediately.
+
+
+When removing a team member, also review your [API keys](/cloud/account-settings/api-keys) — they may have copied keys that remain active.
+
+
+
+Role-based access control (restricting what individual members can do) is available on **Enterprise** plans. On other plans, all members have full access to the organization's resources.
+
+
+
+
+ Manage your personal account settings
+
+
+ View and manage API keys for your organization
+
+
diff --git a/docs/cloud/account-settings/profile-settings.mdx b/docs/cloud/account-settings/profile-settings.mdx
new file mode 100644
index 00000000..367aeba0
--- /dev/null
+++ b/docs/cloud/account-settings/profile-settings.mdx
@@ -0,0 +1,44 @@
+---
+title: Profile Settings
+subtitle: Manage your personal account and security
+slug: cloud/account-settings/profile-settings
+---
+
+Click your avatar at the bottom of the sidebar to open the **Account** page. It has two tabs: **Profile** and **Security**.
+
+## Profile
+
+The **Profile** tab shows your display name and email address. Update your display name and click **Save** to apply.
+
+## Security
+
+The **Security** tab is where you manage your password and review active sessions.
+
+
+
+Under **Set password**, enter your new password and confirm it. A green checkmark appears when the password meets requirements and both fields match. Check **Sign out of all other devices** if you want to invalidate existing sessions — recommended if you suspect unauthorized access.
+
+Click **Save** to apply.
+
+**Active devices** lists every device currently signed into your account, showing the browser, IP address, and last access time. Use this to verify that all sessions are yours.
+
+
+If you signed up with Google or another SSO provider, password management is handled by the external provider and this section may not appear.
+
+
+
+
+ Manage your team and organization details
+
+
+ View and manage API keys
+
+
diff --git a/docs/cloud/add-parameters.mdx b/docs/cloud/building-workflows/add-parameters.mdx
similarity index 93%
rename from docs/cloud/add-parameters.mdx
rename to docs/cloud/building-workflows/add-parameters.mdx
index 1370ee12..7cf99457 100644
--- a/docs/cloud/add-parameters.mdx
+++ b/docs/cloud/building-workflows/add-parameters.mdx
@@ -10,7 +10,7 @@ Parameters let you create reusable workflows that accept different input values
## Opening the parameters panel
-In the [workflow editor](/cloud/build-a-workflow), click the **Parameters** button in the header bar. A panel appears below the header showing all defined parameters and controls to add new ones.
+In the [workflow editor](/cloud/building-workflows/build-a-workflow), click the **Parameters** button in the header bar. A panel appears below the header showing all defined parameters and controls to add new ones.
@@ -139,14 +139,14 @@ Process item {{ current_index }}: {{ current_value.name }}
Configuration fields for every block type
Execute workflows and fill in parameter values
diff --git a/docs/cloud/build-a-workflow.mdx b/docs/cloud/building-workflows/build-a-workflow.mdx
similarity index 89%
rename from docs/cloud/build-a-workflow.mdx
rename to docs/cloud/building-workflows/build-a-workflow.mdx
index 636edb7d..5437c4dc 100644
--- a/docs/cloud/build-a-workflow.mdx
+++ b/docs/cloud/building-workflows/build-a-workflow.mdx
@@ -21,18 +21,19 @@ A workflow is a sequence of **blocks** that run one after another. Each block do
Every block produces an output. Downstream blocks reference it using the pattern `{{ block_label_output }}`.
-For example, an Extraction block labeled `scrape_listings` produces output that later blocks access as `{{ scrape_listings_output }}`. This is how you chain steps together. One block's output becomes the next block's input.
+For example, an Extraction block labeled `scrape_listings` produces output that later blocks access as `{{ scrape_listings_output }}`. The label is lowercased with spaces replaced by underscores, then `_output` is appended. This is how you chain steps together. One block's output becomes the next block's input.
### Block categories
| Category | What it does | Examples |
|----------|-------------|----------|
-| **Browser Automation** | Interact with web pages using AI | Browser Task, Extraction, Browser Action, Login, File Download |
-| **Data Processing** | Transform data without a browser | Text Prompt, File Parser, Code |
-| **Control Flow** | Branch, loop, validate, or pause | Loop, Conditional, AI Validation, Wait |
-| **Communication** | Send data to external services | Send Email, HTTP Request |
+| **Browser Automation** | Interact with web pages using AI | Browser Task, Browser Action, Extraction, Login, Go to URL, Print Page |
+| **Data and Extraction** | Transform data without a browser | Text Prompt, File Parser |
+| **Control Flow** | Branch, loop, validate, or pause | Loop, Conditional, AI Validation, Code, Wait |
+| **Files** | Download and upload files | File Download, Cloud Storage Upload |
+| **Communication** | Send data to external services or humans | Send Email, HTTP Request, Human Interaction |
-See [Block Reference](/cloud/configure-blocks) for every block type and its configuration fields.
+See [Block Reference](/cloud/building-workflows/configure-blocks) for every block type and its configuration fields.
If your block needs a browser, pick a browser automation block. If it just needs to process data, use Text Prompt or Code. If you need to repeat or branch, use control flow.
@@ -44,7 +45,7 @@ If your block needs a browser, pick a browser automation block. If it just needs
Let's build a real workflow. You'll create an automation that finds the top 5 job listings from Hacker News's monthly "Who is Hiring" thread, summarizes each one, and emails you a digest.
-This uses 5 blocks across 3 categories: browser automation, data processing, and communication.
+This uses 5 blocks across 3 categories: browser automation, data and extraction, and communication.
@@ -192,7 +193,7 @@ This uses 5 blocks across 3 categories: browser automation, data processing, and
Click **Save**, then click **Run** in the top right. A parameters page opens where you can review and confirm the workflow's settings. Click **Run Workflow** to start execution.
- The [live execution view](/cloud/monitor-a-run) opens. Watch as Skyvern navigates Hacker News, extracts listings, summarizes each one, and sends your email.
+ The [live execution view](/cloud/getting-started/monitor-a-run) opens. Watch as Skyvern navigates Hacker News, extracts listings, summarizes each one, and sends your email.
@@ -235,7 +236,7 @@ When you create a blank workflow, hover over the **+** on the start edge to add
Click any block on the canvas to open its configuration panel on the right. Each block has a **Label** field (which determines its output variable name) and block-specific settings. Browser-based blocks share additional fields like URL, Engine, and Max Retries.
-See [Block Reference](/cloud/configure-blocks) for the full list of fields on each block type.
+See [Block Reference](/cloud/building-workflows/configure-blocks) for the full list of fields on each block type.
### Connecting and reordering blocks
@@ -266,8 +267,8 @@ The **Run** button also saves before starting execution.
| **Save** (disk icon) | Save the current workflow definition |
| **Template** (bookmark icon) | Toggle whether this workflow is saved as an organization template |
| **History** (clock icon) | View past runs of this workflow |
-| **Parameters** | Open the parameters panel (see [Workflow Parameters](/cloud/add-parameters)) |
-| **Run** (play icon) | Save and start a new execution (see [Running Workflows](/cloud/run-a-workflow)) |
+| **Parameters** | Open the parameters panel (see [Workflow Parameters](/cloud/building-workflows/add-parameters)) |
+| **Run** (play icon) | Save and start a new execution (see [Running Workflows](/cloud/building-workflows/run-a-workflow)) |
---
@@ -277,21 +278,21 @@ The **Run** button also saves before starting execution.
Configuration fields for every block type
Pass dynamic values into your workflows
Execute workflows and track results
diff --git a/docs/cloud/configure-blocks.mdx b/docs/cloud/building-workflows/configure-blocks.mdx
similarity index 97%
rename from docs/cloud/configure-blocks.mdx
rename to docs/cloud/building-workflows/configure-blocks.mdx
index 9a1a5ffe..1eee47bb 100644
--- a/docs/cloud/configure-blocks.mdx
+++ b/docs/cloud/building-workflows/configure-blocks.mdx
@@ -4,7 +4,7 @@ subtitle: Reference for every block available in the workflow editor
slug: cloud/configure-blocks
---
-Workflows are built from blocks. Each block performs one specific operation: navigating a page, extracting data, making an API call, or branching logic. This page covers every block type available in the [editor](/cloud/build-a-workflow), grouped by category.
+Workflows are built from blocks. Each block performs one specific operation: navigating a page, extracting data, making an API call, or branching logic. This page covers every block type available in the [editor](/cloud/building-workflows/build-a-workflow), grouped by category.
## Quick reference
@@ -136,7 +136,7 @@ Navigate the browser directly to a specific URL without AI interaction.
| Field | Type | Description |
|-------|------|-------------|
-| **URL** | text | **(Required)** The URL to navigate to. Supports [parameter references](/cloud/add-parameters#referencing-parameters-in-blocks). |
+| **URL** | text | **(Required)** The URL to navigate to. Supports [parameter references](/cloud/building-workflows/add-parameters#referencing-parameters-in-blocks). |
### Print Page
@@ -192,7 +192,7 @@ Repeat a sequence of blocks for each item in a list. Child blocks are placed ins
| **Loop Value** | text | The list to iterate over. Use a parameter reference (e.g., `{{ my_list }}`) or a natural language description (e.g., "Extract links of the top 5 posts") which triggers automatic extraction. |
| **Continue if Empty** | boolean | Mark the loop as complete if the list is empty |
-Inside loop blocks, use these [reserved variables](/cloud/add-parameters#reserved-variables):
+Inside loop blocks, use these [reserved variables](/cloud/building-workflows/add-parameters#reserved-variables):
- `{{ current_value }}`: the current item
- `{{ current_index }}`: the iteration number (0-based)
@@ -311,7 +311,7 @@ Send an email notification, optionally with file attachments from previous block
|-------|------|-------------|
| **Recipients** | text | Comma-separated email addresses |
| **Subject** | text | Email subject line |
-| **Body** | text | Email body. Supports [parameter references](/cloud/add-parameters#referencing-parameters-in-blocks). |
+| **Body** | text | Email body. Supports [parameter references](/cloud/building-workflows/add-parameters#referencing-parameters-in-blocks). |
| **File Attachments** | text | Path to files to attach |
### HTTP Request
diff --git a/docs/cloud/manage-workflows.mdx b/docs/cloud/building-workflows/manage-workflows.mdx
similarity index 88%
rename from docs/cloud/manage-workflows.mdx
rename to docs/cloud/building-workflows/manage-workflows.mdx
index 601029f9..add7b71e 100644
--- a/docs/cloud/manage-workflows.mdx
+++ b/docs/cloud/building-workflows/manage-workflows.mdx
@@ -4,7 +4,7 @@ subtitle: Create, organize, clone, import, and export reusable automations
slug: cloud/manage-workflows
---
-Once you've [built a workflow](/cloud/build-a-workflow), the **Workflows** page is where you organize, share, and manage them. Each workflow is a reusable sequence of blocks that you can run with different inputs each time.
+Once you've [built a workflow](/cloud/building-workflows/build-a-workflow), the **Workflows** page is where you organize, share, and manage them. Each workflow is a reusable sequence of blocks that you can run with different inputs each time.
Click **Workflows** in the left sidebar to open it.
@@ -92,14 +92,14 @@ Upload a PDF of your Standard Operating Procedure and Skyvern generates a workfl
- Build automations visually with the drag-and-drop canvas
+ Build automations visually with the workflow editor
Make workflows reusable with dynamic input values
diff --git a/docs/cloud/run-a-workflow.mdx b/docs/cloud/building-workflows/run-a-workflow.mdx
similarity index 85%
rename from docs/cloud/run-a-workflow.mdx
rename to docs/cloud/building-workflows/run-a-workflow.mdx
index 88bd1137..ec5581ea 100644
--- a/docs/cloud/run-a-workflow.mdx
+++ b/docs/cloud/building-workflows/run-a-workflow.mdx
@@ -18,7 +18,7 @@ Both take you to the parameters page where you fill in runtime values before the
## Filling in parameters
-The parameters page shows all [workflow parameters](/cloud/add-parameters) defined in the editor. Fill in a value for each one, or leave them at their defaults. Parameters without defaults must be filled in before running.
+The parameters page shows all [workflow parameters](/cloud/building-workflows/add-parameters) defined in the editor. Fill in a value for each one, or leave them at their defaults. Parameters without defaults must be filled in before running.
## Run settings
@@ -37,7 +37,7 @@ Below the parameters, you can configure settings for this specific run:
## Monitoring a run
-After clicking **Run**, you're taken to the live execution view, the same interface described in [Watching Live Execution](/cloud/monitor-a-run).
+After clicking **Run**, you're taken to the live execution view, the same interface described in [Watching Live Execution](/cloud/getting-started/monitor-a-run).
For workflows, the left panel also shows a **block timeline**: a list of all blocks in the workflow. Completed blocks show a checkmark. The currently executing block is highlighted.
@@ -68,7 +68,7 @@ After a run completes, open it from **Runs** in the sidebar. The run detail page
- **Recording**: Full video replay of the browser session
- **Parameters**: The values you submitted for this run
-See [Reviewing results](/cloud/monitor-a-run#reviewing-results) for details on the Actions, Recording, Parameters, and Diagnostics tabs.
+See [Reviewing results](/cloud/getting-started/monitor-a-run#reviewing-results) for details on the Actions, Recording, Parameters, and Diagnostics tabs.
---
@@ -93,14 +93,14 @@ Access past runs from two places:
Create and edit workflows in the visual editor
Configuration fields for every block type
diff --git a/docs/cloud/monitor-a-run.mdx b/docs/cloud/getting-started/monitor-a-run.mdx
similarity index 65%
rename from docs/cloud/monitor-a-run.mdx
rename to docs/cloud/getting-started/monitor-a-run.mdx
index 3714308b..d341412b 100644
--- a/docs/cloud/monitor-a-run.mdx
+++ b/docs/cloud/getting-started/monitor-a-run.mdx
@@ -4,7 +4,7 @@ subtitle: Monitor, interact with, and control running tasks
slug: cloud/monitor-a-run
---
-When you run a task from the [Discover page](/cloud/run-a-task), you're taken to the live execution screen where you can watch the browser in real time.
+When you run a task from the [Discover page](/cloud/getting-started/run-a-task), you're taken to the live execution screen where you can watch the browser in real time.
@@ -34,9 +34,9 @@ The live browser stream is active while the task is still in progress:
| `paused` | Waiting for human interaction |
| `completed` | Stream closed. View the recording instead. |
| `failed` | Stream closed. View the recording instead. |
-| `terminated` | Stream closed |
-| `timed_out` | Stream closed |
-| `canceled` | Stream closed |
+| `terminated` | Stream closed. View the recording instead. |
+| `timed_out` | Stream closed. View the recording instead. |
+| `canceled` | Stream closed. View the recording instead. |
Once a task reaches a final state, the live stream closes. Open the run from **Runs** in the sidebar to access the full recording, screenshots, and action history.
@@ -61,32 +61,20 @@ Taking control pauses the AI agent. Remember to release control so the agent can
You can cancel a task at any time while it's running or queued. Click the **Cancel** button in the task header. A confirmation dialog appears before the task is stopped. The task transitions to `canceled` and any configured webhook fires with the canceled status.
+
+Credits for actions already taken are still consumed. Canceling stops future actions but does not refund past ones.
+
+
---
## Reviewing results
-Once a task finishes, open it from **Runs** to see the full results.
+Once a task finishes, open it from **Runs** to see the full results. The run detail page has five tabs:
-### Actions tab
+- **Overview**: The AI's reasoning timeline alongside browser screenshots. Each Thought, Block, and Action card shows what the agent saw and why it acted.
+- **Output**: The complete JSON output and any downloaded files.
+- **Parameters**: The configuration you submitted: URL, prompt, engine, proxy location, webhook URL, data schema, and other settings.
+- **Recording**: Full video replay of the browser session. Every task is recorded automatically.
+- **Code**: Auto-generated Python code to reproduce this task via the API or SDK (when code generation is enabled).
-Step-by-step breakdown of every action the AI took. Each entry shows:
-
-- **Action type**: Click, Type, Scroll, Select, etc.
-- **Success or failure** indicator
-- **AI reasoning**: Why the agent chose this action
-- **Input details**: For type actions, the text that was entered
-
-### Recording tab
-
-Full video replay of the browser session. Every task is recorded automatically.
-
-### Parameters tab
-
-The configuration you submitted: URL, prompt, engine, proxy location, webhook URL, data schema, and other settings.
-
-### Diagnostics tab
-
-Debug information for troubleshooting:
-- **LLM prompts**: The exact prompts sent to the AI model
-- **Element trees**: The DOM structure the AI analyzed
-- **Annotated screenshots**: Screenshots with element labels the AI used for decisions
+For a full walkthrough of each tab, see [Run Details](/cloud/viewing-results/run-details).
diff --git a/docs/cloud/overview.mdx b/docs/cloud/getting-started/overview.mdx
similarity index 87%
rename from docs/cloud/overview.mdx
rename to docs/cloud/getting-started/overview.mdx
index a043c47a..a28fa5ed 100644
--- a/docs/cloud/overview.mdx
+++ b/docs/cloud/getting-started/overview.mdx
@@ -31,9 +31,6 @@ Where you create and monitor automations.
### Agents
-{/* TODO: Replace with screenshot of Agents section */}
-
-
Ready-made automation templates. Each agent is preconfigured with a prompt, target URL, and settings. Pick one to see it work or use it as a starting point for your own task.
### General
@@ -53,7 +50,7 @@ Every automation in Skyvern Cloud follows the same pattern:
Type what you want into the prompt bar. Include the target URL and your instructions in one go. Something like "Get the top post from https://news.ycombinator.com" or "Fill out the contact form at https://example.com/contact with my details."
- A cloud browser opens and you see it navigate in real time. Pages load, elements highlight, actions fire. An agent log streams the AI's reasoning, showing every Thought and Decision, so you can follow along. If the AI gets stuck, hit **Take Control** to jump in and help.
+ A cloud browser opens and you see it navigate in real time. Pages load, elements highlight, actions fire. An agent log streams the AI's reasoning — what it sees on the page, what it plans to do, and why — so you can follow along. If the AI gets stuck, hit **Take Control** to jump in and help.
Extracted data appears as structured JSON on the run detail page. Every run also includes an output view, full recording, the parameters you submitted, and auto-generated code to reproduce the task via API.
@@ -70,7 +67,7 @@ That's it. The next guide walks you through this flow with a real example.
Follow along with a real example to see Skyvern Cloud in action
@@ -79,6 +76,6 @@ That's it. The next guide walks you through this flow with a real example.
icon="book"
href="/getting-started/core-concepts"
>
- Understand tasks, workflows, blocks, and other building blocks
+ Understand tasks, workflows, and other foundational concepts
diff --git a/docs/cloud/run-a-task.mdx b/docs/cloud/getting-started/run-a-task.mdx
similarity index 77%
rename from docs/cloud/run-a-task.mdx
rename to docs/cloud/getting-started/run-a-task.mdx
index a5ab6fe3..16c8eecb 100644
--- a/docs/cloud/run-a-task.mdx
+++ b/docs/cloud/getting-started/run-a-task.mdx
@@ -135,10 +135,51 @@ Click any template to launch it with pre-filled configuration, or use it as a st
---
+## Tips for better results
+
+**Write specific prompts.** Include the exact goal, target fields, and what "done" looks like.
+
+| Instead of | Write |
+|-----------|-------|
+| "Get some data from this site" | "Extract the product name, price, and availability from the first 5 results on amazon.com/s?k=wireless+mouse" |
+| "Fill out the form" | "Fill the contact form at example.com/contact with name 'Jane Doe', email 'jane@example.com', and message 'Demo request'" |
+
+**Control cost with Max Steps.** Set **Max Steps Override** to a reasonable limit (e.g., 10–20 for simple tasks) during development. Each step consumes one credit. Remove the cap once you've confirmed the task works.
+
+**Debug failures in order.** If a task fails or produces wrong results:
+
+1. Check the **Failure Reason** at the top of the run detail page
+2. Read the **Thought cards** in the Overview timeline to find where the AI went off track
+3. Watch the **Recording** to see what actually happened on screen
+4. Review **Parameters** to confirm the inputs were correct
+
+---
+
## What happens next
1. Your prompt is sent to Skyvern
2. A cloud browser opens and navigates to the target URL (or finds one from your prompt)
3. The AI analyzes the page, plans actions, and executes them step by step
-4. You're taken to the [live execution view](/cloud/monitor-a-run) where you can watch it happen in real time
+4. You're taken to the [live execution view](/cloud/getting-started/monitor-a-run) where you can watch it happen in real time
5. When complete, results appear on the run detail page under **Runs**
+
+---
+
+## Next steps
+
+
+
+ Monitor runs, take control of the browser, and review results
+
+
+ Turn a successful task into a reusable multi-step workflow
+
+
diff --git a/docs/cloud/run-your-first-task.mdx b/docs/cloud/getting-started/run-your-first-task.mdx
similarity index 88%
rename from docs/cloud/run-your-first-task.mdx
rename to docs/cloud/getting-started/run-your-first-task.mdx
index c0922357..c39873f3 100644
--- a/docs/cloud/run-your-first-task.mdx
+++ b/docs/cloud/getting-started/run-your-first-task.mdx
@@ -34,9 +34,9 @@ Next to your prompt, you'll see an engine selector. Click it to switch engines:
|--------|---------------|
| **Skyvern 1.0** | Tasks with a simple, single goal: filling a form, searching for information on Google, reading content from a page |
| **Skyvern 2.0** | Complex, multi-step tasks. Scores state-of-the-art 85.85% on the WebVoyager benchmark |
-| **Skyvern 2.0 with code** | The default engine. Same capabilities as Skyvern 2.0, plus auto-generates reusable code and a workflow from the run |
+| **Skyvern 2.0 with Code** | The default engine. Same capabilities as Skyvern 2.0, plus auto-generates reusable code and a workflow from the run |
-For this example, keep the default **Skyvern 2.0 with code** selected.
+For this example, keep the default **Skyvern 2.0 with Code** selected.
Click the **send button** (arrow icon to the right of the input). Skyvern generates a workflow from your prompt and opens it in the workflow editor. Click **Run** in the top right, confirm the parameters, then click **Run workflow** to start execution.
@@ -48,7 +48,7 @@ Click the **gear icon** next to send to configure additional options before runn
| **Webhook Callback URL** | Endpoint to receive the extracted data when the run completes |
| **Proxy Location** | Route Skyvern through one of the available proxies |
| **Browser Session ID** | Reuse a persistent browser session to keep login state |
-| **Browser Address** | Connect to a specific browser server for the task run |
+| **CDP Address** | Connect to your own browser via Chrome DevTools Protocol |
| **2FA Identifier** | Identifier for a 2FA code to handle two-factor auth automatically |
| **Extra HTTP Headers** | Custom HTTP request headers (dict format) |
| **Generate Script** | Auto-generate reusable scripts from a successful run |
@@ -88,17 +88,19 @@ The **Extracted Information** block shows your results as structured JSON:
]
```
+Your result will differ — the #1 post changes constantly. The structure is what matters.
+
The agent log on the right confirms what happened. You'll see a final Thought summarizing the result.
### Tabs
Below the extracted data, five tabs give you different views of the run:
-- **Overview**: The default view. Shows extracted data and the agent log with every Thought and Decision.
-- **Output**: The raw JSON output from the run.
+- **Overview**: The AI's reasoning timeline alongside browser screenshots. Each Thought, Block, and Action card shows what the agent saw and why it acted.
+- **Output**: The complete JSON output and any downloaded files.
- **Parameters**: The exact configuration that was submitted (URL, prompt, engine, schema). Useful for reproducing or tweaking the run.
- **Recording**: Full video replay of the browser session, start to finish.
-- **Code**: Auto-generated code snippets to reproduce this task via the API or SDK.
+- **Code**: Auto-generated Python code to reproduce this task via the API or SDK.
## Try something bigger
@@ -106,7 +108,7 @@ Now that you've seen the basic flow, here are a few ideas to try next:
- **Fill a form**: Point Skyvern at a contact form and tell it what to enter in each field
- **Compare prices**: Extract product names and prices from an e-commerce page using a data schema
-- **Navigate a flow**: Use the Advanced engine to walk through a multi-page checkout or signup process
+- **Navigate a flow**: Use Skyvern 2.0 to walk through a multi-page checkout or signup process
- **Use an Agent template**: Check the **Agents** section in the sidebar for pre-built automations you can run instantly
---
diff --git a/docs/cloud/managing-credentials/credentials-overview.mdx b/docs/cloud/managing-credentials/credentials-overview.mdx
new file mode 100644
index 00000000..bcaca34f
--- /dev/null
+++ b/docs/cloud/managing-credentials/credentials-overview.mdx
@@ -0,0 +1,96 @@
+---
+title: Credentials Overview
+subtitle: Securely store login details, payment info, and secrets for your automations
+slug: cloud/managing-credentials/credentials-overview
+---
+
+The **Credentials** page stores sensitive values — passwords, payment cards, and secrets — so your workflows can use them without you pasting passwords into prompts.
+
+
+Credentials **never reach the LLM**. The AI agent decides *where* to type, but the actual values are injected directly into the browser by the automation layer. Your credentials aren't exposed in prompts, logs, or model provider APIs.
+
+
+
+
+## What you can store
+
+**[Password credentials](/cloud/managing-credentials/password-credentials)** — username, password, and optional 2FA configuration. Used with Login blocks to automate full sign-in flows, including two-factor authentication.
+
+**[Credit card credentials](/cloud/managing-credentials/credit-card-credentials)** — payment card details (number, expiration, CVV, cardholder name). Used in workflows that complete purchases or fill billing forms.
+
+**Secret credentials** — a single sensitive string: API key, bearer token, encryption key, or anything you don't want hardcoded. Create one from **+ Add → Secret** and reference it in any parameter field:
+
+```
+{{ credential_name.secret_value }}
+```
+
+## External credential providers
+
+If your organization already manages secrets in a dedicated vault, reference them directly from **Bitwarden**, **1Password**, or **Azure Key Vault** by adding credential parameters in the [workflow editor](/cloud/building-workflows/add-parameters).
+
+### Bitwarden
+
+Works with hosted Bitwarden and the self-hosted [Vaultwarden](https://github.com/dani-garcia/vaultwarden) fork. Supports passwords, credit cards, and identity data (SSN, address, phone numbers).
+
+Point a credential parameter at a specific vault item using the **Collection ID** and **Item ID** from the Bitwarden web UI. Alternatively, set a **URL Parameter Key** so Bitwarden matches credentials by the target URL — useful when the same workflow runs against different sites.
+
+For identity data, specify an **Identity Key** and a comma-separated list of **Identity Fields** (e.g., `ssn, address, phone`).
+
+### 1Password
+
+Connects via a service account token. Supports passwords and credit cards.
+
+**One-time setup:** Go to **Settings** → find the **1Password** card → paste your [service account token](https://developer.1password.com/docs/service-accounts/get-started/) → click **Update**. The status indicator turns **Active** once validated.
+
+In the workflow editor, select **1Password** as the credential source and provide the **Vault ID** and **Item ID** from your 1Password web URLs.
+
+
+Credit cards from 1Password need a text field named **"Expire Date"** on the item in `MM/YYYY` format. This is a workaround for how 1Password structures card data.
+
+
+### Azure Key Vault
+
+Pulls credentials stored as Azure secrets. Supports passwords with optional TOTP.
+
+**One-time setup:** Go to **Settings** → find the **Azure** card → enter your **Tenant ID**, **Client ID**, and **Client Secret** → click **Update**.
+
+In the workflow editor, select **Azure Key Vault** as the credential source and point it at your vault by name. Provide the **secret names** that store the username and password (and optionally a TOTP secret for 2FA) — not the values themselves.
+
+### Which source should you use?
+
+| Source | Best for |
+|--------|----------|
+| **Skyvern built-in** | Fastest setup — create credentials directly in the UI, no external dependencies |
+| **Bitwarden** | Teams already using Bitwarden who don't want to manage credentials in two places |
+| **1Password** | Teams already using 1Password with service account access |
+| **Azure Key Vault** | Enterprise environments with centrally managed Azure secrets |
+
+You can mix sources within the same workflow — one Login block using Skyvern-stored credentials and another using Azure Key Vault.
+
+## Deleting credentials
+
+Click the **trash icon** on any credential. Deletion is permanent — the Skyvern team can't restore deleted credentials. If a workflow references a deleted credential, it will fail at the login step until you assign a replacement.
+
+
+
+ Store logins with optional 2FA
+
+
+ Store payment details for purchase workflows
+
+
+ Configure and manage two-factor authentication
+
+
diff --git a/docs/cloud/managing-credentials/credit-card-credentials.mdx b/docs/cloud/managing-credentials/credit-card-credentials.mdx
new file mode 100644
index 00000000..3ed2d1e0
--- /dev/null
+++ b/docs/cloud/managing-credentials/credit-card-credentials.mdx
@@ -0,0 +1,53 @@
+---
+title: Credit Card Credentials
+subtitle: Store payment details for purchase and checkout workflows
+slug: cloud/managing-credentials/credit-card-credentials
+---
+
+Credit card credentials store payment details so the AI can fill checkout forms without you embedding card numbers in prompts or parameters.
+
+## Creating a credit card credential
+
+Click **+ Add → Credit Card** from the Credentials page. Provide the full card details:
+
+| Field | Description |
+|-------|------------|
+| **Name** | A label like "Corporate Visa" to identify it later |
+| **Cardholder Name** | Name printed on the card |
+| **Number** | Full card number (spaces added automatically as you type) |
+| **Brand** | Card network: Visa, Mastercard, American Express, Discover, JCB, Diners Club, Maestro, UnionPay, RuPay, or Other |
+| **Expiration** | Month and year in `MM/YY` format (slash inserted automatically) |
+| **CVV** | 3 or 4 digit security code |
+
+After saving, the card number is masked — only the last four digits are ever shown again.
+
+## Using credit cards in workflows
+
+Credit cards work with **Browser Task** and **Browser Action** blocks that interact with checkout pages. The AI identifies payment fields on the page and fills them with the stored details.
+
+To reference a credit card, add a **Credential parameter** (type: `credential_id`) and select the card when running the workflow. This lets the same checkout workflow work with different payment methods.
+
+
+Credit cards from **1Password** need a text field named **"Expire Date"** in `MM/YYYY` format on the 1Password item. See [Credentials Overview](/cloud/managing-credentials/credentials-overview#1password) for details.
+
+
+## Editing and deleting
+
+Credit card credentials can't be edited after creation. If card details change, delete the old credential and create a new one. Click the **trash icon** to delete — the action is permanent.
+
+
+
+ Store login details with optional 2FA
+
+
+ All credential types, external providers, and security model
+
+
diff --git a/docs/cloud/managing-credentials/password-credentials.mdx b/docs/cloud/managing-credentials/password-credentials.mdx
new file mode 100644
index 00000000..15031463
--- /dev/null
+++ b/docs/cloud/managing-credentials/password-credentials.mdx
@@ -0,0 +1,74 @@
+---
+title: Password Credentials
+subtitle: Store login details and use them in automated workflows
+slug: cloud/managing-credentials/password-credentials
+---
+
+Password credentials store a username, password, and optional 2FA configuration. Reference them from Login blocks in your workflows, and Skyvern handles the entire sign-in flow — including entering 2FA codes.
+
+## Creating a password credential
+
+Click **+ Add → Password** from the Credentials page. Three fields: **Name** (a label like "Salesforce Production"), **Username or Email**, and **Password**. The password field has an eye icon to toggle visibility.
+
+
+
+Save the credential and it's ready to use in a workflow.
+
+## Adding two-factor authentication
+
+If the site requires 2FA, expand the **Two-Factor Authentication** accordion below the password fields. Three options, depending on how the site delivers codes:
+
+
+
+| Method | How it works |
+|--------|-------------|
+| **Authenticator App** | Paste the TOTP secret key and Skyvern generates codes locally on demand — fully automated, no delay. Preferred when the site supports it. |
+| **Email** | Provide the email address that receives codes. Skyvern waits for you to push the code via the [2FA tab](/cloud/managing-credentials/totp-setup) or API. Identifier auto-fills from the Username field. |
+| **Text Message** | Provide the phone number that receives codes. Same push-based flow as Email. |
+
+
+Authenticator App is always the best option when available. Email and Text require either manual code entry or setting up automatic forwarding.
+
+
+
+The secret key is the base32-encoded string behind the QR code you'd scan in an authenticator app. Most password managers let you view it:
+
+- **Bitwarden**: Edit the login → TOTP field → copy the key
+- **1Password**: Edit the login → One-Time Password → copy the secret
+- **Site settings**: Many sites show a "Can't scan?" link during 2FA setup that reveals the text key
+
+If you only have a QR code, decode it to extract the `secret=` parameter from the `otpauth://totp/...?secret=BASE32KEY` URI.
+
+
+## Using credentials in workflows
+
+The most common pattern is a **Login block**. Select the stored credential from the dropdown, and Skyvern navigates to the login page, enters the username and password, handles 2FA if configured, and waits for the page to confirm authentication. See [Block Reference → Login](/cloud/building-workflows/configure-blocks) for details.
+
+For workflows that need different accounts at runtime, use a **Credential parameter** (type: `credential_id`). When someone runs the workflow, they pick which credential to use from a dropdown.
+
+You can also pull credentials from **Bitwarden**, **1Password**, or **Azure Key Vault**. See [External Providers](/cloud/managing-credentials/credentials-overview#external-credential-providers).
+
+## Managing credentials
+
+Stored credentials show the name, credential ID, username (plain text), password (always masked), and 2FA method if configured.
+
+
+Credentials can't be edited after creation. To change a password, delete the old credential and create a new one.
+
+
+
+
+ Push verification codes and manage 2FA for Email and Text methods
+
+
+ Configure Login blocks and other blocks that use credentials
+
+
diff --git a/docs/cloud/managing-credentials/totp-setup.mdx b/docs/cloud/managing-credentials/totp-setup.mdx
new file mode 100644
index 00000000..5efee53c
--- /dev/null
+++ b/docs/cloud/managing-credentials/totp-setup.mdx
@@ -0,0 +1,81 @@
+---
+title: 2FA / TOTP Setup
+subtitle: Configure two-factor authentication for automated logins
+slug: cloud/managing-credentials/totp-setup
+---
+
+Skyvern handles 2FA through two mechanisms. **Authenticator App (TOTP)** generates codes locally from your secret key — fully automatic. **Email/SMS** waits for you to push the code via the UI or API. Both are configured on the [password credential](/cloud/managing-credentials/password-credentials) itself.
+
+## Authenticator App (TOTP)
+
+The preferred method. Store a TOTP secret key in a password credential, and Skyvern generates valid 6-digit codes on demand during login flows. The Login block enters credentials, detects the 2FA prompt, generates a fresh code, and enters it — all automatic.
+
+**Setup:** Create a password credential → expand **Two-Factor Authentication** → select **Authenticator App** → paste the TOTP secret key into the **Authenticator Key** field.
+
+The secret key is the base32-encoded string behind the QR code you'd normally scan. Copy it from your password manager (Bitwarden: TOTP field; 1Password: One-Time Password field) or look for a "Can't scan the QR code?" link during the site's 2FA setup.
+
+## Email and SMS codes
+
+When a site sends codes via email or text, someone (or something) needs to deliver the code to Skyvern.
+
+The flow:
+
+1. Login block enters username and password
+2. Site sends a 2FA code to the configured email or phone
+3. You push the code to Skyvern via the **2FA tab** or the API
+4. Skyvern enters the code and completes the login
+
+### Pushing a code manually
+
+Open the **2FA** tab on the Credentials page. The **Push a 2FA Code** form has two fields:
+
+| Field | What to enter |
+|-------|--------------|
+| **Identifier** | The email address or phone number that received the code |
+| **Verification content** | The full email/SMS body, or just the code itself — Skyvern extracts the digits automatically |
+
+
+If multiple workflows are running simultaneously, click **Add optional metadata** to link the code to a specific run using the workflow run ID, workflow ID, or task ID.
+
+
+### Pushing codes via API
+
+For production, automate code delivery. Set up a forwarding rule that sends 2FA emails/texts to a script, and the script calls:
+
+```bash
+curl -X POST "https://api.skyvern.com/v1/credentials/totp" \
+ -H "x-api-key: YOUR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "totp_identifier": "user@example.com",
+ "content": "Your verification code is 847291",
+ "source": "email_forwarder"
+ }'
+```
+
+The `source` field is a free-text label for your own tracking (e.g., `"email_forwarder"`, `"twilio_webhook"`).
+
+This turns email-based 2FA into something nearly as automated as authenticator app — the main difference is latency while the email arrives and gets forwarded.
+
+## Viewing past codes
+
+The table below the push form shows all 2FA codes your organization has received: identifier, extracted code, source type, associated workflow run, and timestamps. Filter by identifier, OTP type (numeric code vs. magic link), and number of results per page.
+
+Use this for auditing and debugging — confirming that a code was received and delivered to the right run.
+
+
+
+ Create credentials with 2FA methods attached
+
+
+ All credential types, external providers, and security model
+
+
diff --git a/docs/cloud/running-tasks.mdx b/docs/cloud/running-tasks.mdx
deleted file mode 100644
index af446118..00000000
--- a/docs/cloud/running-tasks.mdx
+++ /dev/null
@@ -1,359 +0,0 @@
----
-title: Running Tasks
-subtitle: Go deeper with task configuration in the Cloud UI
-slug: cloud/running-tasks
----
-
-You've run your first task. Now let's explore what else you can do—access websites as if you're in another country, extract structured data into JSON, watch the AI work in real-time, and turn successful tasks into reusable workflows.
-
-
-New to Skyvern? Start with [Getting Started](/cloud/getting-started) to run your first task.
-
-
----
-
-## The Discover page
-
-The **Discover** page is where you run ad-hoc tasks. Open [app.skyvern.com](https://app.skyvern.com) and you'll land here.
-
-
-
-
-
-You'll see two main areas:
-
-**The Prompt Box** — Enter a URL and describe what you want Skyvern to do. Click the gear icon to access advanced settings before running.
-
-**Workflow Templates** — A carousel of pre-built automations for common tasks. Click any template to see how it's built, or use it as a starting point for your own.
-
-Tasks are flexible. You can:
-- Extract data from any website without writing scrapers
-- Fill out forms with data you provide
-- Navigate multi-page flows (search → filter → extract)
-- Download files like invoices and reports
-- Monitor prices, inventory, or content changes
-
-The power comes from combining a clear prompt with the right settings. Let's walk through the key configurations.
-
----
-
-## Access geo-restricted content
-
-You're trying to scrape product prices from a UK retailer. But when you run the task, you see USD prices—the site detected you're in the US and redirected you.
-
-This is where **Proxy Location** changes everything.
-
-
-
-
-
-Skyvern routes your browser through residential IP addresses worldwide. The site sees a real UK visitor, shows GBP prices, and doesn't block you.
-
-**To set it up:**
-
-1. Click the gear icon to open settings
-2. Find **Proxy Location** in the dropdown
-3. Select the country you need
-
-
-
-
-
-Available locations include United States (default), United Kingdom, Germany, France, Japan, Australia, Canada, Brazil, and more. Select **None** if you're accessing internal tools that don't need a proxy.
-
-The browser's timezone adjusts automatically to match the proxy location. Sites that check for mismatches between IP and timezone won't flag you.
-
----
-
-## Extract structured data
-
-By default, Skyvern returns whatever data makes sense for your prompt. For consistent output you can process programmatically, define a **Data Schema**.
-
-Say you're extracting product information. Without a schema, you might get:
-
-```json
-{"info": "The product costs $79.99 and is in stock"}
-```
-
-With a schema, you get predictable fields:
-
-```json
-{
- "product_name": "Wireless Headphones",
- "price": 79.99,
- "in_stock": true
-}
-```
-
-**To set it up:**
-
-1. Click the gear icon to open settings
-2. Scroll to **Data Schema**
-3. Enter a JSON Schema defining your fields
-
-
-
-
-
-```json
-{
- "type": "object",
- "properties": {
- "product_name": {
- "type": "string",
- "description": "The full product name"
- },
- "price": {
- "type": "number",
- "description": "Current price in USD, without currency symbol"
- },
- "in_stock": {
- "type": "boolean",
- "description": "Whether the product is currently available"
- }
- }
-}
-```
-
-The `description` field is crucial—it tells the AI exactly what to look for. "Price in USD without currency symbol" works better than just "the price."
-
-**Extracting lists:**
-
-Need multiple items? Wrap your fields in an array:
-
-```json
-{
- "type": "object",
- "properties": {
- "products": {
- "type": "array",
- "description": "Top 5 search results",
- "items": {
- "type": "object",
- "properties": {
- "name": { "type": "string", "description": "Product name" },
- "price": { "type": "number", "description": "Price in USD" }
- }
- }
- }
- }
-}
-```
-
-
-A schema doesn't guarantee all fields are populated. If the data isn't on the page, fields return `null`.
-
-
----
-
-## Configure your task
-
-Click the gear icon to access all task settings. Here's what each one does:
-
-
-
-
-
-### Core settings
-
-| Setting | What it does |
-|---------|--------------|
-| **Navigation Goal** | Your main prompt—what should Skyvern do? |
-| **Navigation Payload** | JSON data to use in form fields (names, addresses, etc.) |
-| **Data Extraction Goal** | Describe what data you want extracted |
-| **Data Schema** | JSON Schema for structured output |
-
-### Cost and limits
-
-| Setting | What it does |
-|---------|--------------|
-| **Max Steps Override** | Limit how many actions the AI can take. You're billed per step, so this caps costs. If the task hits this limit, it stops with a `timed_out` status. |
-
-
-Always set a max steps limit during development. A task that gets stuck in a loop will keep consuming steps until it hits this limit.
-
-
-### Advanced settings
-
-| Setting | What it does |
-|---------|--------------|
-| **Proxy Location** | Route traffic through a specific country |
-| **Webhook Callback URL** | Receive a POST request when the task completes (instead of polling) |
-| **2FA Identifier** | Handle two-factor authentication during login |
-| **Browser Address** | Connect to your own browser for local development |
-| **Extra HTTP Headers** | Add custom headers (e.g., Authorization tokens) |
-| **Max Screenshot Scrolls** | How many times to scroll for lazy-loaded content |
-| **Include Action History** | Include past actions when verifying completion |
-
----
-
-## Watch the live browser
-
-Once you click run, Skyvern opens a cloud browser and starts executing. You can watch it work in real-time via a live VNC stream.
-
-
-
-
-
-The execution screen shows:
-
-| Area | What you see |
-|------|--------------|
-| **Left panel** | Task configuration—URL, prompt, current status badge |
-| **Center** | Live browser stream. Watch pages load, forms fill, buttons click. |
-| **Right panel** | Action list showing what the AI has done, with step count and cost |
-
-### Take control of the browser
-
-See the **Take Control** button? Click it to pause the AI and control the browser yourself.
-
-
-
-
-
-Use this when:
-- A CAPTCHA appears that the AI can't solve
-- The site requires an unusual login flow
-- You need to manually navigate past an unexpected popup
-
-Click **Stop Controlling** to hand control back to the AI.
-
-### Stop a running task
-
-Changed your mind? Click the **Cancel** button in the header. You'll be billed for steps already taken, but no further steps will run.
-
----
-
-## Review your results
-
-After the task completes, you'll see the results page. You can also find all past runs in **Runs** in the sidebar.
-
-
-
-
-
-The header shows:
-- **Task ID** — Unique identifier (click to copy)
-- **Status badge** — `completed`, `failed`, `terminated`, `timed_out`, or `canceled`
-- **Rerun button** — Run the same task again
-- **API & Webhooks** — Copy the API command or test webhook delivery
-
-### Extracted data
-
-If the task completed successfully, the **Extracted Information** section shows your output:
-
-
-
-
-
-```json
-{
- "product_name": "Wireless Bluetooth Headphones",
- "price": 79.99,
- "in_stock": true
-}
-```
-
-If something went wrong, you'll see **Failure Reason** with details about what happened.
-
-### Results tabs
-
-Four tabs let you dig deeper:
-
-
-
-
-
-**Actions** — Step-by-step breakdown of everything the AI did. Each action shows a screenshot, what was done, and why. The right side shows total steps, actions, and cost.
-
-
-
-
-
-**Recording** — Full video replay of the browser session. Scrub through to see exactly what happened.
-
-
-
-
-
-**Parameters** — The configuration you submitted: URL, navigation goal, payload, schema, proxy, and all other settings. Useful for debugging or recreating the task.
-
-**Diagnostics** — Debug information including LLM prompts and responses, element trees, and annotated screenshots. Use this when you need to understand why the AI made a specific decision.
-
-### Understanding failures
-
-If your task failed or terminated, check these in order:
-
-1. **Recording** — Watch what happened. Did the AI get stuck? Click the wrong thing?
-2. **Actions tab** — Read the AI's reasoning for each step. Where did it go wrong?
-3. **Diagnostics** — See the full LLM prompt and response to understand the decision
-
-Common fixes:
-- **Completed too early** → Add clearer completion criteria: "COMPLETE when you see the confirmation page with order number"
-- **Wrong element clicked** → Add visual descriptions: "Click the blue Submit button at the bottom of the form"
-- **Timed out** → Increase max steps, or simplify the task into smaller pieces
-
----
-
-## Turn a task into a workflow
-
-Found a task that works well? Save it as a reusable **Workflow** so you can run it again with different inputs.
-
-Before running your task, expand Advanced Settings and toggle **Publish Workflow** on.
-
-
-
-
-
-After the task completes successfully, Skyvern creates a workflow you can find in the **Workflows** section. From there you can:
-
-- Run it again with different data
-- Edit the steps in the visual workflow builder
-- Schedule it to run automatically
-- Share it with your team
-
----
-
-## Tips for better results
-
-### Start simple, then iterate
-
-1. Run with just URL and prompt first—no advanced settings
-2. Watch the live browser to understand what the AI does
-3. Add constraints based on what you observe
-
-### Write better prompts
-
-The prompt drives everything. A good prompt includes:
-
-- **Clear goal** — "Get the price of the first product" not "Get product info"
-- **Completion criteria** — "COMPLETE when you see the order confirmation page"
-- **Visual hints** — "Click the blue Add to Cart button below the price"
-- **What to extract** — "Extract the price as a number without the currency symbol"
-
-### Control costs
-
-- Set **Max Steps** to a reasonable limit for your task
-- Watch the **Actions tab** to see if steps are being wasted
-- Use **Data Schema** to get exactly the fields you need—nothing extra
-
----
-
-## What's next?
-
-
-
- Create multi-step automations with the visual workflow builder
-
-
- Pull structured data from websites into JSON format
-
-
diff --git a/docs/cloud/viewing-results/downloading-artifacts.mdx b/docs/cloud/viewing-results/downloading-artifacts.mdx
new file mode 100644
index 00000000..17b54d15
--- /dev/null
+++ b/docs/cloud/viewing-results/downloading-artifacts.mdx
@@ -0,0 +1,95 @@
+---
+title: Downloading Artifacts
+subtitle: Access recordings, screenshots, and output files from your runs
+slug: cloud/viewing-results/downloading-artifacts
+---
+
+Every run generates artifacts — video recordings, screenshots at each decision point, extracted data as JSON, and any downloaded files. Most are visible in the [Run Details](/cloud/viewing-results/run-details) page, but you can also save them locally or pull them into another system.
+
+## From the UI
+
+| Artifact | How to download |
+|----------|----------------|
+| **Recordings** | Recording tab → right-click the video → **Save video as...** |
+| **Screenshots** | Overview tab → click a timeline item → right-click the image → **Save image as...** |
+| **Extracted data** | Output tab → select the JSON text and copy |
+| **Downloaded files** | Output tab → **Workflow Run Downloaded Files** → click any link |
+| **Generated code** | Code tab → **Copy** button in the top right |
+
+## From the API
+
+Use the API when you need artifacts programmatically — for a pipeline, an archive, or integration with another tool.
+
+### List artifacts for a run
+
+```bash
+curl -X GET "https://api.skyvern.com/v1/runs/{run_id}/artifacts" \
+ -H "x-api-key: YOUR_API_KEY"
+```
+
+Filter by type using the `artifact_type` parameter (repeatable):
+
+```bash
+curl -X GET "https://api.skyvern.com/v1/runs/{run_id}/artifacts?artifact_type=screenshot_llm&artifact_type=recording" \
+ -H "x-api-key: YOUR_API_KEY"
+```
+
+### Download an artifact
+
+Each artifact in the response includes a `signed_url` — a temporary, pre-authenticated download link:
+
+```json
+{
+ "artifact_id": "art_abc123",
+ "artifact_type": "screenshot_llm",
+ "signed_url": "https://storage.example.com/screenshot.png?token=...",
+ "created_at": "2025-01-15T10:30:00Z"
+}
+```
+
+```bash
+curl -o screenshot.png "SIGNED_URL_HERE"
+```
+
+
+Signed URLs expire after a short period. Always generate a fresh URL via the API rather than storing old ones.
+
+
+### Artifact types
+
+| Value | What it is |
+|-------|-----------|
+| `recording` | Full browser session video |
+| `screenshot_llm` | Screenshots with numbered element annotations — what the AI analyzed |
+| `screenshot_action` | Screenshots taken after each browser action |
+| `screenshot_final` | Final page state when the run ended |
+| `llm_prompt` | Complete prompt sent to the LLM (goal, element tree, system instructions) |
+| `llm_request` | Raw API request payload sent to the model provider |
+| `llm_response` | Raw model response |
+| `llm_response_parsed` | Parsed action list — what the AI decided to do |
+| `visible_elements_tree` | DOM tree of interactive elements the AI could see (JSON) |
+| `html_scrape` | Full page HTML at the time of the step |
+| `skyvern_log` | Formatted execution logs |
+| `har` | HTTP Archive file — every network request the browser made |
+| `trace` | Browser trace file for performance analysis |
+
+
+The annotated screenshots (`screenshot_llm`) and parsed action lists (`llm_response_parsed`) are the most useful for debugging. Annotated screenshots show which elements the AI identified, and the action list shows what it decided to do with them.
+
+
+
+
+ Understand what each tab shows and how to debug failures
+
+
+ Full API reference for artifact retrieval
+
+
diff --git a/docs/cloud/viewing-results/run-details.mdx b/docs/cloud/viewing-results/run-details.mdx
new file mode 100644
index 00000000..82f8d684
--- /dev/null
+++ b/docs/cloud/viewing-results/run-details.mdx
@@ -0,0 +1,118 @@
+---
+title: Run Details
+subtitle: Inspect actions, outputs, recordings, and parameters for any run
+slug: cloud/viewing-results/run-details
+---
+
+Every run has a detail page showing what the AI saw, what it decided, what it extracted, and whether anything went wrong. Click any run in [Run History](/cloud/viewing-results/run-history) to get here.
+
+
+
+## The header
+
+The top of the page shows the workflow title, a color-coded status badge, and the run ID. Three buttons on the right:
+
+- **API & Webhooks** — the exact API request that would reproduce this run, including endpoint, headers, and payload
+- **Edit** — jump to the workflow editor
+- **Rerun** — start a new run pre-filled with the same parameters (appears after the run finishes)
+
+While a run is in progress, a **Cancel** button appears instead of Rerun.
+
+
+
+## Extracted information
+
+On success, the **Extracted Information** section appears above the tabs — the structured data you asked for, displayed as JSON.
+
+
+
+The output includes a `summary` (natural language description of what was accomplished), `extracted_information` (structured data matching your schema), and a `failure_reason` field that's `null` on success. On failure, a **Failure Reason** section appears instead with error details in a red box.
+
+## The five tabs
+
+### Overview
+
+The left panel shows the browser state (live stream while running, screenshots after completion). The right panel shows the AI's reasoning timeline.
+
+
+
+The timeline is a chronological feed of three card types:
+
+| Card | What it shows |
+|------|--------------|
+| **Thought** | The AI's internal reasoning — what it sees, what it plans to do, and why. Tagged with "Decision." |
+| **Block** | When a workflow block starts or finishes (e.g., "Extraction"). Green checkmark = success. |
+| **Action** | Individual browser operations (e.g., "#1 Extract Data") with a description of what was done. |
+
+Click any timeline item to see its corresponding screenshot in the left panel. The **Actions** and **Steps** counters at the top give you a quick sense of how much work the run involved.
+
+
+When a run produces wrong results, start with the Thought cards. Read the AI's reasoning to find where it went off track — a misidentified element, or a premature "goal met" conclusion.
+
+
+### Output
+
+All output data in one place. **Workflow Run Outputs** shows the complete JSON with line numbers and syntax highlighting. **Workflow Run Downloaded Files** lists any files the workflow downloaded — click to download directly.
+
+### Parameters
+
+The configuration that produced this run: workflow input parameters (key-value pairs), webhook URL, proxy location, and HTTP headers. Use this to verify a run received the right inputs or to recreate a past result.
+
+When you select a block in the Overview timeline, this tab also shows that block's configuration — prompt, URL, schema, and other settings.
+
+### Recording
+
+Full video replay of the browser session. Every run is recorded automatically. Scrub through to see exactly what appeared on screen at any point.
+
+
+If the run was canceled before the browser started, you'll see a "No recording available" message.
+
+
+### Code
+
+Python code generated from the run. This tab appears when code generation is enabled (the default engine). Copy the code to run outside Skyvern, or click **Copy & Explain** for an AI-generated explanation.
+
+
+
+If the workflow uses cached code, a **Cache Key selector** lets you switch between versions.
+
+## Debugging failed runs
+
+Work through these in order:
+
+1. **Read the Failure Reason** at the top. It usually tells you what happened — timeout, navigation error, or missing element.
+2. **Walk the Thought cards** in the Overview timeline. Find the point where the AI made an incorrect assumption about page state.
+3. **Check the Parameters tab** to confirm inputs were correct. A wrong URL or missing parameter is a common cause.
+4. **Watch the Recording** to see what actually happened. Popups, CAPTCHAs, and unexpected page layouts are often obvious on video.
+
+## Run statuses
+
+| Status | Meaning |
+|--------|---------|
+| `created` | Initialized, waiting to be queued |
+| `queued` | Waiting for an available browser slot |
+| `running` | Actively executing — browser stream is live |
+| `completed` | Finished successfully |
+| `failed` | Stopped due to an error |
+| `terminated` | Stopped by the system (e.g., resource limits) |
+| `canceled` | Stopped by you via the Cancel button |
+| `timed_out` | Exceeded the configured time limit |
+
+While a run is in progress, the status badge updates automatically, the Overview tab shows a live browser stream, and the timeline grows as new items complete.
+
+
+
+ Access recordings, screenshots, and output files
+
+
+ Monitor and take control of running tasks
+
+
diff --git a/docs/cloud/viewing-results/run-history.mdx b/docs/cloud/viewing-results/run-history.mdx
new file mode 100644
index 00000000..004c569f
--- /dev/null
+++ b/docs/cloud/viewing-results/run-history.mdx
@@ -0,0 +1,55 @@
+---
+title: Run History
+subtitle: Find, filter, and inspect past task and workflow executions
+slug: cloud/viewing-results/run-history
+---
+
+The **Runs** page shows every task and workflow execution, sorted newest-first. Use it to check on past runs, find failures, or verify that a scheduled workflow completed.
+
+
+
+## Finding a specific run
+
+**Search** narrows the list by run ID or workflow parameter value. Type part of an ID (e.g., `wr_49338`) or a parameter you passed (like a URL), and matching results appear as you type.
+
+
+
+**Filter by Status** opens a multi-select dropdown. Check one or more statuses to narrow results — `failed` and `timed_out` together show everything that went wrong. Filters and search can be combined: search for a parameter value while filtering to only failed runs.
+
+Available statuses: **Created**, **Queued**, **Running**, **Completed**, **Failed**, **Terminated**, **Canceled**, **Timed Out**.
+
+## Reading the table
+
+Each row is one run. The columns:
+
+| Column | What it shows |
+|--------|--------------|
+| **Run ID** | Workflow run (`wr_...`) or task (`tsk_...`) identifier. Hover for the full ID. |
+| **Detail** | Workflow title or task description. A lightning bolt icon means code generation was used. |
+| **Status** | Color-coded badge — green (completed), yellow (running/queued/created), red (failed/timed_out), orange (terminated/canceled). |
+| **Created At** | Start time in your local timezone. Hover for UTC. |
+
+Workflow runs have a **settings icon** in the last column. Click it to expand the row and see the parameters passed to that run. If your search matches something inside the parameters, the row auto-expands so you can see the match in context.
+
+## Opening a run
+
+Click any row to jump to its [detail page](/cloud/viewing-results/run-details). Hold **Ctrl** (or **Cmd** on Mac) to open in a new tab.
+
+Pagination controls at the bottom let you adjust the page size (5, 10, 20, or 50 items) and navigate between pages. Settings are stored in the URL, so you can bookmark a filtered view or share it with a teammate.
+
+
+
+ Inspect actions, outputs, recordings, and parameters for any run
+
+
+ Save recordings, screenshots, and output files
+
+
diff --git a/docs/cookbooks/bulk-invoice-downloader.mdx b/docs/cookbooks/bulk-invoice-downloader.mdx
new file mode 100644
index 00000000..4a44e18c
--- /dev/null
+++ b/docs/cookbooks/bulk-invoice-downloader.mdx
@@ -0,0 +1,1057 @@
+---
+title: Bulk Invoice Downloader
+subtitle: Download invoices from a customer portal, parse PDFs, and email a summary
+slug: cookbooks/bulk-invoice-downloader
+---
+
+Automate invoice collection from any vendor portal.
+
+This cookbook creates a workflow that takes in a vendor portal URL, logs in using saved credentials, finds order history, filters by date and finally downloads and emails the invoices as PDFs.
+
+---
+
+## What you'll build
+
+A workflow that:
+
+1. Logs into a customer account portal
+2. Navigates to order history and filters by date
+3. Extracts order metadata from the page
+4. Downloads invoice PDFs for each order
+5. Parses invoice data from each PDF
+6. Emails a summary with PDFs attached
+
+---
+
+## Prerequisites
+
+- **Skyvern Cloud API key** — Get one at [app.skyvern.com/settings](https://app.skyvern.com/settings) → API Keys
+
+Install the SDK:
+
+
+```bash Python
+pip install skyvern
+```
+
+```bash TypeScript
+npm install @skyvern/client
+```
+
+
+Set your API key:
+
+```bash
+export SKYVERN_API_KEY="your-api-key"
+```
+
+---
+
+## Sample Vendor Portal
+
+We'll use *Ember Roasters*, a fake coffee retailer website created for agent automation testing.
+Change `portal_url` to use your vendor's portal URL.
+
+| Field | Value |
+| -------------- | ---------------------------- |
+| URL | https://ember-roasters.vercel.app/ |
+| Login email | demo@manicule.dev |
+| Login password | helloworld |
+
+
+---
+
+## Step 1: Store credentials
+
+Before defining the workflow, store the login email and password Skyvern will use. This keeps secrets out of your workflow definition and away from LLMs.
+
+
+```python Python
+import os
+import asyncio
+from skyvern import Skyvern
+
+async def main():
+ client = Skyvern(api_key=os.getenv("SKYVERN_API_KEY"))
+
+ credential = await client.create_credential(
+ name="Vendor Portal",
+ credential_type="password",
+ credential={
+ "username": "demo@manicule.dev",
+ "password": "helloworld"
+ }
+ )
+
+ print(f"Credential ID: {credential.credential_id}")
+ # Save this ID for your workflow: cred_xxx
+
+asyncio.run(main())
+```
+
+```typescript TypeScript
+import { SkyvernClient } from "@skyvern/client";
+
+const client = new SkyvernClient({
+ apiKey: process.env.SKYVERN_API_KEY,
+});
+
+const credential = await client.createCredential({
+ name: "Vendor Portal",
+ credential_type: "password",
+ credential: {
+ username: "demo@manicule.dev",
+ password: "helloworld",
+ },
+});
+
+console.log(`Credential ID: ${credential.credential_id}`);
+```
+
+```bash cURL
+curl -X POST "https://api.skyvern.com/v1/credentials" \
+ -H "x-api-key: $SKYVERN_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Vendor Portal",
+ "credential_type": "password",
+ "credential": {
+ "username": "demo@manicule.dev",
+ "password": "helloworld"
+ }
+ }'
+```
+
+
+---
+
+## Step 2: Define workflow parameters
+
+Parameters are the inputs your workflow accepts. Defining them upfront lets you run the same workflow against different portals, date ranges, or recipients.
+
+This workflow uses the following parameters:
+
+- **`portal_url`** — The vendor portal's login URL.
+- **`start_date` / `end_date`** — Date range for filtering invoices.
+- **`recipient_email`** — Where to send the summary email.
+- **`credentials`** — The ID of the stored credential to use for login.
+
+**`smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`** are SMTP configuration that Skyvern fills on its own. The `send_email` block requires these four parameters to connect to your mail server.
+
+
+```json JSON
+{
+ "parameters": [
+ {
+ "key": "portal_url",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "string"
+ },
+ {
+ "key": "start_date",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "string"
+ },
+ {
+ "key": "end_date",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "string"
+ },
+ {
+ "key": "recipient_email",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "string"
+ },
+ {
+ "key": "credentials",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "credential_id",
+ "default_value": "your-credential-id"
+ },
+ {
+ "key": "smtp_host",
+ "parameter_type": "aws_secret",
+ "aws_key": "SKYVERN_SMTP_HOST_AWS_SES"
+ },
+ {
+ "key": "smtp_port",
+ "parameter_type": "aws_secret",
+ "aws_key": "SKYVERN_SMTP_PORT_AWS_SES"
+ },
+ {
+ "key": "smtp_username",
+ "parameter_type": "aws_secret",
+ "aws_key": "SKYVERN_SMTP_USERNAME_SES"
+ },
+ {
+ "key": "smtp_password",
+ "parameter_type": "aws_secret",
+ "aws_key": "SKYVERN_SMTP_PASSWORD_SES"
+ }
+ ]
+}
+```
+
+```yaml YAML
+parameters:
+ - key: portal_url # <-- parameter name
+ parameter_type: workflow # <-- always set to "workflow"
+ workflow_parameter_type: string # <-- can be string, file_url, credential_id, etc
+ - key: start_date
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: end_date
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: recipient_email
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: credentials
+ parameter_type: workflow
+ workflow_parameter_type: credential_id
+ default_value: your-credential-id # <-- replace this
+ - key: smtp_host
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_HOST_AWS_SES
+ - key: smtp_port
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_PORT_AWS_SES
+ - key: smtp_username
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_USERNAME_SES
+ - key: smtp_password
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_PASSWORD_SES
+```
+
+
+---
+
+## Step 3: Create the workflow definition
+
+The workflow chains together several blocks to automate the full invoice collection process:
+
+1. **Login block** — Authenticates to the vendor portal using stored credentials
+2. **Navigation block** — Navigates to order history and applies date filters
+3. **Extraction block** — Extracts order metadata from the filtered results
+4. **For loop + File download** — Iterates over each order and downloads its invoice PDF
+5. **For loop + File parser** — Parses each downloaded PDF to extract structured data
+6. **Send email** — Sends a summary with PDFs attached to the recipient
+
+We will add these blocks one by one in the **workflow definition**, a YAML/JSON file that contains the complete description of your workflow logic
+
+### Workflow Definition format
+
+
+```json JSON
+{
+ "title": "Bulk Invoice Downloader",
+ "description": "Download invoices from vendor portals, parse PDFs to extract amounts, and email a summary with attachments.",
+ "proxy_location": "RESIDENTIAL",
+ "workflow_definition": {
+ "version": 1,
+ "parameters": [],
+ "blocks": []
+ }
+}
+```
+
+```yaml YAML
+title: Bulk Invoice Downloader
+description: "Download invoices from vendor portals, parse PDFs to extract amounts, and email a summary with attachments."
+proxy_location: RESIDENTIAL # <-- defaults to RESIDENTIAL
+workflow_definition:
+ version: 1 # <-- auto-increments when you make changes
+ parameters:
+ ... # <-- defined in Step 2
+
+ blocks:
+ ... # <-- defined one by one in the next steps
+```
+
+
+### Login block
+
+The `login` block authenticates using stored credentials. Skyvern injects the username/password directly into form fields without exposing them to the LLM.
+
+
+```json JSON
+{
+ "block_type": "login",
+ "label": "login_block",
+ "url": "{{portal_url}}",
+ "title": "login_block",
+ "parameter_keys": ["credentials"],
+ "navigation_goal": "Log in using the provided credentials.\nHandle any cookie consent popups.\nCOMPLETE when on the account dashboard or orders page.",
+ "error_code_mapping": {
+ "INVALID_CREDENTIALS": "Login failed - incorrect email or password",
+ "ACCOUNT_LOCKED": "Account has been locked or suspended"
+ },
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+}
+```
+
+```yaml YAML
+- block_type: login
+ label: login_block
+ url: "{{portal_url}}"
+ title: login_block
+ parameter_keys:
+ - credentials
+ navigation_goal: |
+ Log in using the provided credentials.
+ Handle any cookie consent popups.
+ COMPLETE when on the account dashboard or orders page.
+ error_code_mapping:
+ INVALID_CREDENTIALS: Login failed - incorrect email or password
+ ACCOUNT_LOCKED: Account has been locked or suspended
+ max_retries: 0
+ engine: skyvern-1.0
+```
+
+
+**Why `error_code_mapping`?** It surfaces specific failures in your workflow output, so you can handle "wrong password" differently from "account locked."
+
+### Navigation block
+
+Navigate to the orders page and apply the date filter.
+
+
+```json JSON
+{
+ "block_type": "navigation",
+ "label": "nav_block",
+ "url": "",
+ "title": "nav_block",
+ "engine": "skyvern-1.0",
+ "parameter_keys": ["start_date", "end_date"],
+ "navigation_goal": "Navigate to Order History or My Orders.\nFilter orders between {{ start_date }} and {{ end_date }}.\nClick the Filter button.\nCOMPLETE when filtered results are visible.",
+ "max_retries": 0
+}
+```
+
+```yaml YAML
+- block_type: navigation
+ label: nav_block
+ url: ""
+ title: nav_block
+ engine: skyvern-1.0
+ parameter_keys:
+ - start_date
+ - end_date
+ navigation_goal: |
+ Navigate to Order History or My Orders.
+ Filter orders between {{ start_date }} and {{ end_date }}.
+ Click the Filter button.
+ COMPLETE when filtered results are visible.
+ max_retries: 0
+```
+
+
+### Extraction block
+
+Extract order metadata from the filtered results. The `data_schema` tells Skyvern exactly what structure to return.
+
+
+```json JSON
+{
+ "block_type": "extraction",
+ "label": "data_extraction_block",
+ "url": "",
+ "title": "data_extraction_block",
+ "data_extraction_goal": "Extract all visible orders: order ID, date, total amount, and status.",
+ "data_schema": {
+ "orders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "order_id": {
+ "type": "string",
+ "description": "Unique identifier for the order"
+ },
+ "date": {
+ "type": "string",
+ "description": "Date when the order was placed"
+ },
+ "total": {
+ "type": "number",
+ "description": "Total amount for the order"
+ },
+ "status": {
+ "type": "string",
+ "description": "Current status of the order"
+ }
+ },
+ "required": ["order_id", "date", "total", "status"]
+ }
+ }
+ },
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+}
+```
+
+```yaml YAML
+- block_type: extraction
+ label: data_extraction_block
+ url: ""
+ title: data_extraction_block
+ data_extraction_goal: "Extract all visible orders: order ID, date, total amount, and status."
+ data_schema:
+ orders:
+ type: array
+ items:
+ type: object
+ properties:
+ order_id:
+ type: string
+ description: Unique identifier for the order
+ date:
+ type: string
+ description: Date when the order was placed
+ total:
+ type: number
+ description: Total amount for the order
+ status:
+ type: string
+ description: Current status of the order
+ required:
+ - order_id
+ - date
+ - total
+ - status
+ max_retries: 0
+ engine: skyvern-1.0
+```
+
+
+The output is accessible as `{{ data_extraction_block_output.orders }}` in subsequent blocks.
+
+### Download invoices block
+
+Iterate over each order and click its "Download Invoice" button. `continue_on_failure: true` ensures one failed download doesn't stop the entire workflow.
+
+
+```json JSON
+{
+ "block_type": "for_loop",
+ "label": "for_1_block",
+ "loop_variable_reference": "{{data_extraction_block_output.orders}}",
+ "continue_on_failure": true,
+ "next_loop_on_failure": true,
+ "complete_if_empty": true,
+ "loop_blocks": [
+ {
+ "block_type": "file_download",
+ "label": "inv_download_block",
+ "url": "",
+ "title": "inv_download_block",
+ "navigation_goal": "Find order {{ current_value.order_id }}.\nClick Download Invoice.\nCOMPLETE when the PDF download starts.",
+ "download_suffix": "invoice_{{ current_value.order_id }}.pdf",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ }
+ ]
+}
+```
+
+```yaml YAML
+- block_type: for_loop
+ label: for_1_block
+ loop_variable_reference: "{{data_extraction_block_output.orders}}"
+ continue_on_failure: true
+ next_loop_on_failure: true
+ complete_if_empty: true
+ loop_blocks:
+ - block_type: file_download
+ label: inv_download_block
+ url: ""
+ title: inv_download_block
+ navigation_goal: |
+ Find order {{ current_value.order_id }}.
+ Click Download Invoice.
+ COMPLETE when the PDF download starts.
+ download_suffix: invoice_{{ current_value.order_id }}.pdf
+ max_retries: 0
+ engine: skyvern-1.0
+```
+
+
+**Key pattern:** Inside a loop, `{{ current_value }}` gives you the current item being iterated over.
+
+### Parse invoices block
+
+Use `file_url_parser` to extract structured data from each downloaded PDF.
+
+
+```json JSON
+{
+ "block_type": "for_loop",
+ "label": "for_2_block",
+ "loop_variable_reference": "{{data_extraction_block_output.orders}}",
+ "continue_on_failure": true,
+ "next_loop_on_failure": true,
+ "complete_if_empty": true,
+ "loop_blocks": [
+ {
+ "block_type": "file_url_parser",
+ "label": "parse_block",
+ "file_url": "SKYVERN_DOWNLOAD_DIRECTORY/invoice_{{ current_value.order_id }}.pdf",
+ "file_type": "pdf",
+ "json_schema": {
+ "type": "object",
+ "properties": {
+ "invoice_id": {
+ "type": "string",
+ "description": "Unique identifier for the invoice"
+ },
+ "amount": {
+ "type": "number",
+ "description": "Total amount of the invoice"
+ },
+ "date": {
+ "type": "string",
+ "description": "Date of the invoice, typically in YYYY-MM-DD format"
+ }
+ },
+ "required": ["invoice_id", "amount", "date"]
+ }
+ }
+ ]
+}
+```
+
+```yaml YAML
+- block_type: for_loop
+ label: for_2_block
+ loop_variable_reference: "{{data_extraction_block_output.orders}}"
+ continue_on_failure: true
+ next_loop_on_failure: true
+ complete_if_empty: true
+ loop_blocks:
+ - block_type: file_url_parser
+ label: parse_block
+ file_url: SKYVERN_DOWNLOAD_DIRECTORY/invoice_{{ current_value.order_id }}.pdf
+ file_type: pdf
+ json_schema:
+ type: object
+ properties:
+ invoice_id:
+ type: string
+ description: Unique identifier for the invoice
+ amount:
+ type: number
+ description: Total amount of the invoice
+ date:
+ type: string
+ description: Date of the invoice, typically in YYYY-MM-DD format
+ required:
+ - invoice_id
+ - amount
+ - date
+```
+
+
+The output is accessible as `{{ for_2_block_output }}` in subsequent blocks.
+
+### Email block
+
+Send a summary email with PDFs attached.
+
+
+```json JSON
+{
+ "block_type": "send_email",
+ "label": "email_block",
+ "smtp_host_secret_parameter_key": "smtp_host",
+ "smtp_port_secret_parameter_key": "smtp_port",
+ "smtp_username_secret_parameter_key": "smtp_username",
+ "smtp_password_secret_parameter_key": "smtp_password",
+ "sender": "hello@skyvern.com",
+ "recipients": ["{{recipient_email}}"],
+ "subject": "Ember Roasters Invoices from {{start_date}} to {{end_date}}",
+ "body": "{{for_2_block_output}}",
+ "file_attachments": ["SKYVERN_DOWNLOAD_DIRECTORY"]
+}
+```
+
+```yaml YAML
+- block_type: send_email
+ label: email_block
+ smtp_host_secret_parameter_key: smtp_host
+ smtp_port_secret_parameter_key: smtp_port
+ smtp_username_secret_parameter_key: smtp_username
+ smtp_password_secret_parameter_key: smtp_password
+ sender: hello@skyvern.com
+ recipients:
+ - "{{recipient_email}}"
+ subject: "Ember Roasters Invoices from {{start_date}} to {{end_date}}"
+ body: "{{for_2_block_output}}"
+ file_attachments:
+ - SKYVERN_DOWNLOAD_DIRECTORY
+```
+
+
+### Complete workflow definition
+
+Save this complete definition to `invoice-workflow.yaml` (or `.json`) before running.
+
+
+
+
+```json JSON
+{
+ "title": "Bulk Invoice Downloader",
+ "description": "Download invoices from vendor portals, parse PDFs to extract amounts, and email a summary with attachments.",
+ "proxy_location": "RESIDENTIAL",
+ "webhook_callback_url": "",
+ "persist_browser_session": false,
+ "workflow_definition": {
+ "version": 1,
+ "parameters": [
+ { "key": "portal_url", "parameter_type": "workflow", "workflow_parameter_type": "string" },
+ { "key": "start_date", "parameter_type": "workflow", "workflow_parameter_type": "string" },
+ { "key": "end_date", "parameter_type": "workflow", "workflow_parameter_type": "string" },
+ { "key": "recipient_email", "parameter_type": "workflow", "workflow_parameter_type": "string" },
+ { "key": "credentials", "parameter_type": "workflow", "workflow_parameter_type": "credential_id", "default_value": "your-credential-id" },
+ { "key": "smtp_host", "parameter_type": "aws_secret", "aws_key": "SKYVERN_SMTP_HOST_AWS_SES" },
+ { "key": "smtp_port", "parameter_type": "aws_secret", "aws_key": "SKYVERN_SMTP_PORT_AWS_SES" },
+ { "key": "smtp_username", "parameter_type": "aws_secret", "aws_key": "SKYVERN_SMTP_USERNAME_SES" },
+ { "key": "smtp_password", "parameter_type": "aws_secret", "aws_key": "SKYVERN_SMTP_PASSWORD_SES" }
+ ],
+ "blocks": [
+ {
+ "block_type": "login",
+ "label": "login_block",
+ "url": "{{portal_url}}",
+ "title": "login_block",
+ "parameter_keys": ["credentials"],
+ "navigation_goal": "Log in using the provided credentials.\nHandle any cookie consent popups.\nCOMPLETE when on the account dashboard or orders page.",
+ "error_code_mapping": {
+ "INVALID_CREDENTIALS": "Login failed - incorrect email or password",
+ "ACCOUNT_LOCKED": "Account has been locked or suspended"
+ },
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "navigation",
+ "label": "nav_block",
+ "url": "",
+ "title": "nav_block",
+ "engine": "skyvern-1.0",
+ "parameter_keys": ["start_date", "end_date"],
+ "navigation_goal": "Navigate to Order History or My Orders.\nFilter orders between {{ start_date }} and {{ end_date }}.\nClick the Filter button.\nCOMPLETE when filtered results are visible.",
+ "max_retries": 0
+ },
+ {
+ "block_type": "extraction",
+ "label": "data_extraction_block",
+ "url": "",
+ "title": "data_extraction_block",
+ "data_extraction_goal": "Extract all visible orders: order ID, date, total amount, and status.",
+ "data_schema": {
+ "orders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "order_id": { "type": "string", "description": "Unique identifier for the order" },
+ "date": { "type": "string", "description": "Date when the order was placed" },
+ "total": { "type": "number", "description": "Total amount for the order" },
+ "status": { "type": "string", "description": "Current status of the order" }
+ },
+ "required": ["order_id", "date", "total", "status"]
+ }
+ }
+ },
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "for_loop",
+ "label": "for_1_block",
+ "loop_variable_reference": "{{data_extraction_block_output.orders}}",
+ "continue_on_failure": true,
+ "next_loop_on_failure": true,
+ "complete_if_empty": true,
+ "loop_blocks": [
+ {
+ "block_type": "file_download",
+ "label": "inv_download_block",
+ "url": "",
+ "title": "inv_download_block",
+ "navigation_goal": "Find order {{ current_value.order_id }}.\nClick Download Invoice.\nCOMPLETE when the PDF download starts.",
+ "download_suffix": "invoice_{{ current_value.order_id }}.pdf",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ }
+ ]
+ },
+ {
+ "block_type": "for_loop",
+ "label": "for_2_block",
+ "loop_variable_reference": "{{data_extraction_block_output.orders}}",
+ "continue_on_failure": true,
+ "next_loop_on_failure": true,
+ "complete_if_empty": true,
+ "loop_blocks": [
+ {
+ "block_type": "file_url_parser",
+ "label": "parse_block",
+ "file_url": "SKYVERN_DOWNLOAD_DIRECTORY/invoice_{{ current_value.order_id }}.pdf",
+ "file_type": "pdf",
+ "json_schema": {
+ "type": "object",
+ "properties": {
+ "invoice_id": { "type": "string", "description": "Unique identifier for the invoice" },
+ "amount": { "type": "number", "description": "Total amount of the invoice" },
+ "date": { "type": "string", "description": "Date of the invoice, typically in YYYY-MM-DD format" }
+ },
+ "required": ["invoice_id", "amount", "date"]
+ }
+ }
+ ]
+ },
+ {
+ "block_type": "send_email",
+ "label": "email_block",
+ "smtp_host_secret_parameter_key": "smtp_host",
+ "smtp_port_secret_parameter_key": "smtp_port",
+ "smtp_username_secret_parameter_key": "smtp_username",
+ "smtp_password_secret_parameter_key": "smtp_password",
+ "sender": "hello@skyvern.com",
+ "recipients": ["{{recipient_email}}"],
+ "subject": "Ember Roasters Invoices from {{start_date}} to {{end_date}}",
+ "body": "{{for_2_block_output}}",
+ "file_attachments": ["SKYVERN_DOWNLOAD_DIRECTORY"]
+ }
+ ]
+ }
+}
+```
+```yaml YAML
+title: Bulk Invoice Downloader
+description: "Download invoices from vendor portals, parse PDFs to extract amounts, and email a summary with attachments."
+proxy_location: RESIDENTIAL
+webhook_callback_url: ""
+persist_browser_session: false
+workflow_definition:
+ version: 1
+ parameters:
+ - key: portal_url
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: start_date
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: end_date
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: recipient_email
+ parameter_type: workflow
+ workflow_parameter_type: string
+ - key: credentials
+ parameter_type: workflow
+ workflow_parameter_type: credential_id
+ default_value: your-credential-id # <-- replace this
+ - key: smtp_host
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_HOST_AWS_SES
+ - key: smtp_port
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_PORT_AWS_SES
+ - key: smtp_username
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_USERNAME_SES
+ - key: smtp_password
+ parameter_type: aws_secret
+ aws_key: SKYVERN_SMTP_PASSWORD_SES
+
+ blocks:
+ - block_type: login
+ label: login_block
+ url: "{{portal_url}}"
+ title: login_block
+ parameter_keys:
+ - credentials
+ navigation_goal: |
+ Log in using the provided credentials.
+ Handle any cookie consent popups.
+ COMPLETE when on the account dashboard or orders page.
+ error_code_mapping:
+ INVALID_CREDENTIALS: Login failed - incorrect email or password
+ ACCOUNT_LOCKED: Account has been locked or suspended
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: navigation
+ label: nav_block
+ url: ""
+ title: nav_block
+ engine: skyvern-1.0
+ parameter_keys:
+ - start_date
+ - end_date
+ navigation_goal: |
+ Navigate to Order History or My Orders.
+ Filter orders between {{ start_date }} and {{ end_date }}.
+ Click the Filter button.
+ COMPLETE when filtered results are visible.
+ max_retries: 0
+
+ - block_type: extraction
+ label: data_extraction_block
+ url: ""
+ title: data_extraction_block
+ data_extraction_goal: "Extract all visible orders: order ID, date, total amount, and status."
+ data_schema:
+ orders:
+ type: array
+ items:
+ type: object
+ properties:
+ order_id:
+ type: string
+ description: Unique identifier for the order
+ date:
+ type: string
+ description: Date when the order was placed
+ total:
+ type: number
+ description: Total amount for the order
+ status:
+ type: string
+ description: Current status of the order
+ required:
+ - order_id
+ - date
+ - total
+ - status
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: for_loop
+ label: for_1_block
+ loop_variable_reference: "{{data_extraction_block_output.orders}}"
+ continue_on_failure: true
+ next_loop_on_failure: true
+ complete_if_empty: true
+ loop_blocks:
+ - block_type: file_download
+ label: inv_download_block
+ url: ""
+ title: inv_download_block
+ navigation_goal: |
+ Find order {{ current_value.order_id }}.
+ Click Download Invoice.
+ COMPLETE when the PDF download starts.
+ download_suffix: invoice_{{ current_value.order_id }}.pdf
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: for_loop
+ label: for_2_block
+ loop_variable_reference: "{{data_extraction_block_output.orders}}"
+ continue_on_failure: true
+ next_loop_on_failure: true
+ complete_if_empty: true
+ loop_blocks:
+ - block_type: file_url_parser
+ label: parse_block
+ file_url: SKYVERN_DOWNLOAD_DIRECTORY/invoice_{{ current_value.order_id }}.pdf
+ file_type: pdf
+ json_schema:
+ type: object
+ properties:
+ invoice_id:
+ type: string
+ description: Unique identifier for the invoice
+ amount:
+ type: number
+ description: Total amount of the invoice
+ date:
+ type: string
+ description: Date of the invoice, typically in YYYY-MM-DD format
+ required:
+ - invoice_id
+ - amount
+ - date
+
+ - block_type: send_email
+ label: email_block
+ smtp_host_secret_parameter_key: smtp_host
+ smtp_port_secret_parameter_key: smtp_port
+ smtp_username_secret_parameter_key: smtp_username
+ smtp_password_secret_parameter_key: smtp_password
+ sender: hello@skyvern.com
+ recipients:
+ - "{{recipient_email}}"
+ subject: "Ember Roasters Invoices from {{start_date}} to {{end_date}}"
+ body: "{{for_2_block_output}}"
+ file_attachments:
+ - SKYVERN_DOWNLOAD_DIRECTORY
+```
+
+
+
+---
+
+## Step 4: Run the workflow
+
+Create the workflow from your definition file and execute it using the SDK.
+
+
+```python Python
+import os
+import asyncio
+from skyvern import Skyvern
+
+async def main():
+ client = Skyvern(api_key=os.getenv("SKYVERN_API_KEY"))
+
+ # Create workflow from YAML file
+ workflow = await client.create_workflow(
+ yaml_definition=open("invoice-workflow.yaml").read()
+ )
+ print(f"Created workflow: {workflow.workflow_permanent_id}")
+
+ # Run the workflow
+ run = await client.run_workflow(
+ workflow_id=workflow.workflow_permanent_id,
+ parameters={
+ "portal_url": "https://ember-roasters.vercel.app",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "recipient_email": "your-email@company.com" # <-- replace this
+ }
+ )
+ print(f"Started run: {run.run_id}")
+
+ # Poll for completion
+ while True:
+ result = await client.get_run(run.run_id)
+ if result.status in ["completed", "failed", "terminated"]:
+ break
+ print(f"Status: {result.status}")
+ await asyncio.sleep(10)
+
+ print(f"Final status: {result.status}")
+ if result.status == "completed":
+ print("Invoices downloaded and email sent successfully")
+
+asyncio.run(main())
+```
+
+```typescript TypeScript
+import { SkyvernClient } from "@skyvern/client";
+import * as fs from "fs";
+
+async function main() {
+ const client = new SkyvernClient({
+ apiKey: process.env.SKYVERN_API_KEY,
+ });
+
+ // Create workflow from YAML file
+ const workflow = await client.createWorkflow({
+ body: {
+ yaml_definition: fs.readFileSync("invoice-workflow.yaml", "utf-8"),
+ },
+ });
+ console.log(`Created workflow: ${workflow.workflow_permanent_id}`);
+
+ // Run the workflow
+ const run = await client.runWorkflow({
+ body: {
+ workflow_id: workflow.workflow_permanent_id,
+ parameters: {
+ portal_url: "https://ember-roasters.vercel.app",
+ start_date: "2025-01-01",
+ end_date: "2025-01-31",
+ recipient_email: "your-email@company.com", // <-- replace this
+ },
+ },
+ });
+ console.log(`Started run: ${run.run_id}`);
+
+ // Poll for completion
+ while (true) {
+ const result = await client.getRun(run.run_id);
+ if (["completed", "failed", "terminated"].includes(result.status)) {
+ console.log(`Final status: ${result.status}`);
+ if (result.status === "completed") {
+ console.log("Invoices downloaded and email sent successfully");
+ }
+ break;
+ }
+ console.log(`Status: ${result.status}`);
+ await new Promise((r) => setTimeout(r, 10000));
+ }
+}
+
+main();
+```
+
+```bash cURL
+# Create workflow
+WORKFLOW=$(curl -s -X POST "https://api.skyvern.com/v1/workflows" \
+ -H "x-api-key: $SKYVERN_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d "{\"yaml_definition\": $(cat invoice-workflow.yaml | jq -Rs .)}")
+
+WORKFLOW_ID=$(echo "$WORKFLOW" | jq -r '.workflow_permanent_id')
+echo "Created workflow: $WORKFLOW_ID"
+
+# Run workflow (replace parameter values below)
+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\",
+ \"parameters\": {
+ \"portal_url\": \"https://ember-roasters.vercel.app\",
+ \"start_date\": \"2025-01-01\",
+ \"end_date\": \"2025-01-31\",
+ \"recipient_email\": \"your-email@company.com\"
+ }
+ }")
+
+RUN_ID=$(echo "$RUN" | jq -r '.run_id')
+echo "Started run: $RUN_ID"
+
+# Poll for completion
+while true; do
+ RESULT=$(curl -s "https://api.skyvern.com/v1/runs/$RUN_ID" \
+ -H "x-api-key: $SKYVERN_API_KEY")
+ STATUS=$(echo "$RESULT" | jq -r '.status')
+ echo "Status: $STATUS"
+
+ if [[ "$STATUS" == "completed" || "$STATUS" == "failed" || "$STATUS" == "terminated" ]]; then
+ echo "Workflow finished with status: $STATUS"
+ break
+ fi
+ sleep 10
+done
+```
+
+
+---
+
+## Resources
+
+
+
+ Complete parameter reference for all block types
+
+
+ Securely store and use login credentials
+
+
+ Download, parse, and upload files in workflows
+
+
+ Handle failures and retries in production
+
+
diff --git a/docs/cookbooks/healthcare-portal-data.mdx b/docs/cookbooks/healthcare-portal-data.mdx
new file mode 100644
index 00000000..dbc72113
--- /dev/null
+++ b/docs/cookbooks/healthcare-portal-data.mdx
@@ -0,0 +1,1099 @@
+---
+title: Healthcare Portal Data Extraction
+subtitle: Extract patient demographics and billing data from OpenEMR
+slug: cookbooks/healthcare-portal-data
+---
+
+This cookbook extracts two datasets from [OpenEMR](https://www.open-emr.org/), an open-source EHR, using the public demo at `https://demo.openemr.io/openemr/index.php`:
+
+1. **Patient demographics** from Patient/Client > Finder
+2. **Encounter billing data** from Reports > Visits > Superbill
+
+**Demo credentials:** `admin` / `pass` (resets daily at 8:00 AM UTC)
+
+---
+
+## Prerequisites
+
+- A [Skyvern Cloud](https://app.skyvern.com) account or [self-hosted](/self-hosted/overview) deployment
+- The Skyvern SDK (for API usage)
+
+
+```bash Python
+pip install skyvern
+```
+
+```bash TypeScript
+npm install @skyvern/client
+```
+
+
+---
+
+## Why a single task isn't enough
+
+A basic task pointed at OpenEMR with a vague prompt will partially work, but hits four problems in production:
+
+
+```python Python
+result = await client.run_task(
+ url="https://demo.openemr.io/openemr/index.php",
+ prompt="Log in and extract the patient list",
+)
+```
+
+```typescript TypeScript
+const result = await client.runTask({
+ body: {
+ url: "https://demo.openemr.io/openemr/index.php",
+ prompt: "Log in and extract the patient list",
+ },
+});
+```
+
+
+| Problem | Impact |
+|---------|--------|
+| No proxy | Production EHR portals sit behind WAFs that block datacenter IPs |
+| Login every run | Wastes steps, fragile with session complexity |
+| Vague navigation | OpenEMR uses iframes and dynamic menus — needs explicit goals |
+| No pagination | Only gets page 1 of multi-page results |
+
+The sections below solve each one.
+
+---
+
+## Residential proxies
+
+Route the browser through a residential IP to bypass WAF/bot detection. The demo works without one, but production portals require it.
+
+
+
+ In the run panel, expand **Advanced Settings** and set **Proxy Location** to a country (e.g., **United States**).
+
+
+
+ ```python Python
+ result = await client.run_task(
+ url="https://demo.openemr.io/openemr/index.php",
+ prompt="Log in with username 'admin' and password 'pass', confirm the Calendar page loads",
+ proxy_location="RESIDENTIAL",
+ )
+ ```
+
+ ```typescript TypeScript
+ const result = await client.runTask({
+ body: {
+ url: "https://demo.openemr.io/openemr/index.php",
+ prompt: "Log in with username 'admin' and password 'pass', confirm the Calendar page loads",
+ proxy_location: "RESIDENTIAL",
+ },
+ });
+ ```
+
+
+
+
+See [Proxy & Geolocation](/going-to-production/proxy-geolocation) for all available locations.
+
+---
+
+## Browser profiles
+
+Log in once, save the browser state as a profile, and skip login on future runs.
+
+
+
+
+
+ Go to **Workflows** in the sidebar and create a new workflow. Add a **Navigation** block with URL `https://demo.openemr.io/openemr/index.php` and goal: "Log in with username 'admin' and password 'pass'. Confirm the Calendar page loads."
+
+ On the **Start** node, expand the settings and enable **Save & Reuse Session**. Set **Proxy Location** to a country (e.g., **United States**).
+
+
+
+
+
+
+ Run the workflow and wait for it to complete.
+
+
+ Browser profile creation is done via the API. Use the `create_browser_profile` call from the API/SDK tab with the completed workflow run ID. Name it `openemr-demo-admin`.
+
+
+
+
+
+ ```python Python
+ import asyncio
+ from skyvern import Skyvern
+
+ async def main():
+ client = Skyvern(api_key="YOUR_API_KEY")
+
+ # 1. Create workflow that saves browser state
+ workflow = await client.create_workflow(
+ json_definition={
+ "title": "OpenEMR Login",
+ "persist_browser_session": True,
+ "workflow_definition": {
+ "parameters": [],
+ "blocks": [
+ {
+ "block_type": "navigation",
+ "label": "login",
+ "url": "https://demo.openemr.io/openemr/index.php",
+ "navigation_goal": (
+ "Log in with username 'admin' and password 'pass'. "
+ "Confirm the Calendar page or main dashboard loads."
+ ),
+ }
+ ],
+ },
+ }
+ )
+
+ # 2. Run with residential proxy
+ run = await client.run_workflow(
+ workflow_id=workflow.workflow_permanent_id,
+ proxy_location="RESIDENTIAL",
+ wait_for_completion=True,
+ )
+ print(f"Login: {run.status}") # completed
+
+ # 3. Save profile (retry while session archives)
+ profile = None
+ for attempt in range(10):
+ try:
+ profile = await client.create_browser_profile(
+ name="openemr-demo-admin",
+ workflow_run_id=run.run_id,
+ )
+ break
+ except Exception as e:
+ if "persisted" in str(e).lower() and attempt < 9:
+ await asyncio.sleep(2)
+ continue
+ raise
+
+ print(f"Profile: {profile.browser_profile_id}")
+
+ asyncio.run(main())
+ ```
+
+ ```typescript TypeScript
+ import { Skyvern } from "@skyvern/client";
+
+ async function main() {
+ const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
+
+ // 1. Create workflow that saves browser state
+ const workflow = await client.createWorkflow({
+ body: {
+ json_definition: {
+ title: "OpenEMR Login",
+ persist_browser_session: true,
+ workflow_definition: {
+ parameters: [],
+ blocks: [
+ {
+ block_type: "navigation",
+ label: "login",
+ url: "https://demo.openemr.io/openemr/index.php",
+ navigation_goal:
+ "Log in with username 'admin' and password 'pass'. " +
+ "Confirm the Calendar page or main dashboard loads.",
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ // 2. Run with residential proxy
+ const run = await client.runWorkflow({
+ body: {
+ workflow_id: workflow.workflow_permanent_id,
+ proxy_location: "RESIDENTIAL",
+ },
+ waitForCompletion: true,
+ });
+ console.log(`Login: ${run.status}`);
+
+ // 3. Save profile (retry while session archives)
+ let profile;
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ profile = await client.createBrowserProfile({
+ name: "openemr-demo-admin",
+ workflow_run_id: run.run_id,
+ });
+ break;
+ } catch (e) {
+ if (String(e).toLowerCase().includes("persisted") && attempt < 9) {
+ await new Promise((r) => setTimeout(r, 2000));
+ continue;
+ }
+ throw e;
+ }
+ }
+
+ console.log(`Profile: ${profile.browser_profile_id}`);
+ }
+
+ main();
+ ```
+
+
+
+
+
+`persist_browser_session` is a workflow definition property — set it when creating the workflow, not when running it. See [Browser Profiles](/optimization/browser-profiles) for the full lifecycle.
+
+
+---
+
+## Extract patient demographics
+
+Navigate to **Patient/Client > Finder** and extract the results table.
+
+
+
+ Create a workflow with two blocks:
+
+ 1. **Navigation** block — URL: `https://demo.openemr.io/openemr/index.php`. Goal: "Click Patient/Client in the top menu, then click Finder. Click Search to display all patients."
+ 2. **Extraction** block — Goal: "Extract all patient rows from the Patient Finder results table." Paste the patient schema into **Data Schema**.
+
+ On the **Start** node, set **Proxy Location** to a country (e.g., **United States**). Run the workflow.
+
+
+
+ ```python Python
+ import asyncio
+ from skyvern import Skyvern
+
+ PATIENT_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "patients": {
+ "type": "array",
+ "description": "Patient rows from the Patient Finder results table",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "Patient full name (Last, First)"},
+ "pid": {"type": "string", "description": "Patient ID number"},
+ "dob": {"type": "string", "description": "Date of birth (YYYY-MM-DD)"},
+ "phone_home": {"type": "string", "description": "Home phone number"},
+ },
+ },
+ },
+ },
+ }
+
+ async def main():
+ client = Skyvern(api_key="YOUR_API_KEY")
+
+ run = await client.run_task(
+ url="https://demo.openemr.io/openemr/index.php",
+ prompt=(
+ "Click Patient/Client in the top menu, then click Finder. "
+ "Click Search to display all patients."
+ ),
+ data_extraction_schema=PATIENT_SCHEMA,
+ proxy_location="RESIDENTIAL",
+ browser_session_id="YOUR_SESSION_ID",
+ )
+
+ while run.status not in ["completed", "failed", "terminated", "timed_out", "canceled"]:
+ await asyncio.sleep(5)
+ run = await client.get_run(run.run_id)
+
+ print(run.output)
+
+ asyncio.run(main())
+ ```
+
+ ```typescript TypeScript
+ import { Skyvern } from "@skyvern/client";
+
+ const PATIENT_SCHEMA = {
+ type: "object",
+ properties: {
+ patients: {
+ type: "array",
+ description: "Patient rows from the Patient Finder results table",
+ items: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Patient full name (Last, First)" },
+ pid: { type: "string", description: "Patient ID number" },
+ dob: { type: "string", description: "Date of birth (YYYY-MM-DD)" },
+ phone_home: { type: "string", description: "Home phone number" },
+ },
+ },
+ },
+ },
+ } as const;
+
+ async function main() {
+ const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
+
+ let run = await client.runTask({
+ body: {
+ url: "https://demo.openemr.io/openemr/index.php",
+ prompt:
+ "Click Patient/Client in the top menu, then click Finder. " +
+ "Click Search to display all patients.",
+ data_extraction_schema: PATIENT_SCHEMA,
+ proxy_location: "RESIDENTIAL",
+ browser_session_id: "YOUR_SESSION_ID",
+ },
+ });
+
+ while (!["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
+ await new Promise((r) => setTimeout(r, 5000));
+ run = await client.getRun(run.run_id);
+ }
+
+ console.log(JSON.stringify(run.output, null, 2));
+ }
+
+ main();
+ ```
+
+
+
+
+**Example output:**
+
+```json
+{
+ "patients": [
+ { "name": "Belford, Phil", "pid": "1", "dob": "1972-02-09", "phone_home": "333-444-2222" },
+ { "name": "Underwood, Susan Ardmore", "pid": "2", "dob": "1967-02-08", "phone_home": "4443332222" },
+ { "name": "Moore, Wanda", "pid": "3", "dob": "2007-02-18", "phone_home": null }
+ ]
+}
+```
+
+
+The demo resets daily and community users add test patients, so exact records may differ.
+
+
+
+Browser profiles cannot be used directly with standalone tasks. Create a [browser session](/optimization/browser-sessions) from the profile first, then pass the session ID. See [Pagination with browser sessions](#pagination-with-browser-sessions) below for the full pattern.
+
+
+---
+
+## Extract encounter billing data
+
+Navigate to **Reports > Visits > Superbill**, set a date range, and extract the report.
+
+
+
+ Create a workflow with two blocks:
+
+ 1. **Navigation** block — Goal: "Click Reports in the top menu, then Visits, then Superbill. Set the From date to 2020-01-01 and the To date to today. Click Submit."
+ 2. **Extraction** block — Goal: "Extract all encounter rows from the Superbill report." Paste the encounter schema into **Data Schema**.
+
+ On the **Start** node, set **Proxy Location** to a country (e.g., **United States**). Run the workflow.
+
+
+
+ ```python Python
+ ENCOUNTER_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "encounters": {
+ "type": "array",
+ "description": "Encounter rows from the Superbill report",
+ "items": {
+ "type": "object",
+ "properties": {
+ "patient_name": {"type": "string", "description": "Patient name"},
+ "encounter_date": {"type": "string", "description": "Date of encounter (YYYY-MM-DD)"},
+ "provider": {"type": "string", "description": "Provider name"},
+ "billing_code": {"type": "string", "description": "CPT or billing code"},
+ "code_description": {"type": "string", "description": "Description of the billing code"},
+ "charge": {"type": "number", "description": "Fee amount in USD"},
+ },
+ },
+ },
+ },
+ }
+
+ run = await client.run_task(
+ url="https://demo.openemr.io/openemr/index.php",
+ prompt=(
+ "Click Reports in the top menu, then Visits, then Superbill. "
+ "Set the From date to 2020-01-01 and the To date to today. Click Submit."
+ ),
+ data_extraction_schema=ENCOUNTER_SCHEMA,
+ proxy_location="RESIDENTIAL",
+ browser_session_id="YOUR_SESSION_ID",
+ )
+ ```
+
+ ```typescript TypeScript
+ const ENCOUNTER_SCHEMA = {
+ type: "object",
+ properties: {
+ encounters: {
+ type: "array",
+ description: "Encounter rows from the Superbill report",
+ items: {
+ type: "object",
+ properties: {
+ patient_name: { type: "string", description: "Patient name" },
+ encounter_date: { type: "string", description: "Date of encounter (YYYY-MM-DD)" },
+ provider: { type: "string", description: "Provider name" },
+ billing_code: { type: "string", description: "CPT or billing code" },
+ code_description: { type: "string", description: "Description of the billing code" },
+ charge: { type: "number", description: "Fee amount in USD" },
+ },
+ },
+ },
+ },
+ } as const;
+
+ let run = await client.runTask({
+ body: {
+ url: "https://demo.openemr.io/openemr/index.php",
+ prompt:
+ "Click Reports in the top menu, then Visits, then Superbill. " +
+ "Set the From date to 2020-01-01 and the To date to today. Click Submit.",
+ data_extraction_schema: ENCOUNTER_SCHEMA,
+ proxy_location: "RESIDENTIAL",
+ browser_session_id: "YOUR_SESSION_ID",
+ },
+ });
+ ```
+
+
+
+
+**Example output:**
+
+```json
+{
+ "encounters": [
+ {
+ "patient_name": "Phil Lopez",
+ "encounter_date": "2024-06-01",
+ "provider": "Administrator Administrator",
+ "billing_code": "99213",
+ "code_description": "Office/outpatient visit, est patient, low complexity",
+ "charge": 50.00
+ }
+ ]
+}
+```
+
+---
+
+## Pagination with browser sessions
+
+A [Browser Profile](/optimization/browser-profiles) is a saved snapshot. A [Browser Session](/optimization/browser-sessions) is a live browser instance that persists between tasks. Use sessions to paginate: extract page 1, click Next, extract page 2.
+
+
+
+
+
+ Go to **Browsers** in the sidebar. Click **Create Session**. Set **Proxy Location** to a country (e.g., **United States**) and configure the timeout.
+
+
+
+
+
+
+ Run a task against the session: "Click Patient/Client > Finder. Click Search. Extract all patient rows."
+
+
+ Run another task against the same session: "Click Next to go to the next page. Extract all patient rows." Repeat until no more results.
+
+
+
+
+
+ ```python Python
+ import asyncio
+ from skyvern import Skyvern
+
+ PATIENT_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "patients": {
+ "type": "array",
+ "description": "Patient rows from the current page",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "Patient full name (Last, First)"},
+ "pid": {"type": "string", "description": "Patient ID number"},
+ "dob": {"type": "string", "description": "Date of birth (YYYY-MM-DD)"},
+ "phone_home": {"type": "string", "description": "Home phone number"},
+ },
+ },
+ },
+ },
+ }
+
+ async def extract_page(client, session_id, page_num):
+ prompt = (
+ "Click Patient/Client in the top menu, then click Finder. "
+ "Click Search to display all patients. "
+ "Extract all patient rows from the results table."
+ ) if page_num == 1 else (
+ "Click Next to go to the next page of results. "
+ "Extract all patient rows from the table."
+ )
+
+ run = await client.run_task(
+ url="https://demo.openemr.io/openemr/index.php",
+ prompt=prompt,
+ browser_session_id=session_id,
+ data_extraction_schema=PATIENT_SCHEMA,
+ )
+
+ while run.status not in ["completed", "failed", "terminated", "timed_out", "canceled"]:
+ await asyncio.sleep(5)
+ run = await client.get_run(run.run_id)
+
+ return run
+
+ async def main():
+ client = Skyvern(api_key="YOUR_API_KEY")
+
+ session = await client.create_browser_session(
+ browser_profile_id="YOUR_PROFILE_ID",
+ proxy_location="RESIDENTIAL",
+ )
+
+ all_patients = []
+ for page in range(1, 11):
+ run = await extract_page(client, session.browser_session_id, page)
+
+ if run.status != "completed":
+ break
+
+ patients = run.output.get("patients", [])
+ if not patients:
+ break
+
+ all_patients.extend(patients)
+ print(f"Page {page}: {len(patients)} patients ({len(all_patients)} total)")
+
+ print(f"Done: {len(all_patients)} patients")
+
+ asyncio.run(main())
+ ```
+
+ ```typescript TypeScript
+ import { Skyvern } from "@skyvern/client";
+
+ const PATIENT_SCHEMA = {
+ type: "object",
+ properties: {
+ patients: {
+ type: "array",
+ description: "Patient rows from the current page",
+ items: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Patient full name (Last, First)" },
+ pid: { type: "string", description: "Patient ID number" },
+ dob: { type: "string", description: "Date of birth (YYYY-MM-DD)" },
+ phone_home: { type: "string", description: "Home phone number" },
+ },
+ },
+ },
+ },
+ } as const;
+
+ async function extractPage(client: Skyvern, sessionId: string, pageNum: number) {
+ const prompt =
+ pageNum === 1
+ ? "Click Patient/Client in the top menu, then click Finder. " +
+ "Click Search to display all patients. " +
+ "Extract all patient rows from the results table."
+ : "Click Next to go to the next page of results. " +
+ "Extract all patient rows from the table.";
+
+ let run = await client.runTask({
+ body: {
+ url: "https://demo.openemr.io/openemr/index.php",
+ prompt,
+ browser_session_id: sessionId,
+ data_extraction_schema: PATIENT_SCHEMA,
+ },
+ });
+
+ while (!["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
+ await new Promise((r) => setTimeout(r, 5000));
+ run = await client.getRun(run.run_id);
+ }
+
+ return run;
+ }
+
+ async function main() {
+ const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
+
+ const session = await client.createBrowserSession({
+ browser_profile_id: "YOUR_PROFILE_ID",
+ proxy_location: "RESIDENTIAL",
+ });
+
+ const allPatients: any[] = [];
+ for (let page = 1; page <= 10; page++) {
+ const run = await extractPage(client, session.browser_session_id, page);
+
+ if (run.status !== "completed") break;
+
+ const patients = run.output?.patients ?? [];
+ if (patients.length === 0) break;
+
+ allPatients.push(...patients);
+ console.log(`Page ${page}: ${patients.length} patients (${allPatients.length} total)`);
+ }
+
+ console.log(`Done: ${allPatients.length} patients`);
+ }
+
+ main();
+ ```
+
+
+ **Expected output:**
+
+ ```
+ Page 1: 25 patients (25 total)
+ Page 2: 18 patients (43 total)
+ Done: 43 patients
+ ```
+
+
+
+
+You cannot use `browser_profile_id` and `browser_session_id` in the same request. Use the profile to create the session, then pass only the session ID to tasks.
+
+
+---
+
+## Error handling
+
+OpenEMR can timeout or show session-expired pages. Use `error_code_mapping` on workflow blocks to classify failures, and `max_retries` to retry automatically.
+
+
+
+ On each Navigation and Extraction block, expand **Advanced Settings** and enable **Error Messages**. Add this JSON:
+
+ ```json
+ { "session_expired": "Session expired, login required, or access denied page" }
+ ```
+
+
+
+
+
+
+ `max_retries` is only available via the API. In the Cloud UI, Skyvern uses its default retry behavior. For fine-grained retry control, use the API/SDK approach.
+
+
+
+ Set `error_code_mapping` and `max_retries` directly on workflow blocks:
+
+ ```json
+ {
+ "block_type": "extraction",
+ "label": "extract_patients",
+ "data_extraction_goal": "Extract all patient rows from the table",
+ "error_code_mapping": {
+ "session_expired": "Session expired, login required, or access denied page"
+ },
+ "max_retries": 3
+ }
+ ```
+
+ For standalone tasks, handle retries in your calling code:
+
+
+ ```python Python
+ async def run_with_retry(client, session_id, page_num, max_retries=3):
+ for attempt in range(max_retries + 1):
+ run = await extract_page(client, session_id, page_num)
+ if run.status == "completed":
+ return run
+
+ is_session_error = "session" in (run.failure_reason or "").lower()
+ if is_session_error and attempt < max_retries:
+ await asyncio.sleep(2 ** attempt * 5)
+ continue
+
+ return run
+ return run
+ ```
+
+ ```typescript TypeScript
+ async function runWithRetry(
+ client: Skyvern, sessionId: string, pageNum: number, maxRetries = 3
+ ) {
+ let run;
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
+ run = await extractPage(client, sessionId, pageNum);
+ if (run.status === "completed") return run;
+
+ const isSessionError = (run.failure_reason ?? "").toLowerCase().includes("session");
+ if (isSessionError && attempt < maxRetries) {
+ await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 5000));
+ continue;
+ }
+ return run;
+ }
+ return run!;
+ }
+ ```
+
+
+
+
+See [Error Handling](/going-to-production/error-handling) and [CAPTCHA & Bot Detection](/going-to-production/captcha-bot-detection) for more.
+
+---
+
+## Complete workflow
+
+This workflow combines everything: navigate to the Patient Finder, extract demographics, navigate to Superbill, and extract billing data — with error recovery and residential proxy.
+
+
+
+
+
+ Go to **Workflows** and create a new workflow named "OpenEMR Daily Extract." On the **Start** node, enable **Save & Reuse Session** and set **Proxy Location** to a country (e.g., **United States**).
+
+
+ Add a **Navigation** block. Set URL to `https://demo.openemr.io/openemr/index.php` and goal: "Click Patient/Client > Finder. Click Search to display all patients. If a login page appears, log in with 'admin'/'pass'." In **Advanced Settings**, enable **Error Messages** and add `{"session_expired": "Session expired, login required, or access denied page"}`.
+
+
+ Add an **Extraction** block. Set goal: "Extract all patient rows from the Patient Finder results table." Paste the patient schema into **Data Schema**.
+
+
+ Add another **Navigation** block. Set goal: "Click Reports > Visits > Superbill. Set From to 2020-01-01, To to today. Click Submit." Add the same error messages mapping.
+
+
+ Add another **Extraction** block. Set goal: "Extract all encounter rows from the Superbill report." Paste the encounter schema into **Data Schema**.
+
+
+ Click **Run**. The workflow navigates to the Patient Finder, extracts demographics, then navigates to Superbill and extracts billing data.
+
+
+
+ For multi-page results, combine with the [pagination pattern](#pagination-with-browser-sessions) above.
+
+
+
+ ```python Python
+ import asyncio
+ from skyvern import Skyvern
+
+ PATIENT_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "patients": {
+ "type": "array",
+ "description": "Patient rows from the Patient Finder results table",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "Patient full name (Last, First)"},
+ "pid": {"type": "string", "description": "Patient ID number"},
+ "dob": {"type": "string", "description": "Date of birth (YYYY-MM-DD)"},
+ "phone_home": {"type": "string", "description": "Home phone number"},
+ },
+ },
+ },
+ },
+ }
+
+ ENCOUNTER_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "encounters": {
+ "type": "array",
+ "description": "Encounter rows from the Superbill report",
+ "items": {
+ "type": "object",
+ "properties": {
+ "patient_name": {"type": "string", "description": "Patient name"},
+ "encounter_date": {"type": "string", "description": "Date of encounter (YYYY-MM-DD)"},
+ "provider": {"type": "string", "description": "Provider name"},
+ "billing_code": {"type": "string", "description": "CPT or billing code"},
+ "code_description": {"type": "string", "description": "Description of the billing code"},
+ "charge": {"type": "number", "description": "Fee amount in USD"},
+ },
+ },
+ },
+ },
+ }
+
+ SESSION_ERROR = "Session expired, login required, or access denied page"
+
+ async def main():
+ client = Skyvern(api_key="YOUR_API_KEY")
+
+ workflow = await client.create_workflow(
+ json_definition={
+ "title": "OpenEMR Daily Extract",
+ "persist_browser_session": True,
+ "workflow_definition": {
+ "parameters": [],
+ "blocks": [
+ {
+ "block_type": "navigation",
+ "label": "open_patient_finder",
+ "url": "https://demo.openemr.io/openemr/index.php",
+ "navigation_goal": (
+ "Click Patient/Client in the top menu, then click Finder. "
+ "Click Search to display all patients. "
+ "If a login page appears, log in with username 'admin' and password 'pass'."
+ ),
+ "error_code_mapping": {"session_expired": SESSION_ERROR},
+ "max_retries": 3,
+ },
+ {
+ "block_type": "extraction",
+ "label": "extract_patients",
+ "data_extraction_goal": "Extract all patient rows from the Patient Finder results table",
+ "data_schema": PATIENT_SCHEMA,
+ "error_code_mapping": {"session_expired": SESSION_ERROR},
+ "max_retries": 2,
+ },
+ {
+ "block_type": "navigation",
+ "label": "open_superbill",
+ "navigation_goal": (
+ "Click Reports in the top menu, then Visits, then Superbill. "
+ "Set the From date to 2020-01-01 and the To date to today. Click Submit."
+ ),
+ "error_code_mapping": {"session_expired": SESSION_ERROR},
+ "max_retries": 3,
+ },
+ {
+ "block_type": "extraction",
+ "label": "extract_encounters",
+ "data_extraction_goal": "Extract all encounter rows from the Superbill report",
+ "data_schema": ENCOUNTER_SCHEMA,
+ "error_code_mapping": {"session_expired": SESSION_ERROR},
+ "max_retries": 2,
+ },
+ ],
+ },
+ }
+ )
+ print(f"Workflow: {workflow.workflow_permanent_id}")
+
+ run = await client.run_workflow(
+ workflow_id=workflow.workflow_permanent_id,
+ browser_profile_id="YOUR_PROFILE_ID",
+ proxy_location="RESIDENTIAL",
+ wait_for_completion=True,
+ )
+
+ print(f"Status: {run.status}")
+ print(f"Output: {run.output}")
+
+ asyncio.run(main())
+ ```
+
+ ```typescript TypeScript
+ import { Skyvern } from "@skyvern/client";
+
+ const PATIENT_SCHEMA = {
+ type: "object",
+ properties: {
+ patients: {
+ type: "array",
+ description: "Patient rows from the Patient Finder results table",
+ items: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Patient full name (Last, First)" },
+ pid: { type: "string", description: "Patient ID number" },
+ dob: { type: "string", description: "Date of birth (YYYY-MM-DD)" },
+ phone_home: { type: "string", description: "Home phone number" },
+ },
+ },
+ },
+ },
+ } as const;
+
+ const ENCOUNTER_SCHEMA = {
+ type: "object",
+ properties: {
+ encounters: {
+ type: "array",
+ description: "Encounter rows from the Superbill report",
+ items: {
+ type: "object",
+ properties: {
+ patient_name: { type: "string", description: "Patient name" },
+ encounter_date: { type: "string", description: "Date of encounter (YYYY-MM-DD)" },
+ provider: { type: "string", description: "Provider name" },
+ billing_code: { type: "string", description: "CPT or billing code" },
+ code_description: { type: "string", description: "Description of the billing code" },
+ charge: { type: "number", description: "Fee amount in USD" },
+ },
+ },
+ },
+ },
+ } as const;
+
+ const SESSION_ERROR = "Session expired, login required, or access denied page";
+
+ async function main() {
+ const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
+
+ const workflow = await client.createWorkflow({
+ body: {
+ json_definition: {
+ title: "OpenEMR Daily Extract",
+ persist_browser_session: true,
+ workflow_definition: {
+ parameters: [],
+ blocks: [
+ {
+ block_type: "navigation",
+ label: "open_patient_finder",
+ url: "https://demo.openemr.io/openemr/index.php",
+ navigation_goal:
+ "Click Patient/Client in the top menu, then click Finder. " +
+ "Click Search to display all patients. " +
+ "If a login page appears, log in with username 'admin' and password 'pass'.",
+ error_code_mapping: { session_expired: SESSION_ERROR },
+ max_retries: 3,
+ },
+ {
+ block_type: "extraction",
+ label: "extract_patients",
+ data_extraction_goal: "Extract all patient rows from the Patient Finder results table",
+ data_schema: PATIENT_SCHEMA,
+ error_code_mapping: { session_expired: SESSION_ERROR },
+ max_retries: 2,
+ },
+ {
+ block_type: "navigation",
+ label: "open_superbill",
+ navigation_goal:
+ "Click Reports in the top menu, then Visits, then Superbill. " +
+ "Set the From date to 2020-01-01 and the To date to today. Click Submit.",
+ error_code_mapping: { session_expired: SESSION_ERROR },
+ max_retries: 3,
+ },
+ {
+ block_type: "extraction",
+ label: "extract_encounters",
+ data_extraction_goal: "Extract all encounter rows from the Superbill report",
+ data_schema: ENCOUNTER_SCHEMA,
+ error_code_mapping: { session_expired: SESSION_ERROR },
+ max_retries: 2,
+ },
+ ],
+ },
+ },
+ },
+ });
+ console.log(`Workflow: ${workflow.workflow_permanent_id}`);
+
+ const run = await client.runWorkflow({
+ body: {
+ workflow_id: workflow.workflow_permanent_id,
+ browser_profile_id: "YOUR_PROFILE_ID",
+ proxy_location: "RESIDENTIAL",
+ },
+ waitForCompletion: true,
+ });
+
+ console.log(`Status: ${run.status}`);
+ console.log(`Output: ${JSON.stringify(run.output, null, 2)}`);
+ }
+
+ main();
+ ```
+
+
+
+
+| Technique | Purpose |
+|-----------|---------|
+| Residential proxy | Bypass WAF/bot detection |
+| Browser profile | Skip login on every run |
+| Navigation goals | Explicit menu clicks for iframe-based UI |
+| JSON schemas | Consistent, structured output |
+| Session reuse | Paginate multi-page results |
+| Error mapping + retries | Recover from session timeouts |
+
+
+The OpenEMR demo resets daily at 8:00 AM UTC, so profiles expire every day. In production, re-run your login workflow weekly or whenever extractions fail with auth errors. See [Browser Profiles](/optimization/browser-profiles) for the refresh pattern.
+
+
+---
+
+## Resources
+
+
+
+ Full lifecycle: create, refresh, and delete saved browser state
+
+
+ All proxy locations and country-specific routing options
+
+
+ Securely store and use login credentials
+
+
+ Error code mapping, failure classification, and retry strategies
+
+
+ JSON schema design and the interactive schema builder
+
+
diff --git a/docs/cookbooks/job-application-filler.mdx b/docs/cookbooks/job-application-filler.mdx
new file mode 100644
index 00000000..37a2ac83
--- /dev/null
+++ b/docs/cookbooks/job-application-filler.mdx
@@ -0,0 +1,975 @@
+---
+title: Job Applications Pipeline
+subtitle: Search for jobs and automatically apply using your resume
+slug: cookbooks/job-application-filler
+---
+
+Automate job applications across multiple postings on any job portal.
+
+This cookbook creates a workflow that logs into a job portal, searches for relevant positions based on your resume, extracts job listings, and applies to each one with AI-generated responses tailored to the role.
+
+---
+
+## What you'll build
+
+A workflow that:
+
+1. Parses your resume PDF to extract structured information
+2. Logs into a job portal using saved credentials
+3. Searches for relevant jobs based on your resume
+4. Extracts a list of matching job postings
+5. For each job: extracts details, generates tailored answers, and submits the application
+
+---
+
+## Prerequisites
+
+- **Skyvern Cloud API key** — Get one at [app.skyvern.com/settings](https://app.skyvern.com/settings) → API Keys
+
+Install the SDK:
+
+
+```bash Python
+pip install skyvern
+```
+
+```bash TypeScript
+npm install @skyvern/client
+```
+
+
+Set your API key:
+
+```bash
+export SKYVERN_API_KEY="your-api-key"
+```
+
+---
+
+## Sample Job Portal
+
+We'll use *Job Stash*, a fake job board website created for agent automation testing.
+Change `job_portal_url` to use your job portal's URL.
+
+| Field | Value |
+| -------------- | ---------------------------- |
+| URL | https://job-stash.vercel.app |
+| Login email | demo@manicule.dev |
+| Login password | helloworld |
+
+
+---
+
+## Step 1: Store credentials
+
+Before defining the workflow, store the login email and password Skyvern will use.
+
+This makes sure your passwords are never stored in the shareable workflow definition and never sent to LLMs.
+
+
+```python Python
+import os
+import asyncio
+from skyvern import Skyvern
+
+async def main():
+ client = Skyvern(api_key=os.getenv("SKYVERN_API_KEY"))
+
+ credential = await client.create_credential(
+ name="Job Portal",
+ credential_type="password",
+ credential={
+ "username": "demo@manicule.dev",
+ "password": "helloworld"
+ }
+ )
+
+ print(f"Credential ID: {credential.credential_id}")
+ # Save this ID for your workflow: cred_xxx
+
+asyncio.run(main())
+```
+
+```typescript TypeScript
+import { SkyvernClient } from "@skyvern/client";
+
+const client = new SkyvernClient({
+ apiKey: process.env.SKYVERN_API_KEY,
+});
+
+const credential = await client.createCredential({
+ name: "Job Portal",
+ credential_type: "password",
+ credential: {
+ username: "demo@manicule.dev",
+ password: "helloworld",
+ },
+});
+
+console.log(`Credential ID: ${credential.credential_id}`);
+```
+
+```bash cURL
+curl -X POST "https://api.skyvern.com/v1/credentials" \
+ -H "x-api-key: $SKYVERN_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Job Portal",
+ "credential_type": "password",
+ "credential": {
+ "username": "demo@manicule.dev",
+ "password": "helloworld"
+ }
+ }'
+```
+
+
+---
+
+## Step 2: Define workflow parameters
+
+Parameters are the inputs your workflow accepts. Defining them upfront lets you run the same workflow against different job portals.
+
+This workflow uses the following parameters:
+
+- **`resume`** — URL to your resume PDF.
+- **`credentials`** — The ID of the stored credential to use for login.
+- **`job_portal_url`** — The job portal's login URL.
+
+
+```json JSON
+{
+ "parameters": [
+ {
+ "key": "resume",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "file_url"
+ },
+ {
+ "key": "credentials",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "credential_id",
+ "default_value": "your-credential-id"
+ },
+ {
+ "key": "job_portal_url",
+ "description": "URL of the job portal",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "string"
+ }
+ ]
+}
+```
+
+```yaml YAML
+parameters:
+ - key: resume
+ parameter_type: workflow
+ workflow_parameter_type: file_url
+ - key: credentials
+ parameter_type: workflow
+ workflow_parameter_type: credential_id
+ default_value: your-credential-id # <-- replace this
+ - key: job_portal_url
+ description: URL of the job portal
+ parameter_type: workflow
+ workflow_parameter_type: string
+```
+
+
+---
+
+## Step 3: Create the workflow definition
+
+The workflow chains together several blocks to automate the full job application process:
+
+1. **PDF Parser block** — Extracts structured data from your resume
+2. **Login block** — Authenticates to the job portal using stored credentials
+3. **Navigation block** — Searches for relevant jobs based on your resume
+4. **Extraction block** — Extracts a list of job postings from search results
+5. **For loop** (Go to URL + Extraction + Action + Extraction + Text Prompt + Navigation) — Iterates over each job to extract details, generates tailored answers using an LLM, and submits the application.
+
+We will add these blocks one by one in the **workflow definition**, a YAML/JSON file that contains the complete description of your workflow logic
+
+### Workflow Definition format
+
+
+```json JSON
+{
+ "title": "Search and Apply for Jobs Workflow",
+ "description": "Search for jobs on any portal, extract listings, generate tailored answers with AI, and submit applications automatically.",
+ "proxy_location": "RESIDENTIAL",
+ "workflow_definition": {
+ "version": 1,
+ "parameters": [],
+ "blocks": []
+ }
+}
+```
+
+```yaml YAML
+title: Search and Apply for Jobs Workflow
+description: "Search for jobs on any portal, extract listings, generate tailored answers with AI, and submit applications automatically."
+proxy_location: RESIDENTIAL # <-- defaults to RESIDENTIAL
+workflow_definition:
+ version: 1 # <-- auto-increments when you make changes
+ parameters:
+ ... # <-- defined in Step 2
+
+ blocks:
+ ... # <-- defined one by one in the next steps
+```
+
+
+### PDF Parser block
+
+The `pdf_parser` block extracts structured information from your resume PDF. Skyvern uses AI to identify your name, contact info, work experience, education, and skills.
+
+
+```json JSON
+{
+ "block_type": "pdf_parser",
+ "label": "parsed_resume",
+ "file_url": "{{ resume }}"
+}
+```
+
+```yaml YAML
+- block_type: pdf_parser
+ label: parsed_resume
+ file_url: "{{ resume }}"
+```
+
+
+The parsed data is accessible as `{{ parsed_resume_output }}` in subsequent blocks.
+
+### Login block
+
+The `login` block authenticates using stored credentials. Skyvern injects the username/password directly into form fields without exposing them to the LLM.
+
+
+```json JSON
+{
+ "block_type": "login",
+ "label": "login_to_portal",
+ "url": "{{ job_portal_url }}",
+ "title": "login_to_portal",
+ "parameter_keys": ["credentials"],
+ "navigation_goal": "Log in using the provided credentials.\nHandle any cookie consent popups.\nCOMPLETE when on the account dashboard or orders page.",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+}
+```
+
+```yaml YAML
+- block_type: login
+ label: login_to_portal
+ url: "{{ job_portal_url }}"
+ title: login_to_portal
+ parameter_keys:
+ - credentials
+ navigation_goal: |
+ Log in using the provided credentials.
+ Handle any cookie consent popups.
+ COMPLETE when on the account dashboard or orders page.
+ max_retries: 0
+ engine: skyvern-1.0
+```
+
+
+### Search for jobs block
+
+Navigate to the job search and find relevant positions based on the parsed resume.
+
+
+```json JSON
+{
+ "block_type": "navigation",
+ "label": "search_for_job",
+ "url": "",
+ "title": "search_for_job",
+ "engine": "skyvern-1.0",
+ "navigation_goal": "Search for a relevant job based on the parsed resume: {{parsed_resume_output}}",
+ "max_retries": 0
+}
+```
+
+```yaml YAML
+- block_type: navigation
+ label: search_for_job
+ url: ""
+ title: search_for_job
+ engine: skyvern-1.0
+ navigation_goal: "Search for a relevant job based on the parsed resume: {{parsed_resume_output}}"
+ max_retries: 0
+```
+
+
+### Extract jobs list block
+
+Extract job postings from the search results. The `data_schema` tells Skyvern exactly what structure to return.
+
+
+```json JSON
+{
+ "block_type": "extraction",
+ "label": "jobs_list_extraction",
+ "url": "",
+ "title": "jobs_list_extraction",
+ "data_extraction_goal": "Extract all visible job profiles: job page url, title, employer, location, and type (full time, part time, etc)",
+ "data_schema": {
+ "jobs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "job_page_url": {
+ "type": "string",
+ "description": "Link to the apply page for the job"
+ },
+ "title": {
+ "type": "string",
+ "description": "Title of the role offered"
+ },
+ "employer": {
+ "type": "string",
+ "description": "Name of the company posting the job profile"
+ },
+ "location": {
+ "type": "string",
+ "description": "Where is the job role based. City and country,"
+ },
+ "type": {
+ "type": "string",
+ "description": "Type for employment: full-time. part-time, contractual, internship etc."
+ }
+ },
+ "required": ["job_page_url", "title", "employer", "location"]
+ }
+ }
+ },
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+}
+```
+
+```yaml YAML
+- block_type: extraction
+ label: jobs_list_extraction
+ url: ""
+ title: jobs_list_extraction
+ data_extraction_goal: "Extract all visible job profiles: job page url, title, employer, location, and type (full time, part time, etc)"
+ data_schema:
+ jobs:
+ type: array
+ items:
+ type: object
+ properties:
+ job_page_url:
+ type: string
+ description: Link to the apply page for the job
+ title:
+ type: string
+ description: Title of the role offered
+ employer:
+ type: string
+ description: Name of the company posting the job profile
+ location:
+ type: string
+ description: Where is the job role based. City and country,
+ type:
+ type: string
+ description: "Type for employment: full-time. part-time, contractual, internship etc."
+ required:
+ - job_page_url
+ - title
+ - employer
+ - location
+ max_retries: 0
+ engine: skyvern-1.0
+```
+
+
+The output is accessible as `{{ jobs_list_extraction_output.jobs }}` in subsequent blocks.
+
+### Apply to jobs loop
+
+Iterate over each job posting and complete the application. This loop contains multiple blocks that work together:
+
+1. **goto_url** — Navigate to the job page
+2. **extraction** — Extract detailed job information
+3. **action** — Click the Apply button
+4. **extraction** — Extract application form questions
+5. **text_prompt** — Generate tailored answers using AI
+6. **navigation** — Fill out and submit the application
+
+
+```json JSON
+{
+ "block_type": "for_loop",
+ "label": "for_each_job",
+ "loop_variable_reference": "{{jobs_list_extraction_output.jobs}}",
+ "continue_on_failure": true,
+ "next_loop_on_failure": true,
+ "complete_if_empty": true,
+ "loop_blocks": [
+ {
+ "block_type": "goto_url",
+ "label": "go_to_each_job",
+ "url": "{{ current_value.job_page_url }}"
+ },
+ {
+ "block_type": "extraction",
+ "label": "extract_job_detail",
+ "url": "",
+ "title": "extract_job_detail",
+ "data_extraction_goal": "Extract every detail about the job role present on the page",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "action",
+ "label": "click_apply",
+ "url": "",
+ "title": "click_apply",
+ "navigation_goal": "Find Apply button and click it.\n\nCOMPLETE when the job application is visible on the screen",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "extraction",
+ "label": "extract_questions",
+ "url": "",
+ "title": "extract_questions",
+ "data_extraction_goal": "Extract every question in the job application form as a list",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "text_prompt",
+ "label": "answer_form_questions",
+ "prompt": "Given:\nresume: {{resume}}\nparsed resume: {{parsed_resume_output}}\ninformation about the job profile: {{extract_job_detail_output}}\napplication form questions: {{extract_questions_output}}\n\nYou are applying for {{ current_value.title }} at {{ current_value.employer }}.\n\nWrite thoughtful and impressive answers to each question using the information from resume and parsed resume output."
+ },
+ {
+ "block_type": "navigation",
+ "label": "apply_to_job",
+ "url": "",
+ "title": "apply_to_job",
+ "engine": "skyvern-1.0",
+ "navigation_goal": "Given:\n{{answer_form_questions_output}}\n\nFill out all of the form fields, including the optional fields.\n\nIf you dont know the answer to an optional question, leave it blank. If you dont know the answer to a required question such as referral name put N/A or something equivalent.\n\nCOMPLETE when the application form has been successfully submitted.\n\nMore context:\nresume: {{resume}}\nperson_information: {{parsed_resume_output}}",
+ "max_retries": 0
+ }
+ ]
+}
+```
+
+```yaml YAML
+- block_type: for_loop
+ label: for_each_job
+ loop_variable_reference: "{{jobs_list_extraction_output.jobs}}"
+ continue_on_failure: true
+ next_loop_on_failure: true
+ complete_if_empty: true
+ loop_blocks:
+ - block_type: goto_url
+ label: go_to_each_job
+ url: "{{ current_value.job_page_url }}"
+
+ - block_type: extraction
+ label: extract_job_detail
+ url: ""
+ title: extract_job_detail
+ data_extraction_goal: Extract every detail about the job role present on the page
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: action
+ label: click_apply
+ url: ""
+ title: click_apply
+ navigation_goal: |
+ Find Apply button and click it.
+
+ COMPLETE when the job application is visible on the screen
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: extraction
+ label: extract_questions
+ url: ""
+ title: extract_questions
+ data_extraction_goal: Extract every question in the job application form as a list
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: text_prompt
+ label: answer_form_questions
+ prompt: |
+ Given:
+ resume: {{resume}}
+ parsed resume: {{parsed_resume_output}}
+ information about the job profile: {{extract_job_detail_output}}
+ application form questions: {{extract_questions_output}}
+
+ You are applying for {{ current_value.title }} at {{ current_value.employer }}.
+
+ Write thoughtful and impressive answers to each question using the information from resume and parsed resume output.
+
+ - block_type: navigation
+ label: apply_to_job
+ url: ""
+ title: apply_to_job
+ engine: skyvern-1.0
+ navigation_goal: |
+ Given:
+ {{answer_form_questions_output}}
+
+ Fill out all of the form fields, including the optional fields.
+
+ If you dont know the answer to an optional question, leave it blank. If you dont know the answer to a required question such as referral name put N/A or something equivalent.
+
+ COMPLETE when the application form has been successfully submitted.
+
+ More context:
+ resume: {{resume}}
+ person_information: {{parsed_resume_output}}
+ max_retries: 0
+```
+
+
+**Key pattern:** `continue_on_failure: true` ensures that if one application fails, the workflow continues to the next job. Inside the loop, `{{ current_value }}` gives you the current job being processed.
+
+### Complete workflow definition
+
+Save this complete definition as `job-application-workflow.yaml` (or `.json`) before running.
+
+
+
+```json JSON
+{
+ "title": "Search and Apply for Jobs Workflow",
+ "description": "Search for jobs on any portal, extract listings, generate tailored answers with AI, and submit applications automatically.",
+ "proxy_location": "RESIDENTIAL",
+ "workflow_definition": {
+ "version": 1,
+ "parameters": [
+ { "key": "resume", "parameter_type": "workflow", "workflow_parameter_type": "file_url" },
+ { "key": "credentials", "parameter_type": "workflow", "workflow_parameter_type": "credential_id", "default_value": "your-credential-id" },
+ { "key": "job_portal_url", "description": "URL of the job portal", "parameter_type": "workflow", "workflow_parameter_type": "string" }
+ ],
+ "blocks": [
+ {
+ "block_type": "pdf_parser",
+ "label": "parsed_resume",
+ "file_url": "{{ resume }}"
+ },
+ {
+ "block_type": "login",
+ "label": "login_to_portal",
+ "url": "{{ job_portal_url }}",
+ "title": "login_to_portal",
+ "parameter_keys": ["credentials"],
+ "navigation_goal": "Log in using the provided credentials.\nHandle any cookie consent popups.\nCOMPLETE when on the account dashboard or orders page.",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "navigation",
+ "label": "search_for_job",
+ "url": "",
+ "title": "search_for_job",
+ "engine": "skyvern-1.0",
+ "navigation_goal": "Search for a relevant job based on the parsed resume: {{parsed_resume_output}}",
+ "max_retries": 0
+ },
+ {
+ "block_type": "extraction",
+ "label": "jobs_list_extraction",
+ "url": "",
+ "title": "jobs_list_extraction",
+ "data_extraction_goal": "Extract all visible job profiles: job page url, title, employer, location, and type (full time, part time, etc)",
+ "data_schema": {
+ "jobs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "job_page_url": { "type": "string", "description": "Link to the apply page for the job" },
+ "title": { "type": "string", "description": "Title of the role offered" },
+ "employer": { "type": "string", "description": "Name of the company posting the job profile" },
+ "location": { "type": "string", "description": "Where is the job role based. City and country," },
+ "type": { "type": "string", "description": "Type for employment: full-time. part-time, contractual, internship etc." }
+ },
+ "required": ["job_page_url", "title", "employer", "location"]
+ }
+ }
+ },
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "for_loop",
+ "label": "for_each_job",
+ "loop_variable_reference": "{{jobs_list_extraction_output.jobs}}",
+ "continue_on_failure": true,
+ "next_loop_on_failure": true,
+ "complete_if_empty": true,
+ "loop_blocks": [
+ {
+ "block_type": "goto_url",
+ "label": "go_to_each_job",
+ "url": "{{ current_value.job_page_url }}"
+ },
+ {
+ "block_type": "extraction",
+ "label": "extract_job_detail",
+ "url": "",
+ "title": "extract_job_detail",
+ "data_extraction_goal": "Extract every detail about the job role present on the page",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "action",
+ "label": "click_apply",
+ "url": "",
+ "title": "click_apply",
+ "navigation_goal": "Find Apply button and click it.\n\nCOMPLETE when the job application is visible on the screen",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "extraction",
+ "label": "extract_questions",
+ "url": "",
+ "title": "extract_questions",
+ "data_extraction_goal": "Extract every question in the job application form as a list",
+ "max_retries": 0,
+ "engine": "skyvern-1.0"
+ },
+ {
+ "block_type": "text_prompt",
+ "label": "answer_form_questions",
+ "prompt": "Given:\nresume: {{resume}}\nparsed resume: {{parsed_resume_output}}\ninformation about the job profile: {{extract_job_detail_output}}\napplication form questions: {{extract_questions_output}}\n\nYou are applying for {{ current_value.title }} at {{ current_value.employer }}.\n\nWrite thoughtful and impressive answers to each question using the information from resume and parsed resume output."
+ },
+ {
+ "block_type": "navigation",
+ "label": "apply_to_job",
+ "url": "",
+ "title": "apply_to_job",
+ "engine": "skyvern-1.0",
+ "navigation_goal": "Given:\n{{answer_form_questions_output}}\n\nFill out all of the form fields, including the optional fields.\n\nIf you dont know the answer to an optional question, leave it blank. If you dont know the answer to a required question such as referral name put N/A or something equivalent.\n\nCOMPLETE when the application form has been successfully submitted.\n\nMore context:\nresume: {{resume}}\nperson_information: {{parsed_resume_output}}",
+ "max_retries": 0
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+```yaml YAML
+title: Search and Apply for Jobs Workflow
+description: "Search for jobs on any portal, extract listings, generate tailored answers with AI, and submit applications automatically."
+proxy_location: RESIDENTIAL
+workflow_definition:
+ version: 1
+ parameters:
+ - key: resume
+ parameter_type: workflow
+ workflow_parameter_type: file_url
+ - key: credentials
+ parameter_type: workflow
+ workflow_parameter_type: credential_id
+ default_value: your-credential-id # <-- replace this
+ - key: job_portal_url
+ description: URL of the job portal
+ parameter_type: workflow
+ workflow_parameter_type: string
+
+ blocks:
+ - block_type: pdf_parser
+ label: parsed_resume
+ file_url: "{{ resume }}"
+
+ - block_type: login
+ label: login_to_portal
+ url: "{{ job_portal_url }}"
+ title: login_to_portal
+ parameter_keys:
+ - credentials
+ navigation_goal: |
+ Log in using the provided credentials.
+ Handle any cookie consent popups.
+ COMPLETE when on the account dashboard or orders page.
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: navigation
+ label: search_for_job
+ url: ""
+ title: search_for_job
+ engine: skyvern-1.0
+ navigation_goal: "Search for a relevant job based on the parsed resume: {{parsed_resume_output}}"
+ max_retries: 0
+
+ - block_type: extraction
+ label: jobs_list_extraction
+ url: ""
+ title: jobs_list_extraction
+ data_extraction_goal: "Extract all visible job profiles: job page url, title, employer, location, and type (full time, part time, etc)"
+ data_schema:
+ jobs:
+ type: array
+ items:
+ type: object
+ properties:
+ job_page_url:
+ type: string
+ description: Link to the apply page for the job
+ title:
+ type: string
+ description: Title of the role offered
+ employer:
+ type: string
+ description: Name of the company posting the job profile
+ location:
+ type: string
+ description: Where is the job role based. City and country,
+ type:
+ type: string
+ description: "Type for employment: full-time. part-time, contractual, internship etc."
+ required:
+ - job_page_url
+ - title
+ - employer
+ - location
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: for_loop
+ label: for_each_job
+ loop_variable_reference: "{{jobs_list_extraction_output.jobs}}"
+ continue_on_failure: true
+ next_loop_on_failure: true
+ complete_if_empty: true
+ loop_blocks:
+ - block_type: goto_url
+ label: go_to_each_job
+ url: "{{ current_value.job_page_url }}"
+
+ - block_type: extraction
+ label: extract_job_detail
+ url: ""
+ title: extract_job_detail
+ data_extraction_goal: Extract every detail about the job role present on the page
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: action
+ label: click_apply
+ url: ""
+ title: click_apply
+ navigation_goal: |
+ Find Apply button and click it.
+
+ COMPLETE when the job application is visible on the screen
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: extraction
+ label: extract_questions
+ url: ""
+ title: extract_questions
+ data_extraction_goal: Extract every question in the job application form as a list
+ max_retries: 0
+ engine: skyvern-1.0
+
+ - block_type: text_prompt
+ label: answer_form_questions
+ prompt: |
+ Given:
+ resume: {{resume}}
+ parsed resume: {{parsed_resume_output}}
+ information about the job profile: {{extract_job_detail_output}}
+ application form questions: {{extract_questions_output}}
+
+ You are applying for {{ current_value.title }} at {{ current_value.employer }}.
+
+ Write thoughtful and impressive answers to each question using the information from resume and parsed resume output.
+
+ - block_type: navigation
+ label: apply_to_job
+ url: ""
+ title: apply_to_job
+ engine: skyvern-1.0
+ navigation_goal: |
+ Given:
+ {{answer_form_questions_output}}
+
+ Fill out all of the form fields, including the optional fields.
+
+ If you dont know the answer to an optional question, leave it blank. If you dont know the answer to a required question such as referral name put N/A or something equivalent.
+
+ COMPLETE when the application form has been successfully submitted.
+
+ More context:
+ resume: {{resume}}
+ person_information: {{parsed_resume_output}}
+ max_retries: 0
+```
+
+
+
+---
+
+## Step 4: Run the workflow
+
+Create the workflow from your definition file and execute it using the SDK.
+
+
+```python Python
+import os
+import asyncio
+from skyvern import Skyvern
+
+async def main():
+ client = Skyvern(api_key=os.getenv("SKYVERN_API_KEY"))
+
+ # Create workflow from YAML file
+ workflow = await client.create_workflow(
+ yaml_definition=open("job-application-workflow.yaml").read()
+ )
+ print(f"Created workflow: {workflow.workflow_permanent_id}")
+
+ # Run the workflow
+ run = await client.run_workflow(
+ workflow_id=workflow.workflow_permanent_id,
+ parameters={
+ "resume": "https://your-resume-url.com/resume.pdf", # <-- replace this
+ "job_portal_url": "https://job-stash.vercel.app"
+ }
+ )
+ print(f"Started run: {run.run_id}")
+
+ # Poll for completion
+ while True:
+ result = await client.get_run(run.run_id)
+ if result.status in ["completed", "failed", "terminated"]:
+ break
+ print(f"Status: {result.status}")
+ await asyncio.sleep(10)
+
+ print(f"Final status: {result.status}")
+ if result.status == "completed":
+ print("Job applications submitted successfully")
+
+asyncio.run(main())
+```
+
+```typescript TypeScript
+import { SkyvernClient } from "@skyvern/client";
+import * as fs from "fs";
+
+async function main() {
+ const client = new SkyvernClient({
+ apiKey: process.env.SKYVERN_API_KEY,
+ });
+
+ // Create workflow from YAML file
+ const workflow = await client.createWorkflow({
+ body: {
+ yaml_definition: fs.readFileSync("job-application-workflow.yaml", "utf-8"),
+ },
+ });
+ console.log(`Created workflow: ${workflow.workflow_permanent_id}`);
+
+ // Run the workflow
+ const run = await client.runWorkflow({
+ body: {
+ workflow_id: workflow.workflow_permanent_id,
+ parameters: {
+ resume: "https://your-resume-url.com/resume.pdf", // <-- replace this
+ job_portal_url: "https://job-stash.vercel.app",
+ },
+ },
+ });
+ console.log(`Started run: ${run.run_id}`);
+
+ // Poll for completion
+ while (true) {
+ const result = await client.getRun(run.run_id);
+ if (["completed", "failed", "terminated"].includes(result.status)) {
+ console.log(`Final status: ${result.status}`);
+ if (result.status === "completed") {
+ console.log("Job applications submitted successfully");
+ }
+ break;
+ }
+ console.log(`Status: ${result.status}`);
+ await new Promise((r) => setTimeout(r, 10000));
+ }
+}
+
+main();
+```
+
+```bash cURL
+# Create workflow
+WORKFLOW=$(curl -s -X POST "https://api.skyvern.com/v1/workflows" \
+ -H "x-api-key: $SKYVERN_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d "{\"yaml_definition\": $(cat job-application-workflow.yaml | jq -Rs .)}")
+
+WORKFLOW_ID=$(echo "$WORKFLOW" | jq -r '.workflow_permanent_id')
+echo "Created workflow: $WORKFLOW_ID"
+
+# Run workflow (replace resume URL with your own)
+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\",
+ \"parameters\": {
+ \"resume\": \"https://your-resume-url.com/resume.pdf\",
+ \"job_portal_url\": \"https://job-stash.vercel.app\"
+ }
+ }")
+
+RUN_ID=$(echo "$RUN" | jq -r '.run_id')
+echo "Started run: $RUN_ID"
+
+# Poll for completion
+while true; do
+ RESULT=$(curl -s "https://api.skyvern.com/v1/runs/$RUN_ID" \
+ -H "x-api-key: $SKYVERN_API_KEY")
+ STATUS=$(echo "$RESULT" | jq -r '.status')
+ echo "Status: $STATUS"
+
+ if [[ "$STATUS" == "completed" || "$STATUS" == "failed" || "$STATUS" == "terminated" ]]; then
+ echo "Workflow finished with status: $STATUS"
+ break
+ fi
+ sleep 10
+done
+```
+
+
+---
+
+## Resources
+
+
+
+ Complete parameter reference for all block types
+
+
+ Securely store and use login credentials
+
+
+ Upload and parse files in workflows
+
+
+ Handle failures and retries in production
+
+
diff --git a/docs/cookbooks/overview.mdx b/docs/cookbooks/overview.mdx
new file mode 100644
index 00000000..2ad696e0
--- /dev/null
+++ b/docs/cookbooks/overview.mdx
@@ -0,0 +1,81 @@
+---
+title: Overview
+subtitle: End-to-end workflow examples for common automation scenarios
+slug: cookbooks/overview
+---
+
+Cookbooks are complete, production-ready workflow examples that demonstrate how to use Skyvern Browser Automation agent in real-world use-cases.
+
+Each cookbook walks through the workflow step-by-step, explaining design decisions and showing you how to adapt it for your use case.
+
+---
+
+## Available cookbooks
+
+
+
+ Download invoices from vendor portals, parse PDFs to extract amounts, and email a summary with attachments.
+
+
+ Search for jobs on any portal, extract listings, generate tailored answers with AI, and submit applications automatically.
+
+
+
+---
+
+## Getting your API key
+
+All cookbooks require a Skyvern API key. Here's how to get one:
+
+
+
+ Sign up at [app.skyvern.com](https://app.skyvern.com) or log in to your existing account.
+
+
+ Click **Settings** in the left sidebar, then copy your **API Keys**.
+
+
+
+
+
+ Set the `SKYVERN_API_KEY` environment variable in your shell or `.env` file.
+ ```bash
+ export SKYVERN_API_KEY="your-api-key"
+ ```
+ Or add it to your `.env` file:
+ ```bash
+ SKYVERN_API_KEY=your-api-key
+ ```
+
+
+
+---
+
+## Other Resources
+
+If you're new to Skyvern workflows, start with these foundational guides:
+
+
+
+ Learn workflow basics: parameters, blocks, and data passing
+
+
+ Complete reference for all block types
+
+
diff --git a/docs/debugging/faq.mdx b/docs/debugging/faq.mdx
index 37f6dd7c..b3c0a5fe 100644
--- a/docs/debugging/faq.mdx
+++ b/docs/debugging/faq.mdx
@@ -96,7 +96,7 @@ Parameters are available in prompts using `{{ parameter_name }}` syntax. See [Wo
2. **Reference in workflow** — Select the credential in the Login block's credential dropdown
3. **Never hardcode** — Don't put passwords directly in prompts
-The AI will automatically fill the credential fields when it encounters a login form. See [Credentials](/credentials/introduction) for setup instructions.
+The AI will automatically fill the credential fields when it encounters a login form. See [Credentials](/sdk-reference/credentials) for setup instructions.
diff --git a/docs/docs.json b/docs/docs.json
index 53dff4c4..956b8ff4 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -90,20 +90,46 @@
{
"group": "Getting Started",
"pages": [
- "cloud/overview",
- "cloud/run-your-first-task",
- "cloud/run-a-task",
- "cloud/monitor-a-run"
+ "cloud/getting-started/overview",
+ "cloud/getting-started/run-your-first-task",
+ "cloud/getting-started/run-a-task",
+ "cloud/getting-started/monitor-a-run"
]
},
{
"group": "Building Workflows",
"pages": [
- "cloud/build-a-workflow",
- "cloud/manage-workflows",
- "cloud/add-parameters",
- "cloud/configure-blocks",
- "cloud/run-a-workflow"
+ "cloud/building-workflows/build-a-workflow",
+ "cloud/building-workflows/manage-workflows",
+ "cloud/building-workflows/add-parameters",
+ "cloud/building-workflows/configure-blocks",
+ "cloud/building-workflows/run-a-workflow"
+ ]
+ },
+ {
+ "group": "Viewing Results",
+ "pages": [
+ "cloud/viewing-results/run-history",
+ "cloud/viewing-results/run-details",
+ "cloud/viewing-results/downloading-artifacts"
+ ]
+ },
+ {
+ "group": "Managing Credentials",
+ "pages": [
+ "cloud/managing-credentials/credentials-overview",
+ "cloud/managing-credentials/password-credentials",
+ "cloud/managing-credentials/credit-card-credentials",
+ "cloud/managing-credentials/totp-setup"
+ ]
+ },
+ {
+ "group": "Account & Settings",
+ "pages": [
+ "cloud/account-settings/api-keys",
+ "cloud/account-settings/billing-usage",
+ "cloud/account-settings/organization-settings",
+ "cloud/account-settings/profile-settings"
]
},
{
@@ -134,7 +160,7 @@
"sdk-reference/complete-reference"
]
},
- {
+ {
"group": "TypeScript SDK",
"pages": [
"ts-sdk-reference/overview",
@@ -151,9 +177,26 @@
}
]
},
+ {
+ "tab": "Cookbooks",
+ "pages": [
+ "cookbooks/overview",
+ "cookbooks/bulk-invoice-downloader",
+ "cookbooks/job-application-filler",
+ "cookbooks/healthcare-portal-data"
+ ]
+ },
{
"tab": "API Reference",
"openapi": "api-reference/openapi.json"
+ },
+ {
+ "tab": "Cookbooks",
+ "pages": [
+ "cookbooks/overview",
+ "cookbooks/bulk-invoice-downloader",
+ "cookbooks/job-application-filler"
+ ]
}
]
},
diff --git a/docs/getting-started/core-concepts.mdx b/docs/getting-started/core-concepts.mdx
index 1caeb50a..ad32283f 100644
--- a/docs/getting-started/core-concepts.mdx
+++ b/docs/getting-started/core-concepts.mdx
@@ -227,7 +227,7 @@ Credentials provide secure storage for authentication data. Skyvern encrypts cre
- **1Password** — Sync from your 1Password vault
- **Azure Key Vault** — Enterprise secret management
-See [Credentials](/credentials/introduction) for setup instructions.
+See [Credentials](/sdk-reference/credentials) for setup instructions.
---
diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx
index 8fa3b4ad..0086490e 100644
--- a/docs/getting-started/quickstart.mdx
+++ b/docs/getting-started/quickstart.mdx
@@ -372,7 +372,7 @@ A browser window will open on your machine (if you chose headful mode). Recordin
Store credentials securely for sites that require authentication
diff --git a/docs/images/browser-session-create.mp4 b/docs/images/browser-session-create.mp4
new file mode 100644
index 00000000..e4510bbf
Binary files /dev/null and b/docs/images/browser-session-create.mp4 differ
diff --git a/docs/images/cloud/change-password.png b/docs/images/cloud/change-password.png
new file mode 100644
index 00000000..98981765
Binary files /dev/null and b/docs/images/cloud/change-password.png differ
diff --git a/docs/images/cloud/credentials-2fa-setup.png b/docs/images/cloud/credentials-2fa-setup.png
new file mode 100644
index 00000000..912ab4af
Binary files /dev/null and b/docs/images/cloud/credentials-2fa-setup.png differ
diff --git a/docs/images/cloud/credentials-add-password.png b/docs/images/cloud/credentials-add-password.png
new file mode 100644
index 00000000..94b36db6
Binary files /dev/null and b/docs/images/cloud/credentials-add-password.png differ
diff --git a/docs/images/cloud/credentials-overview.png b/docs/images/cloud/credentials-overview.png
new file mode 100644
index 00000000..b850e334
Binary files /dev/null and b/docs/images/cloud/credentials-overview.png differ
diff --git a/docs/images/cloud/discover-page.png b/docs/images/cloud/discover-page.png
deleted file mode 100644
index 77314df2..00000000
Binary files a/docs/images/cloud/discover-page.png and /dev/null differ
diff --git a/docs/images/cloud/live-browser-view.png b/docs/images/cloud/live-browser-view.png
deleted file mode 100644
index 28507f40..00000000
Binary files a/docs/images/cloud/live-browser-view.png and /dev/null differ
diff --git a/docs/images/cloud/results-overview.png b/docs/images/cloud/results-overview.png
deleted file mode 100644
index c311217c..00000000
Binary files a/docs/images/cloud/results-overview.png and /dev/null differ
diff --git a/docs/images/cloud/run-history-overview.png b/docs/images/cloud/run-history-overview.png
new file mode 100644
index 00000000..a4c2c1f4
Binary files /dev/null and b/docs/images/cloud/run-history-overview.png differ
diff --git a/docs/images/cloud/run-history-search.png b/docs/images/cloud/run-history-search.png
new file mode 100644
index 00000000..21ddc7b8
Binary files /dev/null and b/docs/images/cloud/run-history-search.png differ
diff --git a/docs/images/cloud/settings-api-key.png b/docs/images/cloud/settings-api-key.png
new file mode 100644
index 00000000..9ef59f22
Binary files /dev/null and b/docs/images/cloud/settings-api-key.png differ
diff --git a/docs/images/cloud/settings-billing-overview.png b/docs/images/cloud/settings-billing-overview.png
new file mode 100644
index 00000000..8fc25ccb
Binary files /dev/null and b/docs/images/cloud/settings-billing-overview.png differ
diff --git a/docs/images/cloud/task-run-actions-tab.png b/docs/images/cloud/task-run-actions-tab.png
new file mode 100644
index 00000000..041eb966
Binary files /dev/null and b/docs/images/cloud/task-run-actions-tab.png differ
diff --git a/docs/images/cloud/task-run-details.png b/docs/images/cloud/task-run-details.png
new file mode 100644
index 00000000..f1381177
Binary files /dev/null and b/docs/images/cloud/task-run-details.png differ
diff --git a/docs/images/cloud/task-run-results.png b/docs/images/cloud/task-run-results.png
new file mode 100644
index 00000000..ac9ba77b
Binary files /dev/null and b/docs/images/cloud/task-run-results.png differ
diff --git a/docs/images/cloud/workflow-run-code-tab.png b/docs/images/cloud/workflow-run-code-tab.png
new file mode 100644
index 00000000..3b97f342
Binary files /dev/null and b/docs/images/cloud/workflow-run-code-tab.png differ
diff --git a/docs/images/navigation-error-messages.mp4 b/docs/images/navigation-error-messages.mp4
new file mode 100644
index 00000000..9957d8d8
Binary files /dev/null and b/docs/images/navigation-error-messages.mp4 differ
diff --git a/docs/images/workflow-start.mp4 b/docs/images/workflow-start.mp4
new file mode 100644
index 00000000..5ae8f9e7
Binary files /dev/null and b/docs/images/workflow-start.mp4 differ
diff --git a/docs/sdk-reference/browser-profiles.mdx b/docs/sdk-reference/browser-profiles.mdx
index 446dd507..fe2b6a67 100644
--- a/docs/sdk-reference/browser-profiles.mdx
+++ b/docs/sdk-reference/browser-profiles.mdx
@@ -28,8 +28,10 @@ print(profile.browser_profile_id) # bpf_abc123
|-----------|------|----------|-------------|
| `name` | `str` | Yes | Display name for the profile. |
| `description` | `str` | No | Optional description. |
-| `workflow_run_id` | `str` | No | The workflow run ID to snapshot. The run must have used `persist_browser_session=True`. |
-| `browser_session_id` | `str` | No | The browser session ID to snapshot. |
+| `workflow_run_id` | `str` | Conditional | The workflow run ID to snapshot. The run must have used `persist_browser_session=True`. Required if `browser_session_id` is not provided. |
+| `browser_session_id` | `str` | Conditional | The browser session ID to snapshot. Required if `workflow_run_id` is not provided. |
+
+You must provide either `workflow_run_id` or `browser_session_id`.
### Returns `BrowserProfile`
diff --git a/docs/sdk-reference/complete-reference.mdx b/docs/sdk-reference/complete-reference.mdx
index 67309cb2..53224569 100644
--- a/docs/sdk-reference/complete-reference.mdx
+++ b/docs/sdk-reference/complete-reference.mdx
@@ -262,7 +262,7 @@ versions = await client.get_workflow_versions(
```python
updated = await client.update_workflow(
- workflow_id: str, # The version ID (not permanent ID).
+ workflow_id: str, # The permanent ID (wpid_...).
json_definition: dict | None = None,
yaml_definition: str | None = None,
) -> Workflow
@@ -349,7 +349,7 @@ profile = await client.create_browser_profile(
name: str,
description: str | None = None,
workflow_run_id: str | None = None, # Run must have used persist_browser_session=True.
- browser_session_id: str | None = None,
+ browser_session_id: str | None = None, # One of workflow_run_id or browser_session_id is required.
) -> BrowserProfile
```
diff --git a/docs/sdk-reference/workflows.mdx b/docs/sdk-reference/workflows.mdx
index c0e452a0..c5b8f551 100644
--- a/docs/sdk-reference/workflows.mdx
+++ b/docs/sdk-reference/workflows.mdx
@@ -107,15 +107,25 @@ Create a new workflow from a JSON or YAML definition.
```python
workflow = await client.create_workflow(
json_definition={
- "blocks": [
- {
- "block_type": "task",
- "label": "extract_data",
- "prompt": "Extract the top 3 products",
- "url": "https://example.com/products",
- }
- ],
- "parameters": [],
+ "title": "Extract Products",
+ "workflow_definition": {
+ "parameters": [
+ {
+ "key": "target_url",
+ "parameter_type": "workflow",
+ "workflow_parameter_type": "string",
+ "description": "URL to scrape",
+ }
+ ],
+ "blocks": [
+ {
+ "block_type": "task",
+ "label": "extract_data",
+ "prompt": "Extract the top 3 products",
+ "url": "{{ target_url }}",
+ }
+ ],
+ },
},
)
print(workflow.workflow_permanent_id)
@@ -229,17 +239,20 @@ Update an existing workflow's definition.
```python
updated = await client.update_workflow(
- "wf_abc123",
+ "wpid_abc123",
json_definition={
- "blocks": [
- {
- "block_type": "task",
- "label": "extract_data",
- "prompt": "Extract the top 5 products",
- "url": "https://example.com/products",
- }
- ],
- "parameters": [],
+ "title": "Extract Products",
+ "workflow_definition": {
+ "blocks": [
+ {
+ "block_type": "task",
+ "label": "extract_data",
+ "prompt": "Extract the top 5 products",
+ "url": "https://example.com/products",
+ }
+ ],
+ "parameters": [],
+ },
},
)
print(f"Updated to v{updated.version}")
@@ -249,7 +262,7 @@ print(f"Updated to v{updated.version}")
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
-| `workflow_id` | `str` | Yes | The workflow version ID (not the permanent ID). |
+| `workflow_id` | `str` | Yes | The workflow's permanent ID (`wpid_...`). |
| `json_definition` | `WorkflowCreateYamlRequest` | No | Updated workflow definition as JSON. |
| `yaml_definition` | `str` | No | Updated workflow definition as YAML. |
diff --git a/docs/ts-sdk-reference/browser-profiles.mdx b/docs/ts-sdk-reference/browser-profiles.mdx
index 1655c580..a4d6cd46 100644
--- a/docs/ts-sdk-reference/browser-profiles.mdx
+++ b/docs/ts-sdk-reference/browser-profiles.mdx
@@ -28,8 +28,10 @@ console.log(profile.browser_profile_id); // bpf_abc123
|-----------|------|----------|-------------|
| `name` | `string` | Yes | Display name for the profile. |
| `description` | `string` | No | Optional description. |
-| `workflow_run_id` | `string` | No | The workflow run ID to snapshot. The run must have used `persist_browser_session: true`. |
-| `browser_session_id` | `string` | No | The browser session ID to snapshot. |
+| `workflow_run_id` | `string` | Conditional | The workflow run ID to snapshot. The run must have used `persist_browser_session: true`. Required if `browser_session_id` is not provided. |
+| `browser_session_id` | `string` | Conditional | The browser session ID to snapshot. Required if `workflow_run_id` is not provided. |
+
+You must provide either `workflow_run_id` or `browser_session_id`.
### Returns `BrowserProfile`
diff --git a/docs/ts-sdk-reference/complete-reference.mdx b/docs/ts-sdk-reference/complete-reference.mdx
index 47f16128..ae02692a 100644
--- a/docs/ts-sdk-reference/complete-reference.mdx
+++ b/docs/ts-sdk-reference/complete-reference.mdx
@@ -239,7 +239,7 @@ const versions = await skyvern.getWorkflowVersions(
```typescript
const updated = await skyvern.updateWorkflow(
- workflowId: string, // The version ID (not permanent ID).
+ workflowId: string, // The permanent ID (wpid_...).
request?: { json_definition?: object; yaml_definition?: string },
): Promise
```
@@ -329,7 +329,7 @@ const profile = await skyvern.createBrowserProfile({
name: string,
description?: string,
workflow_run_id?: string, // Run must have used persist_browser_session: true.
- browser_session_id?: string,
+ browser_session_id?: string, // One of workflow_run_id or browser_session_id is required.
}): Promise
```