Skip to main content
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:
StatusWhat it means
createdRun initialized, not yet queued
queuedWaiting for an available browser
runningAI is navigating and executing
completedSuccess—check output for results
failedSystem error (browser crash, network failure, exception)
terminatedAI determined the goal is unachievable (login blocked, page unavailable)
timed_outExceeded max_steps or time limit
canceledManually 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.
{
  "run_id": "tsk_v2_486305187432193504",
  "status": "failed",  // <-- Check this field
  "output": null,
  "failure_reason": "Login failed: Invalid credentials",
  ...
}
The status field is at the top level of both responses.

For tasks

Poll get_run until the status is terminal:
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)

For workflows

Same polling pattern works for workflow runs:
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)
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:
{
  "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

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()

For workflows

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()

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:
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"
    }
)

In workflows

Add error_code_mapping to individual blocks (navigation, task, validation):
The JSON examples below include comments (//) for clarity. Remove comments before using in actual workflow definitions—JSON does not support comments.
{
  "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"
      }
    }
  ]
}

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:
{
  "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'"
}
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
ContextFieldExample
Polling responserun.output.errorrun.output.get("error") in Python
Webhook payloadoutput.errorSame structure as polling
Successful runoutput contains your extracted dataNo error key present

Step 4: Respond in code

Now you can write clean switch/match logic:
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}

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:
{
  "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"
      }
    }
  ]
}
ParameterPurpose
complete_criterionCondition that must be true to continue to the next block
terminate_criterionCondition that stops the workflow immediately
error_code_mappingMaps 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 CodeDescriptionTypical Response
login_failedCredentials wrong, account locked, or MFARefresh credentials, retry
captcha_requiredCAPTCHA blocking automationUse human_interaction block or browser profile
not_foundTarget data doesn’t existReturn empty result, don’t retry
maintenanceSite temporarily downSchedule retry with backoff
rate_limitedToo many requestsAdd delays, use different proxy
access_deniedPermission issueCheck account permissions
timeoutTask took too longIncrease max_steps, simplify task

Next steps

Reliability Tips

Write prompts that fail less often

Webhooks

Get notified when runs complete or fail