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 byinput_schemaoutputs.<task-id>- output of any earlier task (if it defines one)self- data about the current task, not available outside itconfig- 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:
| Slot | Where | Syntax |
|---|---|---|
url, method | fetch action | string template |
body | fetch action | shape |
for | delay action | literal duration, or a $: yielding integer * |
case | switch | raw expression |
output | task | shape |
output | process | shape |
* A static
forcan 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.