---
title: "Update block"
method: PUT
path: "/v1/communities/{communityId}/projects/{projectId}/script/blocks/{blockId}"
tags: ["Script"]
---

# Update block

`PUT /v1/communities/{communityId}/projects/{projectId}/script/blocks/{blockId}`

## Request

- Base URL: `https://api.subo.ai`
- URL: `https://api.subo.ai/v1/communities/{communityId}/projects/{projectId}/script/blocks/{blockId}`
- Auth: API key in header `X-API-Key`

Update a single main-section block's content, options, constraints, action config, or position. Project must be `inactive`. This endpoint does not update outro blocks; use `PUT /script` with `section=outro` + `id` for outro content/pause updates.

## Path parameters

- `communityId` string, required
- `projectId` string, required
- `blockId` string, required

## Request body

- object: All fields optional. Only provided fields are updated.
  - `name` string: The block's variable token, e.g. `belonging` -> `[Belonging]`. Renaming does not rewrite existing references.
  - `section` 'main' | 'intro' | 'outro', nullable
  - `type` 'single_punch' | 'multi_punch' | 'open_text' | 'open_numeric' | 'rating' | 'opinion_scale' | 'nps' | 'ranking' | 'content_block' | 'action_block' | 'calculated_block'
  - `answer_style` 'full_text' | 'emoji_only' | 'select_menu' | 'buttons', nullable
  - `image_url` string, uri, nullable
  - `thumbnail_url` string, uri, nullable
  - `prompt` string
  - `options` object[], nullable
    - `id` integer, nullable: Backend answer id. Omit (or send null) to insert; include the existing id to update in place. Preserves response.answer_id references.
    - `value` string, required
    - `label` string, nullable
    - `emoji` string, nullable
    - `score_values` object, nullable
    - `display_order` integer, nullable
    - `anchor_position` 'last' | 'null', nullable
    - `is_deleted` boolean, nullable: Hide this option (keeping its responses) or, with false, restore a hidden one. Ignored on create.
  - `min` integer, nullable
  - `max` integer, nullable
  - `required` boolean, nullable
  - `precondition` string, nullable
  - `action_kind` 'give_xp' | 'give_achievement' | 'give_role', nullable
  - `action_config` object, nullable
    - `xp_amount` integer, nullable
    - `xp_role_id` integer, nullable
    - `role_id` string, nullable
    - `fire_scope` 'respondent' | 'session', nullable
  - `position` integer, nullable
  - `continue` object, nullable
    - `after` 'pause' | 'click'
    - `pause` integer, nullable
    - `label` string, nullable

## Response `200`

Updated script

