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. 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. 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. To illustrate how this works, we’ll use the following fictional metadata object:

{
  "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 in Cortex. Follow the Creating Plugins documentation, making sure to choose Specific entity types as the context. This ensures your visualization displays when viewing an entity that contains the custom data.

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 in Cortex. This template is where you’ll build your visualization.

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

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

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

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

Last updated

Was this helpful?