Custom Metadata
Custom metadata lets you expose platform-specific fields that aren't part of the standard CI HUB asset schema — things like project codes, expiry dates, approval status, or any domain-specific attribute your platform tracks.
customMetadata in the info response defines the schema — what fields exist and their types. The values array on each asset is the runtime display of metadata. These are two separate systems: customMetadata enables editing and filtering; values controls what appears in the asset detail panel. See Metadata Values for the values array reference.
Overview
Custom metadata involves two things:
Declare the fields in the info handler — tells CI HUB what fields exist and their types
Return the values on each asset — populated per-asset in search, getFolder, etc.
Declaring fields in info
The info handler is called once when the user connects. Return customMetadata as a list of field definitions:
info: async (locals) => {
return send({
customMetadata: [
{
id: 'projectCode',
name: 'Project Code',
type: 'TEXT',
},
{
id: 'expiryDate',
name: 'Expiry Date',
type: 'DATE',
},
{
id: 'approved',
name: 'Approved',
type: 'BOOLEAN',
},
{
id: 'region',
name: 'Region',
type: 'SELECT',
options: [
{ id: 'eu', name: 'Europe' },
{ id: 'us', name: 'Americas' },
{ id: 'apac', name: 'Asia Pacific' },
],
},
],
})
},Field types
| Type | Description |
|---|---|
TEXT | Free-form string |
DATE | ISO date string (2024-03-15) |
BOOLEAN | true / false |
SELECT | One value from a predefined list — requires options |
MULTI_SELECT | Multiple values from a predefined list |
Returning values on assets
In search, getFolder, and other content handlers, include customMetadata on each asset:
const mapToAsset = (item: any) => ({
id: item.id,
name: item.title,
// ...standard asset fields...
customMetadata: {
projectCode: item.project?.code ?? null,
expiryDate: item.expiryDate ?? null,
approved: item.status === 'APPROVED',
region: item.regionId ?? null,
},
})Return null for missing fields rather than omitting them — this ensures the field appears in the UI as empty rather than disappearing.
Editable metadata
To allow users to edit custom metadata fields in CI HUB, declare them in updateAssetOptions in the info response:
info: async (locals) => {
return send({
customMetadata: [
{ id: 'projectCode', name: 'Project Code', type: 'TEXT' },
{ id: 'expiryDate', name: 'Expiry Date', type: 'DATE' },
],
updateAssetOptions: {
editableFields: ['projectCode', 'expiryDate'],
},
})
},The updateAsset handler will then receive the changed values in requestData.body.customMetadata:
updateAsset: async (locals, requestData) => {
const { assetId } = requestData.params
const { customMetadata } = requestData.body
try {
if (customMetadata) {
await callApi(locals, `/api/assets/${assetId}/metadata`, {
method: 'PATCH',
data: {
projectCode: customMetadata.projectCode,
expiryDate: customMetadata.expiryDate,
},
})
}
return send({ id: assetId })
} catch (error) {
return sendError(`Error updating metadata: ${error}`, 400)
}
},Localized (multi-language) metadata
If your platform stores metadata in multiple languages, return them as an object keyed by locale code:
const mapToAsset = (item: any) => ({
id: item.id,
name: item.title,
customMetadata: {
description: {
en: item.descriptions?.en ?? null,
de: item.descriptions?.de ?? null,
fr: item.descriptions?.fr ?? null,
},
keywords: {
en: item.keywords?.en?.join(', ') ?? null,
de: item.keywords?.de?.join(', ') ?? null,
},
},
})You must also declare the available locales in the info response so CI HUB shows the locale switcher:
info: async (locals) => {
return send({
customMetadata: [
{ id: 'description', name: 'Description', type: 'TEXT' },
{ id: 'keywords', name: 'Keywords', type: 'TEXT' },
],
dataLocales: ['en', 'de', 'fr'],
})
},Custom metadata as search filters
You can also expose custom metadata fields as searchable filters — see Filters → for how to read active filter values and pass them to your API.
A typical pattern is to list the metadata field as both a customMetadata declaration (for display) and as a filter option (for searching):
info: async (locals) => {
const regions = await callApi(locals, '/api/regions')
return send({
customMetadata: [
{
id: 'region',
name: 'Region',
type: 'SELECT',
options: regions.map((r: any) => ({ id: r.id, name: r.label })),
},
],
searchConfigs: [
{
id: 'region',
name: 'Region',
type: 'SELECT',
options: regions.map((r: any) => ({ id: r.id, name: r.label })),
},
],
})
},