zudo-slack-wisdom
GitHub repository

Type to search...

to open search from anywhere

Schema Mutability — What Can (and Cannot) Change After Creation

Why select-column options are effectively closed post-create, and the narrow exceptions

Overview

The blanket claim "the schema is fixed at creation and cannot be altered via API" is close to true but not exactly true. The nuance matters for anyone designing a select column that mirrors an external status field: over-provisioning options at create time is cheap; adding one later is not.

slackLists.update Accepts Only Four Fields

slackLists.update is the only method that mutates an existing list's top-level shape, and it accepts exactly id, name, description_blocks, and todo_mode. None of the 12 slackLists.* methods touches a column's type, name, or option set directly.

No Documented Path Adds, Renames, or Removes a Select Option

Writes are validated against the column's existing definition: sending an option value that is not in options.choices[] fails with invalid_option_id (the items.create variant of the same error reads "Option ID provided does not match column definition"). The cell shape {column_id, select: string[]} has no structural room to carry a new option's label or color — there is nowhere in the write contract to define an option, only to select one that already exists.

This is reinforced by what's absent from the error registry: there is no option_id_to_create anywhere across the roughly 50 documented Lists errors, while row and column creation analogues (row_id_to_create, column_id_to_create) both exist. That asymmetry is structural evidence the option set is deliberately closed to the API — row and column creation got an escape hatch; option creation did not.

The One Real Exception: todo_mode

slackLists.update with todo_mode: true causes task-tracking columns to be added to an existing list — Completed (todo_completed), Assignee (todo_assignee), and Due date (todo_due_date) — per the method's own usage prose. This is the one documented, post-creation schema addition in the entire API surface. Conversely, creating a list with todo_mode: false is safe to treat as final for those three columns.

column_id_to_create — An Undocumented Crack, Not a Feature

column_id_to_create is named only inside the column_id_not_provided error string ("The column_id or column_id_to_create field must be provided"). It has no argument definition on any method's docs page, no worked sample, and is absent from all three official SDKs' types.

Undocumented — do not rely on this

column_id_to_create has zero confirmed real-world usage (code search finds only mirrors of the same error string, never an actual call site) and no live-API verification exists either way. Treat it as an unproven validator artifact, not a supported column-creation path. If a new column is genuinely needed, use the sanctioned recreate-with-copy_from_list_idworkaround below instead of probing this field in production.

Humans Can Still Edit the Schema in the UI

Everything above describes the API surface. A human with edit access can add options, delete options, and rename labels directly in the Slack UI at any time — schema mutability is "closed to the API," not "closed, full stop."

This has one direct operational consequence: a cached valuelabel map can go stale mid-life. A deleted option starts returning invalid_option_id on writes that used to succeed; a label rename is safe by contrast, because writes address value, never label. On an unexpected invalid_option_id, re-read list_metadata.schema via items.info rather than trusting a hardcoded constant that assumes the schema never changed.

Detecting Drift Proactively

Reacting to invalid_option_id after a write already failed works, but it means a business state can silently remap onto the wrong option for however long it takes a write to hit that particular slug — a status column with low write volume could carry a stale assumption for days. The more defensive pattern is to check for drift before it can affect a write at all:

  1. Fingerprint the resolved contract at registration. Hash the parts of list_metadata.schema that a write actually depends on — column ids and their types, each select column's format, and every option's value/label/color triplet — with SHA-256, and persist that fingerprint alongside the list_id when a List is registered for sync.

  2. Re-verify every sync cycle, before writing. Call items.info, recompute the same fingerprint from the fresh list_metadata.schema, and compare it to the persisted value before the cycle's writes go out — not after one has already failed.

  3. On any mismatch, fail closed. Mark the List registration unhealthy, halt sync against it, and persist a bounded error code for operators to act on. Do not attempt to auto-adapt to the new schema: a renamed option, a re-typed column, or a reordered option list can silently remap what a business state means, and a sync loop that "helpfully" reconciles against the new shape can commit that remap without a human ever reviewing it. Fail-closed here is a direct extension of the same policy this section's own Errors Reference applies to every other permanent-configuration error: retrying (or auto-adapting) cannot fix a schema that changed out from under you — only a human, or a deliberate redeploy, can.

One deployment nuance is worth a sentence: fingerprint composition is itself a compatibility surface. During an additive migration window — rolling out a Worker version that adds a new column to the schema it registers — an already-deployed Worker and the newly-deployed one will compute different fingerprints for the same list unless the just-added column is excluded from the hash until the rollout completes. Otherwise the two versions disagree about what the "correct" schema is mid-rollout, and one of them fails closed against a schema that is, from its own valid perspective, unexpectedly different — even though nothing is actually wrong.

The Sanctioned Workaround: Recreate via copy_from_list_id

The documented API path for "I need one more option" is a new list: slackLists.create with copy_from_list_id and include_copied_list_records. You cannot pass both copy_from_list_id and schema in the same call — that combination fails with invalid_copy_and_schema_args — so the new list starts as an exact schema copy of the source, and any option additions still have to happen by hand in the UI afterward, or by recreating again with a hand-edited source list. Either way, this mints a brand-new list_id and new column_ids for every column, which you have to re-persist.

Computed Columns Are Unwritable

created_by, last_edited_by, created_time, and last_edited_time are computed by Slack and always return uneditable_column on a write attempt, regardless of schema state.

Limits to Design Around

From slackLists.create's documented constraints:

LimitValue
Options per select column100
Selected values per cell50
Allowed chip colorsindigo, blue, cyan, pink, yellow, green, gray, red, purple, orange, brown

Practical Consequence: Over-Provision at Creation Time

Because no API path edits an existing column's option set, the working pattern is to over-provision status options when the list is created — bake in spare slugs you don't use yet, up to the 100-option cap. Adding a genuinely new option later means either a human edits the list by hand in the UI, or the list gets rebuilt via the copy_from_list_id workaround above.

For the write contract that this closed option set constrains, see Writing List Items and Select Columns. For invalid_option_id and the other errors this page's constraints produce, see Errors Reference.

Revision History

CreatedUpdated