Grafana Scenes

by grafana1ccacf29049fApache-2.0279 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated today

Build Grafana plugin pages using the @grafana/scenes framework. Use when creating new scene pages, adding panels/visualizations, setting up drilldown navigation, defining variables, configuring query runners, building table/timeseries/stat panels, or extending SceneObjectBase for custom scene objects. Triggers on any work involving SceneApp, SceneAppPage, EmbeddedScene, SceneQueryRunner, SceneDataTransformer, PanelBuilders, SceneFlexLayout, QueryVariable, or drilldown/tab configuration in Grafana plugins.

Instructions onlySoftware Development
AI-generated overview

Builds Grafana plugin pages with the @grafana/scenes framework, covering scenes, panels, variables and drilldowns.

What it does
This skill provides guidance and code patterns for building Grafana plugin pages using the @grafana/scenes framework. It shows how to compose scene trees such as SceneApp, SceneAppPage and EmbeddedScene, add panels and visualizations, define query runners and data transformers, set up variables, and configure drilldown navigation and tabs. It also covers extending SceneObjectBase for custom scene objects and lists common pitfalls.
When to use it
Use it when creating new Grafana scene pages or adding panels, visualizations, variables, query runners or drilldown navigation to a Grafana plugin. It is also relevant when extending SceneObjectBase for custom scene objects or working with SceneApp, SceneAppPage, EmbeddedScene, PanelBuilders or related scene APIs.
Requirements
Requires a Grafana plugin project using the @grafana/scenes package and TypeScript/React tooling. No scripts are shipped; the skill is instructions and code examples only.

@grafana/scenes Framework

Build reactive, data-driven Grafana plugin pages with declarative scene objects.

Core Concepts

Scenes composes a tree of objects: SceneApp → SceneAppPage → EmbeddedScene → layouts → panels. Each node can own data ($data), variables ($variables), time ranges ($timeRange), and behaviors ($behaviors) that propagate down the tree.

Quick Start: New Scene Page

1. Create the scene file

typescript
// src/components/scenes/MyFeature/scene.tsximport {  EmbeddedScene, SceneFlexLayout, SceneFlexItem,  SceneQueryRunner, SceneVariableSet, QueryVariable,  PanelBuilders, VariableValueSelectors, SceneControlsSpacer,} from '@grafana/scenes';
export function getMyFeatureScene(params: { datasource: DataSourceRef }) {  const queryRunner = new SceneQueryRunner({    datasource: params.datasource,    queries: [{ refId: 'A', expr: 'up{cluster=~"$cluster"}', instant: true, format: 'table' }],  });
  const panel = PanelBuilders.table()    .setData(queryRunner)    .setTitle('My Table')    .build();
  return new EmbeddedScene({    $variables: new SceneVariableSet({      variables: [        new QueryVariable({          name: 'cluster',          query: 'label_values(up, cluster)',          datasource: params.datasource,          isMulti: true, includeAll: true, defaultToAll: true,        }),      ],    }),    controls: [new VariableValueSelectors({}), new SceneControlsSpacer()],    body: new SceneFlexLayout({      direction: 'column',      children: [new SceneFlexItem({ body: panel })],    }),  });}

2. Create the page

typescript
// src/components/scenes/MyFeature/MyFeature.tsximport { SceneAppPage, SceneTimeRange } from '@grafana/scenes';
export function getMyFeaturePage(params) {  return new SceneAppPage({    title: 'My Feature',    url: '/a/my-plugin-id/my-feature',    routePath: 'my-feature/*',    $timeRange: new SceneTimeRange({ from: 'now-1h', to: 'now' }),    getScene: () => getMyFeatureScene(params),    drilldowns: [],  });}

3. Register in SceneApp

Add the page to the SceneApp pages array in the root scene file.

Key Patterns

Drilldowns (click-through navigation)

typescript
drilldowns: [{  routePath: ':cluster/*',  getPage: (match, parent) => new SceneAppPage({    title: decodeURIComponent(match.params.cluster),    url: `${parent.state.url}/${match.params.cluster}`,    routePath: `${match.params.cluster}/*`,    getScene: () => detailScene(decodeURIComponent(match.params.cluster)),  }),}]

Tabs (sub-pages within a detail view)

Pass tabs: [SceneAppPage, ...] instead of getScene on a SceneAppPage. Each tab is itself a SceneAppPage with its own scene.

Query with transformations

Wrap a SceneQueryRunner in SceneDataTransformer to apply Grafana transforms or custom RxJS operators:

typescript
new SceneDataTransformer({  $data: queryRunner,  transformations: [    { id: 'organize', options: { renameByName: { 'Value #A': 'CPU' } } },    (ctx) => (source) => source.pipe(map((frames) => /* custom transform */)),  ],})

Custom scene object

Extend SceneObjectBase with a static Component for custom interactive UI:

typescript
class MyWidget extends SceneObjectBase<MyWidgetState> {  static Component = ({ model }: SceneComponentProps<MyWidget>) => {    const state = model.useState();    return <div>{state.value}</div>;  };}

Table column overrides

Build ConfigOverrideRule objects for drill-down links, filtering, units, widths, custom cells.

Panel types

PanelBuilders.table(), .timeseries(), .stat(), .gauge(), .barchart() — chain .setData(), .setTitle(), .setUnit(), .setOption(), .setOverrides(), then .build().

Common Pitfalls

  • Always use routePath: 'path/*' (with wildcard) on pages that have drilldowns or tabs
  • encodeURIComponent/decodeURIComponent URL params — K8s names can contain /
  • Variables referenced in queries as $varName must exist in an ancestor SceneVariableSet
  • getScene is called lazily; don't create side effects in the factory
  • For instant queries, set both instant: true and format: 'table'

Resources

Source and attribution

Source:grafana/skillsinskills/grafana-plugins/grafana-scenesat commit1ccacf2

License: Apache-2.0

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

More from grafana/skills

React 19 Plugin Migration

grafana

Guides migration of a Grafana plugin to React 19 compatibility through ordered build, dependency and source-code steps.

Software Development279updated today

Plugin Bundle Size

grafana

Guides optimisation of Grafana app plugin bundle size using React.lazy, Suspense and webpack code splitting.

Software Development279updated today

Check Npm

grafana

Read-only audit of npm, yarn, or pnpm configuration for supply-chain hardening in a JS/TS repository.

Security279updated today

Mimir

grafana

Guides standing up and operating Grafana Mimir for scalable, multi-tenant, long-term Prometheus and OTLP metrics storage.

DevOps & Cloud279updated today

K6 Trend Analysis

grafana

Analyze Grafana Cloud k6 test run trends over time. Detects slow metric drift (e.g., P95 latency creeping up while still passing thresholds), computes headroom to thresholds, flags anomalies, and recommends threshold tightening. Use when the user asks about test performance trends, wants to know if metrics are degrading, asks whether thresholds should be tightened, or wants a health check across recent runs for a specific test. Trigger on phrases like "how is my test trending", "is P95 getting worse", "check for performance regression", "should I tighten thresholds", "are my tests degrading", "show me trends for test X", "analyze my k6 test runs", or "is my test getting slower". Also trigger when a user asks to check all tests in a project -- run this skill once per test and synthesize.

Awaiting classification279updated today

K6 Test Maintenance

grafana

Maintains and improves existing k6 load-test scripts: threshold tightening, version migration, refactoring, and best-practice audits.

Software Development279updated today