Logic settings
Just like in the Riddle creator, you can set up custom logic flows for your Riddles using the Builder API.
Custom logic flows are available for the following Riddle types via API:
Personality tests, predictors, minigames and leaderboards do not support custom logic - sending a logic property for one of them is rejected with Logic configuration is not allowed for Riddle type "<type>".
You can branch by answers (typically on SingleChoice, MultipleChoice, and standalone Form Dropdown fields), by a condition on a block's answer or a form field's value, or by block/total score. The following will be added soon:
- Branching by a data layer variable
Any other suggestions? Please get in touch via support chat or send an email to hello@riddle.com.
Limits and constraints
- Block count: custom logic is only accepted for Riddles with up to 250 blocks (building a logic tree is recursive, so the cost grows super-linearly). Riddles above that limit have to use the default linear logic. The overall build limit of 500 blocks still applies on top of that.
- Pagination: custom logic and pagination are mutually exclusive. Sending
logictogether withpreset.pagination.isEnabled: trueis rejected - a paginated Riddle shows all blocks on one page, which has no meaningful branching. - Flashcards: a Quiz that contains a
Flashcardblock only accepts linear logic (seeRIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_LINEAR_ONLY_ALLOWEDbelow). - Story: a Story Riddle only accepts linear logic - there is nothing to branch on (no answers, scores, or form fields), so
answer,condition, andscorebranching are all rejected. - Every block has to be used: every block in the
blocksarray must be reachable somewhere in the tree, otherwise the build is rejected (seeRIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_UNUSED_BLOCKS).
Default logic
When you create a Riddle via the Builder API without specifying any logic settings, the Riddle will have a default linear logic flow. This means that the blocks will be shown in the exact order they are defined in the blocks array of the build configuration.
Prerequisites
To be able to work with the logic settings efficiently, it is advised to read about assigning custom IDs to blocks and answer items first. This makes it easier to reference the blocks and answer items in the following logic settings.
Logic settings structure / properties
The logic settings are defined in the logic property in the build configuration, next to the blocks and result/results properties:
{
"type": "Quiz",
"build": {
"title": "My quiz with logic",
"blocks": [
"...",
"...",
"..."
],
"results": [
"..."
],
"logic": {
"...logic tree structure..."
}
}
}
The logic settings structure can be compared to a "tree" where the root of the tree is the start block, and each branch represents a possible path through the Riddle based on user interactions. This means it can nest indefinitely, as long as there are available blocks in the build configuration.
You can use four types of branching:
Linear 'branching'
| Property | Required | Type | Description | Default |
|---|---|---|---|---|
blockId | ✓ | integer | The ID of the block where this logic layer starts. Can reference any block, for example even Content | |
branchingType | string | The type of branching to use. For linear branching it is set to linear, but can also be omitted due to being the default value | linear | |
next | object | An object defining the next block to show after the block defined in blockId has been completed. Send null (or leave it out) to mark this layer as the end of its path | null |
blockId must be a positive integer that exists in the blocks array; branchingType is matched case-insensitively (Answer works just as well as answer).
Example: Sequence of 4 linear blocks
{
"blockId": 1,
"next": {
"blockId": 2,
"next": {
"blockId": 3,
"next": {
"blockId": 4
}
}
}
}
In spoken form: Show block ID 1, then block ID 2, then block ID 3, then block ID 4 - and end the Riddle there.
Answer branching
| Property | Required | Type | Description | Default |
|---|---|---|---|---|
blockId | ✓ | integer | The ID of the block where this logic layer starts. Can only reference blocks which offer selectable answers to branch on; see below | |
branchingType | ✓ | string | The type of branching to use. For answer branching, it is set to answer | linear |
rules | ✓ | object | An array of at least one rule object defining the branching logic. To see the structure of each rule, see below |
Which blocks can be branched on
The block referenced by blockId has to expose selectable answer items - the API validates your answers against exactly those items:
- Question blocks with answer items work, e.g.
SingleChoiceandMultipleChoice. - Form fields: only a standalone
Dropdownfield can be branched on. Any other form field - including a wholeFormBuilderblock - is rejected withAnswer logic branching is currently only allowed with standalone Dropdown form fields (Found <type>). - Blocks that carry no answer items at all (e.g. a
Contentblock) are rejected withAnswer logic block must refer to a question block with choices/answers, found "<type>" block.Such blocks can still be used in linear layers.
Defining rules
Note: Each rule itself is also either a linear or answer branching, meaning it can contain a next OR rules property to define what happens after the block defined in blockId has been completed.
Properties exclusive to the rule object:
| Property | Required | Type | Description | Default |
|---|---|---|---|---|
answers | ✓ | string|int | A non-empty array of answers that trigger this rule. You can use either the answer text or the given answer ID to identify the answer |
Rules for answers:
- Answer texts are matched exactly (no case-insensitive or trimmed matching) - if the value matches neither a title nor an ID, the error lists all valid choices of that block.
- Every answer may only be used once per logic layer: repeating the same answer in another rule of the same layer, or twice within the same rule, is rejected as a non-unique rule.
- Not every answer of a block needs its own rule, but every rule needs at least one answer.
Example: Answer branching with linear next
{
"answers": ["Answer 1", "Answer 2"],
"blockId": 2,
"next": {
"blockId": 3
}
}
In spoken form: If the user selects "Answer 1" or "Answer 2" show block with ID 2, then after completing that block, show block with ID 3.
Example: Answer branching, followed by answer branching / rules
{
"answers": ["Answer 1", "Answer 2"],
"blockId": 2,
"branchingType": "answer",
"rules": [
"..."
]
}
In spoken form: If the user selects "Answer 1" or "Answer 2" show block with ID 2, then after completing that block, follow the rules defined in the rules array (and so on).
Example: First linear branching, then answer branching, then linear branching
{
"blockId": 1,
"next": {
"blockId": 2,
"branchingType": "answer",
"rules": [
{
"answers": ["Answer 1"],
"blockId": 3,
"next": {
"blockId": 4
}
},
{
"answers": ["Answer 2"],
"blockId": 5
}
]
}
}
In spoken form: Start with block ID 1, then show block ID 2. If the user selects "Answer 1", show block ID 3, then block ID 4. If the user selects "Answer 2", show block ID 5.
Condition branching
| Property | Required | Type | Description | Default |
|---|---|---|---|---|
blockId | ✓ | integer | The ID of the block this condition evaluates. Can reference a question block's answer, or a form field's value (including a field nested inside a FormBuilder block) | |
branchingType | ✓ | string | The type of branching to use. For condition branching, it is set to condition | linear |
condition | ✓ | object | An object with field (the answer/value to compare, e.g. an answer's title or ID, or use "score" to compare a question block's own score), operator (see below), and, unless the operator is a standalone check like isAnswered, value (the value to compare against) | |
ifTrue | object | The next logic layer to follow if the condition evaluates to true. Same rules as next/rules - it can itself be linear, answer, condition, or score branching | null | |
ifFalse | object | The next logic layer to follow if the condition evaluates to false | null |
Operators
The allowed operator values depend on the kind of field blockId points at:
| Field kind | Allowed operators |
|---|---|
| Question block answer | equals, notEquals, isAnswered, isNotAnswered |
Question block score (field: "score") | equals, notEquals, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual |
| Form text field | equals, notEquals, contains, notContains, isAnswered, isNotAnswered |
| Form number field | equals, notEquals, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, isAnswered, isNotAnswered |
| Form select/dropdown/radio field | equals, notEquals, isAnswered, isNotAnswered |
| Form checkbox field | isChecked, isNotChecked |
The value beside the operator has to fit it as well, and a mismatch is refused rather than stored: the standalone checks (isAnswered, isChecked, ...) take no value at all, the text comparisons take a single string or number, the numeric comparisons take a single number, and between / notBetween take exactly two numbers, [min, max], with min no greater than max.
A condition here addresses a block or a form field only - never the Riddle's result (magic.result.title, magic.personality.title, magic.score.percentage, ...), since the logic tree is what produces that result in the first place. Those result fields exist on exactly one surface: the conditional automation emails, see Publish settings.
Example: Condition branching on a SingleChoice answer
{
"blockId": 1,
"branchingType": "condition",
"condition": {
"field": "Berlin",
"operator": "equals",
"value": true
},
"ifTrue": {
"blockId": 2
},
"ifFalse": {
"blockId": 3
}
}
In spoken form: Show block ID 1. If the user picked "Berlin" there, continue with block ID 2, otherwise continue with block ID 3.
Referencing a form field nested inside a FormBuilder block: use "<blockId>_<fieldId>" as the condition's blockId is still the FormBuilder block itself, while the field being evaluated is identified this way in the underlying field/value targeting - see custom IDs for how field IDs are assigned.
Score branching
| Property | Required | Type | Description | Default |
|---|---|---|---|---|
blockId | ✓ | integer | The ID of the block this score is evaluated for. For scoreType: "block" this is the block whose own score is checked; for scoreType: "total" it can be any block, since the running total is riddle-wide | |
branchingType | ✓ | string | The type of branching to use. For score branching, it is set to score | linear |
scoreType | ✓ | string | total branches on the cumulative score across every block answered so far (Quiz only); block branches on the score of this node's own block only | |
ranges | ✓ | object | An array of at least one range object mapping a [min, max] score interval to a next block. Ranges must not overlap and each range's min must be less than or equal to its max |
Each range object has min (integer), max (integer), blockId (the block to show for this range) and, like an answer rule, an optional next/rules/ifTrue+ifFalse/ranges describing what comes after.
Example: Score branching on the running total (Quiz only)
{
"blockId": 1,
"branchingType": "score",
"scoreType": "total",
"ranges": [
{
"min": 0,
"max": 50,
"blockId": 2
},
{
"min": 51,
"max": 100,
"blockId": 3
}
]
}
In spoken form: Show block ID 1. If the total score so far is 0-50 points, continue with block ID 2; if it is 51-100 points, continue with block ID 3.
scoreType: "total" is only available on a Quiz - Poll and Form have no running score to branch on.
Pitfalls
Building the logic settings can be tricky. Here are some tips to avoid common pitfalls:
- Result blocks are not part of the logic tree in the API. Instead, they will always be shown at the end of the Riddle logic tree, regardless of the path taken. An end of the logic tree can be defined by adding a logic layer with no
nextproperty or with thenextproperty set to null. - In a Poll or Form, every end of a path automatically leads to the first result page. In a Quiz, the result is picked by the
minPercentage/maxPercentageranges of your result pages instead, no matter which path the visitor took. - The same block may be referenced from several different branches - that is how two rules can converge on the same follow-up question (see the full example below, where both rules end on block 3). What is not allowed is referencing a block again below itself on the same path, which would create an infinite loop.
- Make sure to use custom IDs for blocks and answers to make it easier to reference them in the logic settings.
Reading the logic back
When you read a Riddle back as a build configuration, the logic property behaves as follows:
- A Riddle with the default linear logic does not return a
logicproperty at all - leaving it out rebuilds exactly the same default. - Custom logic built through the API is returned in the same shape you sent (linear, answer, condition, or score - and any nested combination of them), and can be sent back unchanged.
answersare always returned as answer IDs, never as answer texts. - Logic that was configured in the Creator but has no equivalent in a builder config - a condition builder rule targeting a data layer variable or magic variable, or logic referencing question bank / embedded form select blocks - cannot be expressed. In that case the whole
logicproperty is reported as unsupported instead of being returned partially, because a partial tree could not be rebuilt into the same Riddle.
Changing the logic of an existing Riddle
When editing a Riddle, logic is replaced as a whole if you send it and kept as it is if you omit it - there is no per-node merge. Two edit-only rules apply:
- Resetting:
"logic": {"$reset": true}discards the custom logic and falls back to the default linear flow, regenerated from the current block order. It is exclusive - combining$resetwith any other key insidelogicis rejected, and it is only accepted when editing (never in aPOST /riddle-builderrequest). - Deleting a referenced block: deleting a block that the custom logic still points at is rejected unless the same request either supplies a replacement tree or resets the logic. See
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_DELETED_BLOCK_STILL_REFERENCEDbelow.
A Riddle whose logic was reset returns no logic property when read back, exactly like any other Riddle on the default flow.
Troubleshooting / Error codes
The logic settings throws unique error codes which can only occur in this context.
Not sure how exception handling works? Check out the basic API exception handling.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_CIRCULAR_REFERENCE
A circular reference was detected in the logic settings. This means that a block is referenced again going 'up' the logic tree, which would create an infinite loop.
Example faulty logic settings:
{
"blockId": 1,
"next": {
"blockId": 2,
"next": {
"blockId": 1
}
}
}
In spoken form: Show block ID 1, then block ID 2, then block ID 1 again - which sends the visitor around the same two blocks forever.
The error message names the offending node and spells out the loop, so you do not have to walk your own configuration to find it:
Circular reference detected: logic (block ID 1) points back at block ID 1, which already appears earlier on this branch (1 -> 2 -> 1). Point it at another block, or end the branch instead.
The node is named the same way every other logic error names it - logic (block ID 1), plus its branchingType when the node states one, e.g. logic (block ID 1, branchingType: "answer"). The path in brackets reads in the direction you wrote it, from the block that first appears on the branch to the reference that closes the loop.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_INVALID_RULE
The answers provided in an answer branching rule object is either empty or references answers that do not exist in the corresponding block's items array.
Example faulty logic settings:
{
"blockId": 1,
"branchingType": "answer",
"rules": [
{
"answers": [], // empty answers array
"blockId": 2
},
{
"answers": ["Non-existing answer"], // answer does not exist in block's items
"blockId": 3
}
]
}
In spoken form: the first rule says "if the user selects nothing in particular", and the second one branches on an answer block ID 1 does not offer - neither can ever trigger.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_NON_UNIQUE_RULES
The answers provided in an answer branching rule object are not unique. This means that the same answer is referenced in multiple rules of the same logic layer.
Example faulty logic settings:
{
"blockId": 1,
"branchingType": "answer",
"rules": [
{
"answers": ["Answer 1", "Answer 2"],
"blockId": 2
},
{
"answers": ["Answer 2", "Answer 3"], // "Answer 2" is referenced twice
"blockId": 3
}
]
}
In spoken form: "Answer 2" would send the visitor to block ID 2 and to block ID 3 at the same time.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_NON_EXISTING_BLOCK
A blockId in the logic settings references a block that does not exist in the blocks array of the build configuration.
Example faulty logic settings:
{
"blockId": 999, // block with ID 999 does not exist in blocks array
"next": {
"blockId": 2
}
}
In spoken form: Start with a block that the Riddle does not contain, then show block ID 2.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_UNUSED_BLOCKS
Some blocks defined in the blocks array of the build configuration are not referenced in the logic settings. This means that there is no possible way for a user to reach these blocks when taking the Riddle. In the Riddle Creator this is indicated by a warning icon in the publish step, in the Builder API this results in an error to avoid building Riddles with unnecessary/unused blocks. The message lists the block IDs concerned: "The built logic tree does not use all Riddle blocks. Unused block IDs: 4, 5".
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_LINEAR_ONLY_ALLOWED
This can happen in two scenarios: you are trying to use non-linear (answer/condition/score) branching in a Quiz that contains Flashcard blocks - disallowed because Flashcards have complex logic themselves which does not work with non-linear logic - or you are trying to use non-linear branching on a Story Riddle, which has nothing to branch on.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_DEAD_ENDS
The built logic tree contains a dead end: a path that neither ends the flow nor leads anywhere. The message names the block it stops at, so you can go straight to that node:
The built logic tree contains a dead end at block ID 3: it neither ends the flow nor leads anywhere. Please check your logic configuration for that block.
Give that node a next - or, on a branching node, a target for every branch it can take.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_INVALID_NODE
A logic node is invalid. This can happen if the node is missing a blockId, if the blockId is not a positive integer, or if the branchingType is not a recognized value.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_CONDITION_INVALID_MAPPABLE
A condition branching node's condition.field does not refer to a usable value - for example it references a form field that doesn't exist on the blockId block, or a question block that carries no answer items/score to condition on.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_CONDITION_OPERATOR_NOT_ALLOWED
A condition branching node's condition.operator is not one of the operators allowed for the kind of field condition.field refers to. See the operator table under Condition branching above.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_SCORE_RANGE_OVERLAP
A score branching node's ranges contains two ranges that overlap, or a range whose min is greater than its max.
Example faulty logic settings:
{
"blockId": 1,
"branchingType": "score",
"scoreType": "total",
"ranges": [
{ "min": 0, "max": 50, "blockId": 2 },
{ "min": 40, "max": 100, "blockId": 3 }
]
}
In spoken form: a score of 45 points would match both ranges at once, so there is no telling whether block ID 2 or block ID 3 comes next.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_DELETED_BLOCK_STILL_REFERENCED
Only occurs when editing a Riddle: the request deletes a block ("$delete": true) that the Riddle's custom logic still references, and does not resolve that in the same request. The error names which deleted block is still referenced by which logic node.
Fix it by sending, in the same request, either a full replacement logic tree that no longer references the block, or "logic": {"$reset": true}. This never happens while the Riddle still uses the default linear logic - that logic is regenerated from the current block order on every build.
RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_RESET_CONFLICT
"logic": {"$reset": true} was combined with other logic configuration in the same object. Either reset the logic or supply a full replacement tree, not both.
Answer branching step by step
Riddle build configuration (without logic settings in first step):
{
"type": "Quiz",
"build": {
"title": "Logic example (Builder API)",
"blocks": [
{
"id": 1,
"title": "What's the capital of Germany?",
"type": "SingleChoice",
"items": [
{ "title": "Berlin", "isCorrect": true },
{ "title": "Lissabon", "isCorrect": false },
{ "title": "Leipzig", "isCorrect": false }
]
},
{
"id": 2,
"title": "What are valid colors in German?",
"type": "MultipleChoice",
"items": [
{ "title": "rot", "isCorrect": true },
{ "title": "schwarz", "isCorrect": true },
{ "title": "nero", "isCorrect": false }
]
},
{
"id": 3,
"title": "Order the following colors from lightest to darkest.",
"type": "Order",
"items": [
{ "title": "red" },
{ "title": "blue" },
{ "title": "green" }
]
}
],
"results": [
{
"title": "Thanks for taking the quiz!",
"minPercentage": 0,
"maxPercentage": 100
}
]
}
}
The quiz above consists of three question blocks and one result block. For this example we want to branch based on the "What's the capital of Germany?" question. If the user answers "Berlin", we want to show the second question ("What are valid colors in German?"). If the user answers "Lissabon" or "Leipzig", we want to skip the second question and go directly to the third question ("Order the following colors from lightest to darkest.").
The logic tree / settings then look like this:
{
"blockId": 1,
"branchingType": "answer",
"rules": [
{
"answers": ["Berlin"],
"blockId": 2,
"next": {
"blockId": 3
}
},
{
"answers": ["Lissabon", "Leipzig"],
"blockId": 3
}
]
}
In spoken form: Show block ID 1 ("What's the capital of Germany?"). If the user answers "Berlin", show block ID 2 and then block ID 3. If the user answers "Lissabon" or "Leipzig", skip straight to block ID 3 and end there.
Note: Result blocks are not part of the logic tree in the API. Instead, they will always be shown at the end of the Riddle logic tree, regardless of the path taken. An end of the logic tree can be defined by adding a logic layer with no next property or with the next property set to null.
Optional: Referencing answers by ID
As with the blocks, the answer items can also be referenced by their ID instead of the answer text which makes it less confusing and more deterministic when creating logic rules for complex build configurations. Learn more
Step by step guide to convert from answer text to answer IDs:
- Add explicit IDs to the answers in the
itemsproperty of the SingleChoice question block:
{
"id": 1,
"title": "What's the capital of Germany?",
"type": "SingleChoice",
"items": [
{ "id": 11, "title": "Berlin", "isCorrect": true },
{ "id": 22, "title": "Lissabon", "isCorrect": false },
{ "id": 33, "title": "Leipzig", "isCorrect": false }
]
}
- Use the answer IDs (i.e. 11, 22, 33) in the logic settings instead of the answer text:
{
"blockId": 1,
"branchingType": "answer",
"rules": [
{
"answers": [11],
"blockId": 2,
"next": {
"blockId": 3
}
},
{
"answers": [22, 33],
"blockId": 3
}
]
}
In spoken form: exactly the same flow as above - if the user picks answer 11 ("Berlin"), show block ID 2 and then block ID 3; if they pick answer 22 or 33, go straight to block ID 3.
Full example
Two complete request bodies that build the logic described above.
- A quiz that branches on the answer to its first question. Whoever answers "Berlin" also gets the colors question before the ordering question, whoever answers "Lissabon" or "Leipzig" goes straight to the ordering question, and both paths end on the same result page.
{
"type": "Quiz",
"build": {
"title": "Logic example (Builder API)",
"blocks": [
{
"id": 1,
"title": "What's the capital of Germany?",
"type": "SingleChoice",
"items": [
{ "title": "Berlin", "isCorrect": true },
{ "title": "Lissabon", "isCorrect": false },
{ "title": "Leipzig", "isCorrect": false }
]
},
{
"id": 2,
"title": "What are valid colors in German?",
"type": "MultipleChoice",
"items": [
{ "title": "rot", "isCorrect": true },
{ "title": "schwarz", "isCorrect": true },
{ "title": "nero", "isCorrect": false }
],
"explanation": {
"title": "Correct!",
"description": "Only 'rot' and 'schwarz' are valid colors in German"
}
},
{
"id": 3,
"title": "Order the following colors from lightest to darkest.",
"type": "Order",
"items": [
{ "title": "red" },
{ "title": "blue" },
{ "title": "green" }
],
"itemsShuffled": true,
"guesses": 3
}
],
"logic": {
"blockId": 1,
"branchingType": "answer",
"rules": [
{
"answers": ["Berlin"],
"blockId": 2,
"next": {
"blockId": 3
}
},
{
"answers": ["Lissabon", "Leipzig"],
"blockId": 3
}
]
},
"results": [
{
"title": "Thanks for taking the quiz!",
"minPercentage": 0,
"maxPercentage": 100
}
]
}
}
- A poll that branches on a standalone form dropdown field. The visitor picks their favourite programming language and sees only the follow-up question for that language, Python leading to block ID 2, JavaScript to block ID 3 and Java to block ID 4, before the Riddle ends.
{
"type": "Poll",
"build": {
"title": "Programming Language Quiz",
"blocks": [
{
"id": 1,
"title": "Select your favorite programming language:",
"type": "FormField",
"fieldType": "Dropdown",
"items": [
{ "title": "Python" },
{ "title": "JavaScript" },
{ "title": "Java" }
]
},
{
"id": 2,
"title": "Why do you like Python?",
"type": "SingleChoice",
"items": [
{ "title": "Easy to learn" },
{ "title": "Great libraries" },
{ "title": "Versatile" }
]
},
{
"id": 3,
"title": "Why do you like JavaScript?",
"type": "SingleChoice",
"items": [
{ "title": "Web development" },
{ "title": "Versatile" },
{ "title": "Large community" }
]
},
{
"id": 4,
"title": "Why do you like Java?",
"type": "SingleChoice",
"items": [
{ "title": "Platform independence" },
{ "title": "Strong typing" },
{ "title": "Enterprise use" }
]
}
],
"logic": {
"blockId": 1,
"branchingType": "answer",
"rules": [
{
"answers": ["Python"],
"blockId": 2
},
{
"answers": ["JavaScript"],
"blockId": 3
},
{
"answers": ["Java"],
"blockId": 4
}
]
},
"result": {
"title": "Thanks for sharing your preferences!"
}
}
}

