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 these Riddle types:

A leaderboard has no blocks to branch over, so sending a logic property for one is rejected with Logic configuration is not allowed for Riddle type "Leaderboard". The same goes for a placeholder.

You can branch by answers (typically on SingleChoice, MultipleChoice, standalone Form Dropdown fields, personality items, and the Win/Lose outcome of a Wheel of Fortune), by a condition on a block's answer or a form field's value, by block/total score, and - on the minigame blocks - by whether the player solved the game or came out ahead. 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 logic together with preset.pagination.isEnabled: true is rejected - a paginated Riddle shows all blocks on one page, which has no meaningful branching.
  • Flashcards: a Quiz that contains a Flashcard block only accepts linear logic (see RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_LINEAR_ONLY_ALLOWED below).
  • Story: a Story Riddle only accepts linear logic - there is nothing to branch on (no answers, scores, or form fields), so answer, condition, score, solved, and won branching are all rejected.
  • Every block has to be used: every block in the blocks array must be reachable somewhere in the tree, otherwise the build is rejected (see RIDDLE_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 six types of branching:

Linear 'branching'

PropertyRequiredTypeDescriptionDefault
blockId✓integerThe ID of the block where this logic layer starts. Can reference any block, for example even Content
branchingTypestringThe type of branching to use. For linear branching it is set to linear, but can also be omitted due to being the default valuelinear
nextobjectAn 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 pathnull

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

PropertyRequiredTypeDescriptionDefault
blockId✓integerThe ID of the block where this logic layer starts. Can only reference blocks which offer selectable answers to branch on; see below
branchingType✓stringThe type of branching to use. For answer branching, it is set to answerlinear
rules✓objectAn 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. SingleChoice and MultipleChoice. On a Poll or Predictor these are the block's items, on a Quiz its answers, and on a personality test the block's personality items - a personality single choice branches exactly like any other question block.
  • Form fields: only a standalone Dropdown field can be branched on. Any other form field - including a whole FormBuilder block - is rejected with Answer logic branching is currently only allowed with standalone Dropdown form fields (Found <type>).
  • Wheel of Fortune (WheelSpinner): a wheel branches on the outcome of the spin, not on its fields. It offers exactly two answers: Win (ID 1) and Lose (ID 0). A free-spin field re-spins the wheel instead of ending the block, so it is not an outcome and cannot be branched on - a wheel made up of nothing but free spins has no outcome at all and is rejected with its own message. On a wheel, an outcome that no rule lists ends the flow on the Riddle's result, so branching on Win alone needs no Lose rule.
  • Blocks that carry no answer items at all (e.g. a Content block) are rejected with Answer logic block must refer to a question block with choices/answers, found "<type>" block. Such blocks can still be used in linear layers.

On every block other than a Wheel of Fortune, an answer that no rule lists follows the first rule - which is also where an unanswered block (a timer that ran out, a skip) goes.

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:

PropertyRequiredTypeDescriptionDefault
answers✓string|intA 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

PropertyRequiredTypeDescriptionDefault
blockId✓integerThe 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✓stringThe type of branching to use. For condition branching, it is set to conditionlinear
condition✓objectAn object with field (what is compared - see below), operator (see below), and, unless the operator is a standalone check like isEmpty, value (the value to compare against). No other key is accepted
ifTrueobjectThe next logic layer to follow if the condition evaluates to true. Same rules as next/rules - it can itself be any branching typenull
ifFalseobjectThe next logic layer to follow if the condition evaluates to falsenull

Addressing a field

condition.field is a string naming what is compared:

  • A bare block ID ("3") compares that block's own answer - or, for a standalone FormField block, that field's value.
  • "<blockId>.<formFieldId>" ("3.1") addresses a field nested inside a FormBuilder block. The field IDs are the same ones a read-back reports for the block's content.formConfig.formBuilder.formElements; see custom IDs. The underscore form ("3_1") is rejected with a message pointing at the dotted form.
  • Data layer items and magic variables cannot be used in a condition built through the Builder API.

Operators

The allowed operator values depend on the kind of field condition.field refers to:

Field kindAllowed operators
Question block answer (SingleChoice, MultipleChoice, Upvote, TextEntry on a Poll, Personality or Predictor)equals, notEquals, contains, notContains, isSkipped, isEmpty, isNotEmpty
The same blocks on a Quizthe same, plus isCorrect, isNotCorrect
Form text field (Name, Email, Phone, URL, ShortText, LongText)equals, notEquals, contains, notContains, startsWith, notStartsWith, endsWith, notEndsWith, isEmpty, isNotEmpty, isSkipped
Form number field (Number, Rating, DatePicker, TimePicker)equals, notEquals, greaterThan, lessThan, greaterThanOrEquals, lessThanOrEquals, between, notBetween, isEmpty, isNotEmpty, isSkipped
Form select field (Country, Dropdown, RadioButtons)equals, notEquals, isEmpty, isNotEmpty, isSkipped
Form checkbox fieldisChecked, isNotChecked, isSkipped

An operator the field does not allow is rejected with RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_CONDITION_OPERATOR_NOT_ALLOWED, and the message lists the operators that field does accept.

The value beside the operator has to fit it as well, and a mismatch is refused rather than stored: the standalone checks (isEmpty, isNotEmpty, isSkipped, isChecked, isNotChecked, isCorrect, isNotCorrect) take no value at all (an explicit null is accepted, since that is what a read-back emits), the text comparisons take a single string or number, the numeric comparisons take a single number (a numeric string like "80" is fine), and between / notBetween take exactly two numbers, [min, max], with min no greater than max. On a question block or a select field, the value of an equals/notEquals comparison has to be one of that block's actual choices - given either as its title or as its ID - and the error lists the valid ones if it is not.

Condition branching is never available on a minigame's game blocks (SlotMachine, WheelSpinner, Sudoku, Minesweeper, Crosswords): none of them exposes a comparable answer or field, so a condition naming one is rejected with RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_CONDITION_INVALID_MAPPABLE. Branch those blocks with solved, won, score, or answer instead.

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": "1",
        "operator": "equals",
        "value": "Berlin"
    },
    "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.

