Skip to main content

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:

FieldDescription
codeThe event type being evaluated
identifierThe ID of the resource being evaluated (Card ID for CONTENT_READ and ACTIVITY_REPORTED, Activity ID for HEALTH_MEASURE_REPORTED)
valueA single string value (used by some internal signals)
valuesResponse values from an Activity or Health Measure
scoresAggregate score from a Scored Activity
tagsThe participant's current Tags
groupsThe participant's current Groups
extrasThe participant's current Extras
eventsHistorical event memory for advanced journey logic
varsSession variables from journey memory
info

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

info

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
Finding IDs and Codes

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"
info

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

warning

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

warning

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

warning

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"
info

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

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

warning

splitat returns a string, so comparisons like < "120" are lexicographic (string order), not numeric.

warning

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:

SignalFires when
PARTICIPANT_CREATEDA coach creates a new participant
PARTICIPANT_SIGNED_UPA participant registers via the app
PARTICIPANT_CONSENTEDA participant consents to use the app

Coach-Added Participant

  1. Coach adds participant details
  2. Email sent with a link to create their password (if email address provided)
  3. Wait for consent
  4. On consent, trigger a sub-journey (e.g. Welcome Journey)

Self-Registered Participant

  1. Participant creates profile with phone number
  2. Acknowledges consent
  3. Verifies account via SMS code
  4. On verification, a sub-journey (e.g. Welcome Journey) is auto-triggered