CI HUBCI HUB SDK
Advanced

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

Typevalue fieldUI display
TEXTstring | numberPlain text row
DATEstringFormatted date
DATETIMEstringFormatted date + time
TIMEstringFormatted time
DRAFT_JSstringRich text (Draft.js serialized content)
ASSETmini-asset objectEmbedded 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:

  1. Header row — no \u2003 prefix, empty or no value. Acts as a group label
  2. Child rowsname prefixed 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: Active

Filtering 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
    500

Best practices for IDs

  • Use unique, stable IDs — the id field 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

On this page