Skip to main content

Writing expressions

Overview

Dopt offers a simple, SQL-like expression language which you can use to craft conditions when targeting users (and groups) for entry into flows and when creating true / false branching logic within your flow.

Using user and group properties and identifiers

For both users and groups, Dopt offers three system-defined accessors for identifier, createdAt, and updatedAt, and allows access into identified properties.

Each of these can be combined with the user or group. For example, user.identifer and group.identifer and user.properties and group.properties.

  • identifier: the string identifier the user or group was identified with.
  • properties: the properties the user or group was identified with; these properties should be accessed like user.properties.foo.
  • createdAt: the datetime when the user or group was first identified to Dopt.
  • updatedAt: the datetime when the user properties or group properties were most recently updated to Dopt.

For example, let’s say a user and a group were identified as following:

User properties
{
"identifier": "55c8-2a34",
"properties": {
"email": "oneill@acme.com",
"name": "O'Neil",
"activated": false,
"projects": 2,
"roles": ["Marketing", "Admin"],
"roleIds": [103, 4491]
}
}
Group properties
{
"identifier": "ea83-47h1",
"properties": {
"sku": "Pro",
"integration setup": false,
"company": {
"name": "Acme Co",
"email": "@acme.com",
"projects": 42,
"startedAt": "2022-10-19T19:45:13Z"
}
}
}

You can access their identifiers and properties by using:

AccessorsReturns
user.identifier"55c8-2a34"
group.identifier"ea83-47h1"
user.properties.roles["Marketing", "Admin"] -- Returns an array. See the includes function for an example of how to use this array in an expression.
user.properties.roles[0]"Marketing" -- Returns the 0th (first) element of the array.
user.properties.nonexistentAccessing a property which doesn’t exist returns nothing
group.properties["integration setup"]false -- Uses the ["..."] syntax to access a property which has a space in its name. You can use this syntax for all properties, but it is especially valuable where property names are not alphanumeric.
group.properties.company.name"Acme Co" -- Accesses a nested object, "company", and retrieves its "name". You can access objects at any depth by writing out their path. You can use the ["..."] syntax if any of the names in the path are not alphanumeric.
info

Property names are case sensitive and the property name in the expression must match the case of the user or group property. group.properties.Company is different from group.properties.company.

Crafting logical expressions

Types of properties

Dopt supports three basic types, numbers (floats and integers like 1.23 and -1), booleans (true and false), and strings ("my string").

In addition to these simple types, Dopt also allows access into array and object types in *.properties as explained above.

Finally, Dopt allows you to work with datetime types. Both *.createdAt and *.updatedAt have datetime types. Additionally, strings in the *.properties you submit can be converted to datetime by using the date function.

Equality conditions

The three basic types can be equality checked by using == and !=.

user.properties.name == "O'Neill"
group.properties["integration setup"] == false
group.properties.company.projects != 42

Datetimes can also be equality checked using == and !=. If either side of the condition is a datetime, the other side will be automatically converted to datetime if possible.

Both of the following equality checks work:

user.createdAt == group.properties.company.startedAt
user.createdAt == date(group.properties.company.startedAt)

Comparison conditions

Numbers and datetimes can also be compared using >, >=, < and <=. As with equality checks, if either side of the condition is a datetime, the other side will be automatically converted to datetime if possible.

group.properties.company.projects < 42
user.createdAt >= date(group.properties.company.startedAt)

! and truth-y conditions

Using the not operator, !, converts a logical expression to its opposite value.

So, !group.properties["integration setup"] returns true.

If a property has a true or false value, you can directly refer to it when writing conditions.

By refering to properties directly, you check whether they are truth-y. When used in an expression user.properties.example will return true if example is present in user.properties and is not null, false, or 0. Conversely, !user.properties.example will return true if foo is false, 0, or null.

AND and OR conditions

Finally, you can combine multiple conditions together using the AND and OR operators. These are case insensitive, so you can use OR, Or, or or.

In logical expressions, AND takes precendence over OR (similar to how * takes precedence over + in numerical expressions). To specify your own order of operations, you can use parenthesis around conditions.

user.properties.role[0] == "marketing" OR user.properties.role[1] == "sales" // evaluates to true
user.properties.role[0] == "marketing" AND user.properties.role[1] == "sales" // evaluates to false

!(group.properties["integration setup"] OR user.properties.activated) // evaluates to true
group.properties["integration setup"] OR user.properties.activated // evaluates to false

group.properties.projects < 50 OR user.properties.projects < 5 AND user.properties.activated // evaluates to true
(group.properties.projects < 50 OR user.properties.projects < 5) AND user.properties.activated // evaluates to false

In particular, the last two examples demonstrate the importance of parenthesis when crafting meaningful expressions.

Using functions

For working with more complex types like datetimes, strings and arrays, Dopt provides built-in functions in our expression language.

These functions accept both literal types (like "this is a string") as well as properties (user.properties.someString) and functions (now()) as their inputs. Functions return values as their outputs which can be used within other functions or within logical expressions.

Dopt also provides functions which allow you to specify dependencies on other flows and on blocks within those flows within your expression. These functions can be helpful when you’d like to evaluate expressions based on the state of other flows or their blocks.

