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.

Expose a filesystem

Add filesystem to your extension manifest to mount live text and JSON files at /plugins/<your-plugin-id>/. The host derives the prefix from Grafana’s extension registry. A manifest cannot choose another plugin’s prefix. Enable both Assistant extensions and artifacts to use the mount in chat.

Provide read, list, and metadata callbacks. Add create and write when the owning service supports mutations. Each callback receives the current Grafana namespace. Use authenticated Grafana APIs; your backend must authorize each operation for the current caller. A path or chat attachment grants no access.

typescript
import type { AssistantFilesystemContribution } from '@grafana/assistant';

// filesApi is your authenticated service client implementing these contracts.
const filesystem: AssistantFilesystemContribution = {
  agentGuide: '# Project files\nRead config.json before changing configuration.',
  read: (namespace, path) => filesApi.read(namespace, path),
  list: (namespace, input) => filesApi.list(namespace, input),
  metadata: (namespace, path) => filesApi.metadata(namespace, path),
  create: (namespace, content) => filesApi.create(namespace, content),
  write: (namespace, path, content, revision) =>
    filesApi.write(namespace, path, content, revision),
};

// Include filesystem in getAssistantExtensionConfig({ schemaVersion: 1, filesystem }).

File results contain path, content, revision, and an optional title. Paths are relative to the mount, for example projects/config.json. They cannot contain traversal segments, URL escapes, backslashes, query strings, or fragments. Content is a JSON value or plain text; binary content is unsupported.

Create assigns the path atomically: rejection means nothing was created. Write updates an existing file and must enforce the supplied opaque revision atomically in storage. Reject missing files, denied access, and stale revisions. Metadata returns only a title, or undefined for missing or inaccessible files; other errors reject. Do not download full documents just to list metadata.

List receives a relative directory ("" for the mount root), optional filter with type and title, and limit/offset. Define type values in your guide. Apply authorization and filters first, sort by relative path, then paginate. Return { items: [{ path, title? }], hasMore }, with at most limit files and a full page when hasMore is true. Titles use case-insensitive substring matching; combined filters use AND. The tool limits pages to 100. Without explicit pagination, more than 100 matches produces a request to filter or paginate.

The optional agentGuide is Markdown, limited to 32 KiB, exposed at the reserved, read-only /plugins/<your-plugin-id>/AGENT.md. The agent sees the mount and guide path in context and reads the guide when needed. Keep guidance specific to your files: formats, type filters, examples, and supported operations. Exclude AGENT.md from callback listings; the host advertises it separately.

JSON
{"cmd":"view","path":"/plugins/grafana-myplugin-app/AGENT.md"}
{"cmd":"list","path":"/plugins/grafana-myplugin-app","limit":25,"offset":0}
{"cmd":"view","path":"/plugins/grafana-myplugin-app/projects/config.json"}
{"cmd":"write","path":"/plugins/grafana-myplugin-app/projects/config.json","revision":"7","content":{"enabled":true}}
{"cmd":"create","type":"grafana-myplugin-app","content":{"enabled":true}}

Ordinary files are accessible through the tool. To show a file in the canvas, use a registered artifact extension such as /plugins/grafana-myplugin-app/latest.grafana-myplugin-app.check. The existing kind validator and renderer apply. Renderers receive node.resourceRevision for backend change detection; use it instead of the chat write counter when present. Successful create/write/edit attaches those files; attach explicitly adds an existing artifact. Listing and viewing never attach. Ordinary files and AGENT.md do not create canvas tabs.

Chat stores artifact references, while reads and rendering use the live service. Removing or disabling an extension makes its mount unavailable; it never falls back to chat-stored content. Existing conversation-owned plugin artifacts outside /plugins/ retain their projection behavior. Mounted files support list, view, create, write, edit, and artifact attach; rename, delete, and search are unsupported.

If persistence succeeds but canvas attachment fails, the tool returns the saved path and revision with a warning, without an attachment reference. Read that path to inspect or repair the file; do not retry creation. Renderer and toolbar error boundaries reset when the backend revision changes, including external repairs that do not add a conversation write.