# Workflow definition

A workflow’s definition is a tree. The trigger is the root, and each step points to the next one.

## The tree

There is no list of steps. Each step names the next one in `nextAction`. A branch, a loop and a step’s error path each hang their own chain off the step.

```
Trigger trigger (Webhook)
01 total (Code)
  On error · onErrorAction
    02 note_failure (Text Helper)
03 big_order (Branch)
  Over 1,000 · branches[0].firstAction
    04 each_item (Loop)
    Each item · firstLoopAction
      05 item_name (Text Helper)
  Otherwise · branches[1]
```

The example at the end of this page, by step name.

## Three things to get right

- `config`, not `settings`: A step’s settings go in its `config`. `settings` is for the whole workflow only; on a step it is refused.
- **Two type words**: The trigger’s `type` is lowercase, such as `webhook`. A step’s is uppercase: `PIECE`, `CODE`, `BRANCH` or `LOOP`.
- `actionId` on the trigger: The trigger names its trigger in `actionId`. There is no `triggerId`.

Any other key is refused with `400`, which names it. Before you save, [check the definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition): each problem says what would be allowed.

GoRunner fills in a branch’s or loop’s app, a branch’s `config.executionMode` (`FIRST_MATCH`, the mode a run uses when none is written) and `config.branches`, and every `valid`, below, when you save and when you read. A read leaves the stored copy as it is, and sending back only those fill-ins changes nothing.