Example: Condition branching on a field inside a FormBuilder block

{
    "blockId": 3,
    "branchingType": "condition",
    "condition": {
        "field": "3.1",
        "operator": "isNotEmpty"
    },
    "ifTrue": {
        "blockId": 4
    }
}

In spoken form: Show the form block ID 3. If its field 1 was filled in, continue with block ID 4 - otherwise the flow ends on the result.

Score branching

PropertyRequiredTypeDescriptionDefault
blockId✓integerThe 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✓stringThe type of branching to use. For score branching, it is set to scorelinear
scoreType✓stringtotal branches on the running total score as a percentage of the Quiz's max achievable score (Quiz only); block branches on the raw point score of this node's own block only
ranges✓objectAn array of at least one range object mapping a [min, max] score interval to a next block. Ranges must not overlap, must not leave a gap, and each range's min must be less than or equal to its max

The range object

PropertyRequiredTypeDescriptionDefault
min✓integerThe lowest score of this range, inclusive
max✓integerThe highest score of this range, inclusive
blockId✓integerThe block to show when the score falls into this range
branchingTypestringHow the flow continues after blockId: linear, answer, condition, score, solved or wonlinear

Note: A range is itself a logic layer for its blockId. Next to the four properties above it takes the properties of its branchingType, exactly as at the top level: next for linear, rules for answer, condition with ifTrue/ifFalse for condition, scoreType with ranges for score, and so on. Any other branching type than linear has to be named in branchingType, otherwise its properties are rejected.

Range rules

  • No overlap: two ranges must never cover the same score, otherwise there is no telling which branch a player takes (RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_SCORE_RANGE_OVERLAP).
  • min ≤ max in every range (RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_SCORE_RANGE_INVALID).
  • No gaps: the ranges have to resolve every score exactly once. A score that falls between two ranges leaves the player with no next block at all, so the flow simply stops - the build is rejected with RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_SCORE_RANGE_GAP instead.
  • For scoreType: "total" the domain is fixed, so the ranges have to cover 0-100 end to end (which is also what rejects a negative min).
  • For scoreType: "block" only the holes between ranges are checked: a block score is in raw points, may legitimately start above 0 or below it (negative answer scores are allowed), and the highest range is published open-ended, so any score above it still takes that last branch.

Which blocks can be branched on with scoreType: "block"

block is only accepted on blocks that actually award a score - on anything else (a Content block, a Poll single choice, a Wheel of Fortune) every player would land in the range covering 0 and every other range would be unreachable:

Riddle typeBlocks that award a block score
QuizSingleChoice, MultipleChoice, Order, TapAndFind, TextEntry, TypeRush
PollReaction
MinigameMinesweeper, Sudoku, Crosswords, SlotMachine

Anything else is rejected with RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_INVALID_NODE. A Wheel of Fortune stores the item it spun rather than a score - branch on the outcome with answer branching (Win/Lose) instead.

The minigame scores behave quite differently from one another, which is worth knowing before writing ranges for them:

  • Minesweeper is computed from grid size, time, flags and streak, and accumulates across games, so it spans thousands of points with no configured ceiling.
  • Sudoku and Crosswords award a fixed score or nothing at all - that is the solved fork with the number spelled out.
  • A Slot Machine scores the credit balance the player carries on: the block's startBalance if the profit is taken unspun, and 0 after going bust - that is what the won fork splits at.

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 player has scored 0-50% of the Quiz's max achievable score so far, continue with block ID 2; if they are at 51-100%, continue with block ID 3.

