# 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cortex.io/guides/operational-readiness/visualizing-custom-data-in-a-plugin.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
