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:
{
"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.
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.
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:
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.
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.
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.
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.
{"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.