scoreType: "total" is only available on a Quiz, and not because the other types have no running score - a minigame accumulates one just fine. It is the denominator that is missing: the branch is resolved against a percentage, and only Quiz and personality blocks contribute to the max achievable score it divides by. Sending it anywhere else is rejected with "scoreType": "total" (RiddleScore) branching is only available on a Quiz; found on a "<type>" Riddle.

Solved branching

A single "did they solve it?" fork for the minigame blocks that award a score on solving. It is the readable equivalent of a block score branch split at 1 point - the API stores it as exactly that, so the split never has to be restated in your configuration.

PropertyRequiredTypeDescriptionDefault
blockId✓integerThe ID of the minigame block whose outcome is forked on. Available on Sudoku, Crosswords and Minesweeper
branchingType✓stringFor solved branching, it is set to solvedlinear
solvedobjectThe next logic layer to follow when the player solved the game (it awarded any score at all). Leave it out to end the flow on the Riddle's result for that outcomenull
notSolvedobjectThe next logic layer to follow when the player did not solve itnull

Both branches are optional, which is unique to this branching type (and to won): a bare {"blockId": 1, "branchingType": "solved"} shows the block and ends the flow on the result either way, while answer needs one rules entry and score one ranges entry.

solved is not available on a WheelSpinner, which never awards a score (branch it with answer on its Win/Lose outcome), and not on a SlotMachine, whose score is the player's running balance rather than a solve - fork that one with won. Either case is rejected with RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_INVALID_NODE and a message naming the branching type to use instead.

A Sudoku or Crosswords block whose own score is set to 0 is rejected with the same error: a solved game would then score exactly what an unsolved one scores, leaving the solved branch unreachable. Give the block a score of at least 1.

Example: Solved branching on a Sudoku

{
    "blockId": 1,
    "branchingType": "solved",
    "solved": {
        "blockId": 2
    },
    "notSolved": {
        "blockId": 3
    }
}

In spoken form: Show the Sudoku block ID 1. If the player solved it, continue with block ID 2, otherwise continue with block ID 3.

Won branching

The Slot Machine's equivalent of solved: a single "did they come out ahead?" fork.

PropertyRequiredTypeDescriptionDefault
blockId✓integerThe ID of the SlotMachine block whose outcome is forked on. Not available on any other block type
branchingType✓stringFor won branching, it is set to wonlinear
wonobjectThe next logic layer to follow when the player finished above their starting credits. Leave it out to end the flow on the Riddle's result for that outcomenull
notWonobjectThe next logic layer to follow when they did notnull

The split is taken from the block's own startBalance (see Minigame): a Slot Machine's block score is the credit balance the player walks away with, so finishing above startBalance counts as won, while finishing exactly on it (the profit taken unspun) or below it - 0 being bust - does not. Because the fork is derived from the block, it moves with the block's credits and never has to be restated in the logic tree.

As with solved, both branches are optional and omitting both is allowed.

Example: Won branching on a Slot Machine

{
    "blockId": 1,
    "branchingType": "won",
    "won": {
        "blockId": 2
    },
    "notWon": {
        "blockId": 3
    }
}

In spoken form: Show the Slot Machine block ID 1. If the player finished with more credits than they started with, continue with block ID 2, otherwise continue with block ID 3.

Note: because the split is derived from startBalance, changing that value while sending no logic in the same edit leaves the previously built split in place until the tree is rebuilt. Send the logic tree along with the block change to have the fork re-derived.

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 next property or with the next property 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/maxPercentage ranges 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 logic property 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, score, solved, or won - and any nested combination of them), and can be sent back unchanged. answers are 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 logic property 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 $reset with any other key inside logic is rejected, and it is only accepted when editing (never in a POST /riddle-builder request).
  • 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_REFERENCED below.

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/solved/won) 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.

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 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_SCORE_RANGE_INVALID

A single score range is inverted - its min is greater than its max, so no score can ever fall into it:

Score range 2 has "min" (80) greater than "max" (50).

RIDDLE_BUILDER_BLOCK_PROPERTY_LOGIC_SCORE_RANGE_GAP

A score branching node's ranges leave a score with no range to fall into. A player reaching that score would have no next block at all and the flow would simply stop, so the build is rejected instead. See Range rules above.

Example faulty logic settings:

{
    "blockId": 1,
    "branchingType": "score",
    "scoreType": "total",
    "ranges": [
        { "min": 0, "max": 50, "blockId": 2 },
        { "min": 60, "max": 100, "blockId": 3 }
    ]
}
Score ranges must not leave a gap: nothing covers 51-59, between the range ending at 50 and the one starting at 60.

For scoreType: "total" the ends of the scale are checked as well, since its ranges are percentages:

"scoreType": "total" ranges are percentages and must cover 0-100 end to end, found 0-80. Scores outside the ranges have no branch to take.

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:

  1. Add explicit IDs to the answers in the items property 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 }
    ]
}
  1. 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.

  1. 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
            }
        ]
    }
}
  1. 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!"
        }
    }
}