On this page

Every write to an entity’s records follows the same rules for finding the record it addresses and for what an update changes. This page states those rules for a single-record upsert or delete and for an entity synchronization, and lists where a bulk load counts a row instead of refusing it.

OperationWritesRole
POST /api/golden/{entity}/upsertOne record of an entitySTEWARD or ADMIN
DELETE /api/golden/{entity}/deleteDeletes one record of an entitySTEWARD or ADMIN
PUT /api/entities/synchronizeAn entity’s records, from its configured sourcesADMIN
POST /api/tables/load, POST /api/tables/transformA table that belongs to no entityADMIN

An entity’s data is written only through the entity: its synchronization or the upsert. A table load or transform whose target is an entity’s table is refused; see Table loads.

The upsert request

The request carries one record and optional controls:

MemberDefaultEffect
recordRequiredThe record to write. Its _id, when present, is read as a string, so 42 and "42" address the same record
insertfalseCreate a record; refuse the request when the key already names one
updatefalseUpdate an existing record; refuse the request when the key names none
transformationNoneA transformation resource applied to record before the entity’s pipeline. A name that is not a transformation answers 404
commentNoneA comment recorded with the change where auditing is enabled

Setting both insert and update is a 400.

The response reports inserted, updated, the written record, and messages that explain any choice Golden made, such as an _id it ignored. An update that changes nothing still answers updated: true, with a message saying the record already held these values.

How the upsert finds its record

Golden computes the record’s identity from the body as the entity’s pipeline leaves it, after any requested transformation. Two spellings of one key that the pipeline brings to the same canonical value, such as b-123 and B123 under a cleaner that canonicalizes them, are one key. The identity comes from the dataset identity: DEFAULT derives none, COLUMN joins the identity columns, and SCRIPT evaluates the identity script.

The key is resolved in this order:

  1. A live _id addresses the write, whatever its shape.
  2. An _id in history answers for itself: 409 with survivorId when a merge absorbed it, 409 with restoredIds when it was the survivor of a merge that was undone, or 410 when it was deleted. This holds with or without insert: true, on every dataset identity type.
  3. Otherwise, on a COLUMN or SCRIPT dataset whose body yields a usable identity, the identity addresses the write. A live record is updated, a record in history answers as in step 2, and a free identity is created, so the new record’s _id equals its identity. An _id Golden never issued gives way to the identity, and messages names it.
  4. Otherwise an _id that names no record is a 409 saying to omit _id to create a record.
  5. Otherwise the record is created under a new identifier. On a COLUMN or SCRIPT dataset whose identity is absent or has a blank component, messages says so, and that sending the same body again creates another record.

A merge survivor carries a new identifier by design and is written through that _id; its derived identity is not a write address.

Situations and answers

SituationAnswer
DEFAULT dataset, an _id no record has409: no record has the id; omit _id to create a record
No _id, an identity that names a live record (COLUMN or SCRIPT)That record is updated
An _id Golden never issued, beside an identity that names a live recordThe identity’s record is updated; messages names the ignored _id and the identity used
An _id Golden never issued and no identity in the body (COLUMN or SCRIPT)409 naming the dataset’s identity and saying it arrived empty
insert: true with a live _id or a live identity409 with existingIds naming that record
insert: true with a free identityCreated under the identity
insert: true with an _id or identity in historyThe same answer as without the flag: 409 with survivorId, 409 with restoredIds, or 410
insert: true with an _id Golden never issued, on DEFAULT or a COLUMN identityThe _id is not used as the address; messages names it and says what the record was created under
insert: true on a SCRIPT identity that reads _idThe _id is not the address but is an input: the identity computed from it answers as above. A free one is created under it
update: true and nothing found409 naming the key the request sent, or saying it sent none
The survivor of an undone merge409 with restoredIds naming the restored records that are live now; 410 when none is
No _id, or an _id that names no record (or insert: true), with an identity value that cannot key a record400 naming the column or the identity script, and the reason. Nothing is written
An identity script that fails400 naming the failure. Nothing is written

Identity values that cannot key a record

An identity value cannot key a record when it is longer than 255 characters, contains /, \, ?, #, %, [, ] or a control character, or is . or ...

A COLUMN identity of several columns joins their trimmed values with ::. No component may contain :: or start or end with :, so a key splits one way only; the rule applies to a one-column identity too. A SCRIPT identity is one value and is not split.

Read a conflict

A 409 from an upsert or a delete carries a conflict body. The member that is filled says what to do next:

MemberMeaningNext step
survivorIdA merge absorbed the record the request namedWrite to the survivor
restoredIdsThe request named the survivor of a merge that was undone; these are the restored records that are live nowChoose the restored record to write to
existingIdsA live record already holds the key or the source pairUse that identifier
existingIdsTotalAccompanies existingIds: how many records hold it. It exceeds the length of existingIds only when the list was cut—
NoneThe key the request named is wrongCorrect the request

existingIds names at most 20 live records, sorted by identifier; when it is cut, the message says how many of the total it names. A 410 means the addressed record was deleted. See conflict handling for the general status conventions.

What an update writes

An update is a partial merge:

  • A member left out of the body is untouched.
  • A member sent as null is emptied.
  • A member cannot be removed from the record, only emptied.

The same rule applies to an entity synchronization and a table load; see Empty values in a load for how each source format expresses “empty” and “untouched”.

Two parts of a record are fixed after creation:

  • Provenance. An update’s _source may restate entries the record already has; they are compared trimmed and change nothing. A new or different entry is a 400.
  • Raw content. An update carrying _xml, _text or _binary is a 400 naming those members; nothing is written.

A malformed _source is a 400 naming the first problem found, with the entry and the member, in the caller’s language.

