Editing an existing Riddle

PUT /riddle-builder/{UUID} edits a Riddle that already exists, using the same builder config that POST /riddle-builder accepts as build and that GET /riddle-builder/{UUID} returns.

It works on Riddles of every type, but only on Riddles this API can see in full: ones built via the Builder API, created from a template with the MCP tool riddleTemplate_use, or generated by the Riddle AI - see which Riddles the API may manage. A Riddle assembled by hand in the Creator can hold content a builder config cannot express - that is what the read endpoint's warnings report - so editing it from here would mean editing around content the API never saw. Such a Riddle is rejected with a message saying so, and has to be edited in the Creator. Reading it keeps working.

The one rule

An edit only states what changes. Anything the build configuration does not mention keeps whatever is stored.

That is the single difference to a build: no property is required as long as the Riddle already stores a value for it, and nothing is reset. Blocks are addressed by their id and are merged into, not replaced.

Request format

{
    "build": {
        "title": "Favorite color poll (2026)"
    },
    "publish": true
}
PropertyRequiredTypeDescriptionDefault
buildobjectThe build specifications to apply - only what should change
publishbooleanPublish the Riddle after the editfalse

The root of an edit takes nothing else: the Riddle already has a type and lives in a project, and an edit is always processed synchronously (there is no queue).

Step 1: read the Riddle

An edit addresses blocks by the id they are stored with, so start with GET /riddle-builder/{UUID}. Besides build it returns nextBlockId, the ID a newly created block may claim. See reading Riddles back for the full response and for why a read-back holds fewer properties than you sent.

{
    "uuid": "abcdef12",
    "type": "Poll",
    "modifiedAt": "2026-01-01 12:00:00",
    "nextBlockId": 9,
    "build": {
        "title": "Favorite color poll",
        "blocks": [
            {
                "id": 1,
                "type": "SingleChoice",
                "title": "What's your favorite color?",
                "items": [ ... ]
            },
            {
                "id": 3,
                "type": "NPS",
                "title": "How likely are you to recommend us?"
            }
        ],
        "result": { "id": 2, "title": "Thank you" }
    },
    "warnings": []
}

Editing a block

Send the block's id plus only the properties that change. Every other property of that block, and every block you do not list, stays as it is:

{
    "build": {
        "blocks": [
            { "id": 1, "title": "What is your favorite color, really?" }
        ]
    }
}

The NPS block with ID 3 above is not mentioned, so it is untouched - as are the answer items of block 1, its media, its layout settings and the Riddle's result page.

A block cannot change its type: a type contradicting the stored one is rejected - delete the block and add a new one instead. Repeating the correct type is a no-op and always allowed, which is what a round-tripped configuration does. The same holds for the fieldType of a standalone FormField block.

What merges and what is replaced

The merge goes all the way down: blocks by their id, and the collections inside them entry by entry. What is rewritten as a whole says so below. The $create and $delete markers the table mentions are described further down:

What you sendWhat happens
titleReplaced. Omit it to keep the current title.
blocksMerged by id. Blocks you do not list are untouched.
A collection inside a block (items, fields, answers, ...)Merged by id, entry by entry, exactly as blocks are: an entry you do not list stays as it is, an entry you send by id is updated with the properties you send, "$create": true adds one and "$delete": true removes one. See item formats.
resultReplaced as a whole if sent, kept if omitted.
results (Quiz)Replaced as a whole if sent, kept if omitted. Resend every result page you want to keep, not just the one you are changing: there is no id-addressing and no $resultsOrder for this collection.
personalities (Personality Test)Merged by id, exactly like blocks: an entry you do not list stays as it is, "$create": true adds one, "$delete": true removes one. Their order is not editable - there is no $personalitiesOrder. See Editing the personalities below.
scale (Matrix)Replaced as a whole, not merged entry by entry - the ratings are the columns of the whole question. Resend every rating you want to keep.
logicReplaced as a whole if sent, kept if omitted.
preset, publish (settings object), scoring (Predictor)Merged per key into the stored settings - sending {"correct": 50} leaves the other scoring rules exactly as they were.
leaderboardOnly applied when you actually send it; it adds a connection. Omit it to leave the Riddle's connections alone.
riddleConnections (Leaderboard)Replaces the connected set as a whole - resend every UUID you want to keep.
conditions (Placeholder)Replaces the routing rules as a whole - resend every condition you want to keep. "riddleId": null clears the fallback target, but an edit whose result would have neither a fallback nor a single condition is rejected: the Placeholder could no longer display anything. See Placeholder.

