---
title: Build a Workflow
subtitle: Create, run, and test multi-step automations
slug: multi-step-automations/build-a-workflow
---

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

| Characteristic | Tasks | Workflows |
|----------------|-------|-----------|
| **Structure** | Single prompt, one execution | Multiple blocks in sequence |
| **Scope** | Single goal, usually one page | Multi-step process across pages |
| **Reusability** | Run ad-hoc or scheduled | Define once, run with different parameters |
| **Parameterization** | Basic (prompt, URL, extraction schema) | Rich (input parameters, block outputs, Jinja templates) |
| **Control Flow** | Linear | Loops, conditionals, validation gates |
| **Data Passing** | Output returned at the end | Each block can pass data to subsequent blocks |
| **Composition** | Single AI agent execution | Combine navigation, extraction, files, notifications |
| **File Operations** | Download files during task | Download, parse, upload, email files across blocks |
| **Best For** | Prototyping, simple extractions, one-offs | Production automations, bulk processing, complex logic |
| **Run ID Format** | `tsk_*` | `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:

<Frame>
  <img src="/images/skyvern-ss4-workflow.svg" alt="IRS SS-4 Filing Workflow: Navigation → Validation → Extraction → File Download → Send Email" />
</Frame>

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

<Info>
For detailed documentation on all available block types and their parameters, see [Workflow Blocks Reference](/multi-step-automations/workflow-blocks-reference).
</Info>

---

## Step 1: Understand the structure

A workflow definition has two parts: **parameters** (the inputs) and **blocks** (the steps). Here's the complete shape:

```yaml
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:

```python
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.)

<CodeGroup>
```json JSON
{
  "parameters": [
    {
      "key": "resume",
      "parameter_type": "workflow",
      "workflow_parameter_type": "file_url"
    },
    {
      "key": "job_url",
      "parameter_type": "workflow",
      "workflow_parameter_type": "string"
    }
  ]
}
```

```yaml YAML
parameters:
  - key: resume
    parameter_type: workflow
    workflow_parameter_type: file_url
  - key: job_url
    parameter_type: workflow
    workflow_parameter_type: string
```
</CodeGroup>

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:**

| Type | Description | Example value |
|------|-------------|---------------|
| `string` | Text | `"John Smith"` |
| `integer` | Whole number | `42` |
| `float` | Decimal number | `99.99` |
| `boolean` | True or false | `true` |
| `json` | JSON object or array | `{"key": "value"}` |
| `file_url` | URL to a file | `"https://example.com/resume.pdf"` |
| `credential_id` | Reference to a stored credential | `"cred_abc123"` |

For the full parameter reference, see [Workflow Parameters](/multi-step-automations/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.

<CodeGroup>
```json JSON
{
  "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."
    }
  ]
}
```

```yaml YAML
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.
```
</CodeGroup>

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.

<Info>
Skyvern supports 23 block types: navigation, extraction, validation, file download, for loops, conditionals, and more. See [Workflow Blocks Reference](/multi-step-automations/workflow-blocks-reference) for the full list.
</Info>

---

## 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:

<CodeGroup>
```json JSON
{
  "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 }}"
    }
  ]
}
```

```yaml YAML
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:
      - Name: {{ parse_resume_output.name }}
      - Email: {{ parse_resume_output.email }}
      - Experience: {{ parse_resume_output.work_experience }}
```
</CodeGroup>

`{{ 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 }}`.

<CodeGroup>
```json JSON
{
  "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"
        }
      ]
    }
  ]
}
```

```yaml YAML
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
```
</CodeGroup>

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

<CodeGroup>
```python Python
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())
```

```typescript TypeScript
import { SkyvernClient } from "@skyvern/client";

async function main() {
  const client = new SkyvernClient({
    apiKey: process.env.SKYVERN_API_KEY,
  });

  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",
            },
            {
              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 }}",
              ].join("\n"),
            },
          ],
        },
      },
    },
  });

  console.log(`Workflow ID: ${workflow.workflow_permanent_id}`);
}

