Automations
Automations are self-contained state machines written in KATA that transform an input dictionary into an output dictionary. The results are used to automate and customize workflows throughout Cerb.
Syntax
This simple automation, written in KATA:
start:
return:
answer: {{a * b}}
With this input:
a: 5
b: 4
Returns this output:
answer: 20
Dictionaries
A dictionary is simply a set of keys with corresponding values. Keys are a name that ends with a colon (:
). Values are units of data like text, numbers, arrays (lists), booleans (true/false), or dictionaries.
The input and output above are dictionaries.
The input dictionary has a key a:
with the value 5
, and a key b:
with the value 4
.
The output dictionary has a key answer:
with the value 20
.
In KATA, indenting keys with spaces creates a hierarchy of parent/child relationships. start:
has a child return:
which has a child answer:
.
There is a unique key path to any key. In the example above, start:return:answer:
is the path to the key containing the answer. These paths are used by visualizations and error messages to refer to a specific point in a KATA document.
The purpose of an automation is to transform an input dictionary into an output dictionary. Depending on where an automation is used, there are different expected inputs and outputs.
When an automation executes, it creates a new working memory dictionary that starts as a copy of its input. Within this dictionary an automation can store, retrieve, and manipulate data using keys.
Dialects
There are various dialects of KATA: automations, maps, toolbars, events, etc. The dialects share the same overall syntax, but each has a different vocabulary of keys.
In automations, keys describe a declarative set of commands. In other words, an automation serves as a natural language outline of logic and actions to be undertaken, rather than a set of detailed computer programming instructions for carrying out each step.
For instance, the command http.request: fetches a web page by URL and save its status code, headers, and body to a given key in the dictionary.
Names
When the same command appears multiple times within the same parent, each instance must have a unique name. Commands are named by appending a forward slash (/
) and an identifier.
The identifier may contain letters, numbers, and underscores.
For instance, we could “hardcode” the inputs from the first example above as keys:
start:
set/a:
a: 5
set/b:
b: 4
return:
answer: {{a * b}}
We use the set:
command twice in the start:
parent, so we name them set/a:
and set/b:
.
Key names are part of a key’s path (e.g. start:set/b:
).
Scripting
Values can be dynamically generated from the output of scripting.
In the previous examples we had this line:
answer: {{a * b}}
This computes a new value by multiplying the value in placeholder a
by the value in placeholder b
.
Scripting occurs within specific tags:
Tag | |
---|---|
{{ }} |
Output a placeholder or expression |
{% %} |
Execute a control flow statement (looping, branching) |
{# #} |
Include a hidden code comment |
In scripts, you can refer to any key from the working memory dictionary, which are often referred to as placeholders to avoid ambiguity with keys in KATA documents.
Scripting provides several functions and filters for quickly formatting and manipulating data.
Refer to the scripting documentation for a full list of capabilities.
Annotations
In KATA, all values that immediately follow a key are assumed to be text by default:
a: 5
b: 4
In the example above, 5
and 4
are treated as text rather than numbers. However, in certain situations, like mathematical operations, a text value can be automatically coerced into a number.
KATA stands for Key Annotated Tree of Attributes. Annotations are tags on keys with special instructions for handling their value.
Annotations start with @
and are appended to a key name.
For example, we can explicitly specify that a key’s value is an integer (a non-fractional number) with @int
:
start:
set:
a@int: 5
b@int: 4
return:
answer@int: {{a * b}}
In the above automation’s dictionary, the value for the keys a:
, b:
, and answer:
are now strictly numeric.
Annotations are not part of a key’s name or path. The key path start:set/a:a:
points to the value 5
.
Here are some common annotations:
Annotation | |
---|---|
@base64: |
Binary data encoded as Base64 text |
@bit: |
0 (off , false , no , n ) or 1 (any non-false value) |
@bool: |
false (no , n , off , 0 ) or true (any non-false value) |
@csv: |
An array encoded as comma-separated text |
@date: |
A human-readable absolute (Jan 1 2025 08:00 ) or relative (+2 hours ) date |
@float: |
A floating point number |
@int: |
A non-fractional number |
@json: |
A dictionary encoded as JSON text |
@kata: |
A dictionary encoded as KATA text |
@key: |
A copy of the value from the given key path |
@list: |
An array encoded as a line-delimited text block |
@optional: |
Remove the key if the value is empty |
@text: |
A multiple line text block |
@trim: |
Remove the value’s leading and trailing whitespace |
Multiple annotations may be joined with commas. They are evaluated from left to right.
For instance:
start:
set:
a: 5
b@key,int: a
return:
answer@int: {{a * b}}
The line b@key,int: a
is setting the key b:
to the value of the a
key, and then converting it to an integer.
Multiple line values
An annotated key may be followed by an indented block of text to set a multiple-line value:
start:
set:
countries@json:
{
"United States": {
"pop": 330052960,
"pop_est": 2020
},
"China": {
"pop": 1441615562,
"pop_est": 2020
}
}
return:
output@text:
{% set diff = countries.China.pop - countries['United States'].pop %}
There are {{diff|number_format}} more people in China than the USA.
There are 1,111,562,602 more people in China than the USA.
This approach is particularly useful when you need to create a dictionary with keys that contain characters like spaces, which are not valid in KATA keys.
Execution
An automation can store, retrieve, and manipulate data using keys in the working memory dictionary.
During execution, the dictionary also keeps track of an automation’s control flow (conditional branches, call stack, loops, past inputs, etc).
An automation begins execution at the start:
command.
Inputs
Automations that support custom input may optionally provide an inputs:
key to define the possible values that a caller can provide.
Inputs are defined with a type/name:
key. The name
must be unique within the inputs.
The type
must be one of:
array: | An array of values |
record: | A record ID of a given type (converted to a dictionary) |
records: | Multiple record IDs of a given type (converted to an array of dictionaries) |
text: | Text with an optional data type |
The values are available in the inputs
placeholder.
inputs:
record/ticket:
record_type: ticket
required@bool: yes
text/subject:
required@bool: yes
start:
return:
result@text:
Changed the subject on ticket {{inputs.ticket.mask}} to: {{inputs.subject}}
Snippet examples
Automations that support custom inputs:
can provide a snippet:
key for each input.
This is used when inserting the automation into an editor from an interaction.
inputs:
text/subject:
type: freeform
required@bool: yes
snippet:
subject: This is an example subject
Exit states
After execution, an automation concludes in one of the following __exit
states:
State | |
---|---|
return |
The automation completed successfully and provided output in the __return: key |
await |
The automation paused awaiting additional input described by __return: |
error |
The automation failed with an error in __return:error: |
exit |
The automation exited without success or failure (default) |
Error handling
Each command in the automation can result in success or failure.
Commands may provide on_success:
and on_error:
events to run any number of commands in response to success or failure.
The on_error:
event can recover from an error to continue execution.
If the on_error:
event is omitted, a command error immediately exits the automation in the error
state.
This http.request command requests an invalid URL:
start:
http.request:
output: http_response
inputs:
method: GET
url: https://invalid.url.example/
return:
There is no on_error:
event, so the automation immediately exits in the error
state. The return:
command is never reached.
We can add events to handle errors:
start:
http.request:
output: http_response
inputs:
method: GET
url: https://invalid.url.example/
on_success:
# Commands to perform on success
return:
on_error:
# Handle the error or provide a default
return:
The automation now always exits in the return
state.
Simulation
During testing and development, it may not be desirable to execute certain actions. An automation’s execution can be simulated instead.
Each command can provide an on_simulate:
event that is used during simulation instead of executing. This can run any number of alternative commands.
These two special commands are available during simulation:
simulate.success: |
Simulate command output and execute the on_success: event. |
simulate.error: |
Simulate command output and execute the on_error: event. |
The following example simulates an http.request:
command and provides mock output:
start:
http.request:
output: http_response
inputs:
method: GET
url: https://invalid.url.example/
on_simulate:
simulate.success:
status_code: 200
content_type: application/json
body: { "output": "Good job!" }
on_success:
return:
body@key: http_response:body
Even though the URL is invalid, the simulated output is:
body: { "output": "Good job!" }
Continuations
When an automation exits in the await:
state, a snapshot of its current dictionary is saved and assigned a long random identifier. This snapshot is called a continuation.
The continuation identifier is used to resume the automation from the same point at a future time with additional input.
For instance, here’s a basic interaction automation that pauses for user input:
start:
await:
form:
elements:
text/prompt_name:
label: What is your name?
required@bool: yes
return:
output@text:
Hello, {{prompt_name}}!
At the await:
command, the automation will send a web form to the user, create a continuation, and wait for any length of time to resume execution.
An automation that supports continuations can exit in the await:
state any number of times before concluding.
start:
await/intro:
form:
elements:
text/prompt_name:
label: Name:
required@bool: yes
text/prompt_email:
label: Email:
required@bool: yes
type: email
placeholder: you@example.com
# (Confirmation code is generated, saved, and emailed)
await/confirm:
form:
elements:
say/hello:
content@text:
Hello, {{prompt_name}}!
We just sent a confirmation code to {{prompt_email}}
text/prompt_code:
label: Confirmation Code:
required@bool: yes
type: uri
max_length: 8
# (Confirmation code is verified)
return:
output@text:
Thanks, {{prompt_name}}!
Your email address ({{prompt_email}}) been subscribed to our newsletter.
Timers
Automations can use timers to run at a future time either once or on a recurring schedule.
Timers can be created procedurally by automations and interactions, or manually by workers.
A timer specifies a name, a future datetime, an optional schedule, and a block of events KATA to conditionally determine an automation.timer to run.
On the first invocation of the timer, an automation is selected. This may optionally provide inputs.
When the automation concludes:
-
If the timer has a recurring schedule, it is rescheduled for the next occurrence.
-
Otherwise, a one-shot timer is disabled (or optionally deleted) at conclusion.
If the automation ends in the await
state, a continuation is created, and the timer is rescheduled for the given datetime.
The timer stores the continuation ID and the automation pauses at the current point. On the next timer invocation, the automation resumes where it left off rather than starting over.
Schedules are defined in Unix CRON expression format. When multiple expressions are specified, the timer is scheduled for the next most recent occurrence among them.
Policies
The permissions of automations are governed by policies. A policy is a collection of rules which describe the conditions where each action would be permitted or denied.
Scopes
Scope | |
---|---|
callers: |
Rules that determine who can use an interaction, and where |
commands: |
Rules that determine what commands are allowed or denied |
Placeholders
Policies can use placeholders based on the command:
Key | Example | |
---|---|---|
node.id |
The key path of the command | start:record.create: |
node.type |
The name of the command | record.create |
inputs.* |
The dictionary of inputs | inputs.record_type |
output |
The output placeholder name | result |
Rules
Callers
Some automation triggers support callers. A caller contains information about where, and by whom, an automation was started. These details can be used in policy rules.
Trigger |
---|
interaction.worker |
The following policy allows an interaction on project board columns when a worker has write-access on the board, and otherwise denies it:
callers:
cerb.toolbar.projectBoardColumn:
allow/owners@bool:
{{
cerb_record_writeable('project_board', board_id, worker__context, worker_id)
? 'yes'
}}
deny: yes
When a caller policy denies an interaction it is automatically hidden from toolbars.
Commands
This policy allows all commands:
commands:
all:
allow: yes
The above policy is simple but not secure. Instead, we recommend adhering to the “principle of least privilege”1. This means only allowing the minimal set of commands required to accomplish an automation’s purpose.
The following policy only allows:
- HTTP GET requests only to the
https://api.example/
endpoint. - The creation of new task records.
- The reading of all records.
commands:
http.request:
allow/ourApi@bool:
{{inputs.url starts with 'https://api.example/'
and inputs.method == 'GET' ? 'yes'}}
record.create:
allow/newTasks@bool:
{{inputs.record_type|context_alias == 'task' ? 'yes'}}
record.get:
allow: yes
all:
deny: yes
Each command can have multiple allow:
and deny:
rules, but they must have a unique /name
suffix.
A rule with a no
value is ignored (i.e. a failed “allow” does not mean “deny”). Rules are tested in sequence until an explicit allow: yes
or deny: yes
outcome is reached.
If a command matches no rules, the default outcome is deny: yes
.
The all:
key matches all commands. This can be used as a final “catch-all” to allow or deny any command that matches no other rules.
It is also possible to be permissive by default with exceptions. This following policy permits all HTTP requests except connections to unencrypted http://
endpoints:
commands:
http.request:
deny/http@bool:
{{inputs.url starts with 'http://' ? 'yes'}}
allow: yes
Time limit
By default, automations are restricted to a maximum run duration of 25,000 milliseconds (25 seconds).
This can be changed in the policy:
settings:
time_limit_ms: 30000
In most cases, a better approach is to break up long tasks into smaller pieces and use automation timers and queues.
Testing policy rules
You can test policies from the automation simulator.
You can also test rule logic from Setup » Developers » Bot Scripting Tester:
{% set inputs = {
url: 'https://api.example/some/path',
method: 'GET'
} %}
{{inputs.url starts with 'https://api.example/'
and inputs.method == 'GET' ? 'yes'}}
The test object above returns yes
.
We can change the inputs to exceed the granted permissions:
{% set inputs = {
url: 'https://danger.example/',
method: 'POST'
} %}
{{inputs.url starts with 'https://api.example/'
and inputs.method == 'GET' ? 'yes'}}
The above test object now returns blank, which is interpreted as no
and ignored. The policy returns the default deny: yes
.
Triggers
Automations are automatically triggered in response to events within Cerb.
Trigger | Inputs | Await | |
---|---|---|---|
automation.function | √ | A reusable function with shared functionality called by other automations | |
automation.timer | √ | √ | A scheduled automation with continuations |
behavior.action | √ | Execute an automation from a legacy bot behavior | |
data.query | √ | Return results for custom data queries | |
interaction.worker | √ | √ | Worker interactions on toolbars and widgets |
interaction.worker.explore | √ | √ | Worker interactions that use custom logic to return the next record in explore mode |
interaction.website | √ | √ | Website visitor interactions |
map.clicked | √ | Handlers for clicks on map regions and points | |
projectBoard.cardAction | √ | Actions that take place for new cards in a project board column | |
projectBoard.renderCard | √ | Dynamic card layouts on project boards | |
record.changed | √ | Actions that run when record fields change | |
reminder.remind | √ | Actions that run for reminder alerts | |
resource.get | √ | Dynamic resource content | |
scripting.function | √ | Run an automation from the cerb_automation() function in scripting | |
ui.chart.data | √ | Data sources for Chart KATA widgets | |
ui.sheet.data | √ | Data sources for sheets | |
ui.widget | √ | Custom output for card, profile, or workspace widgets | |
webhook.respond | √ | Handlers for webhook listeners |
Events
In functionality that triggers automations (e.g. widgets), event handlers are defined in a KATA dialect.
For events that expect a single handler (e.g. interaction.worker
), the first matching (non-disabled) automation is executed and its end state is returned. This can be used to conditionally respond based on the event/caller.
For events that run all handlers (e.g. projectBoard.cardAction
), all non-disabled automations are executed in order, and their end states are returned.
Global automation events can be edited from Search » Automation Events. This allows event handler KATA to be configured for global events that don’t otherwise have a parent record (e.g. mail filtering).
automation/onlyTasks:
uri: cerb:automation:example.cards.task
disabled@bool:
{{card_type != 'task' ? 'yes'}}
automation/everythingElse:
uri: cerb:automation:example.cards.generic
There can now be multiple enabled:
or disabled:
rules. The first rule to return true
is used. This allows deny-allow
and allow-deny
strategies. By default, all handlers are enabled.
Event | |
---|---|
mail.draft | Modify a new or resumed draft before the editor is opened |
mail.draft.validate | Validate an email draft before sending |
mail.filter | Modify or reject an inbound message based on its properties |
mail.moved | After a ticket is moved to a new group/bucket |
mail.received | After a new email message is received |
mail.reply.validate | Validate before a worker starts a new reply |
mail.route | Determine a destination group inbox given properties of an incoming message |
mail.send | Before a sent message is delivered |
mail.sent | After a sent message is delivered |
record.changed | React to changes in record field values |
record.merge | Allow or deny record merge requests |
record.merged | After a set of records was merged |
record.viewed | After a record profile is viewed by a worker |
reminder.remind | Send notifications about a reminder |
worker.authenticate.failed | After a worker failed to log in (e.g. invalid password) |
worker.authenticated | After a worker logged in successfully |
Commands
State transitions
await: | Pauses the automation in the await state with output. Creates a continuation for resuming. |
error: | Unsuccessfully terminates the automation in the error state with output. |
return: | Successfully terminates the automation in the return state with output. |
Flow control
decision: | Conditionally select one of multiple potential outcomes. |
outcome: | A conditional sequence of commands. |
repeat: | Iterate an array and repeat a sequence of commands for each value. |
while: | Conditionally loop a sequence of commands. |
Logging
log: | Log a debug message. |
log.warn: | Log a warning message. |
log.error: | Log an error message. |
log.alert: | Log an alert message. |
Actions
api.command: | Execute an API command and return the response. |
data.query: | Execute a data query and return the response. |
decrypt.pgp: | Decrypt a PGP encrypted message. |
email.parse: | Parse a MIME-encoded email message into a ticket. |
encrypt.pgp: | Encrypt a message for one or more PGP public keys. |
file.read: | Read chunks of bytes from an attachment or automation resource. |
file.write: | Write bytes to an automation resource. |
function: | Execute an automation.function automation and return output. |
http.request: | Send data to an HTTP endpoint and return the response. |
kata.parse: | Parse an arbitrary KATA document with placeholders. |
metric.increment: | Add new samples to a metric. |
queue.pop: | Pop an item from a queue. |
queue.push: | Push an item into a queue. |
record.create: | Create a record. |
record.delete: | Delete a record. |
record.get: | Retrieve a record. |
record.search: | Search records. |
record.update: | Update a record. |
record.upsert: | Create or update a record. |
set: | Set one or more placeholders. |
storage.get: | Retrieve arbitrary data from long-term storage. |
storage.set: | Save arbitrary data to long-term storage. |
storage.delete: | Delete data from long-term storage. |
var.expand: | Expand paths on keys. |
var.push: | Add an element to a list placeholder. |
var.set: | Set a placeholder using a complex key path. |
var.unset: | Unset a placeholder. |
Simulation
simulate.success: |
Simulate command output and execute the on_success: event. |
simulate.error: |
Simulate command output and execute the on_error: event. |
Editor
The automation editor includes syntax highlighting, autocompletion for the KATA syntax, a step-based debugger with full access to the current state, a simulator, and a reference for each trigger event.
A contextual toolbar provides interactions for adding inputs, commands, and exit states.
Change History
When editing an automation you can review past versions by clicking on the Change History button in the editor toolbar.
This opens a popup that displays the differences between a past version and the current version.
The left editor is read-only, but the right editor may be modified. Any changes will update the differences in real-time.
Export
An automation may be exported by clicking on the Export button in the editor toolbar.
This creates a package that can be imported into another Cerb environment.
Visualizations
The automation editor has a ‘Visualization’ tab with a flowchart for the current script.
Clicking on a node highlights the relevant line of code in the editor.
References
-
Wikipedia: Principle of Least Privilege - https://en.wikipedia.org/wiki/Principle_of_least_privilege ↩