feat: new workflows docs (#4565)

Co-authored-by: Kunal Mishra <kunalm2345@gmail.com>
Co-authored-by: Suchintan <suchintan@users.noreply.github.com>
This commit is contained in:
Naman
2026-02-05 03:34:57 +05:30
committed by GitHub
parent 54bab314ce
commit 734e0e6398
16 changed files with 21972 additions and 17 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ Run browser automations without writing code. This guide walks you through runni
Prefer to integrate via code? Check out the [SDK Quickstart](/getting-started/quickstart) instead.
</Note>
## Opening Skyvern Cloud
## Open Skyvern Cloud
Visit [app.skyvern.com](https://app.skyvern.com) and sign-up or sign-in to your account.
@@ -17,7 +17,7 @@ You'll land on the **Discover** page—your starting point for running automatio
<img src="/images/discover-page.png" alt="discover page in skyvern"/>
## Understanding the Interface
## Understand the Interface
The left sidebar organizes everything you need:

View File

@@ -0,0 +1,359 @@
---
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.
<Note>
New to Skyvern? Start with [Getting Started](/cloud/getting-started) to run your first task.
</Note>
---
## 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.
<Frame>
<img src="/images/cloud/discover-page-annotated.png" alt="The Discover page with annotations" />
</Frame>
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.
<Frame>
<img src="/images/cloud/proxy-location-example.gif" alt="Selecting UK proxy and seeing GBP prices" />
</Frame>
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
<Frame>
<img src="/images/cloud/proxy-location-dropdown.png" alt="Proxy location dropdown showing available countries" />
</Frame>
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
<Frame>
<img src="/images/cloud/data-schema-editor.png" alt="Data schema JSON editor" />
</Frame>
```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" }
}
}
}
}
}
```
<Note>
A schema doesn't guarantee all fields are populated. If the data isn't on the page, fields return `null`.
</Note>
---
## Configure your task
Click the gear icon to access all task settings. Here's what each one does:
<Frame>
<img src="/images/cloud/task-settings-panel.png" alt="Task settings panel" />
</Frame>
### 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. |
<Warning>
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.
</Warning>
### 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.
<Frame>
<img src="/images/cloud/live-execution-view.png" alt="Live browser view during task execution" />
</Frame>
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.
<Frame>
<img src="/images/cloud/take-control-demo.gif" alt="Taking control of the browser to solve a CAPTCHA" />
</Frame>
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.
<Frame>
<img src="/images/cloud/task-results-header.png" alt="Task results page header" />
</Frame>
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:
<Frame>
<img src="/images/cloud/extracted-data-section.png" alt="Extracted data JSON display" />
</Frame>
```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:
<Frame>
<img src="/images/cloud/results-tabs-overview.png" alt="Results tabs showing Actions, Recording, Parameters, Diagnostics" />
</Frame>
**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.
<Frame>
<img src="/images/cloud/actions-tab-detail.png" alt="Actions tab with step-by-step screenshots" />
</Frame>
**Recording** — Full video replay of the browser session. Scrub through to see exactly what happened.
<Frame>
<img src="/images/cloud/recording-tab.png" alt="Recording tab with video player" />
</Frame>
**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.
<Frame>
<img src="/images/cloud/publish-workflow-toggle.png" alt="Publish workflow toggle in settings" />
</Frame>
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?
<CardGroup cols={2}>
<Card
title="Build a Workflow"
icon="diagram-project"
href="/multi-step-automations/build-a-workflow"
>
Create multi-step automations with the visual workflow builder
</Card>
<Card
title="Extract Structured Data"
icon="table"
href="/running-automations/extract-structured-data"
>
Pull structured data from websites into JSON format
</Card>
</CardGroup>

View File

@@ -32,19 +32,38 @@
"running-automations/task-parameters",
"running-automations/extract-structured-data"
]
},
{
"group": "Multi-Step Automations",
"pages": [
"multi-step-automations/build-a-workflow",
"multi-step-automations/workflow-blocks-reference",
"multi-step-automations/file-operations",
"multi-step-automations/workflow-parameters"
]
},
{
"group": "Going to Production",
"pages": [
"going-to-production/webhooks",
"going-to-production/proxy-geolocation",
"going-to-production/error-handling",
"going-to-production/reliability-tips",
"going-to-production/captcha-bot-detection"
]
}
]
},
{
"tab": "Cloud UI",
"groups": [
{
"group": "Cloud UI",
"pages": [
"cloud/getting-started"
]
}
"cloud/getting-started",
"cloud/running-tasks"
]
},
{
"tab": "API Reference",
"openapi": "api-reference/openapi.json"
}
]
},
@@ -69,6 +88,11 @@
}
},
"api": {
"baseUrl": "https://api.skyvern.com",
"auth": {
"method": "key",
"name": "x-api-key"
},
"playground": {
"display": "interactive"
}

View File

@@ -0,0 +1,209 @@
---
title: CAPTCHA & Bot Detection
subtitle: How Skyvern detects, solves, and avoids CAPTCHAs and anti-bot systems
slug: going-to-production/captcha-bot-detection
---
Websites use CAPTCHAs and bot detection to block automated traffic. Skyvern handles both — it detects CAPTCHAs using vision, solves them automatically on Skyvern Cloud, and configures browsers to avoid triggering anti-bot systems in the first place.
---
## CAPTCHA detection
Skyvern detects CAPTCHAs using its LLM vision model. During each step, the AI analyzes the page screenshot and identifies whether a CAPTCHA is present and what type it is.
**Supported CAPTCHA types:**
| Type | Examples |
|------|----------|
| `RECAPTCHA` | Google reCAPTCHA v2, v3 |
| `HCAPTCHA` | hCaptcha checkbox, image challenges |
| `CLOUDFLARE` | Cloudflare Turnstile, Challenge pages |
| `FUNCAPTCHA` | FunCaptcha / ArkoseLabs |
| `MTCAPTCHA` | MTCaptcha |
| `TEXT_CAPTCHA` | Distorted text/number images with an input field |
| `OTHER` | Unrecognized CAPTCHA types |
When the AI detects a CAPTCHA, it emits a `SOLVE_CAPTCHA` action with the identified `captcha_type`. What happens next depends on your deployment.
---
## CAPTCHA solving
### Skyvern Cloud (automatic)
On [Skyvern Cloud](https://app.skyvern.com), CAPTCHAs are solved automatically. When the AI detects a CAPTCHA, Skyvern's solver service handles it in the background. No configuration needed — it works out of the box for all supported CAPTCHA types.
If the solver cannot resolve the CAPTCHA (rare edge cases or novel CAPTCHA types), the task continues with a `SOLVE_CAPTCHA` action failure. Handle this with [error code mapping](/going-to-production/error-handling):
<CodeGroup>
```python Python
result = await client.run_task(
prompt="Submit the application form. COMPLETE when you see confirmation.",
url="https://example.com/apply",
error_code_mapping={
"captcha_failed": "Return this if a CAPTCHA blocks progress after multiple attempts",
},
)
```
```typescript TypeScript
const result = await client.runTask({
body: {
prompt: "Submit the application form. COMPLETE when you see confirmation.",
url: "https://example.com/apply",
error_code_mapping: {
captcha_failed: "Return this if a CAPTCHA blocks progress after multiple attempts",
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Submit the application form. COMPLETE when you see confirmation.",
"url": "https://example.com/apply",
"error_code_mapping": {
"captcha_failed": "Return this if a CAPTCHA blocks progress after multiple attempts"
}
}'
```
</CodeGroup>
### Human Interaction block (workflows)
For workflows where you want a human to solve the CAPTCHA manually, use the **Human Interaction** block. The workflow pauses and notifies you, then resumes after you solve it.
```yaml
blocks:
- block_type: navigation
label: fill_form
url: https://example.com/form
navigation_goal: Fill out the registration form
- block_type: human_interaction
label: solve_captcha
message: "Please solve the CAPTCHA and click Continue"
- block_type: navigation
label: submit_form
navigation_goal: Click the submit button
```
---
## Bot detection avoidance
Skyvern automatically configures the browser to reduce bot detection triggers. These protections apply to every run — no configuration needed.
### Browser fingerprinting
Skyvern launches Chromium with settings that remove common automation signals:
- **`AutomationControlled` disabled** — Removes the Blink feature flag that marks the browser as automated
- **`navigator.webdriver` hidden** — The `enable-automation` flag is suppressed so JavaScript detection scripts don't see a webdriver
- **Viewport and user agent** — Set to match real consumer browsers
- **Locale and timezone** — Automatically matched to the proxy location (see [Proxy & Geolocation](/going-to-production/proxy-geolocation))
### Reducing detection risk
Beyond fingerprinting, how you structure your automation affects detection. Three patterns that help:
**1. Use residential proxies for sensitive sites.** Datacenter IPs are the most common bot signal. Residential proxies route through real ISP addresses. See [Proxy & Geolocation](/going-to-production/proxy-geolocation).
**2. Reuse browser sessions for multi-step flows.** Creating a fresh browser for every step looks suspicious. A persistent session maintains cookies, cache, and history — appearing as a returning user. See [Browser Sessions](/optimization/browser-sessions).
**3. Use browser profiles for repeat visits.** Profiles save browser state from a previous session. Starting with an existing profile means the site sees a known browser with familiar cookies, not a blank slate. See [Browser Profiles](/optimization/browser-profiles).
---
## Self-hosted deployment
<Note>
The sections above apply to Skyvern Cloud. If you're running Skyvern locally, the following differences apply.
</Note>
### CAPTCHA solving
The open-source version does **not** include automatic CAPTCHA solving. When a CAPTCHA is detected, the agent pauses for 30 seconds to allow manual intervention (e.g., solving it in the browser window yourself), then continues.
To handle CAPTCHAs in self-hosted workflows, use the Human Interaction block as described above.
### Browser extensions
Self-hosted deployments can load Chrome extensions for additional stealth or functionality:
```bash
# .env
EXTENSIONS=extension1,extension2
EXTENSIONS_BASE_PATH=/path/to/extensions
```
Extensions are loaded automatically when the browser launches.
### Proxies
Self-hosted deployments need their own proxy infrastructure. The `proxy_location` parameter is not available — configure proxies at the network level or via environment variables.
---
## Troubleshooting
### CAPTCHA blocks the run
**On Skyvern Cloud:** This is rare. If it happens, the CAPTCHA type may be unsupported or the site changed its challenge. Add an `error_code_mapping` entry to detect the failure, and contact [support@skyvern.com](mailto:support@skyvern.com).
**Self-hosted:** Use a Human Interaction block, or solve it manually within the 30-second window.
### Bot detection triggered (access denied)
1. Switch to a residential proxy — `proxy_location="RESIDENTIAL"` or `RESIDENTIAL_ISP` for static IPs
2. Reuse a browser session instead of creating fresh browsers
3. Use a browser profile with existing cookies
4. Add `wait` blocks between rapid actions to reduce behavioral signals
### Cloudflare challenge page loops
Cloudflare sometimes loops through multiple challenges. If a task gets stuck:
- Increase `max_steps` to give the solver more attempts
- Use `RESIDENTIAL_ISP` for a static IP that Cloudflare is more likely to trust
- Use a browser profile that has previously passed the Cloudflare challenge on that domain
---
## Next steps
<CardGroup cols={2}>
<Card
title="Proxy & Geolocation"
icon="globe"
href="/going-to-production/proxy-geolocation"
>
Route traffic through residential proxies in 19 countries
</Card>
<Card
title="Handle 2FA"
icon="lock"
href="/credentials/handle-2fa"
>
Configure TOTP, email, and SMS verification codes
</Card>
<Card
title="Browser Sessions"
icon="window-restore"
href="/optimization/browser-sessions"
>
Persist browser state across multiple runs
</Card>
<Card
title="Error Handling"
icon="triangle-exclamation"
href="/going-to-production/error-handling"
>
Map CAPTCHA and bot failures to custom error codes
</Card>
</CardGroup>

View File

@@ -0,0 +1,694 @@
---
title: Error Handling
subtitle: Build systems that detect, classify, and recover from failures
slug: going-to-production/error-handling
---
Skyvern lets you make your workflows and tasks handle errors gracefully instead of failing silently.
Every run returns a `status`. When it's not `completed`, you need to know what went wrong and respond programmatically. The flow is:
1. **Check `status`** to detect failure states
2. **Read `failure_reason`** to get the raw error description
3. **Set up `error_code_mapping`** to map failures to your own error codes
4. **Respond in code** to branch your logic based on the error code
This page covers each step with exact field locations and full code examples.
---
## Step 1: Check `status`
Every run transitions through these states:
| Status | What it means |
|--------|---------------|
| `created` | Run initialized, not yet queued |
| `queued` | Waiting for an available browser |
| `running` | AI is navigating and executing |
| `completed` | Success—check `output` for results |
| `failed` | System error (browser crash, network failure, exception) |
| `terminated` | AI determined the goal is unachievable (login blocked, page unavailable) |
| `timed_out` | Exceeded `max_steps` or time limit |
| `canceled` | Manually stopped |
**Terminal states:** `completed`, `failed`, `terminated`, `timed_out`, `canceled`
You can detect failures in two ways:
1. by polling `get_run`
2. by checking the webhook payload
Both contain the same `status` field.
<CodeGroup>
```json Polling Response
{
"run_id": "tsk_v2_486305187432193504",
"status": "failed", // <-- Check this field
"output": null,
"failure_reason": "Login failed: Invalid credentials",
...
}
```
```json Webhook Payload
{
"run_id": "tsk_v2_486306851394503256",
"status": "completed", // <-- Check this field
"output": {
"top_post_title": "Linux kernel framework for PCIe device emulation, in userspace"
},
"failure_reason": null,
"errors": [],
"webhook_callback_url": "https://your-server.com/webhook",
"created_at": "2026-01-20T11:58:57.414123",
"finished_at": "2026-01-20T12:00:31.512692",
...
}
```
</CodeGroup>
The `status` field is at the top level of both responses.
### For tasks
Poll `get_run` until the status is terminal:
<CodeGroup>
```python Python
run_id = result.run_id
while True:
run = await client.get_run(run_id)
if run.status in ["completed", "failed", "terminated", "timed_out", "canceled"]:
break
await asyncio.sleep(5)
if run.status == "completed":
process_output(run.output)
else:
handle_failure(run)
```
```typescript TypeScript
const runId = result.run_id;
while (true) {
const run = await client.getRun(runId);
if (["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (run.status === "completed") {
processOutput(run.output);
} else {
handleFailure(run);
}
```
```bash cURL
# Poll until terminal state
while true; do
RESPONSE=$(curl -s -X GET "https://api.skyvern.com/v1/runs/$RUN_ID" \
-H "x-api-key: $SKYVERN_API_KEY")
STATUS=$(echo $RESPONSE | jq -r '.status')
if [[ "$STATUS" == "completed" ]] || [[ "$STATUS" == "failed" ]] || \
[[ "$STATUS" == "terminated" ]] || [[ "$STATUS" == "timed_out" ]] || \
[[ "$STATUS" == "canceled" ]]; then
echo $RESPONSE | jq
break
fi
sleep 5
done
```
</CodeGroup>
### For workflows
Same polling pattern works for workflow runs:
<CodeGroup>
```python Python
run_id = result.run_id
while True:
run = await client.get_run(run_id)
if run.status in ["completed", "failed", "terminated", "timed_out", "canceled"]:
break
await asyncio.sleep(5)
if run.status == "completed":
process_output(run.output)
else:
handle_failure(run)
```
```typescript TypeScript
const runId = result.run_id;
while (true) {
const run = await client.getRun(runId);
if (["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (run.status === "completed") {
processOutput(run.output);
} else {
handleFailure(run);
}
```
```bash cURL
# Poll workflow run until terminal state
while true; do
RESPONSE=$(curl -s -X GET "https://api.skyvern.com/v1/workflow_runs/$WORKFLOW_RUN_ID" \
-H "x-api-key: $SKYVERN_API_KEY")
STATUS=$(echo $RESPONSE | jq -r '.status')
if [[ "$STATUS" == "completed" ]] || [[ "$STATUS" == "failed" ]] || \
[[ "$STATUS" == "terminated" ]] || [[ "$STATUS" == "timed_out" ]] || \
[[ "$STATUS" == "canceled" ]]; then
echo $RESPONSE | jq
break
fi
sleep 5
done
```
</CodeGroup>
**`failed` vs `terminated`:** A `failed` run hit infrastructure problems—retry might work. A `terminated` run means the AI recognized the goal is unachievable with current conditions. Retrying without changes (new credentials, different URL) will produce the same result.
---
## Step 2: Read `failure_reason`
When a run fails or terminates, the `failure_reason` field contains a description of what went wrong. This is a free-text string—useful for logging but hard to branch on programmatically.
The field is available in both the polling response and webhook payload:
```json
{
"run_id": "tsk_v2_486305187432193504",
"status": "terminated",
"output": null,
"failure_reason": "Login failed: The page displayed 'Invalid username or password' after submitting credentials", // <-- Raw error text
...
}
```
### For tasks
<CodeGroup>
```python Python
run = await client.get_run(run_id)
if run.status in ["failed", "terminated"]:
print(f"Run failed: {run.failure_reason}")
# Fragile: parsing free text
if "login" in run.failure_reason.lower():
refresh_credentials()
```
```typescript TypeScript
const run = await client.getRun(runId);
if (["failed", "terminated"].includes(run.status)) {
console.log(`Run failed: ${run.failure_reason}`);
// Fragile: parsing free text
if (run.failure_reason?.toLowerCase().includes("login")) {
refreshCredentials();
}
}
```
```bash cURL
RESPONSE=$(curl -s -X GET "https://api.skyvern.com/v1/runs/$RUN_ID" \
-H "x-api-key: $SKYVERN_API_KEY")
STATUS=$(echo $RESPONSE | jq -r '.status')
FAILURE_REASON=$(echo $RESPONSE | jq -r '.failure_reason')
if [[ "$STATUS" == "failed" ]] || [[ "$STATUS" == "terminated" ]]; then
echo "Run failed: $FAILURE_REASON"
# Fragile: parsing free text
if echo "$FAILURE_REASON" | grep -qi "login"; then
# refresh_credentials
echo "Login error detected"
fi
fi
```
</CodeGroup>
### For workflows
<CodeGroup>
```python Python
run = await client.get_run(workflow_run_id)
if run.status in ["failed", "terminated"]:
print(f"Workflow failed: {run.failure_reason}")
# Fragile: parsing free text
if "login" in run.failure_reason.lower():
refresh_credentials()
```
```typescript TypeScript
const run = await client.getRun(workflowRunId);
if (["failed", "terminated"].includes(run.status)) {
console.log(`Workflow failed: ${run.failure_reason}`);
// Fragile: parsing free text
if (run.failure_reason?.toLowerCase().includes("login")) {
refreshCredentials();
}
}
```
```bash cURL
RESPONSE=$(curl -s -X GET "https://api.skyvern.com/v1/workflow_runs/$WORKFLOW_RUN_ID" \
-H "x-api-key: $SKYVERN_API_KEY")
STATUS=$(echo $RESPONSE | jq -r '.status')
FAILURE_REASON=$(echo $RESPONSE | jq -r '.failure_reason')
if [[ "$STATUS" == "failed" ]] || [[ "$STATUS" == "terminated" ]]; then
echo "Workflow failed: $FAILURE_REASON"
# Fragile: parsing free text
if echo "$FAILURE_REASON" | grep -qi "login"; then
# refresh_credentials
echo "Login error detected"
fi
fi
```
</CodeGroup>
---
## Step 3: Use `error_code_mapping`
`failure_reason` contains an AI-generated description of what went wrong. Define custom error codes to get consistent, actionable error messages.
When the run fails, Skyvern evaluates your natural language error descriptions against the page state and returns the matching code.
**How it works:** The `error_code_mapping` values are LLM-evaluated descriptions—you don't need exact string matches. For example, `"The login credentials are incorrect"` will match pages showing "Invalid password", "Wrong username", "Authentication failed", etc.
### In tasks
Pass `error_code_mapping` as a parameter to `run_task`:
<CodeGroup>
```python Python
result = await client.run_task(
prompt="Log in and download the invoice for January 2024",
url="https://vendor-portal.example.com",
error_code_mapping={
"login_failed": "The login credentials are incorrect, account is locked, or MFA is required",
"invoice_not_found": "No invoice exists for the requested date range",
"maintenance": "The website is down for maintenance or unavailable",
"access_denied": "User does not have permission to view invoices"
}
)
```
```typescript TypeScript
const result = await client.runTask({
body: {
prompt: "Log in and download the invoice for January 2024",
url: "https://vendor-portal.example.com",
error_code_mapping: {
login_failed: "The login credentials are incorrect, account is locked, or MFA is required",
invoice_not_found: "No invoice exists for the requested date range",
maintenance: "The website is down for maintenance or unavailable",
access_denied: "User does not have permission to view invoices",
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Log in and download the invoice for January 2024",
"url": "https://vendor-portal.example.com",
"error_code_mapping": {
"login_failed": "The login credentials are incorrect, account is locked, or MFA is required",
"invoice_not_found": "No invoice exists for the requested date range",
"maintenance": "The website is down for maintenance or unavailable",
"access_denied": "User does not have permission to view invoices"
}
}'
```
</CodeGroup>
### In workflows
Add `error_code_mapping` to individual blocks (navigation, task, validation):
<Note>
The JSON examples below include comments (`//`) for clarity. Remove comments before using in actual workflow definitions—JSON does not support comments.
</Note>
<CodeGroup>
```json JSON
{
"blocks": [
{
"block_type": "navigation",
"label": "login_step",
"url": "https://vendor-portal.example.com/login",
"navigation_goal": "Log in using the stored credentials",
"error_code_mapping": {
"login_failed": "Login credentials are incorrect or account is locked",
"mfa_required": "Two-factor authentication is being requested",
"captcha_blocked": "CAPTCHA is displayed and cannot be bypassed"
}
},
{
"block_type": "navigation",
"label": "download_invoice",
"navigation_goal": "Download the invoice for {{invoice_date}}",
"error_code_mapping": {
"invoice_not_found": "No invoice found for the specified date",
"download_failed": "Invoice exists but download button is broken or missing"
}
}
]
}
```
```yaml YAML
- block_type: navigation
label: login_step
url: "https://vendor-portal.example.com/login"
navigation_goal: "Log in using the stored credentials"
error_code_mapping:
login_failed: "Login credentials are incorrect or account is locked"
mfa_required: "Two-factor authentication is being requested"
captcha_blocked: "CAPTCHA is displayed and cannot be bypassed"
- block_type: navigation
label: download_invoice
navigation_goal: "Download the invoice for {{invoice_date}}"
error_code_mapping:
invoice_not_found: "No invoice found for the specified date"
download_failed: "Invoice exists but download button is broken or missing"
```
</CodeGroup>
### Where the error code appears
When a mapped error occurs, your code appears in `output.error`. This field is available in both polling responses and webhook payloads:
<CodeGroup>
```json Polling Response
{
"run_id": "tsk_v2_486305187432193504",
"status": "terminated",
"output": {
"error": "login_failed" // <-- Your custom code
},
"failure_reason": "Login failed: The page displayed 'Invalid username or password'"
}
```
```json Webhook Payload
{
"run_id": "tsk_v2_486305187432193504",
"status": "terminated",
"output": {
"error": "login_failed" // <-- Your custom code
},
"failure_reason": "Login failed: The page displayed 'Invalid username or password'",
"webhook_callback_url": "https://your-server.com/webhook",
"created_at": "2026-01-20T11:58:57.414123",
"finished_at": "2026-01-20T12:00:31.512692"
}
```
</CodeGroup>
Both `output.error` (your code) and `failure_reason` (raw text) are present. Use `output.error` for branching, `failure_reason` for logging.
**Quick reference:** Where error codes appear
| Context | Field | Example |
|---------|-------|---------|
| Polling response | `run.output.error` | `run.output.get("error")` in Python |
| Webhook payload | `output.error` | Same structure as polling |
| Successful run | `output` contains your extracted data | No `error` key present |
---
## Step 4: Respond in code
Now you can write clean switch/match logic:
<CodeGroup>
```python Python
import asyncio
from skyvern import Skyvern
client = Skyvern(api_key="your-api-key")
async def run_with_error_handling(retries=1):
result = await client.run_task(
prompt="Log in and download the invoice",
url="https://vendor-portal.example.com",
error_code_mapping={
"login_failed": "Login credentials are incorrect or account is locked",
"invoice_not_found": "No invoice for the requested date",
"maintenance": "Site is down for maintenance"
}
)
run_id = result.run_id
# Poll until terminal state
while True:
run = await client.get_run(run_id)
if run.status in ["completed", "failed", "terminated", "timed_out", "canceled"]:
break
await asyncio.sleep(5)
# Handle based on status and error code
if run.status == "completed":
return {"success": True, "data": run.output}
error_code = run.output.get("error") if run.output else None
if error_code == "login_failed":
if retries > 0:
await refresh_credentials("vendor-portal")
return await run_with_error_handling(retries=retries - 1)
return {"success": False, "reason": "login_failed", "details": "Retry limit reached"}
elif error_code == "invoice_not_found":
# Expected condition—no invoice for this period
return {"success": False, "reason": "no_invoice", "date": invoice_date}
elif error_code == "maintenance":
# Schedule retry for later
await schedule_retry(run_id, delay_minutes=60)
return {"success": False, "reason": "scheduled_retry"}
else:
# Unknown error—log and alert
log_error(run_id, run.failure_reason)
return {"success": False, "reason": "unknown", "details": run.failure_reason}
```
```typescript TypeScript
async function runWithErrorHandling(retries = 1) {
const result = await client.runTask({
body: {
prompt: "Log in and download the invoice",
url: "https://vendor-portal.example.com",
error_code_mapping: {
login_failed: "Login credentials are incorrect or account is locked",
invoice_not_found: "No invoice for the requested date",
maintenance: "Site is down for maintenance",
},
},
});
const runId = result.run_id;
// Poll until terminal state
let run;
while (true) {
run = await client.getRun(runId);
if (["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
break;
}
await new Promise((r) => setTimeout(r, 5000));
}
// Handle based on status and error code
if (run.status === "completed") {
return { success: true, data: run.output };
}
const errorCode = run.output?.error;
switch (errorCode) {
case "login_failed":
if (retries > 0) {
await refreshCredentials("vendor-portal");
return runWithErrorHandling(retries - 1);
}
return { success: false, reason: "login_failed", details: "Retry limit reached" };
case "invoice_not_found":
return { success: false, reason: "no_invoice", date: invoiceDate };
case "maintenance":
await scheduleRetry(runId, { delayMinutes: 60 });
return { success: false, reason: "scheduled_retry" };
default:
logError(runId, run.failure_reason);
return { success: false, reason: "unknown", details: run.failure_reason };
}
}
```
</CodeGroup>
---
## Validation blocks as assertions
Validation blocks are assertions that check conditions at critical points—like unit test assertions. If validation fails, the workflow terminates immediately with your error code instead of continuing and failing later with a confusing error.
Use validation blocks after steps where you need to confirm success before proceeding:
<CodeGroup>
```json JSON
{
"blocks": [
{
// First, attempt to log in
"block_type": "navigation",
"label": "login",
"url": "https://vendor-portal.example.com/login",
"navigation_goal": "Log in using stored credentials"
},
{
// Then verify login succeeded before continuing
"block_type": "validation",
"label": "verify_login",
"complete_criterion": "Dashboard or account overview page is visible",
"terminate_criterion": "Login error message, CAPTCHA, or still on login page",
"error_code_mapping": {
"login_failed": "Login error message is displayed",
"captcha_required": "CAPTCHA verification is shown",
"session_expired": "Session timeout message appeared"
}
},
{
// Only runs if validation passed
"block_type": "navigation",
"label": "download_invoice",
"navigation_goal": "Navigate to invoices and download {{invoice_date}}",
"error_code_mapping": {
"invoice_not_found": "No invoice for the specified date"
}
}
]
}
```
```yaml YAML
blocks:
# First, attempt to log in
- block_type: navigation
label: login
url: "https://vendor-portal.example.com/login"
navigation_goal: "Log in using stored credentials"
# Then verify login succeeded before continuing
- block_type: validation
label: verify_login
complete_criterion: "Dashboard or account overview page is visible"
terminate_criterion: "Login error message, CAPTCHA, or still on login page"
error_code_mapping:
login_failed: "Login error message is displayed"
captcha_required: "CAPTCHA verification is shown"
session_expired: "Session timeout message appeared"
# Only runs if validation passed
- block_type: navigation
label: download_invoice
navigation_goal: "Navigate to invoices and download {{invoice_date}}"
error_code_mapping:
invoice_not_found: "No invoice for the specified date"
```
</CodeGroup>
| Parameter | Purpose |
|-----------|---------|
| `complete_criterion` | Condition that must be true to continue to the next block |
| `terminate_criterion` | Condition that stops the workflow immediately |
| `error_code_mapping` | Maps termination conditions to your error codes |
If `verify_login` sees a login error, the workflow terminates with `output.error = "login_failed"`. Your Step 4 code handles it the same way as any other error code.
---
## Common error patterns
| Error Code | Description | Typical Response |
|------------|-------------|------------------|
| `login_failed` | Credentials wrong, account locked, or MFA | Refresh credentials, retry |
| `captcha_required` | CAPTCHA blocking automation | Use `human_interaction` block or browser profile |
| `not_found` | Target data doesn't exist | Return empty result, don't retry |
| `maintenance` | Site temporarily down | Schedule retry with backoff |
| `rate_limited` | Too many requests | Add delays, use different proxy |
| `access_denied` | Permission issue | Check account permissions |
| `timeout` | Task took too long | Increase `max_steps`, simplify task |
---
## Next steps
<CardGroup cols={2}>
<Card
title="Reliability Tips"
icon="shield-check"
href="/going-to-production/reliability-tips"
>
Write prompts that fail less often
</Card>
<Card
title="Webhooks"
icon="webhook"
href="/going-to-production/webhooks"
>
Get notified when runs complete or fail
</Card>
</CardGroup>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,366 @@
---
title: Reliability Tips
subtitle: Write robust prompts and handle edge cases
slug: going-to-production/reliability-tips
---
The difference between a demo and production-ready automation is reliability. This page covers techniques for writing prompts that work consistently, handling dynamic pages, and validating results.
---
## Writing robust prompts
### Anatomy of a good prompt
Every prompt needs four components:
1. **Main goal** (required) — What should happen
2. **Guardrails** — Constraints and boundaries
3. **Payload** — Data to use in form fields
4. **Completion criteria** (required) — How to know when done
```
Your goal is to fill out the contact form with the provided information.
Only fill out required fields. Skip optional fields unless you have data for them.
Here is the information:
{{contact_data}}
COMPLETE when the form is submitted and you see a confirmation message.
TERMINATE if you see an error message or the form cannot be submitted.
```
### What works
**Be explicit about completion.** The AI needs to know when to stop. "COMPLETE when you see 'Order confirmed'" is unambiguous. Without this, the AI might keep navigating or stop too early.
<Tip>
**Don't forget the submit button.** A common failure mode is the AI completing after filling form fields but before clicking Submit. Be explicit: "COMPLETE when you have clicked the Submit button AND see a confirmation message." The AI might consider the form "done" once all fields are filled—make clear that submission is part of the goal.
</Tip>
**Use visual descriptions.** "Click the blue Submit button at the bottom of the form" works better than "click Submit"—there might be multiple buttons, and visual context helps the AI pick the right one. Describe position, color, icons, and surrounding text.
**Start general, then refine.** Begin with simple prompts and add specifics based on failures. Over-specified prompts are brittle; the AI handles variation better when you describe goals rather than exact steps.
**Include termination criteria.** Tell the AI when to give up: "TERMINATE if login fails or account is locked." Without this, the AI might keep trying forever or fail silently.
**Reference visual indicators.** "The invoice download link has a PDF icon next to it" helps the AI identify the right element when there are multiple links with similar text.
### What doesn't work
**Vague goals.** "Do the thing on the website" gives the AI nothing to work with. Be specific about what outcome you want.
**Missing completion criteria.** Without knowing when to stop, the AI keeps navigating indefinitely or terminates at arbitrary points.
**Action lists without context.** "Click button A, then B, then C" breaks when the layout changes or buttons move. Describe the goal instead, and let the AI figure out the steps.
**HTML element names.** "Click the `<button id='submit'>` element" assumes IDs are stable and visible to the AI. They often change between deployments, and the AI works from visual screenshots, not DOM structure.
**Assuming page state.** The AI doesn't know what page you expect to be on. Always describe what you expect to see, not just what to do.
### Autocomplete and dropdown fields
Address fields, city selectors, and other autocomplete inputs are notoriously tricky. The AI might type a value, see suggestions appear, then second-guess itself—erasing and retyping, cycling through options like "Toronto" → "Ontario" → "Canada" → "Toronto" again before settling.
To reduce cycling behavior:
**Be explicit about the exact value.** Instead of "enter the city," say "type 'Toronto' and select it from the dropdown."
**Tell it to stop after selection.** Add "Once you've selected the value, move to the next field—don't modify it further."
**Specify which suggestion to pick.** "Select the first suggestion that matches" or "Choose the option that shows the full address" reduces ambiguity.
```
Fill in the shipping address:
- For the City field, type "Toronto" and select the first matching suggestion
- Do NOT cycle through suggestions or second-guess your selection
- Once a field is filled, move to the next field immediately
{{address_data}}
```
<Warning>
Multi-select fields (where you pick several options from a list) are slower and less reliable than single-select. If the form allows it, consider making multiple API calls with single values rather than one call selecting many options.
</Warning>
---
## Choosing the right block type
Skyvern offers three block types with different tradeoffs between reliability and flexibility. The more interpretation you ask the AI to do, the more room for unexpected behavior.
**Action blocks** are the most deterministic. You tell Skyvern exactly what to do: "Click the Submit button." There's no interpretation—it either finds the element and clicks it, or fails. Use these when you know exactly what action is needed and the page structure is predictable.
**Navigation blocks** give Skyvern a goal: "Fill out the registration form." The AI figures out which fields to fill and in what order. This handles variation in form layouts—fields might be in different positions, have different labels, or be split across tabs—but the AI can misinterpret ambiguous forms or fill fields you didn't intend.
**Task blocks (Navigation V2)** handle multi-step goals: "Log in, navigate to settings, update profile." Maximum flexibility for complex workflows, but more room for the AI to take unexpected paths. A task block might navigate through menus you didn't anticipate or skip steps it deems unnecessary.
<Warning>
**Task blocks keep navigating until they believe the goal is complete.** If you want to click "Next" once and then stop, a Task block will likely click through ALL pages until it reaches the end. For single-action-then-stop behavior, use an Action block or Navigation block with explicit completion criteria like "COMPLETE after clicking Next once."
</Warning>
**Rule of thumb:** Start with the most deterministic block that can accomplish your goal. If a single click solves the problem, don't use a Task block—you're adding unnecessary interpretation where errors can creep in. Reserve Task blocks for genuinely multi-step workflows where you can't predict the exact sequence of actions.
---
## Handling dynamic pages
### Lazy-loaded content
For pages that load content as you scroll:
```python
result = await client.run_task(
prompt="Extract all product listings from the page",
url="https://example.com/products",
max_screenshot_scrolls=5 # Scroll to load more content
)
```
### Popups and modals
Include handling instructions in your prompt:
```
Your goal is to add the item to cart.
If a popup appears asking about newsletter signup, close it by clicking the X.
If a cookie consent banner appears, click Accept.
COMPLETE when the item is in the cart and the cart count increases.
```
### Multi-step forms
For forms spread across multiple pages:
```
Your goal is to complete the checkout process.
Page 1: Fill in shipping address using {{shipping_data}}
Page 2: Select standard shipping
Page 3: Enter payment details using {{payment_data}}
Page 4: Review and submit
COMPLETE when you see the order confirmation number.
TERMINATE if any validation error cannot be resolved.
```
---
## Validation strategies
Workflows can silently produce wrong results—the AI might fill a form with incorrect data, navigate to the wrong page, or extract stale information. Validation blocks let you assert conditions at critical points and fail fast when something goes wrong, rather than discovering the problem downstream.
### Add validation blocks at critical points
**After login:** Verify you're actually authenticated before proceeding. A failed login might redirect to an error page or show a CAPTCHA, and subsequent blocks will fail in confusing ways if you don't catch this early.
```yaml
- block_type: validation
label: verify_login_success
complete_criterion: Dashboard or account page is visible
terminate_criterion: Login error, CAPTCHA, or still on login page
```
**Before form submission:** Catch data entry errors before they become permanent. The AI might have misinterpreted a field or filled in default values instead of your parameters.
```yaml
- block_type: validation
label: verify_form_data
complete_criterion: |
Review page shows correct values:
- Name matches {{name}}
- Email matches {{email}}
terminate_criterion: Data mismatch or missing required fields
```
### Use extraction to verify
For validation that requires exact matching (email addresses, confirmation numbers, prices), extract the values and compare programmatically in your code rather than relying on the AI's judgment:
```python
result = await client.run_task(
prompt="Fill out the form and extract the confirmation number",
data_extraction_schema={
"type": "object",
"properties": {
"confirmation_number": {"type": "string"},
"submitted_email": {"type": "string"}
}
}
)
# Verify in your code
if result.output["submitted_email"] != expected_email:
raise ValueError("Email mismatch")
```
<Warning>
**Validation blocks read from the current page, not from prior block outputs.** If you need to validate extracted data from a previous block, use a Code block to compare values programmatically. Validation blocks evaluate their criteria against what's visible on screen—they don't have access to data extracted by earlier extraction blocks.
</Warning>
---
## ForLoop reliability
Loops are especially prone to cascading failures—when one iteration fails, it can leave the browser in an unexpected state that breaks subsequent iterations. For example, if iteration 3 navigates to an error page and fails, iteration 4 starts from that error page instead of the expected list view, causing it to fail too. One bad item can take down your entire loop.
### Always set `continue_on_failure`
```yaml
- block_type: for_loop
label: process_items
loop_over_parameter_key: items_list
continue_on_failure: true # Don't stop the whole loop if one item fails
loop_blocks:
- block_type: navigation
label: process_item
navigation_goal: "Process: {{ process_item.current_value }}"
```
### Add a reset block
Each iteration should start from a known state. If iteration 3 fails on a detail page, iteration 4 needs to navigate back to the list before it can find its item. Add a reset block at the start of each loop iteration:
```yaml
loop_blocks:
- block_type: navigation
label: reset_to_list
url: "https://example.com/items"
navigation_goal: "Navigate back to the items list"
- block_type: navigation
label: process_item
navigation_goal: "Find and process: {{ process_item.current_value }}"
```
### Include termination criteria per iteration
```yaml
- block_type: navigation
label: process_item
navigation_goal: |
Process the item: {{ process_item.current_value }}
COMPLETE when the item is processed.
TERMINATE if you're not on the expected page or the item doesn't exist.
```
---
## Session timeouts and human-in-the-loop
Browser sessions have a default timeout. If you're using human-in-the-loop workflows where a person must approve an action before proceeding, the session can expire while waiting for approval—especially if the approver is slow or in a different timezone.
**Symptoms:** Runs that worked during testing fail in production with session expiry errors. The workflow waits for human approval, but by the time approval comes, the browser session has timed out.
**Solutions:**
1. **Increase session timeout** — Set a longer `timeout` when creating browser sessions for workflows that include human approval steps
2. **Remove the human step for time-sensitive flows** — If session expiry is causing failures, consider making the workflow fully automated and reviewing results after completion
3. **Split into multiple workflows** — Run the pre-approval steps, wait for human approval outside Skyvern, then trigger a second workflow for post-approval steps using a browser profile to maintain login state
```python
# Longer timeout for human-in-the-loop workflows
session = await client.create_browser_session(
timeout=300, # 5 minutes instead of default
proxy_location="RESIDENTIAL"
)
```
<Note>
For workflows with predictable human approval times, schedule runs to arrive in the approver's inbox during working hours when they can act quickly.
</Note>
---
## Keyboard actions and workarounds
<Note>
Direct keyboard shortcuts (Ctrl+C, Alt+Tab, Esc, etc.) are not currently supported. Here are workarounds for common scenarios.
</Note>
| Scenario | Workaround |
|----------|------------|
| **Copy text** | Use extraction block to get the text value |
| **Paste into field** | Pass the value as a parameter: `{{value_to_paste}}` |
| **Press Escape to close modal** | Click the X button or "Close" link instead. Add to prompt: "If a modal appears, close it by clicking the X button in the top right corner" |
| **Keyboard navigation (Tab, arrows)** | Describe the click target visually instead |
| **Ctrl+S to save** | Click the Save button in the UI, or look for auto-save indicators |
| **Hotkey-only features** | Look for menu alternatives, toolbar buttons, or right-click context menus |
| **Tab between fields** | AI handles field navigation automatically—no workaround needed |
| **Enter to submit** | Explicitly click the Submit button rather than relying on Enter key |
For applications that genuinely require keyboard shortcuts with no UI alternative:
1. **Use the Code block** with custom Playwright scripts—you can call `page.keyboard.press('Escape')` directly
2. **Check for an API** — Many web apps have APIs that bypass the UI entirely
3. **Use browser profiles** with pre-configured settings or extensions that add UI buttons for keyboard-only features
```python
# Example: Using Code block for keyboard actions
- block_type: code
label: press_escape
code: |
page = context["skyvern_page"].page
await page.keyboard.press("Escape")
result = {"escaped": True}
```
---
## Troubleshooting workflow
When a run fails:
1. **Check the recording** — Watch what actually happened
2. **Review screenshots** — See the state at each step
3. **Check LLM reasoning** — Understand why the AI made each decision
4. **Compare parameters** — Verify inputs match expectations
### Common fixes
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| **Completed too early** | Ambiguous completion criteria | Add specific visual indicators. Include "click Submit AND see confirmation" |
| **Completed without submitting** | Submit button not included in goal | Explicitly state that clicking Submit is part of the task |
| **Didn't complete** | Missing completion criteria | Add explicit COMPLETE condition |
| **Wrong element clicked** | Multiple similar elements | Add distinguishing details: position, color, surrounding text |
| **Form field skipped** | Field not visible or labeled differently | Describe the field's visual position |
| **Loop stuck** | No reset between iterations | Add reset block at loop start |
| **Autocomplete cycling** | AI second-guessing dropdown selections | Add "select and move on—don't modify" to prompt |
| **Task navigates too far** | Task block continues until goal complete | Use Action/Navigation block for single-step actions |
| **Session expired** | Human-in-the-loop timeout | Increase timeout or split into multiple workflows |
| **Max steps reached** | Complex page or AI retrying actions | Increase `max_steps_per_run` or simplify the goal |
### When to adjust prompts vs file a bug
**Adjust your prompt if:**
- The AI misunderstood what to do
- Completion criteria were ambiguous
- Visual descriptions were unclear
**File a bug if:**
- Elements are visibly present but not detected
- Actions execute on wrong elements consistently
- Standard UI patterns (dropdowns, checkboxes) don't work
---
## Next steps
<CardGroup cols={2}>
<Card
title="Error Handling"
icon="triangle-exclamation"
href="/going-to-production/error-handling"
>
Map errors to custom codes for programmatic handling
</Card>
<Card
title="Workflow Blocks Reference"
icon="cube"
href="/multi-step-automations/workflow-blocks-reference"
>
Detailed documentation for validation and other blocks
</Card>
</CardGroup>

View File

@@ -0,0 +1,530 @@
---
title: Webhooks
subtitle: Get notified when tasks and workflows complete
slug: going-to-production/webhooks
---
Workflows and task runs are asynchronous. When you call `run_task` or `run_workflow`, the API returns immediately with a run ID, but the actual execution happens in the background and can take variable time.
Instead of polling the `get_runs` endpoint, you can use Webhooks to get notified when they finish.
This page covers setting them up, explains payload structure, signature verification, and handling delivery failures.
---
## Step 1: Set webhook URL
### For tasks
<CodeGroup>
```python Python
result = await client.run_task(
prompt="Get the price of this product",
url="https://example.com/product/123",
webhook_url="https://your-server.com/webhook",
)
```
```typescript TypeScript
const result = await client.runTask({
body: {
prompt: "Get the price of this product",
url: "https://example.com/product/123",
webhook_url: "https://your-server.com/webhook",
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/tasks" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Get the price of this product",
"url": "https://example.com/product/123",
"webhook_url": "https://your-server.com/webhook"
}'
```
</CodeGroup>
### For workflows
Set a default webhook when creating the workflow, or override it per-run:
<CodeGroup>
```python Python
# Set default on workflow (webhook_callback_url goes inside json_definition)
workflow = await client.create_workflow(
json_definition={
"title": "Invoice Downloader",
"webhook_callback_url": "https://your-server.com/webhook",
"workflow_definition": {
"parameters": [],
"blocks": [
{
"block_type": "task",
"label": "download_invoice",
"url": "https://vendor-portal.example.com",
"prompt": "Download the latest invoice"
}
]
}
}
)
# Override for a specific run
run = await client.run_workflow(
workflow_id=workflow.workflow_permanent_id,
parameters={},
webhook_url="https://your-server.com/different-webhook"
)
```
```typescript TypeScript
// Set default on workflow (webhook_callback_url goes inside json_definition)
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "Invoice Downloader",
webhook_callback_url: "https://your-server.com/webhook",
workflow_definition: {
parameters: [],
blocks: [
{
block_type: "task",
label: "download_invoice",
url: "https://vendor-portal.example.com",
prompt: "Download the latest invoice",
},
],
},
},
},
});
// Override for a specific run
const run = await client.runWorkflow({
body: {
workflow_id: workflow.workflow_permanent_id,
parameters: {},
webhook_url: "https://your-server.com/different-webhook",
},
});
```
```bash cURL
# Set default on workflow (webhook_callback_url goes inside json_definition)
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "Invoice Downloader",
"webhook_callback_url": "https://your-server.com/webhook",
"workflow_definition": {
"parameters": [],
"blocks": [
{
"block_type": "task",
"label": "download_invoice",
"url": "https://vendor-portal.example.com",
"prompt": "Download the latest invoice"
}
]
}
}
}'
# Override for a specific run (use workflow_permanent_id from create response)
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"parameters": {},
"webhook_url": "https://your-server.com/different-webhook"
}'
```
</CodeGroup>
<Info>
When creating a workflow, use `webhook_callback_url` inside `json_definition` — this sets the default for all runs. When running a workflow, use `webhook_url` at the top level to override for that specific run.
</Info>
**Quick reference:**
| Context | Parameter | Location |
|---------|-----------|----------|
| Task run | `webhook_url` | Top-level parameter |
| Workflow creation | `webhook_callback_url` | Inside `json_definition` |
| Workflow run (override) | `webhook_url` | Top-level parameter |
<Warning>
**Watch the parameter names.** Using `webhook_url` when creating a workflow (instead of `webhook_callback_url` inside `json_definition`) silently results in no webhook being sent. The API won't return an error—your runs will just complete without notifications.
</Warning>
---
## Step 2: Understand the payload
Skyvern sends a JSON payload with run results. Here's a real example from a completed task:
**Webhook Payload:**
```json
{
"run_id": "tsk_v2_490440779503357994",
"task_id": "tsk_v2_490440779503357994",
"status": "completed",
"output": {
"top_post_title": "Antirender: remove the glossy shine on architectural renderings"
},
"summary": "I have successfully retrieved the title of the top post from the Hacker News homepage.",
"prompt": "Get the title of the top post on Hacker News",
"url": "https://news.ycombinator.com/",
"downloaded_files": [],
"recording_url": "https://skyvern-artifacts.s3.amazonaws.com/v1/production/o_485917350850524254/tsk_490440844256003946/.../recording.webm?AWSAccessKeyId=...&Signature=...&Expires=...",
"screenshot_urls": ["https://skyvern-artifacts.s3.amazonaws.com/v1/production/o_485917350850524254/tsk_490441394011816060/.../screenshot_final.png?AWSAccessKeyId=..."],
"failure_reason": null,
"errors": [],
"step_count": 4,
"run_type": "task_v2",
"app_url": "https://app.skyvern.com/runs/wr_490440779503358000",
"organization_id": "o_485917350850524254",
"workflow_run_id": "wr_490440779503358000",
"workflow_id": "w_490440779503357996",
"workflow_permanent_id": "wpid_490440779503357998",
"proxy_location": "RESIDENTIAL",
"webhook_callback_url": "https://webhook.site/d8d013c1-0481-48d0-8d13-281e8563a508",
"webhook_failure_reason": null,
"created_at": "2026-01-31T15:20:42.160725",
"modified_at": "2026-01-31T15:23:34.993138",
"queued_at": "2026-01-31T15:20:42.371545",
"started_at": "2026-01-31T15:20:44.391756",
"finished_at": "2026-01-31T15:23:34.992815"
}
```
**Request Headers Sent:**
```http
POST /d8d013c1-0481-48d0-8d13-281e8563a508 HTTP/1.1
Host: webhook.site
Content-Type: application/json
x-skyvern-signature: 024025ccf0bbfe1c8978bdaae43fc136fc8b614b92e2f63c3485be5a36866f68
x-skyvern-timestamp: 1769873016
Content-Length: 8208
User-Agent: python-httpx/0.28.1
{...json payload above...}
```
| Field | Type | Description |
|-------|------|-------------|
| `run_id` | string | Unique identifier for this run |
| `task_id` | string | Same as `run_id` |
| `status` | string | `completed`, `failed`, `terminated`, `timed_out`, or `canceled` |
| `output` | object \| null | Extracted data from the task. If you configured [`error_code_mapping`](/going-to-production/error-handling#step-3-use-error_code_mapping), failed runs include `output.error` with your custom error code. |
| `summary` | string | AI-generated description of what was done |
| `prompt` | string | The prompt from the original request |
| `url` | string | The URL from the original request |
| `downloaded_files` | array | Files downloaded during execution |
| `recording_url` | string \| null | Video recording of the browser session |
| `screenshot_urls` | array \| null | Screenshots captured (latest first) |
| `failure_reason` | string \| null | Error message if the run failed |
| `errors` | array | List of errors encountered |
| `step_count` | integer \| null | Number of steps executed |
| `run_type` | string | Type of run: `task_v2`, `openai_cua`, `anthropic_cua` |
| `app_url` | string | Link to view this run in Skyvern Cloud |
| `organization_id` | string | Your organization ID |
| `workflow_run_id` | string | Associated workflow run ID |
| `workflow_id` | string | Internal workflow ID |
| `workflow_permanent_id` | string | Permanent workflow ID used to run the workflow |
| `proxy_location` | string | Proxy location used (e.g., `RESIDENTIAL`) |
| `webhook_callback_url` | string | The webhook URL that received this payload |
| `webhook_failure_reason` | string \| null | Error message if a previous webhook delivery failed (always `null` in the payload you receive) |
| `created_at` | datetime | When the run was created |
| `modified_at` | datetime | When the run was last updated |
| `queued_at` | datetime \| null | When the run entered the queue |
| `started_at` | datetime \| null | When execution began |
| `finished_at` | datetime \| null | When execution completed |
---
## Optional: Verify webhook signatures
Skyvern signs every webhook with your API key using HMAC-SHA256, so you can verify the request actually came from Skyvern before acting on it.
**Headers sent with every webhook:**
- `x-skyvern-signature` — HMAC-SHA256 signature of the payload
- `x-skyvern-timestamp` — Unix timestamp when the webhook was sent
- `Content-Type: application/json`
<CodeGroup>
```python Python
import hmac
import hashlib
from fastapi import Request, HTTPException
async def handle_webhook(request: Request):
signature = request.headers.get("x-skyvern-signature")
payload = await request.body()
expected = hmac.new(
SKYVERN_API_KEY.encode("utf-8"),
msg=payload,
digestmod=hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise HTTPException(status_code=401, detail="Invalid signature")
data = await request.json()
# Process the webhook...
```
```typescript TypeScript
import crypto from "crypto";
import express from "express";
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-skyvern-signature"];
const payload = req.body;
const expected = crypto
.createHmac("sha256", process.env.SKYVERN_API_KEY)
.update(payload)
.digest("hex");
// Use timing-safe comparison to prevent timing attacks
let valid = false;
try {
crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expected)
);
valid = true;
} catch (err) {
valid = false;
}
if (!valid) {
return res.status(401).send("Invalid signature");
}
const data = JSON.parse(payload);
// Process the webhook...
res.status(200).send("OK");
});
```
```go Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("x-skyvern-signature")
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("SKYVERN_API_KEY")))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expected)) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the webhook...
w.WriteHeader(http.StatusOK)
}
```
</CodeGroup>
<Note>
**Use constant-time comparison** to prevent timing attacks:
- Python: `hmac.compare_digest()`
- TypeScript: `crypto.timingSafeEqual()`
- Go: `hmac.Equal()`
Never use simple equality operators (`==` or `===`) for signature comparison as they are vulnerable to timing attacks.
</Note>
<Warning>
**Always validate against the raw request body bytes.** Skyvern normalizes JSON before signing: it removes whitespace (using compact separators) and converts whole-number floats to integers (`3.0` becomes `3`). If you parse the JSON and re-serialize it, the byte representation will differ and signature validation will fail.
</Warning>
---
## Handling webhook failures
Task execution and webhook delivery are independent—a task can succeed while webhook delivery fails. When this happens, the run shows `status: "failed"` even though your data was extracted successfully.
```mermaid
graph TD
A["Run Finishes"] --> B{"Check status"}
B -->|"completed"| C["Task succeeded<br>Webhook delivered"]
B -->|"failed"| D{"Does failure_reason<br>mention 'webhook'?"}
B -->|"terminated"| E["Goal unachievable"]
D -->|"Yes"| F["Webhook delivery failed"]
D -->|"No"| G["Task execution failed"]
F --> H["Check output field<br>Data may still exist!"]
G --> I["Check failure_reason"]
C --> J["Process output"]
style A fill:#f8fafc,stroke:#6366F1,stroke-width:2px,color:#1e293b
style B fill:#f8fafc,stroke:#6366F1,stroke-width:2px,color:#1e293b
style C fill:#6366F1,stroke:#4F46E5,stroke-width:2px,color:#fff
style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b
style E fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#475569
style F fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#92400e
style G fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#991b1b
style H fill:#6366F1,stroke:#4F46E5,stroke-width:2px,color:#fff
style I fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#475569
style J fill:#6366F1,stroke:#4F46E5,stroke-width:2px,color:#fff
```
Webhook delivery can fail due to network issues, server errors, or misconfigured URLs.
When this happens, the run is marked as `failed` and the error is recorded in the `failure_reason` field. Check it by calling `get_run` after the run terminates:
<CodeGroup>
```python Python
run = await client.get_run(run_id)
if run.status == "failed" and "webhook" in (run.failure_reason or "").lower():
print(f"Webhook failed: {run.failure_reason}")
# The task may have completed successfully before webhook delivery failed
# Output data is still available
if run.output:
process_output(run.output)
```
```typescript TypeScript
const run = await client.getRun(runId);
if (run.status === "failed" && run.failure_reason?.toLowerCase().includes("webhook")) {
console.log(`Webhook failed: ${run.failure_reason}`);
// The task may have completed successfully before webhook delivery failed
// Output data is still available
if (run.output) {
processOutput(run.output);
}
}
```
```bash cURL
curl -X GET "https://api.skyvern.com/v1/runs/$RUN_ID" \
-H "x-api-key: $SKYVERN_API_KEY"
# Check status and failure_reason fields in response
```
</CodeGroup>
The `failure_reason` field contains the specific error message, for example:
```json
{
"run_id": "tsk_v2_486305187432193504",
"status": "failed",
"output": {"price": "$29.99"},
"failure_reason": "Failed to run task 2.0: Failed to send webhook. task_v2_id=tsk_v2_486305187432193504"
}
```
<Note>
Even when webhook delivery fails, the task's `output` field may still contain extracted data if the browser automation completed successfully before the webhook attempt.
</Note>
**Common reasons webhooks fail:**
- **Server unreachable** — Your server is down, behind a firewall, or the URL is incorrect. Verify the URL is publicly accessible (not `localhost`) and check your server logs for incoming requests.
- **Timeout** — Skyvern waits 10 seconds for a response. If your server takes longer, the delivery is marked as failed even if processing eventually succeeds. Return `200 OK` immediately and process the payload in a background job.
- **Server returns an error** — Your endpoint received the payload but responded with a non-2xx status code (e.g., 500). Check your server logs to identify the issue.
- **Signature validation fails** — If your verification logic rejects the request, make sure you're validating against the raw request body, not parsed-and-re-serialized JSON (re-serializing changes the byte representation). Also verify you're using the same API key that created the run.
**Recommended pattern:** Always have a fallback polling mechanism for critical workflows. If you don't receive a webhook within your expected window, call `get_run` to check if the run completed and retrieve the data directly.
### Retrying webhooks
Once you've identified and fixed the issue, you can replay the webhook using `retry_run_webhook`.
<Note>
Skyvern does not automatically retry failed webhooks. This is intentional—automatic retries can cause duplicate processing if your server received the payload but returned an error. You must explicitly call `retry_run_webhook` after fixing the issue.
</Note>
<CodeGroup>
```python Python
from skyvern.client import RetryRunWebhookRequest
await client.retry_run_webhook("tsk_v2_486305187432193504")
# Or send to a different URL
await client.retry_run_webhook(
"tsk_v2_486305187432193504",
request=RetryRunWebhookRequest(webhook_url="https://your-server.com/new-webhook")
)
```
```typescript TypeScript
await client.retryRunWebhook("tsk_v2_486305187432193504");
// Or send to a different URL
await client.retryRunWebhook("tsk_v2_486305187432193504", {
webhook_url: "https://your-server.com/new-webhook",
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/runs/tsk_v2_486305187432193504/retry_webhook" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# Or send to a different URL
curl -X POST "https://api.skyvern.com/v1/runs/tsk_v2_486305187432193504/retry_webhook" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://your-server.com/new-webhook"}'
```
</CodeGroup>
<Warning>
`retry_run_webhook` is fire-and-forget—it returns immediately without waiting for delivery confirmation. To verify success, monitor your webhook endpoint directly or check the run's `failure_reason` field after a short delay.
</Warning>
<Tip>
**Implement idempotency.** If you call `retry_run_webhook`, you may receive the same payload twice (once from the original attempt that your server processed but returned an error, and once from the retry). Use the `run_id` as an idempotency key—check if you've already processed this run before taking action.
</Tip>
You can pass a different `webhook_url` to send the payload to a new endpoint—useful if the original URL was misconfigured.
---
## Next steps
<CardGroup cols={2}>
<Card
title="Error Handling"
icon="triangle-exclamation"
href="/going-to-production/error-handling"
>
Handle failures and map custom error codes
</Card>
<Card
title="Reliability Tips"
icon="shield-check"
href="/going-to-production/reliability-tips"
>
Write robust prompts and add validation blocks
</Card>
</CardGroup>

View File

@@ -0,0 +1,116 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1380 500" width="1380" height="500">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
</style>
<!-- Hachure fill pattern -->
<pattern id="hachure" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(135)">
<line x1="0" y1="0" x2="0" y2="6" stroke="#5b7fff" stroke-width="0.6" opacity="0.07"/>
</pattern>
</defs>
<!-- ============ CANVAS ============ -->
<rect width="1380" height="500" fill="#121220"/>
<!-- ============ TITLE SECTION ============ -->
<text x="690" y="46" font-family="Inter, -apple-system, BlinkMacSystemFont, sans-serif" font-size="26" font-weight="700" fill="#ffffff" text-anchor="middle" letter-spacing="-0.5">IRS SS-4 Filing Workflow</text>
<text x="690" y="72" font-family="Inter, -apple-system, sans-serif" font-size="13" fill="#7a85b2" text-anchor="middle" font-weight="400">Automated EIN application, verification, and delivery</text>
<!-- Divider -->
<line x1="128" y1="92" x2="1252" y2="92" stroke="#5b7fff" stroke-opacity="0.2" stroke-width="1" stroke-dasharray="8,5"/>
<!-- ============ CARD 1: NAVIGATION ============ -->
<rect x="128" y="115" width="180" height="200" rx="12" fill="#1a1a38" stroke="#5b7fff" stroke-width="2"/>
<rect x="128" y="115" width="180" height="200" rx="12" fill="url(#hachure)"/>
<circle cx="218" cy="155" r="18" fill="#004bdd" stroke="#5b7fff" stroke-width="1.5"/>
<text x="218" y="161" font-family="Inter, sans-serif" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle">1</text>
<text x="218" y="202" font-family="Inter, sans-serif" font-size="16" font-weight="600" fill="#f0f2ff" text-anchor="middle">Navigation</text>
<line x1="152" y1="218" x2="284" y2="218" stroke="#5b7fff" stroke-opacity="0.25" stroke-width="1" stroke-dasharray="4,4"/>
<text font-family="Inter, sans-serif" font-size="12.5" fill="#a0a8cc" text-anchor="middle">
<tspan x="218" y="244">Fill out SS-4 form</tspan>
<tspan x="218" dy="18">with company info</tspan>
</text>
<!-- ====== ARROW 1→2 ====== -->
<path d="M 313,215 C 330,207 350,223 358,215" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round"/>
<path d="M 351,208 L 361,215 L 351,222" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- ============ CARD 2: VALIDATION ============ -->
<rect x="364" y="115" width="180" height="200" rx="12" fill="#1a1a38" stroke="#5b7fff" stroke-width="2"/>
<rect x="364" y="115" width="180" height="200" rx="12" fill="url(#hachure)"/>
<circle cx="454" cy="155" r="18" fill="#004bdd" stroke="#5b7fff" stroke-width="1.5"/>
<text x="454" y="161" font-family="Inter, sans-serif" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle">2</text>
<text x="454" y="202" font-family="Inter, sans-serif" font-size="16" font-weight="600" fill="#f0f2ff" text-anchor="middle">Validation</text>
<line x1="388" y1="218" x2="520" y2="218" stroke="#5b7fff" stroke-opacity="0.25" stroke-width="1" stroke-dasharray="4,4"/>
<text font-family="Inter, sans-serif" font-size="12.5" fill="#a0a8cc" text-anchor="middle">
<tspan x="454" y="244">Verify the summary</tspan>
<tspan x="454" dy="18">is correct</tspan>
</text>
<!-- ====== ARROW 2→3 ====== -->
<path d="M 549,215 C 566,223 586,207 594,215" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round"/>
<path d="M 587,208 L 597,215 L 587,222" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- ============ CARD 3: EXTRACTION ============ -->
<rect x="600" y="115" width="180" height="200" rx="12" fill="#1a1a38" stroke="#5b7fff" stroke-width="2"/>
<rect x="600" y="115" width="180" height="200" rx="12" fill="url(#hachure)"/>
<circle cx="690" cy="155" r="18" fill="#004bdd" stroke="#5b7fff" stroke-width="1.5"/>
<text x="690" y="161" font-family="Inter, sans-serif" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle">3</text>
<text x="690" y="202" font-family="Inter, sans-serif" font-size="16" font-weight="600" fill="#f0f2ff" text-anchor="middle">Extraction</text>
<line x1="624" y1="218" x2="756" y2="218" stroke="#5b7fff" stroke-opacity="0.25" stroke-width="1" stroke-dasharray="4,4"/>
<text font-family="Inter, sans-serif" font-size="12.5" fill="#a0a8cc" text-anchor="middle">
<tspan x="690" y="244">Extract the assigned</tspan>
<tspan x="690" dy="18">EIN and legal name</tspan>
</text>
<!-- ====== ARROW 3→4 ====== -->
<path d="M 785,215 C 802,208 822,222 830,215" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round"/>
<path d="M 823,208 L 833,215 L 823,222" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- ============ CARD 4: FILE DOWNLOAD ============ -->
<rect x="836" y="115" width="180" height="200" rx="12" fill="#1a1a38" stroke="#5b7fff" stroke-width="2"/>
<rect x="836" y="115" width="180" height="200" rx="12" fill="url(#hachure)"/>
<circle cx="926" cy="155" r="18" fill="#004bdd" stroke="#5b7fff" stroke-width="1.5"/>
<text x="926" y="161" font-family="Inter, sans-serif" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle">4</text>
<text x="926" y="202" font-family="Inter, sans-serif" font-size="16" font-weight="600" fill="#f0f2ff" text-anchor="middle">File Download</text>
<line x1="860" y1="218" x2="992" y2="218" stroke="#5b7fff" stroke-opacity="0.25" stroke-width="1" stroke-dasharray="4,4"/>
<text font-family="Inter, sans-serif" font-size="12.5" fill="#a0a8cc" text-anchor="middle">
<tspan x="926" y="244">Download confirmation</tspan>
<tspan x="926" dy="18">letter PDF</tspan>
</text>
<!-- ====== ARROW 4→5 ====== -->
<path d="M 1021,215 C 1038,222 1058,208 1066,215" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round"/>
<path d="M 1059,208 L 1069,215 L 1059,222" fill="none" stroke="#5b7fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- ============ CARD 5: SEND EMAIL ============ -->
<rect x="1072" y="115" width="180" height="200" rx="12" fill="#1a1a38" stroke="#5b7fff" stroke-width="2"/>
<rect x="1072" y="115" width="180" height="200" rx="12" fill="url(#hachure)"/>
<circle cx="1162" cy="155" r="18" fill="#004bdd" stroke="#5b7fff" stroke-width="1.5"/>
<text x="1162" y="161" font-family="Inter, sans-serif" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle">5</text>
<text x="1162" y="202" font-family="Inter, sans-serif" font-size="16" font-weight="600" fill="#f0f2ff" text-anchor="middle">Send Email</text>
<line x1="1096" y1="218" x2="1228" y2="218" stroke="#5b7fff" stroke-opacity="0.25" stroke-width="1" stroke-dasharray="4,4"/>
<text font-family="Inter, sans-serif" font-size="12.5" fill="#a0a8cc" text-anchor="middle">
<tspan x="1162" y="244">Email the letter to</tspan>
<tspan x="1162" dy="18">founders@company.com</tspan>
</text>
<!-- ============ INPUT / OUTPUT SECTION ============ -->
<!-- Input box -->
<rect x="128" y="345" width="540" height="70" rx="10" fill="#14142a" stroke="#5b7fff" stroke-width="1.5" stroke-opacity="0.35" stroke-dasharray="6,4"/>
<text x="152" y="374" font-family="Inter, sans-serif" font-size="11" font-weight="600" fill="#5b7fff" letter-spacing="1.2">INPUT</text>
<rect x="152" y="385" width="72" height="22" rx="6" fill="#5b7fff" fill-opacity="0.1" stroke="#5b7fff" stroke-width="1" stroke-opacity="0.2"/>
<text x="188" y="400" font-family="SF Mono, Fira Code, Consolas, monospace" font-size="11" fill="#dee1ff" text-anchor="middle">ein_info</text>
<rect x="234" y="385" width="112" height="22" rx="6" fill="#5b7fff" fill-opacity="0.1" stroke="#5b7fff" stroke-width="1" stroke-opacity="0.2"/>
<text x="290" y="400" font-family="SF Mono, Fira Code, Consolas, monospace" font-size="11" fill="#dee1ff" text-anchor="middle">company_name</text>
<rect x="356" y="385" width="138" height="22" rx="6" fill="#5b7fff" fill-opacity="0.1" stroke="#5b7fff" stroke-width="1" stroke-opacity="0.2"/>
<text x="425" y="400" font-family="SF Mono, Fira Code, Consolas, monospace" font-size="11" fill="#dee1ff" text-anchor="middle">responsible_party</text>
<!-- Output box -->
<rect x="688" y="345" width="564" height="70" rx="10" fill="#14142a" stroke="#5b7fff" stroke-width="1.5" stroke-opacity="0.35" stroke-dasharray="6,4"/>
<text x="712" y="374" font-family="Inter, sans-serif" font-size="11" font-weight="600" fill="#5b7fff" letter-spacing="1.2">OUTPUT</text>
<text x="712" y="400" font-family="Inter, sans-serif" font-size="13" fill="#a0a8cc">EIN confirmation letter emailed to recipient</text>
<!-- ============ FOOTER ============ -->
<text x="690" y="465" font-family="Inter, sans-serif" font-size="12" font-weight="500" fill="#5b7fff" opacity="0.3" text-anchor="middle" letter-spacing="0.5">Powered by Skyvern</text>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,818 @@
---
title: Workflow Parameters
subtitle: Configure how your workflow runs
slug: multi-step-automations/workflow-parameters
---
This page covers all parameters you can pass when creating and running workflows.
---
## Quick reference: Creating a workflow
| Parameter | Type | Use this to |
|-----------|------|-------------|
| [`title`](#title) **(Required)** | string | Name the workflow for identification |
| [`workflow_definition`](#workflow_definition) **(Required)** | object | Define blocks, parameters, and logic |
| [`description`](#description) | string | Document what the workflow does |
| [`proxy_location`](#proxy_location-create) | string or object | Set default proxy for all blocks |
| [`webhook_callback_url`](#webhook_callback_url) | string | Get notified when any run of this workflow completes |
| [`persist_browser_session`](#persist_browser_session) | boolean | Keep the browser session alive across blocks |
---
## Quick reference: Running a workflow
| Parameter | Type | Use this to |
|-----------|------|-------------|
| [`workflow_id`](#workflow_id) **(Required)** | string | Specify which workflow to run |
| [`parameters`](#parameters) | object | Pass values for workflow input parameters |
| [`title`](#title-run) | string | Name this specific workflow run |
| [`proxy_location`](#proxy_location-run) | string or object | Override the default proxy location |
| [`webhook_url`](#webhook_url) | string | Get notified when the workflow completes |
| [`browser_session_id`](#browser_session_id) | string | Reuse a persistent browser session |
| [`browser_profile_id`](#browser_profile_id) | string | Reuse a browser profile (cookies, storage) |
---
## Creating a workflow
<Info>
All create parameters (`title`, `workflow_definition`, `description`, `proxy_location`, etc.) are nested inside a **`json_definition`** object — not passed at the top level. You can alternatively use `yaml_definition` (a YAML string) instead.
</Info>
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "...",
"workflow_definition": {...}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "...",
workflow_definition: {...},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "...",
"workflow_definition": {...}
}
}'
```
</CodeGroup>
### `title`
**Required.** Display name for the workflow. This field goes inside the `json_definition` object.
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "Invoice Downloader",
"workflow_definition": {...}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "Invoice Downloader",
workflow_definition: {...},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "Invoice Downloader",
"workflow_definition": {...}
}
}'
```
</CodeGroup>
---
### `workflow_definition`
**Required.** The workflow structure containing parameters and blocks.
```json
{
"workflow_definition": {
"parameters": [...],
"blocks": [...]
}
}
```
#### `parameters`
Input parameters the workflow accepts. Each parameter has:
| Field | Type | Description |
|-------|------|-------------|
| `key` | string | Parameter name, used to reference via `{{ key }}` |
| `parameter_type` | string | Always `"workflow"` for input parameters |
| `workflow_parameter_type` | string | Data type: `string`, `integer`, `float`, `boolean`, `json`, `file_url`, `credential_id` |
| `default_value` | any | Optional default value |
| `description` | string | Optional description |
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "Job Application Workflow",
"workflow_definition": {
"parameters": [
{
"key": "resume",
"parameter_type": "workflow",
"workflow_parameter_type": "file_url",
"description": "URL to the resume PDF"
},
{
"key": "job_urls",
"parameter_type": "workflow",
"workflow_parameter_type": "json",
"default_value": []
},
{
"key": "cover_letter",
"parameter_type": "workflow",
"workflow_parameter_type": "string",
"default_value": ""
}
],
"blocks": [...]
}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "Job Application Workflow",
workflow_definition: {
parameters: [
{
key: "resume",
parameter_type: "workflow",
workflow_parameter_type: "file_url",
description: "URL to the resume PDF",
},
{
key: "job_urls",
parameter_type: "workflow",
workflow_parameter_type: "json",
default_value: [],
},
{
key: "cover_letter",
parameter_type: "workflow",
workflow_parameter_type: "string",
default_value: "",
},
],
blocks: [...],
},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "Job Application Workflow",
"workflow_definition": {
"parameters": [
{
"key": "resume",
"parameter_type": "workflow",
"workflow_parameter_type": "file_url",
"description": "URL to the resume PDF"
},
{
"key": "job_urls",
"parameter_type": "workflow",
"workflow_parameter_type": "json",
"default_value": []
},
{
"key": "cover_letter",
"parameter_type": "workflow",
"workflow_parameter_type": "string",
"default_value": ""
}
],
"blocks": [...]
}
}
}'
```
</CodeGroup>
**Parameter types:**
| Type | Description | Example |
|------|-------------|---------|
| `string` | Text value | `"John Smith"` |
| `integer` | Whole number | `42` |
| `float` | Decimal number | `99.99` |
| `boolean` | True/false | `true` |
| `json` | JSON object or array | `{"key": "value"}` or `["a", "b"]` |
| `file_url` | URL to a file | `"https://example.com/resume.pdf"` |
| `credential_id` | Reference to a stored credential | `"cred_abc123"` |
#### `blocks`
Array of blocks that execute in sequence. See [Workflow Blocks Reference](/multi-step-automations/workflow-blocks-reference) for all block types.
---
### `description`
Optional description of what the workflow does.
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "Invoice Downloader",
"description": "Downloads all invoices from the vendor portal since a given date",
"workflow_definition": {...}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "Invoice Downloader",
description: "Downloads all invoices from the vendor portal since a given date",
workflow_definition: {...},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "Invoice Downloader",
"description": "Downloads all invoices from the vendor portal since a given date",
"workflow_definition": {...}
}
}'
```
</CodeGroup>
---
### `proxy_location` (create)
Default proxy location for all blocks in the workflow. Individual blocks can override this.
See [Proxy & Geo Targeting](/going-to-production/proxy-geolocation) for all available locations.
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "UK Price Checker",
"proxy_location": "RESIDENTIAL_GB",
"workflow_definition": {...}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "UK Price Checker",
proxy_location: "RESIDENTIAL_GB",
workflow_definition: {...},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "UK Price Checker",
"proxy_location": "RESIDENTIAL_GB",
"workflow_definition": {...}
}
}'
```
</CodeGroup>
---
### `webhook_callback_url`
URL to receive a POST request whenever any run of this workflow completes. This is set at the workflow level and applies to all runs. To set a webhook for a single run instead, use [`webhook_url`](#webhook_url) when running the workflow.
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "Invoice Downloader",
"webhook_callback_url": "https://your-server.com/webhook",
"workflow_definition": {...}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "Invoice Downloader",
webhook_callback_url: "https://your-server.com/webhook",
workflow_definition: {...},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "Invoice Downloader",
"webhook_callback_url": "https://your-server.com/webhook",
"workflow_definition": {...}
}
}'
```
</CodeGroup>
---
### `persist_browser_session`
If `true`, the browser session is kept alive across all blocks in the workflow. This means blocks share the same browser context including cookies, local storage, and login state.
Defaults to `false`.
<CodeGroup>
```python Python
workflow = await client.create_workflow(
json_definition={
"title": "Multi-Step Portal Scraper",
"persist_browser_session": True,
"workflow_definition": {...}
}
)
```
```typescript TypeScript
const workflow = await client.createWorkflow({
body: {
json_definition: {
title: "Multi-Step Portal Scraper",
persist_browser_session: true,
workflow_definition: {...},
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"json_definition": {
"title": "Multi-Step Portal Scraper",
"persist_browser_session": true,
"workflow_definition": {...}
}
}'
```
</CodeGroup>
<Info>
Additional advanced parameters are available when creating a workflow, including `totp_verification_url`, `totp_identifier`, `model`, and `extra_http_headers`. See the [API Reference](/api-reference) for the full list.
</Info>
---
## Running a workflow
### `workflow_id`
**Required.** The ID of the workflow to run. This is the `workflow_permanent_id` returned when creating the workflow.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
parameters={...}
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
parameters: {...},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"parameters": {...}
}'
```
</CodeGroup>
---
### `parameters`
Values for the workflow's input parameters. Keys must match the `key` field in the workflow's parameter definitions.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
parameters={
"resume": "https://example.com/resume.pdf",
"job_urls": [
"https://jobs.lever.co/company/position-1",
"https://jobs.lever.co/company/position-2"
],
"cover_letter": "I am excited to apply for this position..."
}
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
parameters: {
resume: "https://example.com/resume.pdf",
job_urls: [
"https://jobs.lever.co/company/position-1",
"https://jobs.lever.co/company/position-2",
],
cover_letter: "I am excited to apply for this position...",
},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"parameters": {
"resume": "https://example.com/resume.pdf",
"job_urls": [
"https://jobs.lever.co/company/position-1",
"https://jobs.lever.co/company/position-2"
],
"cover_letter": "I am excited to apply for this position..."
}
}'
```
</CodeGroup>
Parameters with `default_value` in the workflow definition are optional. Parameters without defaults are required.
---
### `proxy_location` (run)
Override the workflow's default proxy location for this run.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
parameters={...},
proxy_location="RESIDENTIAL_CA"
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
parameters: {...},
proxy_location: "RESIDENTIAL_CA",
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"parameters": {...},
"proxy_location": "RESIDENTIAL_CA"
}'
```
</CodeGroup>
**Proxy locations:**
| Value | Location |
|-------|----------|
| `RESIDENTIAL` | United States **(default)** |
| `RESIDENTIAL_ISP` | United States (static ISP IPs) |
| `RESIDENTIAL_AR` | Argentina |
| `RESIDENTIAL_AU` | Australia |
| `RESIDENTIAL_BR` | Brazil |
| `RESIDENTIAL_CA` | Canada |
| `RESIDENTIAL_DE` | Germany |
| `RESIDENTIAL_ES` | Spain |
| `RESIDENTIAL_FR` | France |
| `RESIDENTIAL_GB` | United Kingdom |
| `RESIDENTIAL_IE` | Ireland |
| `RESIDENTIAL_IN` | India |
| `RESIDENTIAL_IT` | Italy |
| `RESIDENTIAL_JP` | Japan |
| `RESIDENTIAL_MX` | Mexico |
| `RESIDENTIAL_NL` | Netherlands |
| `RESIDENTIAL_NZ` | New Zealand |
| `RESIDENTIAL_TR` | Türkiye |
| `RESIDENTIAL_ZA` | South Africa |
| `NONE` | No proxy |
For US state or city-level targeting, pass a `GeoTarget` object instead:
```json
{
"proxy_location": {
"country": "US",
"subdivision": "CA",
"city": "San Francisco"
}
}
```
`country` is required (ISO 3166-1 alpha-2 code). `subdivision` and `city` are optional.
See [Proxy & Geo Targeting](/going-to-production/proxy-geolocation) for more details.
---
### `webhook_url`
URL to receive a POST request when the workflow completes.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
parameters={...},
webhook_url="https://your-server.com/webhook"
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
parameters: {...},
webhook_url: "https://your-server.com/webhook",
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"parameters": {...},
"webhook_url": "https://your-server.com/webhook"
}'
```
</CodeGroup>
The webhook payload contains the same data as the polling response. See [Webhooks](/going-to-production/webhooks) for authentication and retry options.
---
### `title` (run)
Optional display name for this specific workflow run. Useful for distinguishing runs in the UI and API responses.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
title="January 2025 Invoice Batch",
parameters={...}
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
title: "January 2025 Invoice Batch",
parameters: {...},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"title": "January 2025 Invoice Batch",
"parameters": {...}
}'
```
</CodeGroup>
---
### `browser_session_id`
ID of an existing Skyvern browser session to reuse. The workflow run continues from the current screen state of that session. Browser session IDs start with `pbs_`.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
browser_session_id="pbs_abc123",
parameters={...}
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
browser_session_id: "pbs_abc123",
parameters: {...},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"browser_session_id": "pbs_abc123",
"parameters": {...}
}'
```
</CodeGroup>
---
### `browser_profile_id`
ID of a browser profile to reuse. Browser profiles persist cookies, local storage, and other browser state across runs. Profile IDs start with `bp_`.
<CodeGroup>
```python Python
run = await client.run_workflow(
workflow_id="wpid_123456789",
browser_profile_id="bp_xyz789",
parameters={...}
)
```
```typescript TypeScript
const run = await client.runWorkflow({
body: {
workflow_id: "wpid_123456789",
browser_profile_id: "bp_xyz789",
parameters: {...},
},
});
```
```bash cURL
curl -X POST "https://api.skyvern.com/v1/run/workflows" \
-H "x-api-key: $SKYVERN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wpid_123456789",
"browser_profile_id": "bp_xyz789",
"parameters": {...}
}'
```
</CodeGroup>
<Info>
Additional advanced parameters are available when running a workflow, including `totp_url`, `totp_identifier`, `browser_address`, `extra_http_headers`, and `max_screenshot_scrolls`. See the [API Reference](/api-reference) for the full list.
</Info>
---
## Run response
When you run a workflow, the response contains:
| Field | Type | Description |
|-------|------|-------------|
| `run_id` | string | Unique ID for this run (starts with `wr_`). |
| `status` | string | One of: `created`, `queued`, `running`, `completed`, `failed`, `terminated`, `timed_out`, `canceled`. |
| `output` | object/array/string/null | Extracted data from the workflow, if any. |
| `failure_reason` | string/null | Why the run failed, if applicable. |
| `errors` | array/null | List of errors encountered during the run. |
| `downloaded_files` | array/null | Files downloaded during the run. |
| `recording_url` | string/null | URL to the browser recording. |
| `screenshot_urls` | array/null | Latest screenshots in reverse chronological order. |
| `app_url` | string/null | Link to view this run in the Skyvern UI. |
| `created_at` | datetime | When the run was created. |
| `started_at` | datetime/null | When execution started. |
| `finished_at` | datetime/null | When execution finished. |
---
## Next steps
<CardGroup cols={2}>
<Card
title="Build a Workflow"
icon="hammer"
href="/multi-step-automations/build-a-workflow"
>
Create and run your first workflow
</Card>
<Card
title="Workflow Blocks Reference"
icon="cube"
href="/multi-step-automations/workflow-blocks-reference"
>
Detailed documentation for all block types
</Card>
<Card
title="Proxy & Geo Targeting"
icon="globe"
href="/going-to-production/proxy-geolocation"
>
Full list of proxy locations
</Card>
<Card
title="Webhooks"
icon="webhook"
href="/going-to-production/webhooks"
>
Handle webhook authentication and retries
</Card>
</CardGroup>

View File

@@ -32,11 +32,12 @@ export const SchemaBuilder = () => {
setFields(fields.map((f) => (f.id === id ? { ...f, [key]: value } : f)))
}
// Track duplicate field names
const duplicateNames = useMemo(() => {
const names = fields.map((f) => f.name).filter((n) => n.trim() !== "")
const counts = {}
names.forEach((n) => { counts[n] = (counts[n] || 0) + 1 })
for (const n of names) {
counts[n] = (counts[n] || 0) + 1
}
return new Set(Object.keys(counts).filter((n) => counts[n] > 1))
}, [fields])
@@ -68,7 +69,6 @@ export const SchemaBuilder = () => {
const formattedOutput = useMemo(() => {
const jsonStr = JSON.stringify(schema, null, 2)
if (outputFormat === "python") {
// Only replace JSON literals at value positions (after ": "), not inside quoted strings
return `data_extraction_schema=${jsonStr.replace(/: null/g, ": None").replace(/: true/g, ": True").replace(/: false/g, ": False")}`
}
if (outputFormat === "typescript") {
@@ -290,7 +290,7 @@ A schema doesn't guarantee all fields are populated. If the data isn't on the pa
## Build your schema
Use the interactive builder below to generate a schema, then copy it into your code.
Use the interactive builder to generate a schema, then copy it into your code.
<SchemaBuilder />

View File

@@ -297,10 +297,6 @@ The response from polling (`get_run`) and webhooks have slightly different struc
```
</CodeGroup>
<Note>
Webhook payloads differ from polling responses: webhooks include `summary` and `organization_id`; polling includes `run_request`. Plan your integration accordingly.
</Note>
**Common fields (both polling and webhook):**
| Field | Type | Description |