- object
  - `data` Script
    - `project_id` string
    - `blocks` Block[]
      - `id` integer, nullable
      - `name` string, nullable: Variable token name for this block. Used in pipe variables (`[q_name]`), precondition expressions (`NOT((q_name="value"))`), and calculated formulas. Auto-assigned as `q0`, `q1`, … (by position) when omitted. Must be unique within the script.
      - `section` 'main' | 'intro' | 'outro', nullable: Block section. `intro` blocks are shown before the survey begins (editable and deletable). `outro` blocks are returned only when `GET /script?expand=outro` is used and are read-only in script write endpoints.
      - `type` 'single_punch' | 'multi_punch' | 'open_text' | 'open_numeric' | 'rating' | 'opinion_scale' | 'nps' | 'ranking' | 'content_block' | 'action_block' | 'calculated_block', required
      - `answer_style` 'full_text' | 'emoji_only' | 'select_menu' | 'buttons', nullable: How answer options are displayed. `full_text`: labelled buttons; `emoji_only`: keycap emoji buttons; `select_menu`: dropdown. Applies to single_punch and multi_punch blocks, to `ranking` (which takes `full_text` or `emoji_only` only), and to the scale family (`rating` / `opinion_scale` / `nps`), which accept only `buttons` (default) or `select_menu` — a scale's point rendering is set with `scale.icon`, not here. A select menu is the better widget from six labeled points up, where a button row wraps. Sending `full_text` or `emoji_only` on a scale returns `400 invalid_request`. Null inherits the project default. For Discord-delivered projects, `full_text` and `select_menu` reject options that exceed the Discord character limit (40 for buttons, 50 for select menus) or carry more than one emoji that needs rendering (a button/select label has a single emoji slot) — the request returns `400 invalid_request` with a `violations` array. Use `emoji_only` (its answer list renders every emoji), shorten the option, or remove the extra emoji.
      - `color` string, nullable: Discord embed accent color as a hex string without the leading `#` (e.g. `e1287e`). Applies to content_block and action_block embeds when the project is Discord-delivered. **Requires Premium tier.** Ignored on read if community is on the Basic tier; blocks authored via the API will still persist the value, but it will not render until the community upgrades.
      - `image_url` string, uri, nullable: Image displayed above the question prompt. **Requires Premium tier** to persist via the editor; readable by all tiers.
      - `thumbnail_url` string, uri, nullable: Thumbnail image displayed top-right of the Discord embed. Available on all block types. **Requires Premium tier** to persist via the editor; readable by all tiers.
      - `prompt` string, required
      - `options` object[], nullable: Required for single_punch, multi_punch and ranking blocks — on a ranking these are the ITEMS to be ranked (2-20 of them). Optional for rating / opinion_scale / nps, where options are per-point presentation only: send one option per point with `value` set to that point's NUMBER (e.g. "1"…"5") carrying its `emoji` or its `label`. A `label` on every point is the Likert mechanism — the respondent taps "Strongly agree" while the stored answer stays the number 5, so averages keep working. Omit options entirely for a plain star or number scale.
        - `id` integer, nullable: Backend answer id. Omit (or send null) to insert a new option; include the existing id to update in place — preserves response.answer_id references through value / emoji / score / order edits. On reads this is always populated.
        - `value` string, required
        - `label` string, nullable
        - `emoji` string, nullable: Per-option emoji stored separately from `value`. Displayed alongside the label by the renderer. Discord buttons and select options have a single emoji slot, so this emoji plus any emoji in `value` must total one renderable emoji for `full_text`/`select_menu` styles; otherwise the write is rejected (use `emoji_only`).
        - `score_values` object, nullable: Per-bucket score weights for this option. Keys are bucket names (lowercased, spaces → underscores); values are numeric weights. Only present when `scoring.enabled` is true on the project.
        - `display_order` integer, nullable: Explicit 0-based display position within the question. Omit on create to default to array index.
        - `anchor_position` 'last' | 'null', nullable: When set to 'last', this option is pinned to the end of the list instead of being shuffled — the usual home for an "Other" or "None of the above" choice. Only has an effect when the block sets `randomize_options: true`; without it every option keeps its stored order anyway. Null = participates in normal ordering.
        - `is_deleted` boolean, nullable: Whether this option is hidden. A hidden option is out of the live list (nobody can pick it, it is left out of chart options) but keeps every response ever cast for it, so historical results still resolve. Omitting an option from a block PATCH still means "hide it if it has responses, delete it if it has none" — send `is_deleted: true` on an option you keep in the payload to say so explicitly, or `false` on a hidden one to restore it. Ignored when creating an option (no id). Reads never list hidden options. Destroying an option together with its responses is not available through the API.
      - `min` integer, nullable: Min selections (multi_punch) or min value (open_numeric)
      - `max` integer, nullable: Max selections (multi_punch) or max value (open_numeric)
      - `required` boolean, nullable: Whether a response is required to advance
      - `precondition` string, nullable: Raw skip-logic **hide expression**: when it evaluates to `true` the block is hidden; when `false` (or null) it is shown. **Prefer the structured `show_when` / `hide_when` fields below** — they compile to this and avoid its quoting/operator/casing pitfalls. Reference a prior block by its `name` (matched case-sensitively against the stored, title-cased name, e.g. a block named `NotifyPref` is referenced as `Notifypref`) or by position as `q1`, `q2`, … `qN`; named references are recommended because positions shift when blocks are added or removed. Operators: `=` (or `==`), `!=`, `<`, `>`, `>=`, `<=`, `in` (membership/substring), `AND`, `OR`, `NOT`. String literals use double quotes. To **show only when** a condition holds, wrap it: `NOT((Notifypref = "Other"))`. Invalid expressions (unknown variable, syntax error) are rejected with `400 invalid_request` at write time rather than silently failing open. See the Script tag description for the full reference.
      - `show_when` Condition: A skip-logic condition for `show_when` / `hide_when`. Either a **leaf** (`block` or `variable`, plus `op` and `value`) or a **group** (`all` for AND, `any` for OR, each a list of conditions).
        - `block` string, nullable: Name of the block to test. Resolved at runtime by its stored, title-cased name (you may pass any casing).
        - `variable` string, nullable: Raw variable token instead of a block name: `score`, `score_<bucket>`, `correct_answers`, `q3`, …
        - `op` 'eq' | 'ne' | 'lt' | 'gt' | 'gte' | 'lte' | 'in', nullable: Comparison. `in` requires a list `value`.
        - `value` unknown
        - `all` Condition[], nullable: AND of nested conditions.
        - `any` Condition[], nullable: OR of nested conditions.
      - `hide_when` Condition: A skip-logic condition for `show_when` / `hide_when`. Either a **leaf** (`block` or `variable`, plus `op` and `value`) or a **group** (`all` for AND, `any` for OR, each a list of conditions).
        - `block` string, nullable: Name of the block to test. Resolved at runtime by its stored, title-cased name (you may pass any casing).
        - `variable` string, nullable: Raw variable token instead of a block name: `score`, `score_<bucket>`, `correct_answers`, `q3`, …
        - `op` 'eq' | 'ne' | 'lt' | 'gt' | 'gte' | 'lte' | 'in', nullable: Comparison. `in` requires a list `value`.
        - `value` unknown
        - `all` Condition[], nullable: AND of nested conditions.
        - `any` Condition[], nullable: OR of nested conditions.
      - `action_kind` 'give_xp' | 'give_achievement' | 'give_role', nullable: Required for `action_block`. Null for all other block types.
      - `action_config` object, nullable: Configuration for `action_block`.
        - `xp_amount` integer, nullable: XP to grant for `give_xp`.
        - `xp_role_id` integer, nullable: Achievement ID to grant for `give_achievement`.
        - `role_id` string, nullable: Discord role snowflake to grant for `give_role`.
        - `fire_scope` 'respondent' | 'session', nullable: Idempotency scope. `respondent`: once total; `session`: once per completion session.
      - `correct_answer_ids` integer[], nullable: Canonical correct-answer field. Array of real `option.id` values that are correct. Single-element today (MVP single-select grading); array type future-proofs multi-select. Survives option reorder because ids are stable. Only meaningful when `scoring_enabled` is true on the project.
      - `correct_answer_index` integer, nullable: Deprecated — use `correct_answer_ids` instead. 0-based position into the current `options` array of the first correct answer. Still accepted on input (translated to the option's `id` at write time) and emitted on output for back-compat, but breaks when options are reordered. Only meaningful when `scoring_enabled` is true.
      - `when_correct` string, nullable: Message shown to the respondent immediately after they answer correctly. Only meaningful when `correct_answer_ids` is set. Supports template variables: [UserName], [answer] (their response), [score], [max_score], [score_correct], [max_score_correct], [correct_answers], [max_correct_answers], [score_<bucket>], [max_score_<bucket>], [score_correct_<bucket>], [max_score_correct_<bucket>], and answer-pipe tokens [BlockLabel]. `score_correct` counts only points earned on correctly-answered graded questions; `correct_answers` / `max_correct_answers` count graded questions right vs total.
      - `when_incorrect` string, nullable: Message shown to the respondent immediately after they answer incorrectly. Only meaningful when `correct_answer_ids` is set. Supports the same template variables as `when_correct`.
      - `scale` object, nullable: Whole-scale presentation for `rating` / `opinion_scale` / `nps`. The point RANGE lives in the block's `min` / `max`, not here; per-point emoji or labels live in `options`. Responses store the picked NUMBER, so a scale answer reads back as an integer and compares numerically in skip logic (e.g. an NPS detractor follow-up is `answer <= 6`). Ranges: `rating` starts at min=1 with 2-10 points (default 1-5); `opinion_scale` starts at 0 or 1 with 2-11 points; `nps` is locked to 0-10 with standard anchors. Out-of-range values return `400 invalid_request`.
        - `icon` 'numbers' | 'stars' | 'emoji', nullable: Point rendering. `stars` is the rating default; `numbers` the scale/NPS default. `emoji` requires one option per point carrying that point's emoji.
        - `label_left` string, nullable: Endpoint anchor shown at `min` (e.g. "Not at all likely"). Mutually exclusive with per-point `label`s: name the two ends, or name every point, not both. Fixed and translated automatically on `nps` — sending it there returns `400 invalid_request`.
        - `label_center` string, nullable: Optional midpoint anchor. Only meaningful on an odd point count, which is the only shape with a real middle.
        - `label_right` string, nullable: Endpoint anchor shown at `max` (e.g. "Extremely likely"). Same rules as `label_left`.
      - `randomize_options` boolean, nullable: Shuffle the option order per respondent, so the list a participant sees is not the order you authored. Valid on `single_punch`, `multi_punch` and `ranking`; sending it on `rating` / `opinion_scale` / `nps` returns `400 invalid_request` — a scale is ordered by definition and its points are never shuffled. It matters most on `ranking`, where a fixed order is not a cosmetic default but a measurement bias: whichever item sits first gets tapped first, and that bias lands in the average rank the results report. The shuffle is seeded per respondent per session, so revisiting a question does not reorder it mid-answer. Options carrying `anchor_position: "last"` are appended after the shuffle in their stored order. Defaults to false. Discord poll embeds are out of scope — one embed renders for every viewer, so there is no per-respondent order to vary.
      - `text_analysis` boolean, nullable: `open_text` only: send this block's answers for AI summary. `null` (or absent on read) follows the project's `settings.text_analysis`. Without an explicit value, a block whose question asks for personal data (name, email, phone, address...) is not summarized. Whatever this says, respondents are sent as pseudonyms, personal data in answers (emails, phone numbers, ids...) is removed before sending, and a block whose answers are mostly personal data is not summarized at all. Sending true/false on another block type returns `400 invalid_request`. On update: omit to leave it, send `null` to follow the project again.
      - `rank_top_n` integer, nullable: Ranking only: how many items the respondent is asked to put in order ("rank your top 3"). Omit or null to have every item ranked. It has its own field rather than riding `max`, which means max SELECTIONS on a multi_punch. A ranking submits complete or not at all — every item, or exactly the top N — so average ranks stay comparable across respondents. Each ranked item is stored as one response row whose `value` is the rank position (1 = first) and whose `option_id` is the item; unranked items in top-N mode have no row. Ranking has one interaction model (tap in order) on both Discord and web, so its `answer_style` is only about how the items are drawn: `full_text` (default) or `emoji_only`, which puts the item names in a list above the buttons and is the way to fit long names on Discord. `select_menu` returns `400 invalid_request` — a dropdown can't express an order. The item list is capped at 20 — a full grid of Discord buttons — and more than about 7 items is ranked unreliably, which is what `rank_top_n` is for.
      - `calculated_formula` string, nullable: Formula for `calculated_block` type. Evaluated in script order; earlier calculated blocks can be referenced by subsequent blocks via [BlockLabel]. Three expression types are supported: 1. `argmax([score_a], [score_b], ...)` — returns the display name of the bucket with the highest value. 2. `if <cond> then <val> else <fallback>` — keywords are case-insensitive. `<cond>` supports `>=`, `<=`, `!=`, `==`, `>`, `<` (numeric). A bare numeric condition is truthy if non-zero. 3. Arithmetic: any expression using `[variable]` tokens, digits, `+`, `-`, `*`, `/`, `(`, `)`. Unknown variables resolve to 0. Available variables: `[score]`, `[max_score]`, `[score_correct]`, `[max_score_correct]`, `[correct_answers]`, `[max_correct_answers]`, `[score_<bucket>]`, `[max_score_<bucket>]`, `[score_correct_<bucket>]`, `[max_score_correct_<bucket>]`, answer-pipe tokens `[BlockLabel]`, and previously evaluated `[CalcFieldName]` tokens. `score_correct` / `max_score_correct` accumulate only from correctly-answered graded questions; `correct_answers` / `max_correct_answers` are question counts (right vs total gradeable). Examples: `argmax([score_gryffindor], [score_slytherin], [score_ravenclaw])` `if [score] >= 15 then Excellent else Good` `[score] / [max_score] * 100` `if [correct_answers] == [max_correct_answers] then Perfect! else [correct_answers] out of [max_correct_answers]` `[score_correct] / [max_score_correct] * 100`
      - `position` integer, nullable: 0-based position in the script. Omit to append.
      - `continue` object, nullable: Advance settings for `content_block` and `action_block`.
        - `after` 'pause' | 'click': `pause`: auto-advance after `continue.pause` seconds; `click`: respondent must press a button to continue.
        - `pause` integer, nullable: Seconds to wait before auto-advancing. Only used when `after` is `pause`.
        - `label` string, nullable: Button label shown to the respondent. Only used when `after` is `click`. Defaults to `Got it 👍`.

## Other responses

- `400`: Invalid block definition
- `401`: Invalid or missing API key
- `403`: Forbidden
- `404`: Project or block not found
- `409`: Blocks cannot be modified while the project is active
- `429`: Rate limit exceeded — wait `retry_after` seconds before retrying. The same value appears in the `Retry-After` header.

---

[API](https://skmtc.dev/subo/apis/subo-api.md) · [All operations](https://skmtc.dev/subo/apis/subo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/subo/apis/subo-api/revisions/12eab2ae4505?raw)
