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