/react-19-plugin-migration
Grafana 13 (April 2026) moves from React 18 to React 19. Incompatible plugins will break. **Do not upgrade React to 19** — only make forward-compatible changes.
$ npx -y skills add grafana/skills --skill react-19-plugin-migration --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/react-19-plugin-migration
Context preview
The summary Claude sees to decide when to auto-load this skill.
Grafana 13 (April 2026) moves from React 18 to React 19. Incompatible plugins will break. **Do not upgrade React to 19** — only make forward-compatible changes.
SKILL.md
react-19-plugin-migration.SKILL.mdname: react-19-plugin-migration
license: Apache-2.0
description:
Migrate a Grafana plugin to React 19 compatibility. Use when the user asks to update a plugin
for React 19, prepare for React 19, fix React 19 compatibility, upgrade to React 19, migrate
to React 19, bump grafanaDependency to 12.3.0, externalize jsx-runtime, or run react-detect.
Triggers on phrases like "update plugin for React 19", "React 19 migration", "prepare for
React 19", "plugin React 19 compat", "grafanaDependency 12.3.0", "JSX runtime externals",
"react-detect", "SECRET_INTERNALS", "ReactCurrentOwner", or "ReactCurrentDispatcher".
Migrate Grafana Plugin to React 19
Grafana 13 (April 2026) moves from React 18 to React 19. Incompatible plugins will break. **Do not upgrade React to 19** — only make forward-compatible changes.
All changes go in **one PR**. Execute steps in order. Never manually edit `yarn.lock`.
---
Step 1: Detect plugin context
PLUGIN_JSON=$([ -f src/plugin.json ] && echo "src/plugin.json" \
|| ([ -f plugin/src/plugin.json ] && echo "plugin/src/plugin.json" || echo ""))
PKG_JSON=$([ -f package.json ] && echo "package.json" \
|| ([ -f plugin/package.json ] && echo "plugin/package.json" || echo ""))
PLUGIN_ID=$(jq -r '.id' $PLUGIN_JSON 2>/dev/null)
[ -f yarn.lock ] && PM="yarn" || ([ -f pnpm-lock.yaml ] && PM="pnpm" || PM="npm")
CP_VERSION=$(jq -r '.version' .config/.cprc.json 2>/dev/null)
echo "PLUGIN_ID=$PLUGIN_ID PM=$PM CP=$CP_VERSION"
If `PLUGIN_ID` is empty, ask the user for the plugin root path.
---
Step 2: Scan for compatibility issues
Build the plugin and run the React 19 compatibility scanner:
npm run build 2>&1 | tail -5
npx -y @grafana/react-detect@latest 2>&1
Save the output. It flags:
- `jsxRuntimeImport` / `__SECRET_INTERNALS` → Step 4 fixes this
- `defaultProps` / `propTypes` / `ReactDOM.render` → Step 8 (source fixes)
- `findDOMNode` → Step 6 (dependency bump) or Step 8 (source fix)
If the build fails (plugin hasn't been built before), skip this step and run react-detect after Step 9 instead. If output says "No breaking changes detected", still proceed — jsx-runtime externalization and grafanaDependency bump are always required.
Re-run react-detect after Step 9 to confirm all issues are resolved.
---
Step 3: Update `@grafana/create-plugin`
The scaffolding update brings in externals extraction, jest mocks, Docker fixes, and webpack improvements needed for React 19. **Always do this before `add externalize-jsx-runtime`.**
Requires a clean git working tree. Create a feature branch first if not already on one.
Run the update
npx @grafana/create-plugin@latest update 2>&1
If `yarn install` fails with "engine is incompatible"
The update runs an intermediate `yarn install` without `--ignore-engines`. Complete it manually:
yarn install --ignore-scripts --ignore-engines 2>&1 | tail -10
Commit the intermediate state and re-run:
git add -A && git commit -m "chore: intermediate create-plugin update" --no-verify
npx @grafana/create-plugin@latest update 2>&1
If ESLint 9 migration (004) fails with a parser error
The auto-migration can generate invalid JS on plugins with complex ESLint configs. **Do not skip** — commit what succeeded, then complete the ESLint 9 migration manually:
git add -A && git commit -m "chore: update create-plugin (ESLint 9 migration manual)" --no-verify
Then follow the "Complete ESLint 9 migration" section below to finish.
After the update
Always run install and verify:
yarn install --ignore-scripts --ignore-engines 2>&1 | tail -10
cat .config/.cprc.json
Commit if there are changes:
git add -A && git diff --cached --quiet || git commit -m "chore: update create-plugin scaffolding" --no-verify
---
Step 3b: Complete ESLint 9 migration
The `create-plugin update` bumps ESLint to v9, which requires flat config (`eslint.config.js`) instead of `.eslintrc`. Whether the auto-migration (004) succeeded, partially succeeded, or failed, **you must ensure ESLint works before proceeding**.
Check the current state
ls eslint.config.js .eslintrc* .config/.eslintrc* 2>/dev/null
npx eslint --version 2>&1
Three scenarios:
**A) `eslint.config.js` exists and `yarn lint` passes** — auto-migration succeeded. Proceed.
**B) `eslint.config.js` exists but `yarn lint` fails** — partial migration. Fix the issues:
yarn lint 2>&1 | head -30
Common fixes:
- `Invalid option '--ignore-path'` or `Invalid option '--ext'` → remove those flags from
the `lint` script in `package.json`. In ESLint v9 flat config, ignores and file matching are configured inside `eslint.config.js`, not via CLI flags. Update to: `eslint --cache .`
- `Cannot find module 'eslint-plugin-deprecation'` → remove the import/reference from
`eslint.config.js` (replaced by `@typescript-eslint/no-deprecated`)
- Other dead plugin imports → remove them from the config if the package was removed
**C) No `eslint.config.js` exists** — auto-migration failed. Create one manually:
ls node_modules/@grafana/eslint-config/flat.js 2>/dev/null
If `flat.js` exists, create `eslint.config.js` using it as the base:
import grafanaConfig from '@grafana/eslint-config/flat';
export default [
...grafanaConfig,
{
ignores: ['**/dist/', '**/node_modules/', '**/.config/', '**/coverage/'],
},
];Then migrate any custom rules from the old `.eslintrc` into additional config objects in the array. After creating the flat config:
1. Update the `lint` script: `"lint": "eslint --cache ."` 2. Delete the root `.eslintrc` (leave `.config/.eslintrc` — it's scaffolded and harmless)
Verify lint works
yarn lint 2>&1 | tail -20
Fix auto-fixable issues with `yarn lint --fix`. Commit:
git add -A && git diff --cached --quiet || git commit -m "chore: complete ESLint 9 flat config migration" --no-verify
Read more
name: react-19-plugin-migration license: Apache-2.0 description: Migrate a Grafana plugin to React 19 compatibility. Use when the user asks to update a plugin for React 19, prepare for React 19, fix React 19 compatibility, upgrade to React 19, migrate to React 19, bump grafanaDependency to 12.3.0, externalize jsx-runtime, or run react-detect. Triggers on phrases like "update plugin for React 19", "React 19 migration", "prepare for React 19", "plugin React 19 compat", "grafanaDependency 12.3.0", "JSX runtime externals", "react-detect", "SECRET_INTERNALS", "ReactCurrentOwner", or "ReactCurrentDispatcher".
Migrate Grafana Plugin to React 19
Grafana 13 (April 2026) moves from React 18 to React 19. Incompatible plugins will break. **Do not upgrade React to 19** — only make forward-compatible changes.
All changes go in **one PR**. Execute steps in order. Never manually edit `yarn.lock`.
---
Step 1: Detect plugin context
PLUGIN_JSON=$([ -f src/plugin.json ] && echo "src/plugin.json" \ || ([ -f plugin/src/plugin.json ] && echo "plugin/src/plugin.json" || echo "")) PKG_JSON=$([ -f package.json ] && echo "package.json" \ || ([ -f plugin/package.json ] && echo "plugin/package.json" || echo "")) PLUGIN_ID=$(jq -r '.id' $PLUGIN_JSON 2>/dev/null) [ -f yarn.lock ] && PM="yarn" || ([ -f pnpm-lock.yaml ] && PM="pnpm" || PM="npm") CP_VERSION=$(jq -r '.version' .config/.cprc.json 2>/dev/null) echo "PLUGIN_ID=$PLUGIN_ID PM=$PM CP=$CP_VERSION"
If `PLUGIN_ID` is empty, ask the user for the plugin root path.
---
Step 2: Scan for compatibility issues
Build the plugin and run the React 19 compatibility scanner:
npm run build 2>&1 | tail -5 npx -y @grafana/react-detect@latest 2>&1
Save the output. It flags:
- `jsxRuntimeImport` / `__SECRET_INTERNALS` → Step 4 fixes this
- `defaultProps` / `propTypes` / `ReactDOM.render` → Step 8 (source fixes)
- `findDOMNode` → Step 6 (dependency bump) or Step 8 (source fix)
If the build fails (plugin hasn't been built before), skip this step and run react-detect after Step 9 instead. If output says "No breaking changes detected", still proceed — jsx-runtime externalization and grafanaDependency bump are always required.
Re-run react-detect after Step 9 to confirm all issues are resolved.
---
Step 3: Update `@grafana/create-plugin`
The scaffolding update brings in externals extraction, jest mocks, Docker fixes, and webpack improvements needed for React 19. **Always do this before `add externalize-jsx-runtime`.**
Requires a clean git working tree. Create a feature branch first if not already on one.
Run the update
npx @grafana/create-plugin@latest update 2>&1
If `yarn install` fails with "engine is incompatible"
The update runs an intermediate `yarn install` without `--ignore-engines`. Complete it manually:
yarn install --ignore-scripts --ignore-engines 2>&1 | tail -10
Commit the intermediate state and re-run:
git add -A && git commit -m "chore: intermediate create-plugin update" --no-verify npx @grafana/create-plugin@latest update 2>&1
If ESLint 9 migration (004) fails with a parser error
The auto-migration can generate invalid JS on plugins with complex ESLint configs. **Do not skip** — commit what succeeded, then complete the ESLint 9 migration manually:
git add -A && git commit -m "chore: update create-plugin (ESLint 9 migration manual)" --no-verify
Then follow the "Complete ESLint 9 migration" section below to finish.
After the update
Always run install and verify:
yarn install --ignore-scripts --ignore-engines 2>&1 | tail -10 cat .config/.cprc.json
Commit if there are changes:
git add -A && git diff --cached --quiet || git commit -m "chore: update create-plugin scaffolding" --no-verify
---
Step 3b: Complete ESLint 9 migration
The `create-plugin update` bumps ESLint to v9, which requires flat config (`eslint.config.js`) instead of `.eslintrc`. Whether the auto-migration (004) succeeded, partially succeeded, or failed, **you must ensure ESLint works before proceeding**.
Check the current state
ls eslint.config.js .eslintrc* .config/.eslintrc* 2>/dev/null npx eslint --version 2>&1
Three scenarios:
**A) `eslint.config.js` exists and `yarn lint` passes** — auto-migration succeeded. Proceed.
**B) `eslint.config.js` exists but `yarn lint` fails** — partial migration. Fix the issues:
yarn lint 2>&1 | head -30
Common fixes:
- `Invalid option '--ignore-path'` or `Invalid option '--ext'` → remove those flags from
the `lint` script in `package.json`. In ESLint v9 flat config, ignores and file matching are configured inside `eslint.config.js`, not via CLI flags. Update to: `eslint --cache .`
- `Cannot find module 'eslint-plugin-deprecation'` → remove the import/reference from
`eslint.config.js` (replaced by `@typescript-eslint/no-deprecated`)
- Other dead plugin imports → remove them from the config if the package was removed
**C) No `eslint.config.js` exists** — auto-migration failed. Create one manually:
ls node_modules/@grafana/eslint-config/flat.js 2>/dev/null
If `flat.js` exists, create `eslint.config.js` using it as the base:
import grafanaConfig from '@grafana/eslint-config/flat';
export default [
...grafanaConfig,
{
ignores: ['**/dist/', '**/node_modules/', '**/.config/', '**/coverage/'],
},
];Then migrate any custom rules from the old `.eslintrc` into additional config objects in the array. After creating the flat config:
1. Update the `lint` script: `"lint": "eslint --cache ."` 2. Delete the root `.eslintrc` (leave `.config/.eslintrc` — it's scaffolded and harmless)
Verify lint works
yarn lint 2>&1 | tail -20
Fix auto-fixable issues with `yarn lint --fix`. Commit:
git add -A && git diff --cached --quiet || git commit -m "chore: complete ESLint 9 flat config migration" --no-verify
Public skills for working with Grafana, Prometheus, Loki, Tempo, Pyroscope, k6, and the broader LGTM observability stack. Compatible with Claude Code, Cursor, Codex, and any tool supporting the Agent Skills open standard.
Repo: grafana/skills
Other skills on grafana-skills.
- /admission-control
Use when the user asks to "write a validator", "add validation", "implement admission control", "write a mutating webhook", "add a mutation handler", "validate incoming resources", "implement admission logic", "add admission webhooks", "write ingress validation", or asks how to
Open skill - /app-sdk-concepts
Use when starting any grafana-app-sdk work — scaffolding a Grafana app, initializing a Grafana App Platform app, picking a deployment mode (standalone operator / grafana/apps / frontend-only), wiring app-specific config, or onboarding to the SDK. Covers `grafana-app-sdk` CLI
Open skill - /cue-kind-definition
Author CUE kind definitions for grafana-app-sdk apps - schemas, versioning, field constraints, named type definitions, custom routes, and codegen configuration. Scaffolds kinds via `grafana-app-sdk project kind add`, writes spec/status schemas with type constraints (regex, enum,
Open skill - /reconciler-logic
Implement reconcilers and watchers for grafana-app-sdk apps — write `TypedReconciler[*MyKind]` reconcile functions, apply generation-based skip patterns, do conflict-safe status updates via `resource.UpdateObject`, configure `BasicReconcileOptions` (namespace, label/field
Open skill - /adaptive-metrics
Cut Grafana Cloud Metrics cost by shrinking active-series count with Adaptive Metrics aggregation rules — auto-recommendations from query history, custom exact/regex rules, label-drop config, unused-metric detection, and Alloy remote_write fallback. Use when investigating a high
Open skill - /admin
Manage Grafana Cloud accounts — organizations, stacks, RBAC roles and assignments, SSO/SAML/OAuth/GitHub auth, service accounts for CI/CD, user invites, team membership, and API-driven provisioning. Creates stacks via the Cloud API, mints service-account tokens, applies role
Open skill

