> For the complete documentation index, see [llms.txt](https://docs.cortex.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cortex.io/guides/operational-readiness/visualizing-custom-data-in-a-plugin.md).

# Visualizing custom data in a plugin

Plugins let you build lightweight UI extensions that surface metadata directly in the Cortex UI. Cortex already renders custom data automatically in the **Custom data & metrics tab** on every [entity page](/ingesting-data-into-cortex/entities-overview/entities/details.md). However, many teams want to go further, like adding richer visualizations, grouping related fields, embedding links, or turning custom data into more opinionated dashboards.

This guide shows how to build a plugin page that reads custom data and renders it using your own UI.

### Overview

Cortex supports attaching structured metadata to any catalog entity using [**Custom Data**](/ingesting-data-into-cortex/entities-overview/entities/custom-data.md). This metadata automatically appears in the **Custom Data** tab of the entity page.

If your team wants to:

* organize fields into custom sections
* add tables, charts, or diagrams
* highlight important metadata
* provide context-specific controls
* match a specific visualization or compliance requirement

a plugin can fetch the same custom data and render a tailored UI that sits alongside the built-in Cortex experience.

### Step 1: Add custom data to an entity

To add custom data to an entity, you can use the [Custom Data API](https://docs.cortex.io/api/readme/custom-data#post-api-v1-catalog-tagorid-custom-data). To illustrate how this works, we’ll use the following fictional metadata object:

```json
{
  "service_owner": "Payments Team",
  "languages": "Node.js, TypeScript",
  "dependencies": "Redis, Kafka, BillingAPI",
  "service_tier": "Tier 2",
  "last_reviewed": "2025-11-01",
  "links": "[https://docs.example.com/runbook, https://example.com/architecture]"
}
```

This object is stored under the custom data key `sample-metadata`.

```
sample-metadata
```

### Step 2: Register the plugin

Once you have custom data to visualize, [register your plugin](https://app.getcortexapp.com/admin/plugins/new) in Cortex. Follow the [Creating Plugins documentation](https://docs.cortex.io/streamline/plugins/creating-plugins), making sure to choose `Specific entity types` as the context. This ensures your visualization displays when viewing an entity that contains the custom data.

<figure><img src="/files/e6rfOUyNfiUuh734WZZL" alt=""><figcaption></figcaption></figure>

### Step 3: Download the Plugin Template

After registering the plugin, Cortex provides a downloadable plugin starter template, including:

* React setup
* Plugin Context provider
* Styling tokens (`--cortex-plugin-*`)
* Local development instructions
* A preconfigured route structure

Download the template from the [Register plugin page](https://app.getcortexapp.com/admin/plugins/new) in Cortex. This template is where you’ll build your visualization.

<figure><img src="/files/I9LA9GiSPnVzAPnD01MO" alt=""><figcaption></figcaption></figure>

### Step 4: Build the Custom Data Visualization

Inside the downloaded template:

1. Add a new component
2. Use the Plugin Context to read the current entity tag
3. Fetch custom data using `CortexApi.proxyFetch()`
4. Render a styled table or any UI you choose

Below is a complete example component (`SampleMetadataView.tsx`) that renders the fictional `sample-metadata` object.

#### Example Component

```tsx
import React, { useEffect, useState } from "react";
import { CardTitle } from "@cortexapps/react-plugin-ui";
import { CortexApi } from "@cortexapps/plugin-core";

import { Heading, Section, Subsection } from "./UtilityComponents";
import { usePluginContextProvider } from "./PluginContextProvider";

type SampleMetadata = {
  service_owner?: string;
  languages?: string;
  dependencies?: string;
  service_tier?: string;
  last_reviewed?: string;
  links?: string;
};

const SampleMetadataView: React.FC = () => {
  const context = usePluginContextProvider();

  const [data, setData] = useState<SampleMetadata | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchMetadata = async () => {
      try {
        const tag = encodeURIComponent(context.entity.tag);

        const response = await CortexApi.proxyFetch(
          `https://api.getcortexapp.com/api/v1/catalog/custom-data?tag=${tag}&key=sample-metadata`
        );

        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const json = await response.json();
        const entry = Array.isArray(json) ? json[0] : json;

        setData(entry?.value || null);
      } catch (e: any) {
        setError("Unable to load sample metadata");
      } finally {
        setLoading(false);
      }
    };

    if (context?.entity?.tag) fetchMetadata();
  }, [context]);

  const renderRow = (label: string, value: React.ReactNode) => (
    <tr key={label}>
      <th
        style={{
          textAlign: "left",
          padding: "8px 12px",
          fontWeight: 500,
          width: "35%",
          backgroundColor: "var(--cortex-plugin-input)",
          borderBottom: "1px solid var(--cortex-plugin-border)"
        }}
      >
        {label}
      </th>
      <td
        style={{
          padding: "8px 12px",
          borderBottom: "1px solid var(--cortex-plugin-border)",
          color: "var(--cortex-plugin-primary)"
        }}
      >
        {value || "Not set"}
      </td>
    </tr>
  );

  return (
    <Section>
      <Heading>
        <CardTitle>Sample Metadata</CardTitle>
      </Heading>
      <Subsection>
        {loading && <div>Loading…</div>}
        {error && <div>{error}</div>}
        {!loading && !error && data && (
          <div
            style={{
              borderRadius: 8,
              border: "1px solid var(--cortex-plugin-border)",
              backgroundColor: "var(--cortex-plugin-background)",
              overflowX: "auto"
            }}
          >
            <table style={{ width: "100%", borderCollapse: "collapse" }}>
              <tbody>
                {renderRow("Service Owner", data.service_owner)}
                {renderRow("Languages", data.languages)}
                {renderRow("Dependencies", data.dependencies)}
                {renderRow("Service Tier", data.service_tier)}
                {renderRow("Last Reviewed", data.last_reviewed)}
                {renderRow("Links", data.links)}
              </tbody>
            </table>
          </div>
        )}
      </Subsection>
    </Section>
  );
};

export default SampleMetadataView;
```

#### Add the component to `App.tsx`

Inside the downloaded template, open `App.tsx`.

In a single-view plugin like this, the recommended pattern is to render your visualization directly from `App.tsx`. Here’s what the minimal setup looks like:

#### `src/App.tsx`

```tsx
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import PluginProvider from "./RoutingPluginProvider";
import ErrorBoundary from "./ErrorBoundary";

import SampleMetadataView from "./components/SampleMetadataView";

import "../baseStyles.css";

const App: React.FC = () => {
  const queryClient = new QueryClient();

  return (
    <ErrorBoundary>
      <QueryClientProvider client={queryClient}>
        <PluginProvider enableRouting initialEntries={["/"]}>
          <SampleMetadataView />
        </PluginProvider>
      </QueryClientProvider>
    </ErrorBoundary>
  );
};

export default App;
```

### Step 5: Upload and Preview the Plugin in Cortex

Once your plugin is ready:

1. Run `npm run build` (or equivalent)
2. [Upload](/streamline/plugins/creating-plugins.md#step-4-upload-and-preview-plugin-in-cortex) the generated `UI.html` bundle in the Cortex UI
3. Open an entity of a supported type
4. Select your plugin in the left sidebar
5. Verify your custom visualization displays correctly

<figure><img src="/files/LwZrv2X1amKzNKjQbQnM" alt=""><figcaption></figcaption></figure>

This gives you a full end-to-end workflow for developing, testing, and launching plugin-based custom data visualizations.
