GoRunner docs
Search or askSearch⌘K

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.

TriggertriggerWebhook
01totalCode
On error · onErrorAction
02note_failureText Helper
03big_orderBranch
Over 1,000 · branches[0].firstAction
04each_itemLoop
Each item · firstLoopAction
05item_nameText 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: 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. 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.
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 lists them.
config
The trigger’s settings, by field name: Get a trigger’s fields.
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. 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 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.

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 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" }
}