Dig into each of our "Working with..." sections to learn more about all the functions Dopt supports.

Working with flows

Dopt exposes four functions which allow you to query the flow.started, flow.finished, and flow.stopped states of a user’s flows when crafting targeting expressions. These functions check a user’s flow states via a flow identifier and an optional version and return a boolean indicator of that state.

hasflowstarted

hasflowstarted(
identifier: string,
version?: number
): boolean

Returns true if the flow specified by identifier and version has started for the user. If the optional version is empty, it will check whether the latest version was started.

If the user has not encountered the flow at identifier (and version), it will return false.

// checking the latest version
hasflowstarted("my-flow")

// checking version two specifically
hasflowstarted("my-flow", 2)

isflowinprogress

isflowinprogress(
identifier: string,
version?: number
): boolean

Returns true if the flow specified by identifier and version is in progress for the user (it has started but not stopped or finished). If the optional version is empty, it will check whether the latest version is in progress.

If the user has not encountered the flow at identifier (and version), it will return false.

// checking the latest version
isflowinprogress("my-flow")

// checking version two specifically
isflowinprogress("my-flow", 2)

hasflowfinished

hasflowfinished(
identifier: string,
version?: number
): boolean

Returns true if the flow specified by identifier and version has finished for the user. If the optional version is empty, it will check whether the latest version has finished.

If the user has not encountered the flow at identifier (and version), it will return false.

// checking the latest version
hasflowfinished("my-flow")

// checking version two specifically
hasflowfinished("my-flow", 2)

hasflowstopped

hasflowstopped(
identifier: string,
version?: number
): boolean

Returns true if the flow specified by identifier and version has stopped for the user. If the optional version is empty, it will check whether the latest version has stopped.

If the user has not encountered the flow at identifier (and version), it will return false.

// checking the latest version
hasflowstopped("my-flow")

// checking version two specifically
hasflowstopped("my-flow", 2)

Working with blocks

Dopt exposes four functions which allow you to query the block.entered, block.active, block.exited, and block.transitioned[...] states of a user’s blocks when crafting targeting expressions. These functions check a user’s blocks states via a block's UID and an optional flow version and return a boolean indicator of that state.

You can copy the block's UID from the block's panel in the flow builder. For items within a component block, you can access the UID by editing the block.

hasblockentered

hasblockentered(
uid: string,
version?: number
): boolean

Returns true if the block specified by uid and version has been entered by the user. If the optional version is empty, it will check whether the latest version was entered.

If the user has not encountered the block at uid (and version), it will return false.

// checking the latest version
hasblockentered("-54PbRmBMKA5JkBGT-ljwW")

// checking version two specifically
hasblockentered("-54PbRmBMKA5JkBGT-ljwW", 2)

isblockactive

isblockactive(
uid: string,
version?: number
): boolean

Returns true if the block specified by uid and version is active. If the optional version is empty, it will check whether the latest version is active.

If the user has not encountered the block at uid (and version), it will return false.

// checking the latest version
isblockactive("-54PbRmBMKA5JkBGT-ljwW")

// checking version two specifically
isblockactive("-54PbRmBMKA5JkBGT-ljwW", 2)

hasblockexited

hasblockexited(
uid: string,
version?: number
): boolean

Returns true if the block specified by uid and version has been exited by the user. If the optional version is empty, it will check whether the latest version was exited.

If the user has not encountered the block at uid (and version), it will return false.

// checking the latest version
hasblockexited("-54PbRmBMKA5JkBGT-ljwW")

// checking version two specifically
hasblockexited("-54PbRmBMKA5JkBGT-ljwW", 2)

hasblocktransitioned

hasblocktransitioned(
transitionLabel: string,
uid: string,
version?: number
): boolean

Returns true if the block specified by uid and version has transitioned along transitionLabel. If the optional version is empty, it will check whether the latest version was transitioned.

If the user has not encountered the block at uid (and version), it will return false. If there is no transition with transitionLabel, it will return false.

// checking the latest version
hasblocktransitioned("dismiss", "-54PbRmBMKA5JkBGT-ljwW")

// checking version two specifically
hasblocktransitioned("dismiss", "-54PbRmBMKA5JkBGT-ljwW", 2)

Working with datetimes

Dopt supplies system controlled datetime attributes (*.createdAt and *.updatedAt) and allows you to work with your own datetime values.

If your datetime values are in the form of a string, you will need the string to be in ISO8601 format to have consistent results. If your datetime values are in the form of an integer, Dopt will treat them as unix timestamps (milliseconds since 1970-01-01). Internally, our expression language uses the dayjs package which also explains how it accepts string values and numeric values.

date

date(
input: string | number | property
): datetime

Converts a string or integer or property value into a date.

date(group.properties.company.startedAt)
date("2018-04-13T19:18:17.040+02:00")

// unnecessary but valid
date(user.createdAt)

// invalid
date("foo bar")

// will return nothing because user.properties.activated does not evaluate to a datetime
date(user.properties.activated)

now

now(): datetime

Returns a datetime of the date and time at which the condition is evaluated.

info