main();
```

```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"
          },
          {
            "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\nUse this information:\n{{ parse_resume_output }}"
          }
        ]
      }
    }
  }'
```
</CodeGroup>

### Using YAML

Pass the same definition as a YAML string via `yaml_definition`. This is useful when you store workflow definitions in `.yaml` files.

<CodeGroup>
```python Python
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 }}
"""
)
```

```typescript TypeScript
const workflow = await client.createWorkflow({
  body: {
    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 }}
`.trim(),
  },
});
```

```bash cURL
curl -X POST "https://api.skyvern.com/v1/workflows" \
  -H "x-api-key: $SKYVERN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "yaml_definition": "title: Job Application Workflow\nworkflow_definition:\n  parameters:\n    - key: resume\n      parameter_type: workflow\n      workflow_parameter_type: file_url\n    - key: job_url\n      parameter_type: workflow\n      workflow_parameter_type: string\n  blocks:\n    - label: parse_resume\n      block_type: file_url_parser\n      file_url: \"{{ resume }}\"\n      file_type: pdf\n    - label: apply_to_job\n      block_type: navigation\n      url: \"{{ job_url }}\"\n      navigation_goal: \"Fill out the job application form.\\n\\nUse this information:\\n{{ parse_resume_output }}\"\n"
  }'
```
</CodeGroup>

<Tip>
To load from a file: `await client.create_workflow(yaml_definition=open("workflow.yaml").read())`
</Tip>

**Example response:**

```json
{
  "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.

<CodeGroup>
```python Python
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}")
```

```typescript TypeScript
const run = await client.runWorkflow({
  body: {
    workflow_id: "wpid_123456789",
    parameters: {
      resume: "https://example.com/resume.pdf",
      job_url: "https://jobs.lever.co/company/position",
    },
  },
});

console.log(`Run ID: ${run.run_id}`);
```

```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_url": "https://jobs.lever.co/company/position"
    }
  }'
```
</CodeGroup>

**Example response:**

```json
{
  "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.

<CodeGroup>
```python Python
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}")
```

```typescript TypeScript
const runId = run.run_id;

while (true) {
  const result = await client.getRun(runId);

  if (["completed", "failed", "terminated", "timed_out", "canceled"].includes(result.status)) {
    console.log(`Status: ${result.status}`);
    console.log(`Output: ${JSON.stringify(result.output)}`);
    break;
  }

  await new Promise((resolve) => setTimeout(resolve, 5000));
}
```

```bash cURL
#!/bin/bash
RUN_ID="wr_486305187432193510"

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')
  echo "Status: $STATUS"

  if [[ "$STATUS" == "completed" || "$STATUS" == "failed" || "$STATUS" == "terminated" || "$STATUS" == "timed_out" || "$STATUS" == "canceled" ]]; then
    echo "$RESPONSE" | jq '.output'
    break
  fi

  sleep 5
done
```
</CodeGroup>

### 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.

<CodeGroup>
```python Python
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"
)
```

```typescript TypeScript
const run = await client.runWorkflow({
  body: {
    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",
  },
});
```

```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_url": "https://jobs.lever.co/company/position"
    },
    "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.

---

## Understand the response

The response from polling contains the workflow run status and output from each block.

```json
{
  "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:**

| Field | Type | Description |
|-------|------|-------------|
| `run_id` | string | Unique identifier for this run (format: `wr_*`) |
| `run_type` | string | Always `"workflow_run"` for workflow runs |
| `status` | string | Current status: `created`, `queued`, `running`, `completed`, `failed`, `terminated`, `timed_out`, `canceled` |
| `output` | object | Output from each block, keyed by `{label}_output` |
| `screenshot_urls` | array | Final screenshots from the last blocks |
| `recording_url` | string | Video recording of the browser session |
| `failure_reason` | string \| null | Error description if the run failed |
| `downloaded_files` | array | Files downloaded during the run |
| `created_at` | datetime | When the run started |
| `modified_at` | datetime | When 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:**
```json
{
  "apply_to_job_output": {
    "task_id": "tsk_123456",
    "status": "completed",
    "extracted_information": null,
    "failure_reason": null
  }
}
```

**Extraction blocks:**
```json
{
  "extract_data_output": {
    "product_name": "Widget Pro",
    "price": 99.99,
    "in_stock": true
  }
}
```

**For Loop blocks:**
```json
{
  "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.

<CodeGroup>
```python Python
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())
```

```typescript TypeScript
import { SkyvernClient } from "@skyvern/client";

async function main() {
  const client = new SkyvernClient({
    apiKey: process.env.SKYVERN_API_KEY,
  });

  // Create the workflow
  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",
            },
            {
              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 }}",
              ].join("\n"),
            },
          ],
        },
      },
    },
  });
  console.log(`Created workflow: ${workflow.workflow_permanent_id}`);

  // Run the workflow
  const run = await client.runWorkflow({
    body: {
      workflow_id: workflow.workflow_permanent_id,
      parameters: {
        resume: "https://example.com/resume.pdf",
        job_url: "https://jobs.lever.co/company/position",
      },
    },
  });
  console.log(`Started run: ${run.run_id}`);

  // Poll for results
  while (true) {
    const result = await client.getRun(run.run_id);

    if (["completed", "failed", "terminated", "timed_out", "canceled"].includes(result.status)) {
      console.log(`Final status: ${result.status}`);
      console.log(`Output: ${JSON.stringify(result.output, null, 2)}`);
      break;
    }

    console.log(`Status: ${result.status}`);
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
}

main();
```

```bash cURL
#!/bin/bash

# Create the workflow
WORKFLOW=$(curl -s -X POST "https://api.skyvern.com/v1/workflows" \
  -H "x-api-key: $SKYVERN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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\nUse this information:\n{{ parse_resume_output }}"
          }
        ]
      }
    }
  }')

WORKFLOW_ID=$(echo "$WORKFLOW" | jq -r '.workflow_permanent_id')
echo "Created workflow: $WORKFLOW_ID"

# Run the workflow
RUN=$(curl -s -X POST "https://api.skyvern.com/v1/run/workflows" \
  -H "x-api-key: $SKYVERN_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"workflow_id\": \"$WORKFLOW_ID\",
    \"parameters\": {
      \"resume\": \"https://example.com/resume.pdf\",
      \"job_url\": \"https://jobs.lever.co/company/position\"
    }
  }")

RUN_ID=$(echo "$RUN" | jq -r '.run_id')
echo "Started run: $RUN_ID"

# Poll for results
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')
  echo "Status: $STATUS"

  if [[ "$STATUS" == "completed" || "$STATUS" == "failed" || "$STATUS" == "terminated" || "$STATUS" == "timed_out" || "$STATUS" == "canceled" ]]; then
    echo "$RESPONSE" | jq '.output'
    break
  fi

  sleep 5
done
```
</CodeGroup>

---

## List your workflows

Retrieve all workflows in your organization.

<CodeGroup>
```python Python
workflows = await client.get_workflows()

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

```typescript TypeScript
const workflows = await client.getWorkflows();

for (const workflow of workflows) {
  console.log(`${workflow.workflow_permanent_id}: ${workflow.title}`);
}
```

```bash cURL
curl -X GET "https://api.skyvern.com/v1/workflows" \
  -H "x-api-key: $SKYVERN_API_KEY"
```
</CodeGroup>

---

## Next steps

<CardGroup cols={2}>
  <Card
    title="Workflow Blocks Reference"
    icon="cube"
    href="/multi-step-automations/workflow-blocks-reference"
  >
    Detailed documentation for all 23 block types
  </Card>
  <Card
    title="Workflow Parameters"
    icon="sliders"
    href="/multi-step-automations/workflow-parameters"
  >
    Configure proxies, webhooks, and other run options
  </Card>
  <Card
    title="File Operations"
    icon="file"
    href="/multi-step-automations/file-operations"
  >
    Download, parse, and upload files in workflows
  </Card>
</CardGroup>
