Signals
Signals allow Journeys to react to specific events, such as a participant completing an activity, reading content, or being assigned a tag. They are the building blocks for creating complex, reactive Journeys.
Signal Structure
Each signal expression can use any combination of the following fields:
| Field | Description |
|---|---|
code | The event type being evaluated |
identifier | The ID of the resource being evaluated (Card ID for CONTENT_READ and ACTIVITY_REPORTED, Activity ID for HEALTH_MEASURE_REPORTED) |
value | A single string value (used by some internal signals) |
values | Response values from an Activity or Health Measure |
scores | Aggregate score from a Scored Activity |
tags | The participant's current Tags |
groups | The participant's current Groups |
extras | The participant's current Extras |
events | Historical event memory for advanced journey logic |
vars | Session variables from journey memory |
The entire signal expression string — including both the values in the evaluation context (code, identifier, value, values.*, tags.*, groups.*, extras.*) and the function names (anyof, allof, noneof, splitat, strlen) — is lowercased before evaluation. All string comparisons and function names are therefore case-insensitive.
Signal Codes
CONTENT_READ
Fires when a participant reads a Content Card.
code == "CONTENT_READ" && identifier == "1"
ACTIVITY_REPORTED
Fires when a participant completes an Activity Card.
code == "ACTIVITY_REPORTED" && identifier == "1"
Scored Activity Cards
code == "ACTIVITY_REPORTED" && identifier == "1" && scores < 50
code == "ACTIVITY_REPORTED" && identifier == "1" && scores > 50
Activity Reported with a Categorical Activity Element
In the case below, Activity_Element_Name_Code is the code relating to the
Activity Element. To find the code, click an Activity Element in the Activity's
Activity Elements column and copy its CODE.
code == "ACTIVITY_REPORTED" && identifier == "136" && values.{Activity_Element_Name_Code} == "I want to make changes"
For example:
code == "ACTIVITY_REPORTED" && identifier == "136" && values.ARE_YOU_READY_TO_MAKE_SOME_CHANGES == "I want to make changes"
To find the code for an Activity Element, highlight columns to show the code. Copy the code from the column directly.
Activity Reported with Specific Values
Remember to swap out values.NUMERIC_ELEMENT_ONE_CODE with the code of the
Activity Element you are targeting.
Numeric:
values.{ELEMENT_CODE} == 5
values.NUMERIC_ELEMENT_ONE_CODE == 5
values.NUMERIC_ELEMENT_ONE_CODE <= 5
values.NUMERIC_ELEMENT_ONE_CODE > 5 && values.NUMERIC_ELEMENT_ONE_CODE < 10
values.NUMERIC_ELEMENT_ONE_CODE <= 5 && values.NUMERIC_ELEMENT_TWO_CODE >= 5
String:
values.STRING_ELEMENT_ONE_CODE == "Yes"
values.STRING_ELEMENT_ONE_CODE == "No"
Boolean:
Boolean Activity Elements use the Activity Element's configured option labels:
values.BOOLEAN_ELEMENT_CODE == "Yes 👍"
values.BOOLEAN_ELEMENT_CODE == "No 👎"
HEALTH_MEASURE_REPORTED
Fires when a participant submits a Health Measure.
code == "HEALTH_MEASURE_REPORTED" && identifier == "1"
You can also check specific Activity Element responses:
code == "HEALTH_MEASURE_REPORTED" && identifier == "1" && anyof(values.ELEMENT_CODE, ["Bothered", "Miserable"])
For example:
code == "HEALTH_MEASURE_REPORTED" && identifier == "1" && anyof(values.HOW_ARE_YOU_FEELING_TODAY_1043, ["Bothered", "Miserable"])
With a boolean Activity Element:
code == "HEALTH_MEASURE_REPORTED" && identifier == "1" && values.ELEMENT_CODE == "Yes 👍"
Or with a numeric comparison:
code == "HEALTH_MEASURE_REPORTED" && identifier == "1" && values.ELEMENT_CODE >= 50
For CONTENT_READ and ACTIVITY_REPORTED, the identifier is the Card ID,
found in the URL when opening a Card from the Cards list (e.g.
https://example.web.app/#/activity_plan_templates/340).
For HEALTH_MEASURE_REPORTED, the identifier is the Activity ID, found in
the URL when opening an Activity from the Activities list (e.g.
https://example.web.app/#/activity_types/340).
The values.ELEMENT_CODE is the Activity Element code, found by clicking an
Activity Element in the Activity's Activity Elements column and copying its
CODE.
PARTICIPANT_TAG_ADDED
Fires when a tag is assigned to a participant.
code == "PARTICIPANT_TAG_ADDED" && tags.START_CAMPAIGN_JOURNEY == "true"
code == "PARTICIPANT_TAG_ADDED" && tags.{TAG_CODE} == "true"
Where {TAG_CODE} is the code of the tag, not the name of the tag.
PARTICIPANT_TAG_REMOVED
Fires when a tag is removed from a participant.
code == "PARTICIPANT_TAG_REMOVED" && values.TAG_CODE == "true"
PARTICIPANT_TAG_ADDED uses tags.{TAG_CODE} because the tag is now present on
the participant and available via the tags context. PARTICIPANT_TAG_REMOVED
uses values.{TAG_CODE} because the tag has already been removed from tags at
evaluation time — the handler explicitly sets it in values instead.
PARTICIPANT_GROUP_ADDED
Fires when a participant is added to a group. The syntax below can also be used
as a condition once another signal code has been interpreted (e.g. after
code == "ACTIVITY_REPORTED" && identifier == "1").
code == "PARTICIPANT_GROUP_ADDED" && groups.GROUP_ONE_CODE == "true"
Multiple groups can be checked together:
code == "PARTICIPANT_GROUP_ADDED" && groups.GROUP_ONE_CODE == "true" && groups.GROUP_TWO_CODE == "true"
PARTICIPANT_GROUP_REMOVED
This signal is defined but the handler is not currently registered. It does not currently fire.
Fires when a participant is removed from a group.
code == "PARTICIPANT_GROUP_REMOVED" && values.GROUP_CODE == "true"
PARTICIPANT_EXTRA_ADDED
This signal code is defined but does not currently fire. Use PARTICIPANT_EXTRA_UPDATED instead, which fires for both new and changed extras.
Fires when a participant extra is set.
String:
code == "PARTICIPANT_EXTRA_ADDED" && values.EXTRA_CODE == "test freetext string value"
JSON:
code == "PARTICIPANT_EXTRA_ADDED" && values.JSON_EXTRA_CODE == "value1 string value"
Numeric:
code == "PARTICIPANT_EXTRA_ADDED" && values.NUMERIC_EXTRA_CODE == 4
Boolean:
code == "PARTICIPANT_EXTRA_ADDED" && values.BOOLEAN_EXTRA_CODE == "true"
Time:
code == "PARTICIPANT_EXTRA_ADDED" && values.TIME_EXTRA_CODE == "2024-01-15T09:00:00Z"
PARTICIPANT_EXTRA_UPDATED
Fires when a participant extra is created or updated.
code == "PARTICIPANT_EXTRA_UPDATED" && values.group == "YOUR_EXTRA_GROUP" && values.EXTRA_CODE == "new value"
PARTICIPANT_EXTRA_REMOVED
This signal code is defined but does not currently fire.
Fires when a participant extra is removed.
code == "PARTICIPANT_EXTRA_REMOVED" && values.EXTRA_CODE == "true"
PARTICIPANT_JOURNEY_STARTED
Fires when a participant starts a Journey.
code == "PARTICIPANT_JOURNEY_STARTED"
PARTICIPANT_JOURNEY_COMPLETED
Fires when a participant completes a Journey.
code == "PARTICIPANT_JOURNEY_COMPLETED"
EXTERNAL_EVENT_RECEIVED
Fires when an external event is received for a participant.
code == "EXTERNAL_EVENT_RECEIVED" && value == "my_event_label"
MESSAGE_RESPONDED
Fires when a participant responds to a message.
code == "MESSAGE_RESPONDED" && identifier == "{threadId}"
To also check what the participant replied, use the values.RESPONSE_* key. The
key is built from the message's configured response option labels joined by _,
prefixed with RESPONSE_. For example, if the message had options Yes and
No, the key is RESPONSE_Yes_No:
code == "MESSAGE_RESPONDED" && identifier == "{threadId}" && values.RESPONSE_Yes_No == "Yes"
code == "MESSAGE_RESPONDED" && identifier == "{threadId}" && values.RESPONSE_Yes_No == "No"
The key always reflects the response options configured on the original message,
so if options were Agree and Disagree the key would be
RESPONSE_Agree_Disagree.
MESSAGE_STATUS
Fires when a message delivery status changes. Supported statuses: __SENT__,
__DELV__, __RCVD__, __BOUC__, __FAIL__.
code == "MESSAGE_STATUS" && value == "__SENT__"
code == "MESSAGE_STATUS" && value == "__DELV__"
Conditions
Conditions allow you to evaluate without being triggered a specifc signal/event.
You can evaluate tags, groups, or extras as conditions without listening for
their assignment event. Use them in the condition field or combine them with
another signal code using &&.
Tags:
tags.{tag_code} == "true"
tags.START_CAMPAIGN_CODE == "true"
To check that a tag is not present:
tags.{tag_code} == "false"
Groups:
groups.GROUP_ONE_CODE == "true"
To check that a participant is not in a group:
groups.GROUP_ONE_CODE == "false"
Extras (supports strings, integers, and booleans):
extras.PREFERRED_NEXT_ACTION == "Moving more"
extras.PARTICIPANT_AGE == 37
extras.PARTICIPANT_GRADUATED == "true"
A signal must begin with code ==, so when you only want to evaluate a
tag/group/extra value, place it in the condition field or append it with && to
an existing signal code expression.
The == "false" pattern for tags and groups only works reliably in the
condition field. When used in a signal rule expression, an absent tag or
group evaluates to empty string, not "false".
Operators
| Operator | Description |
|---|---|
&& | Logical AND |
|| | Logical OR |
== | Equals |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
!= | Not equal |
! | Negation |
To match multiple possible values, use anyof:
anyof(tags.TAG_NAME, ["A", "B"])
Functions
splitat
Splits a delimited value (using |) and returns the item at the given index as
a string. Useful for multi-part values like blood pressure readings.
splitat returns a string, so comparisons like < "120" are lexicographic
(string order), not numeric.
If the index is out of bounds the expression produces an evaluation error and
the rule will not match. It does not return false silently.
Given values.BLOOD_PRESSURE_SYS = "118|65":
splitat(values.BLOOD_PRESSURE_SYS, 0) // returns "118"
splitat(values.BLOOD_PRESSURE_SYS, 1) // returns "65"
splitat(values.BLOOD_PRESSURE_SYS, 2) // index out of bounds → evaluation error, rule does not match
Full examples:
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && splitat(values.BLOOD_PRESSURE_SYS, 0) < "120" && splitat(values.BLOOD_PRESSURE_SYS, 1) > "80"
output: false
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && splitat(values.BLOOD_PRESSURE_SYS, 0) < "120" && splitat(values.BLOOD_PRESSURE_SYS, 1) > "80"
output: true
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && splitat(values.BLOOD_PRESSURE_SYS, 0) < "120" && splitat(values.BLOOD_PRESSURE_SYS, 1) > "80" && splitat(values.BLOOD_PRESSURE_SYS, 2) > "100"
output: evaluation error // index out of bounds, rule does not match
anyof
Returns true if any of the specified values match. Works with
pipe-delimited multi-values.
The in keyword can also be used:
code == "ACTIVITY_REPORTED" && identifier == "113" && values.ELEMENT_A in ["A", "B", "C"]
output: false
Given values.ELEMENT_A = "D":
code == "ACTIVITY_REPORTED" && identifier == "113" && anyof(values.ELEMENT_A, ["A", "B", "C"])
output: false
Given values.ELEMENT_A = "C":
code == "ACTIVITY_REPORTED" && identifier == "113" && anyof(values.ELEMENT_A, ["A", "B", "C"])
output: true
Given values.ELEMENT_A = "A|D":
code == "ACTIVITY_REPORTED" && identifier == "113" && anyof(values.ELEMENT_A, ["A", "B", "C"])
output: true
Also works with tags - given tags.TAG_A = "D":
code == "CONTENT_READ" && identifier == "115" && anyof(tags.TAG_A, ["A", "B", "C"])
output: false
Given tags.TAG_A = "B":
code == "CONTENT_READ" && identifier == "115" && anyof(tags.TAG_A, ["A", "B", "C"])
output: true
noneof
Returns true if none of the specified values match. Works with
pipe-delimited multi-values.
Given values.ELEMENT_A = "A":
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && noneof(values.ELEMENT_A, ["A", "B", "C"])
output: false
Given values.ELEMENT_A = "D":
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && noneof(values.ELEMENT_A, ["A", "B", "C"])
output: true
Given values.ELEMENT_A = "A|E":
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && noneof(values.ELEMENT_A, ["A", "B", "C"])
output: false
Also works with tags - given tags.TAG_A = "B":
code == "CONTENT_READ" && identifier == "{activityId}" && noneof(tags.TAG_A, ["A", "B", "C"])
output: false
Given tags.TAG_A = "E":
code == "CONTENT_READ" && identifier == "{activityId}" && noneof(tags.TAG_A, ["A", "B", "C"])
output: true
allof
Returns true if all of the specified values are present. Works with
pipe-delimited multi-values.
Given values.ELEMENT_A = "A|B":
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && allof(values.ELEMENT_A, ["A", "B", "C"])
output: false
Given values.ELEMENT_A = "C|A|B":
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && allof(values.ELEMENT_A, ["A", "B", "C"])
output: true
Given values.ELEMENT_A = "B":
code == "ACTIVITY_REPORTED" && identifier == "{activityId}" && allof(values.ELEMENT_A, ["A", "B", "C"])
output: false
strlen
Returns the length of a string value. Works with Activity Element values and extras.
code == "ACTIVITY_REPORTED" && identifier == "1" && strlen(values.ELEMENT_CODE) > 10
code == "PARTICIPANT_EXTRA_UPDATED" && strlen(values.EXTRA_CODE) > 0
Onboarding Signals
The Onboarding Journey is a special Journey that starts automatically when a participant is onboarded. It uses these built-in signals:
| Signal | Fires when |
|---|---|
PARTICIPANT_CREATED | A coach creates a new participant |
PARTICIPANT_SIGNED_UP | A participant registers via the app |
PARTICIPANT_CONSENTED | A participant consents to use the app |
Coach-Added Participant
- Coach adds participant details
- Email sent with a link to create their password (if email address provided)
- Wait for consent
- On consent, trigger a sub-journey (e.g. Welcome Journey)
Self-Registered Participant
- Participant creates profile with phone number
- Acknowledges consent
- Verifies account via SMS code
- On verification, a sub-journey (e.g. Welcome Journey) is auto-triggered