Skip to content

Preview one field's change across many assets

POST
/batch/asset-fields/preview

DRY RUN. Resolves a typed selection to a distinct, ordered set of assets, partitions every one of them, and mints a SINGLE-USE token that applyBatchAssetFieldEdit spends. Writes no field value, creates no vocabulary term and records no audit event (ADR 0019, #1173, #1119).

Expansion

The selection is a list of typed {kind, id} entries, and the SERVER expands post entries through post membership. A client never sends an expanded asset list, so a post that gained or lost a member between the operator’s last read and this call is expanded against the membership that exists NOW. Duplicates are collapsed to distinct assets — an asset reachable through two selected posts is one target, written once — and the resulting set is ordered BY ASSET ID, never by selection order, so a preview and its apply derive the identical ordered set. A selected post with no members contributes no targets and is reported in empty_posts.

Collections are not a selection kind here.

The two ceilings

At most 500 SELECTION ENTRIES, checked BEFORE any membership query runs, and at most 1000 DISTINCT EXPANDED TARGETS. Both refuse with 422 (selection_entry_ceiling / expanded_target_ceiling) rather than trimming: a partial expansion would silently write a different set than the operator selected. A single post whose membership alone exceeds the expanded ceiling is refused on the same terms.

The six partitions

Every target of a successful preview lands in EXACTLY ONE of would_change, no_op, refused, inapplicable, unreadable and unauthorized, and

expanded  = would_change + no_op + refused + inapplicable
            + unreadable + unauthorized
eligible  = would_change + no_op

refused is a TARGET-LEVEL refusal carrying a machine reason (a required field that the operation would empty, a retired vocabulary term chosen fresh on that target); it is never written at apply. unreadable is the read side of the anti-oracle rule: a caller who may write a field but may not READ it on this subject learns nothing about the stored value, not its emptiness, not its membership, not its set_at.

What the token binds

The ordered target set with each target’s partition; the field’s identity and its configuration fingerprint including the vocabulary options document; the mode; the exact canonical value; and the CALLER. Apply writes only the would_change subset and never re-expands. The token is never authority: the apply re-evaluates the caller’s CURRENT effective permissions, the field’s CURRENT configuration and the reference target’s CURRENT liveness, and refuses when any of them moved.

Takes the mode, the field, the proposed value and the typed selection. It takes NO operator reason and NO confirmation count: both belong to the apply, which is the call that actually changes records.

object
mode
required

What the batch does to each target’s value.

  • overwrite — replace the value on every eligible target. Reports no_op as ZERO even against a target already holding the proposed value: a set advances set_at and writes a history row, so it is a real change to the record.
  • fill_empties — write only where the value is semantically EMPTY. A target holding any non-empty value is no_op. FALSE is a real boolean value and is never empty.
  • append — add the proposed terms to the existing set.
  • remove — take the proposed terms out of the existing set.

append and remove are multi_select ONLY. Every other type refuses them batch-wide with 422 mode_not_supported_for_type rather than being given an invented set semantics.

string
Allowed values: overwrite fill_empties append remove
field_id
required
string format: uuid
selection
required

At least one entry, at most 500. The entry ceiling is checked BEFORE any membership query runs.

Array<object>
>= 1 items

ONE typed selection entry. Typed because the shipped selection store is a bare list of ids with no kind discriminator, and a server that guessed would expand a post id as an asset id and silently write nothing.

object
kind
required

What a selection entry names. post entries are expanded server-side through post membership; asset entries are already targets. Collections are not a selection kind.

string
Allowed values: asset post
id
required
string format: uuid
value
required

The proposed value, in whichever typed member matches the field’s declared type — the same mapping AssetFieldValueWrite uses: text / longtext / rich_text / select / tree → value_text; number / boolean → value_num; date / datetime → value_date; multi_select → value_options; reference → value_ref.

The five types that CANNOT express a semantically empty value (number, boolean, date, datetime, reference) must carry their typed member; omitting it is 400 value_type_mismatch. A multi_select with an empty option set is likewise 400 value_type_mismatch, mirroring the single-target writer.

The three text-shaped types and the two single-slug vocabulary types CAN express emptiness, and what happens then is a property of the field rather than of the batch: a REQUIRED field refuses it (422 required_value_empty), and an OPTIONAL one stores it. Nothing trims: a text field given ” ” stores ” ”. select and tree do NOT treat whitespace lexically — "" never enters the vocabulary pipeline and stores as ”, while ” ” enters it as a slug, matches nothing, and is 422 unknown_slug.

object
value_text
string
nullable
value_num
number
nullable
value_date
string format: date-time
nullable
value_options
Array<string>
nullable
value_ref
string format: uuid
nullable

Preview computed. Nothing was written.

A computed preview and the single-use token that applies it. Nothing has been written: no field value, no vocabulary term, no audit event.

object
token
required

OPAQUE. Bound to the caller, single-use, and the only thing the apply needs. Its structure is not a contract and a client must not parse it.

string
expires_at
required
string format: date-time
operation_id
required

Ties this preview to the audit envelope its apply writes.

string format: uuid
mode
required

ECHOED so an operator surface can show what it is about to do without holding its own copy. The apply reads the mode from the token and never from the request.

string
Allowed values: overwrite fill_empties append remove
field_id
required
string format: uuid
field_code
required
string
field_type
required
string
resolved_value
required

The CANONICAL value the apply will store, after vocabulary canonicalisation and rich-text sanitising — aliases and merge tombstones already followed to their targets, casing and whitespace variants already collapsed onto the existing slug, duplicates already deduped on the canonical slug with order preserved. Preview and apply store the same bytes.

object
value_text
string
nullable
value_num
number
nullable
value_date
string format: date-time
nullable
value_options
Array<string>
nullable
value_ref
string format: uuid
nullable
mintable_terms

Terms that DO NOT YET EXIST in an open vocabulary and would be created by the apply. Listing them is not creating them: a preview mutates no options document. A term is minted at apply ONLY IF at least one target successfully stores it.

Array<string>
selection_entry_count
required
integer
empty_posts

Selected posts that hold no members, and so contributed no targets.

Array<string>
counts
required

The six partition counts plus the two derived totals, so a client can assert the reconciliation rather than recompute it:

expanded = would_change + no_op + refused + inapplicable
           + unreadable + unauthorized
eligible = would_change + no_op
object
expanded
required
integer
eligible
required
integer
would_change
required
integer
no_op
required
integer
refused
required
integer
inapplicable
required
integer
unreadable
required
integer
unauthorized
required
integer
targets
required

Every expanded target, ordered by asset id.

Array<object>

One expanded target and its partition. if_unchanged_since appears ONLY on a would_change target and is that value row’s own set_at — absent where no row exists yet. It is never emitted for an unreadable target, which would disclose that the value exists and when it was last written.

object
asset_id
required
string format: uuid
partition
required

Which of the six partitions a previewed target belongs to. Every target of a successful preview belongs to exactly one.

  • would_change — the apply will write this target.
  • no_op — eligible, and the mode leaves it unchanged.
  • refused — refused ON ITS MERITS for this target, with a machine reason. Never written at apply. Preview-only: a value that MOVES trips the concurrency guard and reports conflict, and a definition or vocabulary change is a batch-wide 409, so nothing can turn into a refused between preview and apply.
  • inapplicable — the field does not apply to this asset’s type. Not an error.
  • unreadable — the caller may not READ this field on this subject. Nothing about the stored value is disclosed.
  • unauthorized — the caller may not write this target: no bulk instrument in its scope, or the ordinary subject authority rule refuses them.
string
Allowed values: would_change no_op refused inapplicable unreadable unauthorized
refusal_reason
string
Allowed values: required_would_empty vocabulary_retired_not_held
if_unchanged_since
string format: date-time
nullable

The request could not have been valid for any state of the system.

ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.

Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.

expected and actual are populated where a number is the whole story: the count a confirm_count_mismatch wanted, and the ceiling and the measured size on the two ceiling refusals. field is the field’s code where one is known.

object
error
required
string
reason
required

Why a batch preview or apply was refused. The status code carries the class and this carries the cause.

400 — the request could not have been valid for ANY state: empty_selection, unknown_mode, unknown_selection_kind, value_type_mismatch, token_required, reason_payload_too_large, reason_required, reason_too_long, confirm_count_invalid, confirm_count_required, confirm_count_not_applicable, confirm_count_mismatch.

403 — authorization, INCLUDING anything not provably this caller’s token: preview_token_invalid, bulk_capability_required, field_write_capability_required, vocabulary_extend_required.

422 — well formed, understood, and refused on its merits: field_archived, field_read_only, field_mirrored, mode_not_supported_for_type, pattern_mismatch, required_value_empty, fill_empties_value_empty, unknown_slug, archived_slug, dangling_reference, selection_entry_ceiling, expanded_target_ceiling.

409, apply only — the token is provably this caller’s own but is spent or stale, or the world moved; the remedy is to re-preview: preview_consumed, preview_expired, definition_drift, vocabulary_drift, reference_invalidated.

dangling_reference (preview) and reference_invalidated (apply) are deliberately NOT the same code. The first says the proposed target never resolved; the second says it resolved when the operator looked and has since stopped. They call for different actions and a client that cannot tell them apart cannot say which.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

On expanded_target_ceiling, the number of SELECTION ENTRIES that expanded to the over-ceiling target count — so an operator can see that a handful of posts reached thousands of assets.

integer
nullable

Authentication required, missing, or invalid

object
error
required

Human-readable error summary

string
Example
the request could not be completed
Example
{
"error": "authentication required: sign in and retry with a valid session or API token"
}

The caller may not reach for this instrument, or may not write this field.

ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.

Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.

expected and actual are populated where a number is the whole story: the count a confirm_count_mismatch wanted, and the ceiling and the measured size on the two ceiling refusals. field is the field’s code where one is known.

object
error
required
string
reason
required

Why a batch preview or apply was refused. The status code carries the class and this carries the cause.

400 — the request could not have been valid for ANY state: empty_selection, unknown_mode, unknown_selection_kind, value_type_mismatch, token_required, reason_payload_too_large, reason_required, reason_too_long, confirm_count_invalid, confirm_count_required, confirm_count_not_applicable, confirm_count_mismatch.

403 — authorization, INCLUDING anything not provably this caller’s token: preview_token_invalid, bulk_capability_required, field_write_capability_required, vocabulary_extend_required.

422 — well formed, understood, and refused on its merits: field_archived, field_read_only, field_mirrored, mode_not_supported_for_type, pattern_mismatch, required_value_empty, fill_empties_value_empty, unknown_slug, archived_slug, dangling_reference, selection_entry_ceiling, expanded_target_ceiling.

409, apply only — the token is provably this caller’s own but is spent or stale, or the world moved; the remedy is to re-preview: preview_consumed, preview_expired, definition_drift, vocabulary_drift, reference_invalidated.

dangling_reference (preview) and reference_invalidated (apply) are deliberately NOT the same code. The first says the proposed target never resolved; the second says it resolved when the operator looked and has since stopped. They call for different actions and a client that cannot tell them apart cannot say which.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

On expanded_target_ceiling, the number of SELECTION ENTRIES that expanded to the over-ceiling target count — so an operator can see that a handful of posts reached thousands of assets.

integer
nullable

Resource not found

object
error
required

Human-readable error summary

string
Example
the request could not be completed

Well formed and understood, and this definition, value or scale refuses it.

ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.

Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.

expected and actual are populated where a number is the whole story: the count a confirm_count_mismatch wanted, and the ceiling and the measured size on the two ceiling refusals. field is the field’s code where one is known.

object
error
required
string
reason
required

Why a batch preview or apply was refused. The status code carries the class and this carries the cause.

400 — the request could not have been valid for ANY state: empty_selection, unknown_mode, unknown_selection_kind, value_type_mismatch, token_required, reason_payload_too_large, reason_required, reason_too_long, confirm_count_invalid, confirm_count_required, confirm_count_not_applicable, confirm_count_mismatch.

403 — authorization, INCLUDING anything not provably this caller’s token: preview_token_invalid, bulk_capability_required, field_write_capability_required, vocabulary_extend_required.

422 — well formed, understood, and refused on its merits: field_archived, field_read_only, field_mirrored, mode_not_supported_for_type, pattern_mismatch, required_value_empty, fill_empties_value_empty, unknown_slug, archived_slug, dangling_reference, selection_entry_ceiling, expanded_target_ceiling.

409, apply only — the token is provably this caller’s own but is spent or stale, or the world moved; the remedy is to re-preview: preview_consumed, preview_expired, definition_drift, vocabulary_drift, reference_invalidated.

dangling_reference (preview) and reference_invalidated (apply) are deliberately NOT the same code. The first says the proposed target never resolved; the second says it resolved when the operator looked and has since stopped. They call for different actions and a client that cannot tell them apart cannot say which.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

On expanded_target_ceiling, the number of SELECTION ENTRIES that expanded to the over-ceiling target count — so an operator can see that a handful of posts reached thousands of assets.

integer
nullable