Note: In a collection which is rewritten as a whole, an entry you re-supply under its original id is re-created under that same ID, so references into it (a blockId_fieldId reference, logic rules, personality scores by ID) keep resolving. An entry you omit - or that you send without an id - loses its old identity. In a merged collection there is nothing to re-supply: the entries your IDs address are the stored ones.

Editing the personalities

Sending personalities on a Personality Test merges by id, exactly like blocks: an entry you do not list stays as it is, an entry you send by id is updated with the properties you send, "$create": true adds one, and "$delete": true removes one.

Their order cannot be changed here. Unlike blocks, this collection has no $personalitiesOrder marker - sending one is rejected - and since the entries are matched by id, the order you send them in has no meaning either. A personality added with "$create": true is appended at the end, every other one keeps the place it has, and the personalities are reordered in the Creator.

A Personality Test needs at least 2 personalities, and that minimum is checked against the count the merge would leave, not against the entries you send: an edit that deletes down to one personality is rejected, and editing an existing personality without touching the count is unaffected either way.

The scores already stored on every answer item follow their personality by identity, not by position:

  • a personality you keep keeps its scores;
  • a personality you add starts at a score of 0 on every existing answer;
  • a personality you delete takes its scores with it, and no other personality inherits them.

So renaming, restyling or shortening the personality list never needs the questions resent - the scoring stays valid on its own. Since the order never moves, a scores array read back is always aligned with the personalities in the order riddle_get reports them.

Adding a personality leaves it scoring 0 everywhere until you say otherwise. Set the real scores by resending the affected blocks' items with their full scores arrays in the same call - blocks are built after the personalities, so what you send simply wins.

{
    "personalities": [
        { "id": 1, "title": "The Explorer" },
        { "$create": true, "id": 3, "title": "The Improviser" }
    ],
    "blocks": [
        {
            "id": 3,
            "items": [
                { "id": 1, "scores": [2, 0, 1] },
                { "id": 2, "scores": [0, 2, 1] }
            ]
        }
    ]
}

Adding a block: $create

"$create": true adds a new block instead of editing an existing one. Such an entry needs no id - though it may carry one to control the ID it gets - and it is built with full create semantics - so everything a POST /riddle-builder request requires for that block type is required here too:

{
    "build": {
        "blocks": [
            {
                "$create": true,
                "type": "SingleChoice",
                "title": "Which color do you like least?",
                "items": [
                    { "title": "green" },
                    { "title": "red" }
                ]
            }
        ]
    }
}

New blocks are appended after the Riddle's existing blocks. To put one somewhere else, send the Riddle's block order along with it in $blocksOrder, the order marker described under reordering: it takes the complete list of block IDs in the order you want, so the new block has to carry an id the list can name.

Choosing the new block's ID

A $create entry needs no id - it gets the next free one assigned. You may send one, though, and then the new block is created under exactly that ID, which is what lets $blocksOrder put it in front of the existing blocks 1 and 4 here:

{
    "build": {
        "blocks": [
            {
                "$create": true,
                "id": 9,
                "type": "SingleChoice",
                "title": "Which color do you like least?",
                "items": [
                    { "id": 1, "title": "green" },
                    { "id": 2, "title": "red" }
                ]
            }
        ],
        "$blocksOrder": [9, 1, 4]
    }
}

