Metadata Values
The values array on an asset displays metadata in the CI HUB asset detail panel. Each entry is a key-value row. Grouping, nesting, and rich content are controlled by conventions in the name and type fields.
Value structure
Each entry in the values array:
{
id: string, // unique identifier
name: string, // display label
type: 'TEXT' | 'ASSET' | 'DRAFT_JS' | 'DATE' | 'DATETIME' | 'TIME',
value: string | number | object // depends on type
}Value types
| Type | value field | UI display |
|---|---|---|
TEXT | string | number | Plain text row |
DATE | string | Formatted date |
DATETIME | string | Formatted date + time |
TIME | string | Formatted time |
DRAFT_JS | string | Rich text (Draft.js serialized content) |
ASSET | mini-asset object | Embedded asset with thumbnail, download, and conversions |
TEXT — the most common type
Most metadata entries use TEXT. The value can be a string or number:
values: [
{ id: 'description', name: 'Description', type: 'TEXT', value: item.description },
{ id: 'pageCount', name: 'Page Count', type: 'TEXT', value: item.pageCount },
{ id: 'duration', name: 'Duration', type: 'TEXT', value: item.duration },
]ASSET — nested mini-assets
Used on PRODUCT-type assets to embed downloadable files inside metadata. The value is a mini-asset object:
{
id: 'linkedFile',
name: 'Linked File',
type: 'ASSET',
value: {
id: file.id,
name: file.name,
version: 1,
mimeType: file.mimeType,
thumbnailUrl: file.thumbnailUrl,
downloadUrl: file.downloadUrl,
conversions: file.conversions,
capabilities: defaultAssetCapabilities,
}
}This renders the mini-asset with its own thumbnail, download button, and conversion options inside the parent asset's detail panel.
Grouping with \u2003 (em space)
CI HUB uses the em space character (\u2003) as an indent prefix in the name field to create visual grouping. The pattern:
- Header row — no
\u2003prefix, empty or novalue. Acts as a group label - Child rows —
nameprefixed with\u2003. Displayed indented under the header
// Header row (no value, no indent)
{ id: 'copyright', name: 'copyright', type: 'TEXT' },
// Child rows (indented with \u2003)
{ id: 'notice', name: '\u2003Notice', value: item.copyright.notice, type: 'TEXT' },
{ id: 'copyrightStatus', name: '\u2003Status', value: item.copyright.status, type: 'TEXT' },This renders as:
copyright
Notice: MIT License
Status: ActiveFiltering out empty groups
Header rows with no children are noise. Filter them out:
const values = [
...parseCustomMetadata(item.customMetadata || []),
...item.copyright.status || item.copyright.notice ?
[{
id: 'copyright',
name: 'copyright',
type: 'TEXT'
}, {
id: 'notice',
name: '\u2003Notice',
value: item.copyright.notice,
type: 'TEXT'
}, {
id: 'copyrightStatus',
name: '\u2003Status',
value: item.copyright.status,
type: 'TEXT'
}] :
[]
].filter(metadate => metadate.value || ['copyright'].includes(metadate.id))Real adapter patterns
Example - createValue helper
some adapters uses a createValue helper that conditionally adds the \u2003 prefix:
const createValue = (id, name, value = '', indent) => ({
id,
name: `${indent ? '\u2003' : ''}${name}`,
type: 'TEXT',
value,
})Metadata templates produce grouped values — a header row for the template name, then indented child rows for each field:
if (metadata && (showHidden || !templateHidden)) {
const prefix = `${scope}#${templateKey}`
acc.push(createValue(prefix, templateName))
acc.push(...(template.fields || []).
filter(field => field && (showHidden || !field.hidden) && isValue(metadata[field.key])).
map(field => createValue(
`${prefix}#${field.key}`,
field.displayName,
getValue(metadata[field.key], field.type),
true
)))
}Example - grouped custom fields
some adapters groups custom fields by field set. A new header is added when the field set name changes:
if (useGroupAndSort && fieldSetName && (!lastFieldSetName || fieldSetName !== lastFieldSetName)) {
lastFieldSetName = fieldSetName
customFieldsToAdd.push({ id: `group-${fieldSetId}`, name: fieldSetName, type: 'TEXT' })
}
customFieldsToAdd.push({
id: fieldId,
name: `${fieldSetName ? '\u2003' : ''}${getCustomFieldName(customFieldsMapping, fieldId, uiLocale)}`,
value,
type: 'TEXT',
})Example - layer-based metadata
some adapters iterates metadata layers and nests properties under each layer name:
Object.keys(item.metadata).
filter(entry => !['xmpMetadata', 'exifMetadata', 'autoTagging'].includes(entry)).
forEach(metadataId => {
let groupHeaderAdded = false
Object.entries(item.metadata[metadataId]).forEach(([key, value]) => {
if (key !== '_displayValues' && isValidPropertyValue(value)) {
if (!groupHeaderAdded) {
values.push({ id: `${metadataId}`, name: `${getLayerName(layerSchemaData, metadataId)}`, type: 'TEXT', value: `` })
groupHeaderAdded = true
}
values.push({
id: `${metadataId}:${key}`,
name: `\u2003${getLayerPropertyName(layerSchemaData, metadataId, key)}`,
type: 'TEXT',
value: getPropertyValueAsString(value)
})
}
})
})Example - multi-level nesting
some adapters nest up to three levels deep using multiple \u2003 characters:
// Level 0: item name (header)
values.push({ id: item.reference, name: item.name, type: 'TEXT', value: '' })
// Level 1: property key
Object.entries(item || {}).forEach(([key, value]) => {
if (Array.isArray(value)) {
values.push({ id: `item.reference#${key}`, name: `\u2003${key}`, type: 'TEXT', value: '' })
// Level 2: array entry name
value?.forEach?.(option => {
values.push({ id: `item.reference#${key}#type_name`, name: `\u2003\u2003${option.type_name}`, type: 'TEXT', value: '' })
// Level 3: nested properties
Object.entries(option || {}).forEach(([innerKey, innerValue]) => {
if (!['type_name'].includes(innerKey) && innerKey.indexOf('reference') === -1) {
values.push({ id: `item.reference#${key}#${innerKey}`, name: `\u2003\u2003\u2003${innerKey}`, type: 'TEXT', value: innerValue })
}
})
})
} else {
values.push({ id: `item.reference#${key}`, name: `\u2003${key}`, type: 'TEXT', value })
}
})This renders as:
Brochure A4
options
Lamination Matte
price: 0.05 EUR
weight: 2g
Binding Stapled
price: 0.02 EUR
quantity
500Best practices for IDs
- Use unique, stable IDs — the
idfield is used for change detection and filtering - Prefix group IDs to avoid collisions:
group-${setId},${scope}#${templateKey} - For nested values, use compound IDs:
${parentId}#${childKey} - Keep IDs valid as HTML id attributes — no spaces, no special characters beyond
#,-,_
See Search Schema | Custom Metadata