The now function will be evaluated on Dopt’s backend as part of an entry condition or a true / false branch block. It does not reflect the time at which the expression was written.

today

today(): datetime

Returns a datetime of the start of the date at which the condition is evaluated.

info

The today function will be evaluated on Dopt’s backend as part of an entry condition or a true / false branch block. It does not reflect the time at which the expression was written.

dateadd

dateadd(
input: datetime | string | number | property,
amount: number | property,
unit: string
): datetime

Adds the specified amount of unit to the input. For example, dateadd(user.createdAt, 10, "days") adds 10 days to the user.createdAt value.

unit must be one of the units supported by dayjs. We do not support quarter. Dopt supports the shortform, longform, and longform plural version of every unit, for example, d and day and days.

dateadd(date(group.properties.company.startedAt), -10, "weeks")
dateadd(group.properties.company.startedAt, -10, "weeks")
dateadd(user.createdAt, 1.5, "y")

// invalid
dateadd(user.createdAt, "100", "century")

// will return nothing because user.properties.activated does not evaluate to a datetime
dateadd(user.properties.activated, 1, "hour")
info

When decimal values are passed for days and weeks, they are rounded to the nearest integer before adding.

datediff

datediff(
a: datetime | string | number | property,
b: datetime | string | number | property,
unit: string
): number

Returns the difference between a and b in terms of unit. This is the equivalent of performing a - b in the unit dimension. For example, datediff("2022-10-13", "2022-10-14", "d") returns -1.

As with dateadd, unit must be one of must be one of the units supported by dayjs. We do not support quarter.

datediff("2022-10-13", "2022-10-14", "d")
datediff(user.createdAt, group.properties.company.startedAt, "w")

// invalid
datediff(user.createdAt, user.properties.updatedAt, "nanosecond")

// will return nothing because user.properties.activated does not evaluate to a datetime
datediff(user.properties.activated, user.createdAt, "hour")

Working with strings

When writing conditions involving strings, you may often find yourself adding together strings, checking whether they contain a value, checking whether they’re empty, and more. Dopt’s expression language’s string functions allow you to deeply interact with strings.

lowercase

lowercase(
input: string | property
): string

Returns the lowercase form of the input string.

lowercase(user.properties.email)
lowercase("my name is Dopt")

// invalid
lowercase(100)
lowercase(false)

// will return nothing because user.properties.projects
// and user.properties.activated are not strings
lowercase(user.properties.projects)
lowercase(user.properties.activated)

uppercase

uppercase(
input: string | property
): string

Returns the uppercase form of the input string.

uppercase(user.properties.email)
uppercase("my name is Dopt")

contains

contains(
input: string | property,
search: string | property
): boolean

Returns whether the input string contains the search string. This function is case-sensitive. If you’d like to perform a case-insensitive search, you can combine this function with lowercase or uppercase.

contains(user.properties.email, "acme")
contains("my name is Dopt", "Dopt")
contains("test string; test string 2; test string 3", user.identifier)

// case insensitive
contains(lowercase(user.properties.email), "acme")

regexcontains

regexcontains(
input: string | property,
regex: string | property
): boolean

Returns whether the input string contains the regex search regular expression. regex must be a valid JavaScript regular expression. This function is case-sensitive. If you’d like to perform a case-insensitive search, you can combine this function with lowercase or uppercase.

regexcontains(user.properties.email, "@[a-z]+.com")
regexcontains("my name is Dopt", "(Dopt)")

// case insensitive
regexcontains(lowercase(user.properties.email), "@[a-z]+.com")

// invalid
regexcontains(user.properties.email, "**")

concat

concat(
a: string | property,
b: string | property,
...
): string

Concatenates all the strings provided in a, b, and any other arguments passed into the function.

concat(user.properties.email, ".com")
concat("my name is Dopt", "1", "2", "3", "4")

// invalid because 1 and false are not strings
concat("my name is Dopt", 1, "2", false, "4")

// will return nothing because user.properties.projects
// and user.properties.activated are not strings
concat(
user.properties.email,
"-",
user.properties.projects,
"-",
user.properties.activated
)

length

length(
input: string | property
): number

Returns the length of the input string.

length(user.properties.email)
length("my name is Dopt")

Working with arrays

Unlike datetimes and strings, arrays cannot be crafted as literal statements within the expression language. For example, you cannot write user.properties.roles == ["Marketing", "Admin"].

Instead, arrays can only be accessed based on *.properties accessors and cannot be equality or comparison checked.

We provide two functions which can simplify working with arrays: length and includes, which allow you to check the length of *.properties arrays and to check whether those arrays contain any specific values.

length

length(
input: property
): number

Returns the length of the input array. This function is an overloaded version of the length function above.

length(user.properties.roles)

includes

includes(
array: property,
item: any
): boolean

Returns whether the input array includes the item. If the item is == to an item in the array, this function will return true. Otherwise, it will return false. input must be a property which evaluates to an array. item can be any type of value accepted by the expression language.

includes(user.properties.roles, "Marketing")
includes(user.properties.roleIds, 1193)

// returns false because first argument is not an array
includes(user.properties.email, "acme")
includes(user.properties.activated, false)