# Logic rules

> How to show, hide, require or skip questions in a trulyform form based on earlier answers: the rule format, how rules run, operators and values.

## What you need to know

- A rule changes one question, its target, based on answers to questions that come before it.
- There are four actions: `show`, `hide`, `require` and `skip`.
- A rule only runs after its source question has been answered. Questions the respondent never saw count as unanswered.
- A question that has a `show` rule stays hidden until one of its show rules matches.
- Conditions compare option IDs for choice questions, never their labels.
- Logic is available on every plan.

## Rule format

Rules live in the definition's `rules` list:

| Key | Notes |
| --- | --- |
| `id` | A UUIDv7, unique within the form. |
| `sourceId` | The question whose answer the rule waits for. For `skip`, the jump happens right after this question. |
| `targetId` | The question the rule acts on. It must come after the source. |
| `mode` | `all`: every condition must match. `any`: one matching condition is enough. |
| `action` | `show`, `hide`, `require` or `skip`. |
| `conditions` | Up to 20 conditions, each `{fieldId, operator, value}`. `fieldId` is the source question or one before it. |

Most rules check the source question itself, so `sourceId` and each condition's `fieldId` are the same. They differ when a rule waits for one question and then also checks earlier answers, such as "after the budget question, if the budget is over 5000 and the project type is Website".

A form can have up to 300 rules.

## What each action does

- **`show`:** the target appears only when the rule matches.
- **`hide`:** the target is hidden when the rule matches.
- **`require`:** the target becomes required when the rule matches. No rule makes a shown question optional, so for "required only when…" leave `required` false and add a `require` rule.
- **`skip`:** after the respondent answers the source question, the form jumps to the target and skips every question in between. It only jumps forward. To end a form early, hide the remaining questions instead.

A `require` rule cannot target a statement, because statements collect no answer.

## How rules run

trulyform goes through the questions in order:

1. For each question, it checks the `show` and `hide` rules that target it, in the order they appear in `rules`. The first one that matches decides whether the question is shown or hidden.
2. If none match, the question is hidden when at least one `show` rule targets it, and shown otherwise.
3. A shown question is required when its `required` is true or a `require` rule targeting it matches.
4. After a question is answered, the first matching `skip` rule whose source is that question jumps forward.

A rule never matches while its source question has no answer, whatever its conditions, so it cannot react to the source being left blank. Once the source has an answer, a condition on a question that is unanswered, hidden or skipped sees no answer: only `is_empty` matches it.

A hidden or skipped question is never required, even when its `required` is true. A follow-up shown by a `show` rule can therefore be `required: true`: it is enforced only when it appears.

Rules always run on the current answers. When a respondent goes back and changes an answer, the questions that are shown, hidden and required change with it.

## Conditions

| Operator | Meaning |
| --- | --- |
| `eq` | Equals the value. Text comparisons ignore letter case and surrounding spaces. |
| `neq` | Does not equal the value. |
| `gt` | Greater than the value. Numbers compare as decimals; dates compare chronologically. |
| `gte` | Greater than or equal to the value. |
| `lt` | Less than the value. |
| `lte` | Less than or equal to the value. |
| `contains` | Text: contains the value. Multiple choice and ranking: the answer includes this option ID. |
| `not_contains` | The opposite of contains. |
| `has_value` | The question has an answer. Takes no value. |
| `is_empty` | The question has no answer, including when it was skipped or hidden. Takes no value. |
| `any_of` | The answer includes at least one of the listed option IDs. The value is a list. |

Comparison values by question type:

