---
title: "Extend Assistant from an app plugin | Grafana Cloud documentation"
description: "Register conversation starters, tool details, inline components, and artifact renderers from a Grafana app plugin."
---

> For a curated documentation index, see [llms.txt](/llms.txt). For the complete documentation index, see [llms-full.txt](/llms-full.txt).

# 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](/docs/grafana-cloud/platform/grafana-assistant/reference/typescript-sdk).

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](https://github.com/grafana/grafana/blob/v12.1.0/public/app/features/plugins/extensions/utils.tsx) 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 ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```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 ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

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

Expand table

| Field                 | Use it to                                                     | Current availability                                                                |
|-----------------------|---------------------------------------------------------------|-------------------------------------------------------------------------------------|
| `starters`            | Suggest first messages on empty chats.                        | Requires the extensions master flag and an enabled plugin.                          |
| `chits`               | Render expanded tool details or inline components in replies. | Same as starters.                                                                   |
| `artifactKinds`       | Validate and render custom files in the canvas.               | Also requires `assistant.frontend.artifacts`.                                       |
| `skills`              | Declare a plugin workflow.                                    | Read-only listing on the Skills settings page; model loading isn’t implemented yet. |
| `promptContributions` | Add 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 ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```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 ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

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

Expand table

| Contribution  | Limits                                                                                                                                                                                   |
|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Starter       | First 3 valid entries; `message` 1–500 characters; optional `label` up to 80. A blank label falls back to the message.                                                                   |
| Skill         | First 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 metadata | `modeId` 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 ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```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 ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

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

### Artifact identity and saved-resource links

Plugin renderers receive optional `node.id`, an opaque host-assigned identity. For chat drafts it stays stable across rewrites, edits, and renames. Deleting and recreating a draft produces a new identity. The identity is scoped to its owning conversation, so a fork in another chat has a different identity. It is derived from transcript history and does not depend on model-authored fields. Hosts without the required conversation/history context omit it. Do not parse it or use it as authorization. Plugins can use it as an idempotency key with their own backend, subject to that backend’s length/format constraints.

A plugin filesystem file can return `url`, a Grafana-local destination under `/a/<registered-plugin-id>/`. The host validates ownership and exposes current `node.resource` metadata with namespace, revision, and URL. The existing Files navigation uses this native URL. Reads still use the owning filesystem and its backend authorization. Links and revisions refresh with the resource rather than being copied from old transcript content. Existing `node.resourceRevision` remains available for compatibility.

These capabilities do not automatically bind a chat draft to a saved resource or synchronize later edits. The plugin owns resource creation and any mapping from host identity to its saved resource. Loading an existing resource uses the mounted filesystem and the existing attach operation. There is no new metadata store.

### Offer an edit in the current conversation

Use the optional `requestEdit` callback in your artifact renderer or toolbar:

tsx ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```tsx
function EditAction({ requestEdit }: ArtifactRendererProps) {
  return requestEdit ? (
    <Button onClick={() => requestEdit({ message: 'Help me fill in the missing values.' })}>
      Edit in chat
    </Button>
  ) : null;
}
```

Assistant appends the message and the current artifact path to the owning chat’s composer, preserves existing text, and focuses the input. The user sends the turn. The callback does not edit the artifact or execute an agent turn automatically. It is unavailable when the host has no owning conversation.

### Plugin draft validation and authoring guides

Artifact kinds may return a boolean from `validate`, or a structured result:

ts ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```ts
validate: () => ({
  valid: true,
  ready: false,
  issues: [{ path: 'steps[2].inputs.channel', message: 'Choose a Slack channel.' }],
}),
instructionsPath: 'AGENT.md',
schemaPath: 'schema.json',
```

`valid` controls draft writes and rendering. A valid draft can have unresolved values. `ready: false` reports that it is incomplete, while still allowing its preview. Readiness is advisory. Your plugin must recheck it, authorization, and backend validation before saving or running a real resource. Successful writes and edits report feedback to Assistant. Rejected writes include field-specific feedback so Assistant can repair the artifact. Renderers and toolbars receive the normalized result in their optional `validation` prop.

Feedback allows at most 20 issues, each with a path of at most 512 characters and a nonempty message of at most 2048 characters. Do not include credentials or sensitive field values. Malformed or throwing validators reject the draft. Existing boolean validators remain supported. Structured results require a host that supports this contract.

Instruction and schema paths are relative to your plugin filesystem. The host advertises their fully qualified paths with the artifact kind, without reading their contents. Assistant is directed to read them before authoring. Supply the files through your filesystem callbacks, or use the host-managed `AGENT.md`. Paths cannot traverse directories, use URL escapes, or refer to another plugin. These are plugin documentation, not system instructions.