To change a draft, read it, change the tree, and send it back whole with [Update a workflow’s draft](https://docs.gorunner.io/api/workflows/update-workflow). Send the `definitionRev` you read as `expectedRev`: if someone saved since, the answer is `409` and nothing is overwritten.

## The definition

- `schemaVersion`: Always `2`. A definition saved without it reads as `0`, and is the same.
- `trigger`: The root: how the workflow starts.
- `settings`: The workflow’s own settings. See below.
- `variables`: Values any step reads as `{{$vars.NAME}}`. Not secret: every read and export shows them. Secret ones are kept apart, in [the variables](https://docs.gorunner.io/api/publish/get-workflow-variables).
- `annotations`: The canvas’s notes, kept as they are.
- `pinnedData`: Old. Don’t write it.

## The trigger

- `name`: Its address, usually `trigger`. No step may share it.
- `type`: `manual`, `cron`, `webhook`, `event`, `error`, `run_finished` or `poll`. A form uses `webhook`.
- `pieceId`, `actionId`: The app and its trigger, such as `webhook` and `catch_webhook`. [Get an app](https://docs.gorunner.io/api/apps/get-app) lists them.
- `config`: The trigger’s settings, by field name: [Get a trigger’s fields](https://docs.gorunner.io/api/apps/get-app-trigger).
- `connectionId`: For a trigger that registers itself with a service.
- `nextAction`: The first step.
- `displayName`: The label on the canvas.
- `pieceVersion`, `valid`: Leave `pieceVersion` out: GoRunner pins the version its workers run, and never changes it after. `valid` is worked out by GoRunner: `true` once an app and its trigger are chosen. It checks nothing, and a value you send isn’t kept.

## A step

- `name`: Its address in expressions: `{{steps.total.result}}`. Unique in the whole tree, and at most 255 characters. Renaming it breaks the expressions that use it.
- `type`: `PIECE` runs an app’s action. `CODE` runs JavaScript. `BRANCH` and `LOOP` hold chains.
- `pieceId`, `actionId`: Such as `http` and `send_request`. `CODE` is `code` / `run_code`, `BRANCH` is `branch` / `evaluate`, `LOOP` is `loop` / `loop_on_items`. Leave them out on a `BRANCH` or `LOOP` and GoRunner fills them in.
- `config`: The action’s settings, by field name: [Get an action’s fields](https://docs.gorunner.io/api/apps/get-app-action). Also how it handles errors, below.
- `connectionId`: A connection’s ID, in the same workspace. Never put secrets in `config`. Publishing needs it when the app needs a connection.
- `skip`: `true` skips this step only. The next one still runs.
- `nextAction`: The next step. It doesn’t run when the error path does.
- `branches`: `BRANCH` only: its arms, in order. Leave `config.branches` out and GoRunner copies the arms there, when they have conditions. If you write it, list the same arms in the same order: a run checks `config.branches` and runs the arm at the same place here.
- `firstLoopAction`: `LOOP` only: the first step run for each item.
- `onErrorAction`: The first error step.
- `displayName`, `pieceVersion`, `valid`: As on the trigger, with `displayName` at most 255 characters. A `BRANCH`, `LOOP` or `CODE` step is always `valid`.

Characters are counted, not bytes. A longer `name` or `displayName` is refused with `400` when you create, update or import a workflow or use a template, and [checking the definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) reports `step_name_too_long`. One already stored stays: you can read it, save it back and publish it, with a warning. A run that reaches that step can’t save its steps until you shorten it.

## Error handling, in config

- `onError`: `stop` ends the run, failed. This is the default. `continue` goes on to the next step. `errorPath` runs `onErrorAction` instead; without one it stops.
- `maxRetries`: 0 to 5. 0 unless set.
- `timeoutSeconds`: Seconds for each try, up to 600. 30 unless set, or what the app needs.

Only `PIECE` and `CODE` steps have them: a branch or loop that fails always stops the run. An error step reads the reason as `{{steps.total.error}}`, and the run ends **Worked**. See [Handle errors](https://docs.gorunner.io/build/handle-errors).

## Branches

Arms are tried in order, and the first that matches runs. Set `config.executionMode` to `ALL_MATCH` to run every arm that matches, in order. `FIRST_MATCH` is the default. Then the run goes on at the branch’s `nextAction`.

- `conditions`: Groups of rules: `[[A, B], [C]]` means (A and B) or C. An arm with no conditions never matches by itself: it is the else path, run only when no other arm matched.
- **A rule**: `{"field": "{{…}}", "operator": "num_gt", "value": 1000}`. `value` is left out for operators that take none.
- **Otherwise**: Saving gives a branch an arm named `Otherwise` with no conditions, last, if it has none. Without it, a run that matches nothing would do nothing.

**Every operator (40)**

`exists`, `not_exists`, `is_empty`, `is_not_empty`, `text_eq`, `text_neq`, `text_contains`, `text_not_contains`, `text_starts_with`, `text_not_starts_with`, `text_ends_with`, `text_not_ends_with`, `text_is_empty`, `text_is_not_empty`, `regex_match`, `regex_not_match`, `num_eq`, `num_neq`, `num_gt`, `num_gte`, `num_lt`, `num_lte`, `date_eq`, `date_neq`, `date_before`, `date_before_or_eq`, `date_after`, `date_after_or_eq`, `bool_is_true`, `bool_is_false`, `list_contains`, `list_not_contains`, `list_is_empty`, `list_is_not_empty`, `list_length_eq`, `list_length_neq`, `list_length_gt`, `list_length_gte`, `list_length_lt`, `list_length_lte`

Any other operator is refused with `400` when you create, update or import a workflow or use a template. One already stored stays: you can read it and save it back, publishing it is refused, and its step fails if it runs. An empty operator waits for publish.

To match one value against several cases, use the [Switch](https://docs.gorunner.io/apps/switch) app.

## Loops

`config.items` is an expression that gives a list, such as `{{trigger.body.items}}`. Each step inside reads `{{steps.each_item.item}}`, with `index` and `total` beside it. `batch_size` hands over several items at once, `max_iterations` caps the loop, and `timeoutSeconds` caps one turn.

## The workflow’s settings

- `timeoutSeconds`: Caps the whole run. 5 minutes unless set, at most 1 hour. A synchronous webhook then answers `504`.
- `concurrencyLimit`: Production runs at once, counting runs that are paused. 0 or left out means no limit.
- `whenFull`: At the limit: `wait` queues the run, the default. `refuse` turns it away; an API or webhook caller gets `429 CONCURRENCY_LIMIT`.
- `errorWorkflowId`: A published workflow in the same workspace, run when this one fails.
- `maintenanceMode`: The workflow’s **Maintenance mode**: after repeated identical failures, Keel drafts a fix. It waits in the draft for you.
- `autoPublishFixes`: Saved, but not used yet: a fix always waits in the draft.

## A complete example

The tree above. It passes the check, and it runs: when the items cost over 1,000, it loops over them. When `total` fails, only `note_failure` runs.

```json
{
  "schemaVersion": 2,
  "trigger": {
    "name": "trigger",
    "type": "webhook",
    "pieceId": "webhook",
    "actionId": "catch_webhook",
    "config": { "methods": ["POST"] },
    "nextAction": {
      "name": "total",
      "type": "CODE",
      "pieceId": "code",
      "actionId": "run_code",
      "config": {
        "code": "return trigger.body.items.reduce((s, i) => s + i.price, 0);",
        "onError": "errorPath",
        "maxRetries": 2,
        "timeoutSeconds": 60
      },
      "onErrorAction": {
        "name": "note_failure",
        "type": "PIECE",
        "pieceId": "text",
        "actionId": "transform",
        "config": { "operation": "uppercase", "text": "{{steps.total.error}}" }
      },
      "nextAction": {
        "name": "big_order",
        "type": "BRANCH",
        "pieceId": "branch",
        "actionId": "evaluate",
        "config": {},
        "branches": [
          {
            "name": "Over 1,000",
            "conditions": [[
              { "field": "{{steps.total.result}}", "operator": "num_gt", "value": 1000 }
            ]],
            "firstAction": {
              "name": "each_item",
              "type": "LOOP",
              "pieceId": "loop",
              "actionId": "loop_on_items",
              "config": { "items": "{{trigger.body.items}}" },
              "firstLoopAction": {
                "name": "item_name",
                "type": "PIECE",
                "pieceId": "text",
                "actionId": "transform",
                "config": { "operation": "uppercase", "text": "{{steps.each_item.item.name}}" }
              }
            }
          },
          { "name": "Otherwise", "conditions": [] }
        ]
      }
    }
  },
  "settings": { "timeoutSeconds": 300, "concurrencyLimit": 5, "whenFull": "wait" },
  "variables": { "CURRENCY": "EUR" }
}
```

Related: [Get started](https://docs.gorunner.io/api/get-started) · [Create a workflow](https://docs.gorunner.io/api/workflows/create-workflow) · [Check a definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) · [Use data from earlier steps](https://docs.gorunner.io/build/use-data-from-earlier-steps)

Source: https://docs.gorunner.io/api/workflow-definition
