Language description

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_LAMBDAFILTERFIND 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.

Was this helpful?