| Kind | Operators | Comparison value |
| --- | --- | --- |
| `short_text` | `eq`, `neq`, `has_value`, `is_empty`, `contains`, `not_contains` | A string, such as "yes". |
| `long_text` | `eq`, `neq`, `has_value`, `is_empty`, `contains`, `not_contains` | A string, such as "yes". |
| `email` | `eq`, `neq`, `has_value`, `is_empty`, `contains`, `not_contains` | A string, such as "yes". |
| `phone` | `eq`, `neq`, `has_value`, `is_empty`, `contains`, `not_contains` | A string, such as "yes". |
| `number` | `eq`, `neq`, `has_value`, `is_empty`, `gt`, `gte`, `lt`, `lte` | A number or decimal string, such as "7" or "2.5". |
| `date` | `eq`, `neq`, `has_value`, `is_empty`, `gt`, `gte`, `lt`, `lte` | A date as "YYYY-MM-DD". |
| `single_choice` | `eq`, `neq`, `has_value`, `is_empty` | An option ID from this question. |
| `dropdown` | `eq`, `neq`, `has_value`, `is_empty` | An option ID from this question. |
| `multiple_choice` | `contains`, `not_contains`, `any_of`, `has_value`, `is_empty` | An option ID from this question. any_of takes a list of option IDs. |
| `consent` | `eq`, `neq`, `has_value`, `is_empty` | true or false. |
| `nps` | `eq`, `neq`, `has_value`, `is_empty`, `gt`, `gte`, `lt`, `lte` | A number or decimal string, such as "7" or "2.5". |
| `opinion_scale` | `eq`, `neq`, `has_value`, `is_empty`, `gt`, `gte`, `lt`, `lte` | A number or decimal string, such as "7" or "2.5". |
| `ranking` | `contains`, `not_contains`, `any_of`, `has_value`, `is_empty` | An option ID from this question. any_of takes a list of option IDs. |
| `matrix` | `has_value`, `is_empty` | Only has_value and is_empty. |
| `file_upload` | `has_value`, `is_empty` | Only has_value and is_empty. |
| `statement` | None | Statements cannot be used in conditions. |
| `website` | `eq`, `neq`, `has_value`, `is_empty`, `contains`, `not_contains` | A string, such as "yes". |

- Text comparisons ignore letter case and the spaces around the answer. Website answers compare the saved address exactly, such as "https://example.com".
- Choice values are option IDs. For `any_of`, the value is a list of option IDs.
- NPS and opinion scale questions with a comment box compare the score.
- A ranking answer always contains every option, so `contains` on a ranking is always true.

## Tips

- **Ask a follow-up only when it applies:** `show` the follow-up when the earlier answer matches. Use `skip` instead to jump past several questions at once.
- **"Other, please specify":** add an Other option, then a short text question right after it with a `show` rule on that option.
- **Different text for different people:** a form has one ending, so pipe answers into it, or add a statement just before the end and `show` it only to some respondents.
- **Several triggers for one question:** use `mode: "any"` with several conditions, or add several `show` rules.

## Example

The [workshop registration](https://trulyform.com/docs/examples.md#workshop-registration) asks which ticket the person wants and uses that answer three times:

```json title="Workshop registration: rules"
[
    {
        "id": "01990000-0000-7000-8000-000000000121",
        "sourceId": "01990000-0000-7000-8000-000000000103",
        "targetId": "01990000-0000-7000-8000-000000000105",
        "mode": "all",
        "action": "hide",
        "conditions": [{"fieldId":"01990000-0000-7000-8000-000000000103","operator":"eq","value":"01990000-0000-7000-8000-000000000112"}]
    },
    {
        "id": "01990000-0000-7000-8000-000000000122",
        "sourceId": "01990000-0000-7000-8000-000000000103",
        "targetId": "01990000-0000-7000-8000-000000000106",
        "mode": "all",
        "action": "show",
        "conditions": [{"fieldId":"01990000-0000-7000-8000-000000000103","operator":"eq","value":"01990000-0000-7000-8000-000000000111"}]
    },
    {
        "id": "01990000-0000-7000-8000-000000000123",
        "sourceId": "01990000-0000-7000-8000-000000000103",
        "targetId": "01990000-0000-7000-8000-000000000107",
        "mode": "all",
        "action": "require",
        "conditions": [{"fieldId":"01990000-0000-7000-8000-000000000103","operator":"eq","value":"01990000-0000-7000-8000-000000000111"}]
    }
]
```

1. The first rule hides the session date question for online tickets.
2. The second shows the dietary needs question only for in-person tickets.
3. The third makes the phone number required for in-person tickets.

The [job application](https://trulyform.com/docs/examples.md#job-application) uses a `skip` rule: people who have never worked on a remote team jump past the follow-up question about it.


---

Previous: [Question types](https://trulyform.com/docs/question-types.md) · Next: [Calculations and piping](https://trulyform.com/docs/calculations-and-piping.md)

All pages: https://trulyform.com/llms.txt. Connected over MCP? Call read_docs with a page name instead.
