Skip to main content
A Workflow chains multiple automation steps into a reusable template. While a Task handles a single goal on one page, a Workflow orchestrates an entire process: filling forms, validating data, downloading files, and sending results. You define the workflow once with blocks, then run it with different parameters each time.

Tasks vs Workflows

CharacteristicTasksWorkflows
StructureSingle prompt, one executionMultiple blocks in sequence
ScopeSingle goal, usually one pageMulti-step process across pages
ReusabilityRun ad-hoc or scheduledDefine once, run with different parameters
ParameterizationBasic (prompt, URL, extraction schema)Rich (input parameters, block outputs, Jinja templates)
Control FlowLinearLoops, conditionals, validation gates
Data PassingOutput returned at the endEach block can pass data to subsequent blocks
CompositionSingle AI agent executionCombine navigation, extraction, files, notifications
File OperationsDownload files during taskDownload, parse, upload, email files across blocks
Best ForPrototyping, simple extractions, one-offsProduction automations, bulk processing, complex logic
Run ID Formattsk_*wr_*

Workflow structure

A workflow is defined as an ordered list of blocks. Blocks execute one after another, and each block can access the outputs of previous blocks. Here’s an IRS EIN registration workflow that fills out a government form, validates the summary, extracts the result, downloads the confirmation letter, and emails it to your team:
IRS SS-4 Filing Workflow: Navigation → Validation → Extraction → File Download → Send Email
Here’s what each block does in this workflow:
  • Navigation takes the company information from ein_info and fills out the multi-page SS-4 form until it reaches the review page
  • Validation checks that the form summary matches the provided information and either continues (if correct) or terminates the workflow (if incorrect)
  • Extraction pulls the assigned EIN number and legal name from the confirmation page into structured JSON output
  • File Download navigates to and downloads the EIN confirmation letter as a PDF file
  • Send Email attaches the downloaded PDF and sends it to the specified recipients with company details in the email body
For detailed documentation on all available block types and their parameters, see Workflow Blocks Reference.

Step 1: Understand the structure

A workflow definition has two parts: parameters (the inputs) and blocks (the steps). Here’s the complete shape:
title: Job Application Workflow        # Display name
workflow_definition:
  parameters:                           # ← Inputs: change per run (Step 2)
    - key: job_url
      parameter_type: workflow
      workflow_parameter_type: string

  blocks:                               # ← Steps: execute in order (Step 3)
    - label: apply_to_job
      block_type: navigation
      url: "{{ job_url }}"
      navigation_goal: Fill out the job application form
You send this definition to the API to create a reusable workflow:
workflow = await client.create_workflow(
    json_definition={                   # ← or yaml_definition="..."
        "title": "...",
        "workflow_definition": {
            "parameters": [...],        # Step 2
            "blocks": [...]             # Step 3
        }
    }
)
The rest of this guide walks through each part, then shows the full API calls for creating, running, and monitoring workflows.

Step 2: Define parameters

The parameters array (inside workflow_definition) defines the inputs your workflow accepts. Instead of hardcoding a resume URL or job page into the workflow definition, you define named inputs that accept different values each time the workflow runs. Each parameter needs three fields:
  • key — the name you reference later with {{ key }}
  • parameter_type — always "workflow" for input parameters
  • workflow_parameter_type — the data type (string, file_url, json, etc.)
{
  "parameters": [
    {
      "key": "resume",
      "parameter_type": "workflow",
      "workflow_parameter_type": "file_url"
    },
    {
      "key": "job_url",
      "parameter_type": "workflow",
      "workflow_parameter_type": "string"
    }
  ]
}
This defines two inputs: a resume file and a job_url string. When you run the workflow later, you pass actual values for these parameters. Available parameter types:
TypeDescriptionExample value
stringText"John Smith"
integerWhole number42
floatDecimal number99.99
booleanTrue or falsetrue
jsonJSON object or array{"key": "value"}
file_urlURL to a file"https://example.com/resume.pdf"
credential_idReference to a stored credential"cred_abc123"
For the full parameter reference, see Workflow Parameters.

