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.
How Rules Work
Section titled “How Rules Work”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' } |
Evaluation Flow
Section titled “Evaluation Flow”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
ifcondition returns true wins — itsthen.goTois used - If a rule’s condition is false and it has an
else, theelse.goTois used immediately
The “Else” Pitfall
Section titled “The “Else” Pitfall”Here is why. Suppose you have two rules for question employmentStatus:
- Rule A: if employed, go to
income— else go tocoverageStartDate - 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
elseclause, so it immediately navigates tocoverageStartDate - 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:
- Rule A: if employed, go to
income - Rule B: if self-employed, go to
businessType - Rule C: if
always, go tocoverageStartDate
Condition Operators
Section titled “Condition Operators”Comparison Operators
Section titled “Comparison Operators”| 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] |
Equality Operators
Section titled “Equality Operators”| Operator | What it checks | Example |
|---|---|---|
same |
Two answers are the same value | ['mailingAddress', 'same', 'homeAddress'] |
notSame |
Two answers differ | ['mailingAddress', 'notSame', 'homeAddress'] |
Presence Operators
Section titled “Presence Operators”| 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'] |
Special Operators
Section titled “Special Operators”| 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 |
The dateDiff Operator
Section titled “The dateDiff Operator”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']Grouped Conditions (AND / OR)
Section titled “Grouped Conditions (AND / OR)”Simple conditions check a single answer, but you often need to combine multiple checks. Use grouped conditions for this:
AND — All conditions must be true
Section titled “AND — All conditions must be true”['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.
Nesting
Section titled “Nesting”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.
Custom Function Conditions
Section titled “Custom Function Conditions”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.
Practical Tips
Section titled “Practical Tips”- Order matters: Put the most specific rules first, general/fallback rules last
- Avoid else with multiple rules: Use
alwaysas a catch-all in the last rule instead - Test all branches: When adding rules, manually walk through each possible answer to verify the flow
- Use AND/OR sparingly: Deep nesting makes rules hard to understand — consider splitting into multiple rules if possible
- dateDiff for ages: Always use
dateDiffrather than comparing raw dates — it handles edge cases correctly
See also
Section titled “See also”- Rule Operators Reference – complete specification of all operators, variable types, and rule structures
- Configure Rules – step-by-step guide for adding rules in Strapi
- Adding Conditional Rules – hands-on tutorial with simple and grouped rules