Golden manages _metadata, _audit, quality and search evidence itself; a request carrying _audit is a 400. Build a write from the record’s business fields and _id rather than from a complete read response; see Record metadata.

Provenance is a uniqueness guard

A _source entry pairs a source (_src) with its record reference (_src_id). Golden stores the caller’s entries trimmed. A pair is never a second address: a creation whose pair is already held is refused, and it never updates the holder.

The guard applies to every creation keyed by a new identifier: a DEFAULT dataset, and a COLUMN or SCRIPT body with no usable identity. A creation under a free identity is not checked by pair, because the identity is its key. Only real pairs are checked — an entry with a non-blank _src_id whose source is not _golden — so a dataset’s default origin and a source sent without a reference are never refused.

Holder of the pairAnswer
One or more live records409 with existingIds and existingIdsTotal
Only records in historyEach holder is followed as an _id in history is: to a live survivor (409 with survivorId), to an undone merge (409 with restoredIds), or to a deletion (410). Of several holders, a live survivor wins over an undo, which wins over a deletion

Two concurrent first sends of one pair end in one record and one 409.

Writes under a data scope

A caller restricted by a data scope is never told about a record outside it. Every answer above that would name or reveal such a record is that record’s 404:

  • An update of a live record outside the scope is a 404 before any refusal of the update, such as a new _source entry, a changed identity or a raw content member.
  • A cut existingIds checks every holder, not only the 20 it names; any holder outside the scope answers the 404, so existingIdsTotal never reveals one.
  • A survivor or restored record outside the scope answers as an unknown id.

Delete a record

DELETE /api/golden/{entity}/delete removes one current record. The record identifier and an optional comment travel in a JSON body:

curl --fail-with-body --silent --show-error \
  --request DELETE \
  --header "Authorization: Bearer ${GOLDEN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"recordId":"<record id>","comment":"<reason>"}' \
  "${GOLDEN_URL}/api/golden/{entity}/delete"

A successful delete answers deleted: true and the deleted record. The delete answers history the way the upsert does and never follows a merge to delete the survivor.

SituationAnswer
A live id, including one sent with surrounding spacesThe record is deleted
An id a merge absorbed409 with survivorId; the survivor is not deleted
The survivor of an undone merge409 with restoredIds; 410 when none of them is live
A deleted id, including a second delete of the same id410
An unknown or blank id, or one outside the caller’s data scope404, naming only the id sent

Tables without history

Golden can answer for an absorbed or deleted record only on a table that keeps history. On a table without history, a merge deletes the records it absorbs, so an upsert or delete of an absorbed or deleted id is treated as unknown, a search for it finds nothing, and reloading an absorbed record’s identity recreates it beside its survivor. Choose history when the table is created for an entity that is reloaded and merged.

Entity synchronization

A synchronization follows the upsert’s rules wherever a bulk load can. Where the upsert answers a refusal, a synchronization counts the row and continues.

RowResult
An identity column the pipeline canonicalizesIdentity computed from the canonical values: two spellings of one key land on one record
DEFAULT dataset, a live _idThat record is updated
DEFAULT dataset, an _id in history (merged, deleted, or an undone survivor)Left out of the run and counted, as on COLUMN and SCRIPT: a load never resurrects or duplicates a record
DEFAULT dataset, an _id Golden never issuedCreated under a new identifier and counted as created without identity
Identity absent or partialCreated under a new identifier and counted as created without identity
No _id (no column, a blank cell, a JDBC row without its identifier column) on a SCRIPT identity that reads itCreated under a new identifier and counted as created without identity. Its provenance entry names the source without a reference
An identity value that cannot key a recordNot written; counted as rejected
An update whose row carries _source entriesEntries already stored change nothing; new entries are not added, and the row is still written. Golden still records its own source entry
An update whose row carries _xml, _text or _binaryThose members are ignored; the rest of the row is written

Re-importing an export of a DEFAULT entity is idempotent.

The run’s completion message reports the rows created without identity and the rejected rows when either count is not zero, and the entity load’s output carries them as unresolved and rejected. The run does not list the rejected rows; check the source’s identity values against the rules above.

Empty values in a load

On a synchronization a source row is a full snapshot of the columns it carries:

SourceEmptied on updateLeft untouched
JDBCA SQL NULL, or an empty arrayA column the query does not select
CSVA blank cellA column the file does not have
JSON"column": nullA member absent from the object

Table loads

A table load or transform writes only into a table that belongs to no entity. On such a table, "column": null or a JDBC NULL in an update empties the member, as for an entity.

RequestAnswer
POST /api/tables/load whose sinkTable belongs to an entity409; nothing is queued. Load the data through the entity’s synchronization
POST /api/tables/transform over an entity’s table409
A run whose target became an entity’s table while it waitedFails when it starts, before reading anything, with the same message
An entity’s table as the source of a load, or exportedAllowed

The table load’s completion summary lists rows it created without identity and rows it rejected. A table destination follows the same rule: its target table must belong to no entity.

Records stored before Golden 3.1.0

Golden 3.1.0 computes the identity after the entity’s pipeline. Some records stored by an earlier release keep their earlier key:

  • A live record created under a generated identifier while its identity column had a value, or stored under a raw spelling its canonical identity no longer equals, is not found by its identity. Its first identity-addressed write creates a record under the identity, once; later writes converge on the new record.
  • The survivor of a merge undone before Golden 3.1.0 keeps answering 410.
  • Rows loaded by an earlier release whose provenance states Golden’s own identifier as the source reference are not rewritten.

Review such records before relying on identity-addressed writes for them.

Golden 3.1.0 · Published 2026-10-20