Step 3: Define blocks

The blocks array (inside workflow_definition) defines the steps your workflow executes. Each block has a block_type that determines what it does: navigate a page, extract data, download a file, send an email. Every block needs two fields: a label (a unique name) and a block_type. The remaining fields depend on the block type.
{
  "blocks": [
    {
      "label": "parse_resume",
      "block_type": "file_url_parser",
      "file_url": "{{ resume }}",
      "file_type": "pdf"
    },
    {
      "label": "apply_to_job",
      "block_type": "navigation",
      "url": "{{ job_url }}",
      "navigation_goal": "Fill out the job application form using the candidate's information."
    }
  ]
}
The first block (parse_resume) parses the resume file passed as an input parameter. The second block (apply_to_job) navigates to the job page and fills out the application. Notice {{ resume }} and {{ job_url }} — double curly braces reference the input parameters you defined in Step 2. Skyvern replaces these with actual values when the workflow runs.
Skyvern supports 23 block types: navigation, extraction, validation, file download, for loops, conditionals, and more. See Workflow Blocks Reference for the full list.

Step 4: Pass data between blocks

When a block completes, its output becomes available to subsequent blocks. This is how you chain steps together: one block extracts data, the next block uses it. Reference a block’s output with {{ label_output }} — the block’s label followed by _output. Access nested fields with dot notation: {{ parse_resume_output.name }}. Here, the apply_to_job block uses specific fields from the parse_resume block’s output:
{
  "blocks": [
    {
      "label": "parse_resume",
      "block_type": "file_url_parser",
      "file_url": "{{ resume }}",
      "file_type": "pdf"
    },
    {
      "label": "apply_to_job",
      "block_type": "navigation",
      "url": "{{ job_url }}",
      "navigation_goal": "Fill out the job application form.\n\nUse this information:\n- Name: {{ parse_resume_output.name }}\n- Email: {{ parse_resume_output.email }}\n- Experience: {{ parse_resume_output.work_experience }}"
    }
  ]
}
{{ parse_resume_output.name }}, {{ parse_resume_output.email }}, and {{ parse_resume_output.work_experience }} are fields from the structured data the parse_resume block extracted from the PDF.

Looping over output data

When a block produces a list, use a for_loop block to process each item. Set loop_over_parameter_key to the output field containing the array, and reference the current item with {{ loop_block_label.current_value }}.
{
  "blocks": [
    {
      "label": "extract_orders",
      "block_type": "extraction",
      "url": "https://example.com/orders",
      "data_extraction_goal": "Extract all order IDs from the page",
      "data_schema": {
        "type": "object",
        "properties": {
          "order_ids": {
            "type": "array",
            "items": { "type": "string" }
          }
        }
      }
    },
    {
      "label": "download_invoices",
      "block_type": "for_loop",
      "loop_over_parameter_key": "extract_orders_output.order_ids",
      "loop_blocks": [
        {
          "label": "download_invoice",
          "block_type": "file_download",
          "url": "https://example.com/invoice/{{ download_invoice.current_value }}",
          "navigation_goal": "Download the invoice PDF"
        }
      ]
    }
  ]
}
The for_loop iterates over the order_ids array from the extraction block. Inside the loop, {{ download_invoice.current_value }} contains one order ID per iteration.

Step 5: Create the workflow

Send the complete workflow definition to the API. Skyvern validates the parameters, blocks, and template references, then returns a workflow_permanent_id you use to run the workflow. The API accepts two formats: pass a JSON object via json_definition, or pass a YAML string via yaml_definition.

Using JSON

import os
import asyncio
from skyvern import Skyvern

