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 theuser
orgroup
was identified with.properties
: the properties theuser
orgroup
was identified with; these properties should be accessed likeuser.properties.foo
.createdAt
: the datetime when theuser
orgroup
was first identified to Dopt.updatedAt
: the datetime when theuser
properties orgroup
properties were most recently updated to Dopt.
For example, let’s say a user
and a group
were identified as following:
{
"identifier": "55c8-2a34",
"properties": {
"email": "oneill@acme.com",
"name": "O'Neil",
"activated": false,
"projects": 2,
"roles": ["Marketing", "Admin"],
"roleIds": [103, 4491]
}
}
{
"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:
Accessors | Returns |
---|---|
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.nonexistent | Accessing 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. |
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 within your expression. These functions can be helpful when you’d like to evaluate expressions based on the state of other flows.
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 last active version was started.
If the user has not encountered the flow at identifier
(and version
), it will return false
.
// checking the last active 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 last active version is in progress.
If the user has not encountered the flow at identifier
(and version
), it will return false
.
// checking the last active 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 last active version has finished.
If the user has not encountered the flow at identifier
(and version
), it will return false
.
// checking the last active 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 last active version has stopped.
If the user has not encountered the flow at identifier
(and version
), it will return false
.
// checking the last active version
hasflowstopped("my-flow")
// checking version two specifically
hasflowstopped("my-flow", 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.
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.
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")
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)