Types
The formula language has the following types, which correspond to the types in standard JSON:
Text
To enter text, you can wrap your values in either single or double quotes
# Double quotes
"this is some text"
# Single quotes
'this is also some text'
There is no difference in behavior between the quote styles.
If you need to use a quote inside some text you can escape it with \
# Escaping text
"As Einstein said \"Never memorize something that you can look up.\""
TRUE/FALSE
Booleans can be accessed like so, note that case is significant:
# true
TRUE
# false
FALSE
Formulas does not support type coercing, ie. []
will not be coerced to FALSE
. All values are truthy by default except for FALSE
& NULL
.
NULL
Null values are written with all caps and are falsy:
NULL
# true
IS_BLANK(NULL)
Numbers
Numbers are written in simple notation.
# number
123
# decimal
123.45
# negative
-123
Arrays
Usually you will interact with arrays that come from your event data, however you can also create an array either using the array function or using array literal syntax.
# creating an array using a function
ARRAY(1, 2, 3)
# [1, 2, 3]
# creating an array using array literal
[1, 2, 3]
# [1, 2, 3]
To access an item in an array, you use square brackets and numbers, the first item is at position zero:
# my_array = ["first", "second", "third"]
my_array[0]
# "first"
Objects
Usually you will interact with object that come from your event data, however you can also create an object using the object function or object literal syntax.
# creating an object using function
OBJECT("key1", "value1", "key2", "value2")
# {key1: "value1", key2: "value2"}
{key1: "value1", key2: "value2"}
# {key1: "value1", key2: "value2"}
When using object literal syntax, quotes are not required around keys unless needed (e..g if you want a key with a space in it).
Operators
Operators allow you to compare values or perform basic math on them:
# check if equal
a = b
# check if greater than
body.count > 1
# multiply
body.count * 5
See the reference section for more details.
Functions
Functions allow you to perform operations on event data.
You call a function by using its name followed by opening and closing parentheses.
All functions are written in uppercase.
MY_FUNCTION()
Most functions will accept one or more arguments, which can be specified like this:
MY_FUNCTION(argument1, argument2)
Function calls can be nested:
MY_FUNCTION(FUNCTION1(), FUNCTION2())
Function chaining
Since function nesting can result in difficult to read expressions, we also support the ability to chain function calls.
Calls are chained like so:
REPLACE(my_event.url, "https", "http[s]") |> UPCASE(%)
The result of the function on the left is made available on the right as %
, where it can be used in any place
Lazy evaluation
Arguments to most functions will be eagerly evaluated, for example take the following nested functions:
MY_FUNCTION(FUNCTION1(), FUNCTION2())
When this is evaluated, FUNCTION1
will be called first, then FUNCTION2
, finally MY_FUNCTION
will be called with the results of the previous two calls passed as arguments.
Exceptions to this rule are IF
AND
and OR
, in these functions arguments are lazily evaluated, this allows you to effectively use these to control execution:
IF(FUNCTION1(), FUNCTION2())
Here FUNCTION2
will only be called if the FUNCTION1
returns true.
Lambdas
Lambdas are a custom, reusable function that you create using the LAMBDA
function. This function is a little different from other functions because you specify the placeholders as the arguments to the function.
LAMBDA(a, b, a + b)
Here a
and b
are the arguments that will need to be passed when the lambda is called and the a + b
is the expression that will be evaluated when the lambda is called.
There are three ways of using lambdas.
Immediately invoked
First they can be immediately invoked i.e. LAMBDA(a, b, a + b)(1, 2)
. This will create a function to add two numbers together and then immediately call it. This might not seem very useful, but it can help avoid repetition in cases where you need to re-use the same calculation in multiple places.
For example if you wanted to check if the current time is between 9 and 5 you might do something like this:
IF(
AND(
DATE("now", "%H") >= 9,
DATE("now", "%H") < 17,
),
"office hours",
"after hours"
)
Using a lambda you could avoid this repetition like so:
LAMBDA(
current_hour,
IF(
AND(
current_hour >= 9,
current_hour < 17,
),
"office hours",
"after hours"
),
)(DATE("now", "%H"))
With an Array function
The next way you can use Lambdas is as an argument to the functions MAP_LAMBDA
, FILTER
, FIND
and REDUCE
. For example let's imagine we have the following array of data in a field called fruit
:
[
{
"name": "apple",
"in_stock": 0
},
{
"name": "banana",
"in_stock": 5
},
{
"name": "pear",
"in_stock": 6
}
]
If we wanted to find all the items that are in stock we could write the following:
FILTER(fruit, LAMBDA(item, item.in_stock > 0))
Or if we wanted to find the item with the name pear
we could do
FIND(fruit, LAMBDA(item, item.name = "pear"))
Or if we wanted to extract all the names we could do this:
MAP_LAMBDA(fruit, LAMBDA(item, item.name))
As a LOCAL or RESOURCE
The final way you can use lambdas is to assign them to LOCAL or to a RESOURCE and then call them from somewhere else. This can be a great way of defining your own re-usable bits of functionality.
Here we have created a lambda for defanging URLs and stored it in a RESOURCE
. The lambda looks like this:
LAMBDA(
url,
url
|> REPLACE(%, ".", "[.]")
|> REPLACE(%, "http", "hxxp")
)
Ensure the lambda in the Resource, is defined inside of single value mode:
Lambda in a resource
With this in place we can use it from any story like so:
RESOURCE.my_lambdas.defang(url)
Tags
Tags are only available in text mode. You can nest tags inside each other. There must always be a corresponding end
tag for each tag.
if/elseif/else/endif
These tags all work together to allow you to conditionally evaluate sections in text mode.
At its simplest you can have an if
and endif
pair:
If this is run with a user named Alice, it will output Hi Alice
, but if the user has no name it will just output Hi
.
You can also add an else
block to act as a catch all:
Now if the user has no name it will output Hi there
.
Finally you can add more conditions using elseif
:
You can add as many additional conditions with elseif
as you like.
As mentioned above, Formulas does not coerce types, with all but NULL
& FALSE
being truthy. To check if a value is blank simply write the following:
for/endfor
The for
tag allows you to repeat the same block of code multiple times for each item in an array.
This will output User names:
followed by the names of all the users in the array users
.
Within a for tag, there is a special FORLOOP
variable available with the following properties:
FORLOOP.index0
: The zero based index of the current loop iteration. That is the first time through the loop this will be 0, the second time 1 and so on.
FORLOOP.index
: The one based index of the current loop iteration. That is the first time through the loop this will be 1, the second time 2 and so on.
FORLOOP.first
: This will be true the first time through the loop.
FORLOOP.last
: This will be true the last time through the loop.
For example, if we wanted to output a comma between every name we could do something like this:
raw/endraw
The raw
tag allows you to escape any content within it, preventing interpolation at runtime. This escaping allows for repeated <
to be expressed within formulas without triggering interpolation.