async def main():
    client = Skyvern(api_key=os.getenv("SKYVERN_API_KEY"))

    workflow = await client.create_workflow(
        json_definition={
            "title": "Job Application Workflow",
            "workflow_definition": {
                "parameters": [
                    {
                        "key": "resume",
                        "parameter_type": "workflow",
                        "workflow_parameter_type": "file_url"
                    },
                    {
                        "key": "job_url",
                        "parameter_type": "workflow",
                        "workflow_parameter_type": "string"
                    }
                ],
                "blocks": [
                    {
                        "label": "parse_resume",
                        "block_type": "file_url_parser",
                        "file_url": "{{ resume }}",
                        "file_type": "pdf"
                    },
                    {
                        "label": "apply_to_job",
                        "block_type": "navigation",
                        "url": "{{ job_url }}",
                        "navigation_goal": (
                            "Fill out the job application form.\n\n"
                            "Use this information:\n"
                            "{{ parse_resume_output }}"
                        )
                    }
                ]
            }
        }
    )

    print(f"Workflow ID: {workflow.workflow_permanent_id}")

asyncio.run(main())

Using YAML

Pass the same definition as a YAML string via yaml_definition. This is useful when you store workflow definitions in .yaml files.
workflow = await client.create_workflow(
    yaml_definition="""\
title: Job Application Workflow
workflow_definition:
  parameters:
    - key: resume
      parameter_type: workflow
      workflow_parameter_type: file_url
    - key: job_url
      parameter_type: workflow
      workflow_parameter_type: string
  blocks:
    - label: parse_resume
      block_type: file_url_parser
      file_url: "{{ resume }}"
      file_type: pdf
    - label: apply_to_job
      block_type: navigation
      url: "{{ job_url }}"
      navigation_goal: |
        Fill out the job application form.

        Use this information:
        {{ parse_resume_output }}
"""
)
To load from a file: await client.create_workflow(yaml_definition=open("workflow.yaml").read())
Example response:
{
  "workflow_permanent_id": "wpid_123456789",
  "workflow_id": "wf_987654321",
  "version": 1,
  "title": "Job Application Workflow",
  "workflow_definition": {
    "parameters": [...],
    "blocks": [...]
  },
  "created_at": "2026-01-20T12:00:00.000000",
  "modified_at": "2026-01-20T12:00:00.000000"
}
Save the workflow_permanent_id — you need it to run the workflow.

Step 6: Run the workflow

Pass values for the parameters you defined in Step 2. The call returns immediately with a run_id — the workflow runs asynchronously in the background.
run = await client.run_workflow(
    workflow_id="wpid_123456789",
    parameters={
        "resume": "https://example.com/resume.pdf",
        "job_url": "https://jobs.lever.co/company/position"
    }
)

print(f"Run ID: {run.run_id}")
Example response:
{
  "run_id": "wr_486305187432193510",
  "status": "created"
}
The response includes a run_id. Use this ID to check status and fetch results.

Step 7: Get results

Workflows run asynchronously, so you need to check back for results. You have two options: poll the API, or receive a webhook when the workflow completes.

Option 1: Polling

Poll get_run until the status reaches a terminal state.
run_id = run.run_id

while True:
    result = await client.get_run(run_id)

    if result.status in ["completed", "failed", "terminated", "timed_out", "canceled"]:
        break

    await asyncio.sleep(5)

print(f"Status: {result.status}")
print(f"Output: {result.output}")

Option 2: Webhooks

Pass a webhook_url when running the workflow. Skyvern sends a POST request to your URL when the workflow reaches a terminal state.
run = await client.run_workflow(
    workflow_id="wpid_123456789",
    parameters={
        "resume": "https://example.com/resume.pdf",
        "job_url": "https://jobs.lever.co/company/position"
    },
    webhook_url="https://your-server.com/webhook"
)
The webhook payload contains the same data as the polling response. See Webhooks for authentication and retry options.

Understand the response