Take the ID from nextBlockId and count up from there if you add several blocks at once; claiming an ID that is already taken is rejected with an error naming the next free one, never silently renumbered. Do it whenever

  • the same request has to reference the new block - $blocksOrder is the usual case, and a logic tree sent along with it is another;
  • you want the ID to be predictable for your own stats, webhook or logic handling instead of learning it from the response.

The same applies inside the new block: its items, fields, results and other entries take their own id, exactly as in a POST /riddle-builder build. See custom IDs and item formats.

Block IDs

IDs are handed out by a counter that only ever grows and that is shared between the Riddle's blocks, results, personalities and attributes. Two consequences:

  • an ID freed by a deletion never comes back, and
  • no ID visible in build tells you which one is free - the highest block ID plus one is usually already taken by a result.

So take the ID for a $create entry from nextBlockId and count up from there if you add several blocks at once. Claiming a taken ID is rejected with an error naming the next free one, never silently renumbered. A $create without an id gets one assigned; sending one yourself creates the block under exactly that ID - see choosing the new block's ID.

Note: The IDs an edit can address are the ones the Riddle had before the request. A block created in the same request cannot also be edited by it - send its final state in the $create entry instead.

Note: All of the above is about block IDs. The IDs inside a block - an answer item's id, a form field's id, a result page block's id - are counted per collection and start at 1 in every single one of them, so the same id legitimately appears in every block of the Riddle. Never take one of those from nextBlockId: read the collection back and use the IDs it reports (or omit id in a $create entry and let one be assigned). nextBlockId only ever answers the question "which top-level block ID is free".

Removing a block: $delete

{
    "build": {
        "blocks": [
            { "id": 3, "$delete": true }
        ]
    }
}

Nothing else about such an entry is built - you can send back a block object read from GET untouched and just add the marker, without stripping its properties first.

Two things are rejected rather than applied:

  • an edit that would leave the Riddle without any blocks (a Riddle needs at least one, otherwise it is neither usable in the Creator nor publishable);
  • deleting a block that the Riddle's custom logic still references - see below.

Deleting a block that the logic references

If the Riddle has custom logic that still references a block you are deleting, the request is rejected with an error naming exactly which deleted block is still referenced by which logic node. Resolve it in the same request, either by

  1. sending a full replacement logic tree that no longer references the deleted block(s), or
  2. discarding the custom logic with "logic": {"$reset": true}, which falls back to the default linear flow.
{
    "build": {
        "blocks": [
            { "id": 3, "$delete": true }
        ],
        "logic": { "$reset": true }
    }
}

$reset is exclusive: combining it with any other key inside logic is rejected, and its value has to be true or false.

This guard never fires when the Riddle's logic is still the automatically generated default one - that logic is regenerated from the current block order on every build, so deleting a block always works there.

Reordering: $blocksOrder and friends

Order is a marker of its own, sitting next to the collection it orders and named after it: $blocksOrder for the Riddle's blocks, $itemsOrder inside a block entry for that block's items, $fieldsOrder for a FormBuilder's fields, and so on.

{
    "build": {
        "blocks": [
            { "id": 1, "$itemsOrder": [2, 1, 3] }
        ],
        "$blocksOrder": [3, 1]
    }
}

The value is always the complete list of that collection's IDs, every ID exactly once. A partial list is rejected: where the IDs you left out belong is exactly what it does not say.

An order marker is applied last, after everything else in the same request, so it describes the collection as that request leaves it:

  • a block you delete in this request must not be listed;
  • a block you create in this request must be listed - which is why a $create entry needs an explicit id to be orderable.

An order on its own is a valid edit.

Which collections can be ordered

Only collections whose entries are stored with an ID of their own, and only at the Riddle level or on a top-level block entry - a block nested inside another block has no addressable entry to put a marker in yet.

