Grafana Cloud

Extend Assistant from an app plugin

Use an extension manifest to add your Grafana app plugin’s content and components to Grafana Assistant. Extensions appear in the Assistant sidebar and workspace. Each contribution is attributed to the plugin that registers it.

This API is experimental and may change between releases. It applies to the Grafana UI, not Slack, Microsoft Teams, or backend investigations.

Before you begin

You need Grafana 12.1 or later, the Assistant app, and the @grafana/assistant SDK with getAssistantExtensionConfig. Install the SDK as described in the TypeScript SDK guide.

Your Assistant environment must enable assistant.frontend.plugin-extensions. An operator can disable all contributions from a plugin with assistant.frontend.plugin-extensions-enabled.<pluginId>. That per-plugin flag defaults to true.

For compatibility with Grafana 12.1, set preload: true in your plugin’s plugin.json. Its extension loader doesn’t discover plugins from addedFunctions declarations alone. This loads your plugin at startup; keep its initial module small.

Register a starter

Start with a proposed first message. This example needs no custom component or tool.

Add the following fields to your existing plugin.json:

JSON
{
  "preload": true,
  "extensions": {
    "addedFunctions": [
      {
        "title": "assistant-extension",
        "targets": ["grafana-assistant-app/extension/v1"]
      }
    ]
  }
}

Register the matching function in module.tsx. If you already export an AppPlugin, add the .addFunction(...) call to that instance.

typescript
import { AppPlugin } from '@grafana/data';
import {
  getAssistantExtensionConfig,
  type AssistantExtensionManifest,
} from '@grafana/assistant';

const manifest: AssistantExtensionManifest = {
  schemaVersion: 1,
  starters: [{
    label: 'Review service errors',
    message: 'Help me review recent service errors in this stack.',
  }],
};

export const plugin = new AppPlugin()
  .addFunction(getAssistantExtensionConfig(manifest));

The plugin.json declaration makes the extension discoverable; the module supplies its content. Register one manifest per plugin. The helper returns the same manifest on every call: define components and callbacks outside React renders, and don’t mutate the manifest after registration.

Open a fresh Assistant chat to find the starter. Selecting it sends message as a visible user message. Use provideQuestions instead for suggestions tied to a page where your plugin is already running.

Choose a contribution

Add only the fields you need to the same manifest.

FieldUse it toCurrent availability
startersSuggest first messages on empty chats.Requires the extensions master flag and an enabled plugin.
chitsRender expanded tool details or inline components in replies.Same as starters.
artifactKindsValidate and render custom files in the canvas.Also requires assistant.frontend.artifacts.
skillsDeclare a plugin workflow.Read-only listing on the Skills settings page; model loading isn’t implemented yet.
promptContributionsAdd a mode with custom system-prompt guidance.Requires the prompt flag and an explicit plugin allowlist.

Registering a renderer doesn’t register a tool or teach the model when to use the renderer. Supply tools separately and explain their output through page context or an enabled prompt contribution.

Render tool details

A tool-details component replaces the expanded content of a tool card. The Assistant keeps the compact row, status, and approval controls. Your component receives toolName, input, result, and status (running, success, or error); result can be undefined while the call runs.

For a plugin with ID grafana-myplugin-app, add a contribution such as:

tsx
import type { ChitContribution, ToolChitProps } from '@grafana/assistant';

function CheckDetails({ result, status }: ToolChitProps) {
  if (status === 'running') {
    return <span>Running check…</span>;
  }
  return <pre>{JSON.stringify(result, null, 2)}</pre>;
}

const chits: ChitContribution[] = [{
  kind: 'tool-details',
  toolNames: ['myplugin_run_check'],
  component: CheckDetails,
}];

Set manifest.chits to this array when constructing the manifest. Use the exact tool name passed by your tool integration. The host accepts your full plugin ID normalized to lowercase underscores (grafana_myplugin_app_) or its shortened prefix (myplugin_). Hyphenated tool names such as grafana-myplugin-app_run_check don’t match. Built-in renderers take precedence; a contribution that claims a reserved or foreign tool name is dropped as a whole.

The supported kinds are tool-details and inline. tool-card and widget detectors aren’t supported.

Render an inline component

An inline contribution replaces a custom tag in an Assistant reply. Set name to a lowercase name matching [a-z][a-z0-9]*, and provide a component that accepts children.