The response from polling contains the workflow run status and output from each block.
{
  "run_id": "wr_486305187432193510",
  "run_type": "workflow_run",
  "status": "completed",
  "output": {
    "parse_resume_output": {
      "name": "John Smith",
      "email": "john@example.com",
      "work_experience": [...]
    },
    "apply_to_job_output": {
      "task_id": "tsk_123456",
      "status": "completed",
      "extracted_information": null
    }
  },
  "screenshot_urls": [
    "https://skyvern-artifacts.s3.amazonaws.com/.../screenshot_final.png"
  ],
  "recording_url": "https://skyvern-artifacts.s3.amazonaws.com/.../recording.webm",
  "failure_reason": null,
  "created_at": "2026-01-20T12:00:00.000000",
  "modified_at": "2026-01-20T12:05:00.000000"
}
Response fields:
FieldTypeDescription
run_idstringUnique identifier for this run (format: wr_*)
run_typestringAlways "workflow_run" for workflow runs
statusstringCurrent status: created, queued, running, completed, failed, terminated, timed_out, canceled
outputobjectOutput from each block, keyed by {label}_output
screenshot_urlsarrayFinal screenshots from the last blocks
recording_urlstringVideo recording of the browser session
failure_reasonstring | nullError description if the run failed
downloaded_filesarrayFiles downloaded during the run
created_atdatetimeWhen the run started
modified_atdatetimeWhen the run was last updated

Block outputs

Each block’s output appears in the output object with the key {label}_output. The structure depends on the block type: Navigation/Action blocks:
{
  "apply_to_job_output": {
    "task_id": "tsk_123456",
    "status": "completed",
    "extracted_information": null,
    "failure_reason": null
  }
}
Extraction blocks:
{
  "extract_data_output": {
    "product_name": "Widget Pro",
    "price": 99.99,
    "in_stock": true
  }
}
For Loop blocks:
{
  "download_invoices_output": [
    { "task_id": "tsk_111", "status": "completed" },
    { "task_id": "tsk_222", "status": "completed" },
    { "task_id": "tsk_333", "status": "completed" }
  ]
}

Complete example

This script creates a job application workflow, runs it, and polls until completion. Copy and run it to get started.
import os
import asyncio
from skyvern import Skyvern

async def main():
    client = Skyvern(api_key=os.getenv("SKYVERN_API_KEY"))

    # Create the workflow
    workflow = await client.create_workflow(
        json_definition={
            "title": "Job Application Workflow",
            "workflow_definition": {
                "parameters": [
                    {
                        "key": "resume",
                        "parameter_type": "workflow",
                        "workflow_parameter_type": "file_url"
                    },
                    {
                        "key": "job_url",
                        "parameter_type": "workflow",
                        "workflow_parameter_type": "string"
                    }
                ],
                "blocks": [
                    {
                        "label": "parse_resume",
                        "block_type": "file_url_parser",
                        "file_url": "{{ resume }}",
                        "file_type": "pdf"
                    },
                    {
                        "label": "apply_to_job",
                        "block_type": "navigation",
                        "url": "{{ job_url }}",
                        "navigation_goal": (
                            "Fill out the job application form.\n\n"
                            "Use this information:\n"
                            "{{ parse_resume_output }}"
                        )
                    }
                ]
            }
        }
    )
    print(f"Created workflow: {workflow.workflow_permanent_id}")

    # Run the workflow
    run = await client.run_workflow(
        workflow_id=workflow.workflow_permanent_id,
        parameters={
            "resume": "https://example.com/resume.pdf",
            "job_url": "https://jobs.lever.co/company/position"
        }
    )
    print(f"Started run: {run.run_id}")

    # Poll for results
    while True:
        result = await client.get_run(run.run_id)

        if result.status in ["completed", "failed", "terminated", "timed_out", "canceled"]:
            break

        print(f"Status: {result.status}")
        await asyncio.sleep(5)

    print(f"Final status: {result.status}")
    print(f"Output: {result.output}")

asyncio.run(main())

List your workflows

Retrieve all workflows in your organization.
workflows = await client.get_workflows()

for workflow in workflows:
    print(f"{workflow.workflow_permanent_id}: {workflow.title}")

Next steps

Workflow Blocks Reference

Detailed documentation for all 23 block types

Workflow Parameters

Configure proxies, webhooks, and other run options

File Operations

Download, parse, and upload files in workflows