Skip to content

Rule Evaluation

Rules are the core mechanism for creating branching, conditional flows in Birdhouse. Every time a user submits an answer, the rule engine evaluates whether the flow should jump to a different question instead of following the default order.

A rule is a simple structure with three parts:

Part Purpose Example
id The questionId this rule applies to dateOfBirth
if A condition to evaluate ['dateOfBirth', 'lessThan', '18', 'year', 'dateDiff']
then Where to go if the condition is true { goTo: 'ageBlocker' }
else (optional) Where to go if the condition is false { goTo: 'coverageStartDate' }
flowchart TD
    A["User submits answer<br/>for question X"] --> B["Find ALL rules where<br/>id matches question X"]
    B --> C{"Any rules found?"}
    C -->|No| D["Go to next question<br/>in default order"]
    C -->|Yes| E["Check first rule's<br/><b>if</b> condition"]
    E -->|True| F["Navigate to<br/><b>then.goTo</b>"]
    E -->|False| G{"Does rule have<br/><b>else</b>?"}
    G -->|Yes| H["Navigate to<br/><b>else.goTo</b>"]
    G -->|No| I["Check next rule<br/>for question X"]
    I --> J{"More rules?"}
    J -->|Yes| E
    J -->|No| D

Key points:

  • All rules for the current question are collected
  • They are checked in order (the order they appear in Strapi)
  • The first rule whose if condition returns true wins — its then.goTo is used
  • If a rule’s condition is false and it has an else, the else.goTo is used immediately

Here is why. Suppose you have two rules for question employmentStatus:

  1. Rule A: if employed, go to incomeelse go to coverageStartDate
  2. Rule B: if self-employed, go to businessType

What happens when the user selects “self-employed”?

  • Rule A’s condition (“employed”) is false
  • Rule A has an else clause, so it immediately navigates to coverageStartDate
  • Rule B never runs

The else on Rule A acts as a catch-all that swallows all non-matching cases. To fix this, remove the else from Rule A and add a third rule at the end that handles the default case:

  1. Rule A: if employed, go to income
  2. Rule B: if self-employed, go to businessType
  3. Rule C: if always, go to coverageStartDate
Operator What it checks Example
equals Answer exactly matches a value ['plan', 'equals', 'PREMIUM']
notEquals Answer does not match ['plan', 'notEquals', 'BASIC']
isIn Answer is one of several values ['region', 'isIn', ['DE', 'AT']]
isNotIn Answer is not any of the values ['region', 'isNotIn', ['US', 'UK']]
greaterThan Numeric comparison ['income', 'greaterThan', 60000]
lessThan Numeric comparison ['age', 'lessThan', 18]
Operator What it checks Example
same Two answers are the same value ['mailingAddress', 'same', 'homeAddress']
notSame Two answers differ ['mailingAddress', 'notSame', 'homeAddress']
Operator What it checks Example
isDefined Answer exists (has been provided) ['email', 'isDefined']
isNotDefined Answer has not been provided ['phone', 'isNotDefined']
contains Answer includes a substring or value ['selectedAddons', 'contains', 'GLASS']
notContains Answer does not include a value ['selectedAddons', 'notContains', 'GLASS']
Operator What it checks Example
always Always true — unconditional jump ['_', 'always']
dateDiff Compares a date answer to now, with granularity ['dateOfBirth', 'lessThan', '18', 'year', 'dateDiff']
Country-specific Checks country fields in address answers Specialized for address validation
Address operators Checks specific fields within address-type answers Street, city, postal code checks

dateDiff is used frequently for age-based logic. It calculates the difference between a date answer and the current date, then compares the result.

Format: [questionId, comparison, value, granularity, 'dateDiff']

Parameter Options
comparison lessThan, greaterThan, equals
granularity year, month, day

Example: “Is the user under 18 years old?”

['dateOfBirth', 'lessThan', '18', 'year', 'dateDiff']

Simple conditions check a single answer, but you often need to combine multiple checks. Use grouped conditions for this:

['AND', [
['plan', 'equals', 'PREMIUM'],
['region', 'equals', 'DE']
]]

This means: the user selected the PREMIUM plan and they are in Germany.

OR — At least one condition must be true

Section titled “OR — At least one condition must be true”
['OR', [
['employmentStatus', 'equals', 'EMPLOYED'],
['employmentStatus', 'equals', 'SELF_EMPLOYED']
]]

This means: the user is either employed or self-employed.

You can nest groups inside groups:

['AND', [
['region', 'equals', 'DE'],
['OR', [
['plan', 'equals', 'PREMIUM'],
['plan', 'equals', 'COMFORT']
]]
]]

This means: the user is in Germany and selected either PREMIUM or COMFORT.

For conditions that cannot be expressed with the built-in operators, rules can use a custom JavaScript function:

(answer, answers, questionId) => boolean
Parameter Contains
answer The current question’s answer
answers All answers collected so far
questionId The current question’s ID

Custom functions are defined in the app code (not in Strapi) and are used for complex business logic that goes beyond simple comparisons.

  1. Order matters: Put the most specific rules first, general/fallback rules last
  2. Avoid else with multiple rules: Use always as a catch-all in the last rule instead
  3. Test all branches: When adding rules, manually walk through each possible answer to verify the flow
  4. Use AND/OR sparingly: Deep nesting makes rules hard to understand — consider splitting into multiple rules if possible
  5. dateDiff for ages: Always use dateDiff rather than comparing raw dates — it handles edge cases correctly