# Getting started with workflows

Create your first automated workflow in the dcupl Console. This tutorial walks you through building a data processing pipeline using the visual editor.

## What you'll build

```mermaid
flowchart LR
  T[Webhook Trigger] --> F[Fetch Products]
  F --> V{Validate}
  V -->|Valid| Tr[Transform]
  V -->|Invalid| L[Log Error]
  Tr --> R[Return Response]
```

A workflow that:

1. Receives a webhook request with the number of products to fetch
2. Fetches those products from a public sample API ([dummyjson.com](https://dummyjson.com/docs/products))
3. Validates them: a product needs a title, a price and at least 50 items in stock
4. Transforms the valid products
5. Logs the invalid ones
6. Returns the transformed products as JSON

## Prerequisites

- A dcupl Console account at [console.dcupl.com](https://console.dcupl.com)
- A project created in the Console, where you are an owner or editor (the **Workflows** menu isn't shown to viewers)
- A runner instance available for deployment

## Step 1: create a new workflow

1. Open your project in the Console (the draft version)
2. Click **Workflows** in the left sidebar
3. On the **Templates** tab, click **+** (Create a new workflow template)
4. Fill in the details:
   - **Workflow Name**: Product Sync
   - **Workflow Key**: product-sync
   - **Description**: Fetches and processes product data
   - **Template**: From Scratch (HTTP)
5. Click **Create Workflow**

The workflow editor opens with the template: an **HTTP Trigger** connected to a **Status Response**. Click **Edit Workflow** to start editing.

## Step 2: add a webhook trigger

1. Open the **HTTP Trigger** node the template created (to add one yourself, drag **HTTP Request** from the **Trigger** section of the node palette)
2. Configure the webhook:
   - **Path**: `/products/sync`
   - **Methods**: Select `POST`

Changes apply as you edit; you save the whole workflow in Step 8. This creates an endpoint that will trigger your workflow when it receives HTTP requests.

## Step 3: add an HTTP Request node

1. From the **Nodes** section of the palette, drag an **HTTP Request** node onto the canvas
2. Connect the trigger's output port to the request node's input port
3. Name it "Fetch Products", set **Method** to `GET`, and set **URL** to:

   ```text
   {{variables.productsApiUrl}}?limit={{request.body.limit}}&select=title,price,stock
   ```

> **Note:** `{{variables.productsApiUrl}}` reads a workflow variable, which you add in Step 7. `{{request.body.limit}}` reads the `limit` field of the webhook's JSON body; a run whose body has no `limit` fails at this node with `Unresolved template placeholder {{request.body.limit}}`.

## Step 4: add validation logic

1. Drag a **Script** node onto the canvas
2. Connect the request node's `main` output to the script node
3. Name it "Validate Products"
4. On the node's **Ports & Schema** tab, click **+ Add Output** twice to add the output ports `valid` and `invalid`
5. In the script editor, check each product and send it to the matching port:

```js title="Validate Products"
// The HTTP Request node emits one item: { body, headers, statusCode, ... }.
// The sample API answers { products: [...], total, skip, limit }.
const products = $json.first().body.products || [];
const isValid = (p) => Boolean(p.title) && p.price > 0 && p.stock >= 50;

return _output.route({
  valid: products.filter(isValid),
  invalid: products.filter((p) => !isValid(p)),
});
```

`$json.first()` is the first incoming item's data; `$json()` returns all of them. Returning a plain value or array sends it to the `main` port instead.

## Step 5: add conditional routing

### Route valid products

1. Drag another **Script** node onto the canvas and name it "Transform Products"
2. Connect the validation node's `valid` port to it
3. Give it this script, which turns each valid product into the shape the response returns:

```js title="Transform Products"
// One item per valid product arrives here
return $json().map((p) => ({ id: p.id, name: p.title, price: p.price, stock: p.stock }));
```

### Route invalid products

1. Drag a **Script** node onto the canvas and name it "Log Errors"
2. Connect the validation node's `invalid` port to it
3. Give it this script:

```js title="Log Errors"
// One item per invalid product arrives here (none when every product is valid)
$json().forEach((p) => console.log('low stock', p.id, p.title, p.stock));

// End this branch without items, so it does not change what the run answers
return [];
```

`console.log` output is recorded when you test with **Enable Data Trace** (Step 9). A script that returns nothing passes its input on, and a branch that ends with items can become part of the run's answer, so the script returns an empty array.

Your workflow now has two parallel paths from the validation node. Connect every custom port that can receive items: items on an unconnected port go to the node's `error` port, and the run fails if that isn't connected either.

> **Watch out:** Don't route with edge conditions: the editor lets you add a condition to an edge, but the workflow runner currently ignores it and passes every item through.

## Step 6: configure the response

The template's **Status Response** node returns only the execution status (`status`, `executionId`, `duration`). To return the transformed products instead:

1. Delete the **Status Response** node
2. From the **Response** section of the palette, drag a **Script Response** node onto the canvas
3. Connect the "Transform Products" node's `main` output to it
4. Give it this script:

```js title="Script Response"
// `data` holds the incoming items' JSON: the transformed products
return { count: data.length, products: data };
```

Return an object: what the script returns is the response body, and an array would arrive as an object with the keys `"0"`, `"1"`, ….

## Step 7: add variables

Keep the API's base URL in a workflow variable, so you can point the workflow at another API without editing nodes.

1. Click the key icon (**Project Variables**) in the workflow toolbar
2. In **Global Workflow Variables**, add the variable:
   - **Name**: `productsApiUrl`
   - **Value**: `https://dummyjson.com/products`
3. Click **Add Variable**

Variable values are masked in the list and encrypted with the runner's key when you deploy, so variables are also where API tokens belong (`Bearer {{variables.apiToken}}` in a header). Until `productsApiUrl` exists, **Deploy** stays disabled and offers **Define Variables**.

## Step 8: save and deploy

### Save your workflow

Click **Save Changes** in the toolbar.

### Deploy to runner

1. Select your target runner in the runner dropdown of the toolbar
2. Click **Deploy**
3. Wait for the runner's status to change to **Up to date**

## Step 9: test your workflow

### From the Console

1. Click **Execute** in the toolbar — the **Test Workflow** panel opens on its **New Test** tab
2. Enter `{ "limit": 5 }` as the **Request Body (JSON)**, and turn on **Enable Data Trace** to record the "Log Errors" output
3. Click **Start Test**
4. View the execution results below the form
5. Open a node on the canvas to see its input and output data, or check the **History** tab for past runs

### Using cURL

```bash
curl -X POST \
  'https://<runner-url>/v3/workflow/<project-id>-product-sync/products/sync' \
  -H 'Content-Type: application/json' \
  -d '{ "limit": 5 }'
```

The runner URL is shown on the Workflows page, and the deployed workflow key is your key prefixed with the project ID. The test panel's **Request Preview** shows the exact command. If you added API-key authentication to the trigger, also send the key in an `api-key` header.

### Expected result

The run answers with what the Script Response returns. With the sample API's current data, four of the first five products have at least 50 items in stock; the fifth goes to "Log Errors":

```json title="Response"
{
  "count": 4,
  "products": [
    { "id": 1, "name": "Essence Mascara Lash Princess", "price": 9.99, "stock": 99 },
    { "id": 3, "name": "Powder Canister", "price": 14.99, "stock": 89 },
    { "id": 4, "name": "Red Lipstick", "price": 12.99, "stock": 91 },
    { "id": 5, "name": "Red Nail Polish", "price": 8.99, "stock": 79 }
  ]
}
```

The sample data belongs to dummyjson.com and may change. The whole workflow as a template file is the example on the [template reference](/console/workflows/reference#example).

## Troubleshooting

### Workflow not triggering

- Verify the endpoint URL is correct
- If the trigger has API-key authentication, check the key is sent in the `api-key` header
- Ensure the runner status shows **Up to date**

### Script errors

- Open the failing node on the canvas to see its error and input data
- Verify `$json()` returns data
- Use `console.log()` in scripts and run the test with **Enable Data Trace** to capture the logs
- Scripts time out after 500 ms by default

### HTTP request failures

- Check the target URL is accessible
- Verify authentication headers are correct
- Review the error output port for details

### Connection issues

- Ensure all required input ports are connected
- Check that every output port that receives items is connected
- Verify the workflow is saved and deployed (status **Up to date**)

## Next steps

- [Workflows Overview](/console/workflows) — Deep dive into nodes, edges, and merge strategies
- [Reporting](/console/workflows/reporting) — Get notified when a workflow fails
- [REST APIs](/console/rapi) — Deploy auto-generated REST APIs from your data models
