genroc

docs / Guides / Process definition

Tasks and expressions

Learn about tasks, and how to pass data around.

To make our process actually do something, we need a way to communicate with the outside world, and that is where the task action comes in.

Action

Let’s say we want to send our users a welcome email two hours after they register.

nameUnique process identifier.: welcome-userUnique process identifier.
input_schemaJSON Schema used to validate the input payload when starting a new instance.:
  typeThe value's JSON type, or a list of types it may take.: objectThe value's JSON type, or a list of types it may take.
  propertiesThe named members of an object, each a schema.:
    user_email:
      typeThe value's JSON type, or a list of types it may take.: stringThe value's JSON type, or a list of types it may take.
  requiredWhich properties must be present. A property not listed here may be absent, and reading it yields null.: [user_email]

tasksOrdered list of execution tasks. Control advances linearly unless a switch case redirects.:
  - idTask identifier, unique within the definition.: wait_some_timeTask identifier, unique within the definition.
    actionDescribes the action to perform. Omit for switch-only (routing) tasks.:
      typeDelay action: parks the instance until a duration elapses (for) or an instant arrives (until).: delayDelay action: parks the instance until a duration elapses (for) or an instant arrives (until).    # the process will be parked for 2 hours
      forA duration from when the task is reached: "2h30m", a number of milliseconds, or a $: expression.: 2hA duration from when the task is reached: "2h30m", a number of milliseconds, or a $: expression.
    switchRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.: nextRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.

  - idTask identifier, unique within the definition.: welcomeTask identifier, unique within the definition.
    actionDescribes the action to perform. Omit for switch-only (routing) tasks.:
      typeHTTP call. URL, method, headers and body are all expressions.: fetchHTTP call. URL, method, headers and body are all expressions.    # this will hit an endpoint on our API
      urlRequest URL. May contain ${ } interpolations, e.g. ${ config.server_url }/path.: "https://api.example.com/emails"
      methodHTTP method, lowercase (e.g. get, post) or a template such as ${ input.method }. Required — the verb is never guessed.: postHTTP method, lowercase (e.g. get, post) or a template such as ${ input.method }. Required — the verb is never guessed.
      bodytasks.welcome.action.body — object{template, to}:
        # passing data from input to the request body
        toto — string: "$: inputinput → object{user_email}.user_emailinput.user_email → string"
        templatetemplate — string: welcome-after-registration
    switchRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.: endRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.

In the example above we’ve used the delay and fetch actions. The delay parks the process for a specified amount of time. While it waits there is no active thread anywhere, it just sits in the database.

The fetch action then hits the specified endpoint. We are sending a body whose parameters can be either expressions or static data.

Reading the fetch result

What if you want to read data instead of sending it? No problem, let’s look at another example.

  - id: load_user
    action:
      type: fetch
      method: get
      url: "https://api.example.com/users/${input.user_id}"
      responses:
        200:
          type: object
          properties:
            email: { type: string }
            activated: { type: boolean }
          required: [email, activated]
    switch:
      - case: "self.result.activated == true"
        goto: end
      - goto: $nudge
    # mapping the task output (what parts of the response we need)
    output:
      email: "$: self.result.email"

To read the fetch result, you need to declare its response type. Genroc validates the data against that schema, so invalid data never gets in.

The result is then available as self.result in the case expression, so we can check whether the account is activated.

Expressions

Genroc supports expressions, which let you compute values dynamically and pass them from one task to another. In an expression you can reference the following variables:

  • input - the process input defined by input_schema
  • outputs.<task-id> - output of any earlier task (if it defines one)
  • self - data about the current task, not available outside it
  • config - configuration values resolved from environment variables (we will get to this later)

You can also use basic operators like ==, !=, <, <=, >, >=, &&, ||, ??.

Expressions can also compose objects or arrays:

{name: "John"}        # returns object
["John", "Marie"]     # returns array

map function

For working with arrays, genroc also supports map function. It takes an array as a first argument and callback as second. In callback you can transform the items into different shapes.

$: map(names, (name) => { who: name })
# Array<names> -> Array<{who: string}>

String template

This is a simple use case: it lets you compose a string value dynamically.

"https://api.example.com/users/${input.user_id}"

The value passed into a template must be a primitive value (string, number or boolean), and it cannot be null.

You can escape ${} by doubling the $, e.g.:

"https://api.example.com/users/$${input.user_id}"

Expression of any type

By prefixing the string with $: you are not creating a template. The whole expression result is used and its type is preserved:

"$: self.result.email"  # string

This way you can also return whole objects, e.g.:

"$: self.result"        # { email: ..., activated: ... }

Raw expressions

In switch/case fields a static value would make no sense, so there is no $: or ${} syntax, you write the expression directly:

switch:
  - case: "self.result.activated == true"
    goto: ...

The same syntax is used also for on_error/case fields, which we will discuss later.

Shapes

Shapes are a convenient way to transform data. The best example is the task output field.

Fully static

A shape can be fully static in YAML, with no expressions at all.

output:
  ok: true

Combined

Define the object shape statically and pass the values dynamically.

output:
  ok: "$: self.result.is_ok"

Expression only

Because expressions can define objects, you can build the whole structure in one expression, or pass a variable that already has the expected shape.

output: "$: { ok: self.result.is_ok }"

Slots

You can use shapes and expressions anywhere a dynamic value is expected. In the examples above that is:

SlotWhereSyntax
url, methodfetch actionstring template
bodyfetch actionshape
fordelay actionliteral duration, or a $: yielding integer *
caseswitchraw expression
outputtaskshape
outputprocessshape

* A static for can use human-readable syntax, e.g. 2h30s; a dynamic one can only give milliseconds as a number.

Every action and clause introduced later brings its own slots, but the syntax is always one of these.

Process output

A process can optionally define an output. It looks like this:

tasks:
  - id: load_user
    ...
    output:
      email: "$: self.result.email"

output:
  email: "$: outputs.load_user.email"

It has the same shape and syntax as a task output. It is evaluated once the process reaches switch: end. It cannot access self, as it is not part of any task.

The output is the contract for the outside world: it is what callers read, and what a parent checks a child’s result against. Task outputs are internal — visible in genctl detail, but nothing outside is written against them.