Element Metadata

Store and retrieve custom metadata on document elements.

Overview

Add-ons can store private metadata (custom data accessible only to the add-on that set it) on elements within the Express document. There are two main approaches for working with element metadata:

  1. Runtime Metadata: Set and modify metadata on existing elements using the Document Sandbox APIs
  2. Import-Time Metadata: Attach metadata to media assets during import using the Add-on UI SDK

Runtime Element Metadata

Get and Set Element Metadata

Add-ons can store metadata on any node within the Express document. Currently, each node can hold up to 3 KB of data, organized as key/value pairs where both keys and values are Strings. Additionally, there is a limit of 20 key/value pairs per node.

All nodes that inherit from the BaseNode class have a addOnData property that can be used to store and retrieve metadata. It is an instance of the AddOnData class, which provides methods to perform operations such as getItem(), setItem(), removeItem(), and clear().

With the remainingQuota property, you can check how much space is left, both in terms of sizeInBytes and numKeys, while keys() returns an array of the keys in use.

data-slots=text
data-variant=info
While Document and Page metadata operate from the addOnUISdk.app.document object and belong to the Add-on UI SDK, Element metadata are part of the Document Sandbox and are accessed through the node.addOnData property.

Example

import { editor } from "express-document-sdk";

// Create some dummy node
const text = editor.createText("Hello, World!");

// Store some metadata as key/value pairs
text.addOnData.setItem("originalText", "Hello, World!");
text.addOnData.setItem("date", new Date().toISOString());

// Retrieve the metadata
console.log("Original text: ", text.addOnData.getItem("originalText"));

// Check the remaining quota
console.log("Remaining quota: ", text.addOnData.remainingQuota);
// {
//   "sizeInBytes": 3062,
//   "numKeys": 19
// }

// Check the keys in use
console.log("Keys in use: ", text.addOnData.keys());
// ["originalText", "date"]

// Remove the metadata
text.addOnData.removeItem("originalText");

// clear all metadata
text.addOnData.clear();

Please note that the addOnData property is iterable with for...of loops, so you can use it to iterate over the key/value pairs; each pair is an array with the key as the first element and the value as the second.

// iterate over key/value pairs
for (let pair of text.addOnData) {
  console.log(pair);
  // ['originalText', 'Hello, World!']
  // ['date', '2025-01-20T11:06:19.051Z']
}

Alternatively, you can use the keys() method to get an array of all keys and then iterate over them.

// Iterate over all keys
text.addOnData.keys().forEach((key) => {
  console.log(`Key: ${key}, Value: ${text.addOnData.getItem(key)}`);
});

Import-Time Metadata for Media Assets (Add-on UI SDK)

When importing media assets (images, videos, animated images) using the Add-on UI SDK, you can attach metadata using the ImportAddOnData parameter. This provides two distinct types of metadata storage:

Container vs. Media Metadata

nodeAddOnData - Container-Level Metadata:

mediaAddOnData - Content-Level Metadata:

data-slots=text
data-variant=warning
Import-time metadata is not supported for PSD/AI assets. An error will be thrown if you attempt to use ImportAddOnData with these file types.

Example: Set Metadata with Add-on UI SDK ImportAddOnData

data-slots=heading, code
data-repeat=3

Add Image

// Store metadata when importing
// ui/index.js (iframe runtime)
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";

addOnUISdk.ready.then(async () => {
  try {
    // Create or fetch your image blob
    const imageBlob = await fetch("./sample-image.png").then(r => r.blob());

    // Import image with ImportAddOnData
    await addOnUISdk.app.document.addImage(
      imageBlob,
      // Optional MediaAttributes
      {
        title: "Sample Test Image",
        author: "Add-on Developer"
      },
      {
        // Container-level metadata (persists with container)
        nodeAddOnData: {
          "imageId": "test_001",
          "category": "demo",
          "importDate": new Date().toISOString(),
          "source": "addon-tester"
        },
        // Content-level metadata (tied to actual image content)
        mediaAddOnData: {
          "resolution": "200x150",
          "format": "PNG",
          "source": "generated_canvas",
          "color": "green"
        }
      }
    );

    console.log("✅ Image imported successfully with metadata!");

  } catch (error) {
    console.error("❌ Failed to import image:", error);
  }
});

Add Video

// ui/index.js (iframe runtime)
// Import a video with container metadata only
await addOnUISdk.app.document.addVideo(videoBlob, {
  title: "Product Demo"
}, {
  nodeAddOnData: {
    "video-category": "product-demo",
    "import-timestamp": new Date().toISOString()
  },
  mediaAddOnData: {
    "resolution": "1920x1080",
    "format": "MP4",
    "duration": "596s",
    "testFlag": "remote_video_test"
  }
});

Add Animated Image

// ui/index.js (iframe runtime)
// Import an animated image with media metadata only
await addOnUISdk.app.document.addAnimatedImage(gifBlob, {
  title: "Animated Logo"
}, {
  mediaAddOnData: {
    "animation-type": "logo",
    "frame-count": "24",
    "duration": "2000ms"
  }
});
data-slots=text
data-variant=info
ImportAddOnData is also supported in drag-and-drop operations via the enableDragToDocument method. See the Drag and Drop guide for more details.

Example: Retrieve Imported Metadata in Document Sandbox

data-slots=heading, code
data-repeat=2

All Media Metadata

// sandbox/code.js (document sandbox)
import { editor } from "express-document-sdk";

