> For the complete documentation index, see [llms.txt](https://docs.scaleflex.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.scaleflex.com/developers-headless/studio/template-builder/custom-metadata.md).

# Custom metadata

A text variable normally takes its value from the render query, which the editor calls **Manual**, or from the source asset's DAM metadata, which it calls **Asset metadata**. `custom-metadata` adds a third **Source type**: your own field names.

The point is that an author binds a variable to `sku` rather than having to remember which slug happens to mean the SKU.

```html
<sfx-template-builder
  base-url="https://templates.example.com"
  token="PROJECT_TOKEN"
  sec-template="SEC_TEMPLATE_KEY"
  stateless
  custom-metadata='[
    { "key": "sku",      "title": "SKU",           "group": "Product" },
    { "key": "price",    "title": "Price",         "group": "Product" },
    { "key": "campaign", "title": "Campaign name" }
  ]'
></sfx-template-builder>
```

Or as a property, which is nicer for anything built at runtime:

```ts
builder.customMetadata = fields.map((f) => ({ key: f.id, title: f.label }))
```

Fields appear in the editor's picker in the order you declare them, grouped under `group` where present, with `title` - or the bare `key` - as the label. Set no model and the source is not offered at all.

### Naming the source

The source type is called **Custom metadata** in the editor by default. `custom-metadata-label` renames it to fit your domain - `External metadata`, `Product attributes`, whatever your authors already call it:

```html
<sfx-template-builder custom-metadata-label="Product attributes" ...></sfx-template-builder>
```

Wording only. The saved template carries the same `custom_ckey` either way. To rename the rest of the editor's vocabulary, see [Editor labels](/developers-headless/studio/template-builder/editor-labels.md).

{% hint style="info" %}
Because the label is yours to set, there is no single name for this source in the product. The end-user documentation does not describe it at all, for that reason - what an author sees depends on your integration, so explaining it is your job rather than ours.
{% endhint %}

### Names only

No values travel with the model, and the editor resolves nothing against it. The binding is stored in the saved template on the variable, as `custom_ckey`:

```xml
<variable annotation_id="text_1" display="Product code" name="code"
          type="text_placeholder" source="URL" custom_ckey="sku"
          default="AB-1234" />
```

The variable then renders exactly like a **Manual** one: your pipeline substitutes the value by putting `$slug=value` in [the render query](/developers-headless/studio/template-builder/render-url.md).

So the round trip is:

1. The author binds a variable to a key from your model. You store the template.
2. You read `custom_ckey` back from the template you stored.
3. You look that field up in your own data.
4. You render with `$<slug>=<that value>`.

Until you do, the editor and any render show the variable's default value.

{% hint style="warning" %}
**The binding key and the delivery key are different things.** `custom_ckey` is the field in *your* model. The **slug** is the name the value travels under in the render query. They do not have to resemble each other - and renaming the slug breaks delivery silently, because nothing arrives under the new name. The editor warns an author who edits a slug, but it cannot know what your pipeline sends.
{% endhint %}

### Image variables

Image variables bind the same way: an author picks a field for the image's variable, and your pipeline renders with `$slug=<image URL>`. The saved variable is `type="image_src"` with the same `source="URL"` and `custom_ckey`.

Mark the fields that hold image URLs with `type: "image"` and the editor offers image variables only those, and text variables only the rest:

```ts
builder.customMetadata = [
  { key: 'sku', title: 'SKU' },
  { key: 'image_url', title: 'Main image', type: 'image' },
]
```

`type` is optional and defaults to `"text"`. Image variables are offered only the fields marked `"image"`, so a model that types nothing offers them none.

If the image layer carries `bg_remove="1"`, the URL you substitute still needs the background-removal parameters appended, exactly as for any image variable. See [Background-removed image sources](/developers-headless/studio/template-builder/render-url.md#background-removed-image-sources).

### Consequences worth knowing

* **The model can change between sessions.** A variable bound to a key your current model does not list keeps its binding. The editor shows it read-only and says so, so opening a template with a narrower model never silently rewrites it.
* **Nothing is validated at render time.** A key you stop sending simply stops being substituted, and the default shows instead.
* **Send it whenever you like.** The widget delivers the model as soon as the editor is ready and re-sends it whenever you change it. Assigning an equal model is a no-op.
* **It applies to DAM-backed embeds too**, not only stateless ones.
* **The variation builder shows the binding but does not fill it.** A saved variation renders from its own values, so a bound variable is filled in there like any other, under a badge naming the field: its `title`, or the bare `key` when the editor has no model. An image variable keeps its URL field and DAM picker. A render URL your pipeline builds still sets the value with `$slug=`.
* **Malformed fields are dropped, not fatal.** An entry with no `key`, a duplicate `key` - first one wins - or a `key` containing a character XML cannot carry, such as a C0 control or an unpaired surrogate, is skipped. The rest of the model still works. Ordinary text, punctuation, accents and emoji are all fine.

{% hint style="info" %}
**In React, `customMetadata` is an array prop** compared by identity like any other. Hoist it to module scope or memoize it, or every render counts as a change. This is harmless - the element de-duplicates by value before it says anything to the editor - but it is wasted work.
{% endhint %}

### Asset metadata needs a session

**Asset metadata**, the second source, reads the DAM's own metadata model and so needs a Hub session. It is unavailable under a [security template](/developers-headless/studio/template-builder/authentication.md), where it is dropped from the picker rather than shown disabled. Custom metadata is the source that works in a guest embed, which is usually why an integration reaches for it.

### Where to go next

* [The render URL](/developers-headless/studio/template-builder/render-url.md) - delivering the values you looked up
* [API reference](/developers-headless/studio/template-builder/api-reference.md) - the model's exact shape
