Recipe: read & update records through the content routes

Recipe: read & update records through the content routes

Recipe: read & update records through the content routes

💡

Prompt example

Read a record's fields, rename it, add it to a collection, and create a new record then populate it — all through the generic content routes, with the property keys mapped back to the entity map.

After search, content updating is the most-used surface. The content routes are the platform's generic CRUD over any entity — the same three calls work for an asset, a genre, a language, etc. The only things that change per entity are the contentDefinitionId and the field names in properties.

Safety (Class C). The generic route can write to shared/global records, so it is non-prod only. When contentDefinitionId is the asset entity, the contentId must be within your run-root anchor (then it behaves like update_asset, Class A). Tear down what you create.

How fields map to the entity map

Everything keys off reference/schema.md (the entity map):

  1. contentDefinitionId = the entity's id. Pick the entity row in the Entities table and copy its contentDefinitionId (e.g. asset3ff29f61-bd0b-4c17-b855-49db5a292aeb, languagee4b10c04-1878-4830-a115-e42d52705059).
  2. properties keys = that entity's field names. They are exactly the rows in the entity's In-scope entity fields table. Only fields marked writable = yes can be set; the type column tells you the value shape.
  3. Reference / lookup fields take a {id, description} shape. A field that points at another entity (e.g. assignedPerson, or one collections/tags/relatedContent element) is written as {"id": "<guid>", "description": "<label>"}. Multi-valued list<guid> fields are arrays of those objects. (A bare GUID is expanded into {id, description} by reading the target's displayName.)
  4. The record envelope returned by get_content is {contentId, contentDefinitionId, languageId, properties{...}}; the field bag lives under properties.

Update semantics (read-modify-write)

update_content is a partial patch: the SDK GETs the live record, deep-merges your properties onto it, then PUTs the whole body. So you pass only the fields you change.

  • Scalars and objects are replaced only when they actually differ.
  • Arrays/lists are element-wise replaced and truncated to the length you pass. To append to a multi-valued field you must read the current list and send existing + new; sending a shorter list deletes the tail.

Python

# Components used: get_content, update_content, create_content, delete_content, search.

def read_record(sdk, content_id, content_definition_id):
    """Return the record's properties bag (keyed by entity field names), or {}."""
    rec = get_content(sdk, content_id, content_definition_id, None) or {}
    return rec.get("properties", {})

def find_content_id(sdk, content_definition_id, master_id, language_id=None):
    """Resolve a record's contentId from its entity + business key (search)."""
    flt = [
        {"fieldName": "contentDefinitionId", "operator": "Equals", "values": content_definition_id},
        {"fieldName": "masterId", "operator": "Equals", "values": master_id},
    ]
    if language_id:
        flt.append({"fieldName": "languageId", "operator": "Equals", "values": language_id})
    items = (search(sdk, filters=flt, size=1) or {}).get("items", [])
    return items[0]["id"] if items else None

def ref(target_id, description=None):
    """Wire shape for a reference/lookup field: {id, description} (description optional)."""
    return {"id": target_id, "description": description} if description else {"id": target_id}

def rename_record(sdk, content_id, content_definition_id, new_name):
    """Patch a single scalar field by its entity field name (writable=yes only)."""
    return update_content(sdk, content_id, content_definition_id, {"displayName": new_name})

def add_to_collection(sdk, content_id, content_definition_id, collection_id, label=None):
    """Append a collection ref WITHOUT truncating: read current, add, write back."""
    current = read_record(sdk, content_id, content_definition_id).get("collections", [])
    updated = current + [ref(collection_id, label)]
    return update_content(sdk, content_id, content_definition_id, {"collections": updated})

def create_then_populate(sdk, content_definition_id, properties, language_id=None):
    """Create a blank record from an entity template, then populate it; return its contentId."""
    blank = create_content(sdk, content_definition_id, language_id) or {}
    content_id = blank.get("contentId")
    update_content(sdk, content_id, content_definition_id, properties, language_id)
    return content_id

def delete_record(sdk, content_id, content_definition_id):
    """Tear down a record created via create_content (Class C cleanup)."""
    return delete_content(sdk, content_id, content_definition_id)

JavaScript

// Components used: getContent, updateContent, createContent, deleteContent, search.
// search advanced opts ride the trailing opts object (see recipes/search-patterns.md).

export async function readRecord(sdk, contentId, contentDefinitionId) {
    // Return the record's properties bag (keyed by entity field names), or {}.
    const rec = (await getContent(sdk, contentId, contentDefinitionId, null)) || {};
    return rec.properties || {};
}

export async function findContentId(sdk, contentDefinitionId, masterId, languageId = null) {
    // Resolve a record's contentId from its entity + business key (search).
    const flt = [
        { fieldName: "contentDefinitionId", operator: "Equals", values: contentDefinitionId },
        { fieldName: "masterId", operator: "Equals", values: masterId },
    ];
    if (languageId) flt.push({ fieldName: "languageId", operator: "Equals", values: languageId });
    const res = await search(sdk, null, flt, null, 1);
    const items = res ? res.items : [];
    return items.length ? items[0].id : null;
}

export function ref(targetId, description = null) {
    // Wire shape for a reference/lookup field: {id, description} (description optional).
    return description ? { id: targetId, description } : { id: targetId };
}

export async function renameRecord(sdk, contentId, contentDefinitionId, newName) {
    // Patch a single scalar field by its entity field name (writable=yes only).
    return await updateContent(sdk, contentId, contentDefinitionId, { displayName: newName });
}

export async function addToCollection(sdk, contentId, contentDefinitionId, collectionId,
        label = null) {
    // Append a collection ref WITHOUT truncating: read current, add, write back.
    const current = (await readRecord(sdk, contentId, contentDefinitionId)).collections || [];
    const updated = [...current, ref(collectionId, label)];
    return await updateContent(sdk, contentId, contentDefinitionId, { collections: updated });
}

export async function createThenPopulate(sdk, contentDefinitionId, properties,
        languageId = null) {
    // Create a blank record from an entity template, then populate it; return its contentId.
    const blank = (await createContent(sdk, contentDefinitionId, languageId)) || {};
    const contentId = blank.contentId;
    await updateContent(sdk, contentId, contentDefinitionId, properties, languageId);
    return contentId;
}

export async function deleteRecord(sdk, contentId, contentDefinitionId) {
    // Tear down a record created via createContent (Class C cleanup).
    return await deleteContent(sdk, contentId, contentDefinitionId);
}

Notes

  • Find vs. read. Use get_content when you already hold the contentId. When you only have a business key (a masterId, a display name), search the entity by contentDefinitionId Equals plus that key and read the matching item's id — that id is the contentId (see recipes/search-patterns.md).
  • masterId groups language variants. A record localized into several languages shares one masterId; each variant has its own id/languageId. Pass language_id to target a specific variant (or None/null for the default).
  • Append safely. Because list fields are truncated to the length you send, the add_to_collection pattern (read current → concat → write) is the safe way to grow any multi-valued field (collections, tags, relatedContent, …).
  • Tear down what you create. Records made with create_content / create_then_populate are Class C side effects — delete them with delete_content (the delete_record helper above) when done. The asset entity is the exception: confine its content_id to your run-root anchor and let the run-root teardown reclaim it.
  • Asset metadata shortcut. For the common asset cases (rename, custom properties, language, security) prefer the purpose-built update_asset / update_asset_language / update_asset_security components — they wrap the same writes with asset-aware, Class A/B confinement. Reach for update_content when you need an arbitrary entity field not exposed by those wrappers.