function retrieveAllMediaMetadata() {
  console.log("Starting metadata retrieval...");

  const documentRoot = editor.documentRoot;
  let mediaContainerCount = 0;

  // Traverse document structure to find media nodes
  for (const page of documentRoot.pages) {
    console.log(`📄 Checking page: ${page.id}`);

    for (const artboard of page.artboards) {
      console.log(`🎨 Checking artboard: ${artboard.id}`);

      // Use recursive traversal to find all MediaContainer nodes
      traverseNodeForMedia(artboard);
    }
  }

  function traverseNodeForMedia(node) {
    // Check if current node is a MediaContainer
    if (node.type === 'MediaContainer') {
      mediaContainerCount++;
      console.log(`\n📦 Found MediaContainer #${mediaContainerCount}: ${node.id}`);

      try {
        // Retrieve container metadata (nodeAddOnData)
        const containerMetadata = {};
        const containerKeys = node.addOnData.keys();

        for (const key of containerKeys) {
          containerMetadata[key] = node.addOnData.getItem(key);
        }

        if (containerKeys.length > 0) {
          console.log('📝 Container metadata (nodeAddOnData):', containerMetadata);
        } else {
          console.log('📝 No container metadata found');
        }

        // Access the media rectangle directly via the mediaRectangle property
        const mediaRectangle = node.mediaRectangle;

        if (mediaRectangle) {
          console.log(`🖼️  Media rectangle type: ${mediaRectangle.type}`);

          try {
            // Retrieve media-specific metadata (mediaAddOnData)
            const mediaMetadata = {};
            const mediaKeys = mediaRectangle.mediaAddOnData.keys();

            for (const key of mediaKeys) {
              mediaMetadata[key] = mediaRectangle.mediaAddOnData.getItem(key);
            }

            if (mediaKeys.length > 0) {
              console.log('🎯 Media metadata (mediaAddOnData):', mediaMetadata);
            } else {
              console.log('🎯 No media metadata found');
            }

          } catch (error) {
            // Handle PSD/AI assets or other errors
            console.log('⚠️  Cannot access mediaAddOnData (likely PSD/AI asset):', error.message);
          }
        } else {
          console.log('⚠️  No media rectangle found');
        }

      } catch (error) {
        console.error('❌ Error accessing container metadata:', error);
      }
    }

    // Recursively traverse all children
    // MediaContainers can be nested inside groups or other containers
    if (node.allChildren) {
      for (const child of node.allChildren) {
        traverseNodeForMedia(child);
      }
    }
  }

  console.log(`\n✅ Metadata retrieval complete! Found ${mediaContainerCount} MediaContainer(s)`);
}

Known MediaContainer

// sandbox/code.js (document sandbox)
// Simple access example for a known MediaContainer
const mediaContainer = /* get MediaContainerNode from document */;

// Access container-level metadata
const containerMetadata = mediaContainer.addOnData;
console.log("Image ID:", containerMetadata.getItem("imageId"));
console.log("Category:", containerMetadata.getItem("category"));

// Access media-level metadata
const mediaRectangle = mediaContainer.mediaRectangle;
const mediaMetadata = mediaRectangle.mediaAddOnData;
console.log("Resolution:", mediaMetadata.getItem("resolution"));
console.log("Format:", mediaMetadata.getItem("format"));

// Check all available keys
console.log("Container keys:", containerMetadata.keys());
console.log("Media keys:", mediaMetadata.keys());

When to Use Each Type

Use nodeAddOnData when:

Use mediaAddOnData when:

Use Cases

Element metadata can be useful in various scenarios:

Runtime Metadata Use Cases

Import-Time Metadata Use Cases

Please refer to the SDK Reference section for AddOnData for a complete list of methods, and the per-element-metadata sample add-on for a demonstrative implementation.

FAQs

Q: How do I store metadata on an element?

A: Use node.addOnData.setItem("key", "value") to store key/value pairs on any node.

Q: How do I retrieve stored metadata?

A: Use node.addOnData.getItem("key") to retrieve the value for a specific key.

Q: What are the storage limits?

A: Each node can store up to 3 KB of data with a maximum of 20 key/value pairs.

Q: How do I check remaining storage space?

A: Use node.addOnData.remainingQuota to get remaining bytes and key count.

Q: How do I remove metadata?

A: Use removeItem("key") for specific keys or clear() to remove all metadata.

Q: How do I iterate over all metadata?

A: Use for...of loops on addOnData or iterate over keys() array with forEach().

Q: Can other add-ons access my metadata?

A: No, metadata is private and only accessible to the add-on that set it.

Q: What types can I store?

A: Only strings are supported for both keys and values.

Q: How do I add metadata when importing media?

A: Use the ImportAddOnData parameter in media import methods like addImage(), addVideo(), and addAnimatedImage().

Q: What's the difference between nodeAddOnData and mediaAddOnData?

A: nodeAddOnData persists with the container even when media is replaced; mediaAddOnData is tied to the media content and shared across copies.

Q: How do I access media-specific metadata?

A: Use mediaRectangleNode.mediaAddOnData to access metadata tied to the media content itself.

Q: Can I use ImportAddOnData with all media types?

A: No, import-time metadata is not supported for PSD/AI assets. An error will be thrown if you attempt to use ImportAddOnData with these file types.

Q: What happens to mediaAddOnData when I copy a media element?

A: All copies of the same media asset share the same mediaAddOnData. Changes to one copy affect all copies.

Q: What happens to nodeAddOnData when I replace media content?

A: nodeAddOnData persists with the container even when the media content is replaced, while mediaAddOnData is reset.