For plugin grafana-myplugin-app, { kind: 'inline', name: 'result', component: Result } renders the tag <grafana_myplugin_app_result>…</grafana_myplugin_app_result>. The prefix comes from your registered plugin ID. Reply HTML is sanitized, so don’t rely on arbitrary attributes surviving. Registration alone doesn’t tell the model to emit the tag.

Add an artifact kind

An artifact kind pairs a file extension with a validator and renderer. Use your exact plugin ID in the extension: .grafana-myplugin-app.check, for example, gives a file path such as /latest.grafana-myplugin-app.check.

tsx
import type { ArtifactKindContribution, ArtifactRendererProps } from '@grafana/assistant';

function isCheck(data: unknown): data is { summary: string } {
  return typeof data === 'object' && data !== null &&
    'summary' in data && typeof data.summary === 'string';
}

function CheckRenderer({ node }: ArtifactRendererProps) {
  return isCheck(node.data) ? <p>{node.data.summary}</p> : null;
}

const artifactKinds: ArtifactKindContribution[] = [{
  ext: '.grafana-myplugin-app.check',
  label: 'Check',
  icon: 'check',
  tabPriority: 0,
  validate: isCheck,
  renderer: CheckRenderer,
}];

Set manifest.artifactKinds when constructing the manifest. The artifact_files tool validates writes and edits against the live registration; the host also validates content before rendering. Return true only for content your renderer supports. A thrown error or any other result rejects the content.

Renderers and optional toolbars receive node (path, ext, data, revision) and a read-only fs with list(), read(path), and has(path). The filesystem includes every artifact in that conversation, including built-in and other plugins’ files. Use node and fs for artifact content. revision is a per-artifact change counter, not a timestamp; use it with path when caching content.

Lower tabPriority values put a kind before other plugin kinds; built-ins stay first. The host currently displays a generic plugin icon regardless of icon. It accepts promptHint but doesn’t send it to the model.

If your plugin becomes unavailable, its saved artifacts remain in the conversation with an unavailable-renderer fallback. Their content isn’t deleted.

Declare skills and modes

A skill has id, title, description, and Markdown content. The host assigns the handle plugin:<pluginId>:<id>. Plugin skills aren’t saved as tenant skills or added to search indexes. They appear read-only when the Skills settings page is available and disappear when the registration is removed. Don’t rely on them reaching the model yet: the catalog and loader are not implemented.

For a custom mode, use promptContributions. In addition to the master flag and enabled plugin, the environment must enable assistant.frontend.plugin-extensions-prompt and include your exact plugin ID in the comma-separated assistant.frontend.plugin-extensions-prompt-allowlist. An empty allowlist enables no plugin modes.

Modes use IDs of the form <pluginId>:<modeId>. prepend and append add guidance around the base prompt; replace removes the base guidance but retains the Assistant’s identity. The active mode’s prompt body is truncated to 16 KiB of UTF-8 text before attribution is added.

The current mode adapter applies modeId, label, description, compose, and prompt. Other fields, including whenToUse, initialMessage, switchable, availability, modeToolNames, icon, and featureState, are accepted but not applied. Don’t rely on them for message delivery, availability checks, or tool restrictions.

Check limits and troubleshoot

The host drops malformed or over-length metadata entries. Mode prompt bodies use the separate truncation rule described above. Unknown fields are ignored; unknown contribution kinds are dropped. An unsupported schemaVersion rejects the entire manifest.

ContributionLimits
StarterFirst 3 valid entries; message 1–500 characters; optional label up to 80. A blank label falls back to the message.
SkillFirst 5 valid, unique IDs; id matches [a-z][a-z0-9-]* and is at most 64 characters; nonblank title up to 80; description up to 500; nonblank content up to 32,768 UTF-8 bytes.
Mode metadatamodeId up to 64 characters; label up to 80; description, whenToUse, and optional initialMessage up to 500 each.

If a contribution doesn’t appear, first check both registration steps and the master and per-plugin flags. Look for [assistant-extensions] warnings in the browser console for invalid fields and ownership conflicts. Check starters in an empty chat, tool details on a matching expanded tool call, and modes against both prompt gates. If the plugin loads only after you visit its page, check the preload compatibility setting.