Co-authored-by: Kunal Mishra <kunalm2345@gmail.com> Co-authored-by: Suchintan <suchintan@users.noreply.github.com>
1116 lines
32 KiB
Plaintext
1116 lines
32 KiB
Plaintext
---
|
|
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>
|