Language description

Referencing data 

Data from previous events, resources, credentials etc can be referenced like so

# accessing properties

Only letters, numbers and underscores can be used here. If your event has a property with another character, say a space, then you can use brackets to reference it:

# accessing properties with spaces
my_event["a prop with a space"]


The formula language has the following types, which correspond to the types in standard JSON:


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.\""


Booleans can be accessed like so, note that case is significant:

# true

# 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 values are written with all caps and are falsy:


# true


Numbers are written in simple notation.

# number

# decimal

# negative


Usually you will interact with arrays that come from your event data, however you can also create an array using the array function.

# creating an array
ARRAY(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"]


# "first"


Usually you will interact with object that come from your event data, however you can also create an object using the object function.

# creating an object
OBJECT("key1", "value1", "key2", "value2")

# {key1: "value1", key2: "value2"}


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


Most functions will accept one or more arguments, which can be specified like this:

MY_FUNCTION(argument1, argument2)

Function calls can be nested:


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:


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:


Here FUNCTION2 will only be called if the FUNCTION1 returns true.


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:

    DATE("now", "%H") >= 9,
    DATE("now", "%H") < 17,
  "office hours",
  "after hours"

Using a lambda you could avoid this repetition like so:

      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, = "pear"))

Or if we wanted to extract all the names we could do this:

MAP_LAMBDA(fruit, LAMBDA(item,


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:

    |> REPLACE(%, ".", "[.]")
    |> REPLACE(%, "http", "hxxp")

Ensure the lambda in the Resource, is defined inside of single value mode:

With this in place we can use it from any story like so:



Tags are only available in single value mode. You can nest tags inside each other. There must always be a corresponding end tag for each tag.


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:


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: