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
}
| Property | Required | Type | Description | Default |
|---|---|---|---|---|
build | ✓ | object | The build specifications to apply - only what should change | |
publish | boolean | Publish the Riddle after the edit | false |
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 send | What happens |
|---|---|
title | Replaced. Omit it to keep the current title. |
blocks | Merged 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. |
result | Replaced 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. |
logic | Replaced 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. |
leaderboard | Only 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 -
$blocksOrderis the usual case, and alogictree 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
buildtells 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
- sending a full replacement
logictree that no longer references the deleted block(s), or - 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
$createentry needs an explicitidto 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:
| Kind | Examples |
|---|---|
| Plain lists of values | recipientCC / 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 else | tracking / trackingNetworks, networks / networkConfigs, variables |
| Lists whose position is their meaning | individualBlockTimes (one per block), a personality answer's scores (one per personality) |
| Collections an edit rewrites as a whole | results, a Placeholder's conditions |
Merged by id, but with a fixed order | personalities (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
| Situation | Result |
|---|---|
A block entry with neither an id nor $create | Rejected - an edit addresses an existing block by its ID |
An id that does not exist in this Riddle | Rejected, with the list of IDs that do exist |
The same id addressed twice in one request | Rejected |
$create and $delete on the same entry | Rejected |
A marker whose value is not true / false | Rejected (a truthy string would otherwise delete a block) |
An unknown marker, e.g. $itemOrder | Rejected, with the list of markers supported there |
Any marker in a POST /riddle-builder request | Rejected - 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 ordered | Rejected, 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 integers | Rejected |
A property that the Riddle type or block does not know, e.g. titel | Rejected, with the list of properties supported there - see below |
The Riddle is still being built by a queued POST request | Rejected - 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
1keeps its answers, media and settings - only the title changes - block
4renames the answer with ID1, removes the one with ID2and gains a new answer; every answer it does not list stays as it is - block
9is new, with the ID taken fromnextBlockId, so$blocksOrdercan name it - block
3is removed, so it must not appear in$blocksOrder $blocksOrderlists every block the Riddle has afterwards, exactly once
Next steps
- Read a Riddle back as a build configuration
- Item formats: how a collection merges by
id, and how to add an entry to it - Switch a configured feature back off
- Assign your own IDs to blocks, items and results
- Define the logic settings / flow of your Riddle
- Re-route a Placeholder by editing its conditions
- Handle exceptions