Everything else is simply sent in the order you want it in, and says so in the error if you try to order it:

KindExamples
Plain lists of valuesrecipientCC / recipientBCC / replyTo, a Recommended Content block's selectedRiddles / riddleTypes / tags, a TypeRush item's answers, reactionScale, a Slot Machine's customSymbols, a Leaderboard's riddleConnections
Maps keyed by something elsetracking / trackingNetworks, networks / networkConfigs, variables
Lists whose position is their meaningindividualBlockTimes (one per block), a personality answer's scores (one per personality)
Collections an edit rewrites as a wholeresults, a Placeholder's conditions
Merged by id, but with a fixed orderpersonalities (Personality Test)

A Personality Test's personalities is the one collection which merges by id and still cannot be reordered: there is no $personalitiesOrder, and sending one is rejected. Because the entries are matched by id, the order you send them in has no meaning either - a "$create" is appended at the end, everything else keeps the place it has, and reordering the personalities is done in the Creator. See Editing the personalities above.

Rules and errors

SituationResult
A block entry with neither an id nor $createRejected - an edit addresses an existing block by its ID
An id that does not exist in this RiddleRejected, with the list of IDs that do exist
The same id addressed twice in one requestRejected
$create and $delete on the same entryRejected
A marker whose value is not true / falseRejected (a truthy string would otherwise delete a block)
An unknown marker, e.g. $itemOrderRejected, with the list of markers supported there
Any marker in a POST /riddle-builder requestRejected - markers only mean something relative to stored content; in a create build the order of the payload is the order
An order marker on a collection that cannot be orderedRejected, naming the reason - send the collection in the order you want instead
An order marker that is incomplete, has an unknown ID, a duplicate ID, or is not a list of integersRejected
A property that the Riddle type or block does not know, e.g. titelRejected, with the list of properties supported there - see below
The Riddle is still being built by a queued POST requestRejected - try again once it is done

Unknown properties are always rejected here. In a POST /riddle-builder build they are ignored unless you ask for them to be rejected with strictProperties: true; an edit has no such option, because there a dropped property is indistinguishable from "the Riddle already looked like that" - you would read back exactly what you sent and never learn that nothing happened.

Every edit is one transaction: if any part of it is rejected, the Riddle stays exactly as it was. The error format is the same as for a build - see exception handling.

Response

PUT /riddle-builder/{UUID} answers with the same Riddle response that POST /riddle-builder returns, including the Riddle data. To get the edited Riddle back as a builder config again, read it with GET /riddle-builder/{UUID}.

Changes land in the draft. An embedded Riddle only changes once it is published again - either with "publish": true in the same request, or via the publish endpoint.

Full example

Rename the Riddle, rewrite one question's title, edit the answers of another, add a new question, remove the NPS block and put the blocks in a new order - in one request:

{
    "build": {
        "title": "Favorite color poll (2026)",
        "blocks": [
            { "id": 1, "title": "What is your favorite color, really?" },
            {
                "id": 4,
                "items": [
                    { "id": 1, "title": "emerald green" },
                    { "id": 2, "$delete": true },
                    { "$create": true, "title": "blue" }
                ]
            },
            {
                "$create": true,
                "id": 9,
                "type": "SingleChoice",
                "title": "Which color do you like least?",
                "items": [
                    { "title": "green" },
                    { "title": "red" }
                ]
            },
            { "id": 3, "$delete": true }
        ],
        "$blocksOrder": [9, 1, 4]
    },
    "publish": true
}
  • block 1 keeps its answers, media and settings - only the title changes
  • block 4 renames the answer with ID 1, removes the one with ID 2 and gains a new answer; every answer it does not list stays as it is
  • block 9 is new, with the ID taken from nextBlockId, so $blocksOrder can name it
  • block 3 is removed, so it must not appear in $blocksOrder
  • $blocksOrder lists every block the Riddle has afterwards, exactly once

Next steps