{"id":"2e99b17a-2267-4853-b801-71cd562e1ab9","entityType":"agent","slug":"clawhub-tenequm-grafana-foundation-sdk","name":"grafana-foundation-sdk","canonicalUrl":"https://www.xpersona.co/agent/clawhub-tenequm-grafana-foundation-sdk","canonicalPath":"/agent/clawhub-tenequm-grafana-foundation-sdk","generatedAt":"2026-10-10T23:48:06.949Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":null},"description":"Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.3K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:grafana-foundation-sdk","sourceUrl":"https://clawhub.ai/tenequm/grafana-foundation-sdk","homepage":"https://clawhub.ai/tenequm/skills/grafana-foundation-sdk","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/tenequm/grafana-foundation-sdk","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/tenequm/skills/grafana-foundation-sdk","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"grafana-foundation-sdk technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":null},"stars":null,"forks":null,"downloads":1263,"packageName":null,"latestVersion":"0.2.4","tractionLabel":"1.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T20:42:44.996Z","lastCrawledAt":"2026-10-10T20:42:44.996Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T20:42:44.996Z","lastVerifiedAt":null,"highlights":[{"version":"0.2.4","createdAt":"2026-08-21T12:01:53.832Z","changelog":"Updated grafana-foundation-sdk from 0.2.3 to 0.2.4. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - deleted `skill-card.md`","fileCount":7,"zipByteSize":22097},{"version":"0.2.3","createdAt":"2026-08-07T13:36:49.974Z","changelog":"Updated grafana-foundation-sdk from 0.2.2 to 0.2.3. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/patterns.md` - modified `references/typescript-api.md` - modified `skill-card.md`","fileCount":7,"zipByteSize":22009},{"version":"0.2.2","createdAt":"2026-07-22T18:44:22.066Z","changelog":"Updated grafana-foundation-sdk from 0.2.1 to 0.2.2. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - added `skill-card.md`","fileCount":7,"zipByteSize":21982},{"version":"0.2.1","createdAt":"2026-07-10T13:49:27.563Z","changelog":"Updated grafana-foundation-sdk from 0.2.0 to 0.2.1. Changes: - modified `CHANGELOG.md` - modified `SKILL.md`","fileCount":7,"zipByteSize":21887},{"version":"0.2.0","createdAt":"2026-06-05T19:46:56.842Z","changelog":"Updated grafana-foundation-sdk from 0.1.0 to 0.2.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/patterns.md` - modified `references/typescript-api.md`","fileCount":7,"zipByteSize":21667},{"version":"0.1.0","createdAt":"2026-06-05T17:41:12.584Z","changelog":"Initial publish of grafana-foundation-sdk 0.1.0. Changes: - added `CHANGELOG.md` - added `LICENSE.txt` - added `SKILL.md` - added `references/patterns.md` - added `references/typescript-api.md`","fileCount":7,"zipByteSize":19770}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:grafana-foundation-sdk","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:grafana-foundation-sdk` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/tenequm/grafana-foundation-sdk before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T23:48:06.947Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-grafana-foundation-sdk/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":null},"readme":"Skill: grafana-foundation-sdk\n\nOwner: tenequm\n\nSummary: Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.\n\nTags: latest:0.2.4\n\nVersion history:\n\nv0.2.4 | 2026-08-21T12:01:53.832Z | user\n\nUpdated grafana-foundation-sdk from 0.2.3 to 0.2.4.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- deleted `skill-card.md`\n\nv0.2.3 | 2026-08-07T13:36:49.974Z | user\n\nUpdated grafana-foundation-sdk from 0.2.2 to 0.2.3.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/patterns.md`\n- modified `references/typescript-api.md`\n- modified `skill-card.md`\n\nv0.2.2 | 2026-07-22T18:44:22.066Z | user\n\nUpdated grafana-foundation-sdk from 0.2.1 to 0.2.2.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- added `skill-card.md`\n\nv0.2.1 | 2026-07-10T13:49:27.563Z | user\n\nUpdated grafana-foundation-sdk from 0.2.0 to 0.2.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n\nv0.2.0 | 2026-06-05T19:46:56.842Z | user\n\nUpdated grafana-foundation-sdk from 0.1.0 to 0.2.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/patterns.md`\n- modified `references/typescript-api.md`\n\nv0.1.0 | 2026-06-05T17:41:12.584Z | user\n\nInitial publish of grafana-foundation-sdk 0.1.0.\nChanges:\n- added `CHANGELOG.md`\n- added `LICENSE.txt`\n- added `SKILL.md`\n- added `references/patterns.md`\n- added `references/typescript-api.md`\n\nArchive index:\n\nArchive v0.2.4: 7 files, 22097 bytes\n\nFiles: CHANGELOG.md (3392b), LICENSE.txt (9157b), references/patterns.md (13365b), references/typescript-api.md (18174b), skill-card.md (2036b), SKILL.md (13692b), _meta.json (141b)\n\nFile v0.2.4:SKILL.md\n\n---\nname: grafana-foundation-sdk\ndescription: Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.\nmetadata:\n  version: \"0.2.4\"\n  categories: \"operations, development\"\n  topics: \"grafana, dashboards, observability, dashboards-as-code, typescript\"\n  upstream: \"@grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/grafana-foundation-sdk\n    emoji: \"📊\"\n---\n\n# Grafana Foundation SDK\n\nThe grafana-foundation-sdk provides strongly typed builder libraries for defining Grafana dashboards as code. Instead of writing raw JSON (which is error-prone and hard to review in diffs), you compose dashboards using chained builder calls that produce valid Grafana JSON.\n\nThe SDK is auto-generated from Grafana's internal CUE schemas via the `cog` tool. It supports Go, TypeScript, Python, PHP, and Java. This skill focuses on **TypeScript** (primary) and **Go** (secondary) since those are the most common choices for infrastructure teams.\n\n## When to use this skill\n\n- Creating new Grafana dashboards from scratch\n- Converting existing hand-written dashboard JSON to typed code\n- Adding panels, variables, or queries to dashboards\n- Building reusable dashboard components (helper functions for common panel patterns)\n- Generating dashboards dynamically based on service lists or configs\n\n## Installation\n\nThe SDK is published as concrete `v0.0.x` tags (latest: **v0.0.16**). Pin explicitly - it is pre-1.0 and the API churns between releases (see Known Gotchas).\n\n**TypeScript:**\n```bash\nnpm install '@grafana/grafana-foundation-sdk@~0.0.16'\n# or\npnpm add '@grafana/grafana-foundation-sdk@~0.0.16'\n```\n\n**Go:**\n```bash\ngo get github.com/grafana/grafana-foundation-sdk/go@v0.0.16\n```\n\n## Core Architecture\n\nEverything follows the **builder pattern**: create a builder, chain configuration methods, call `.build()` (TS) or `.Build()` (Go) to produce the final object. The output is standard Grafana dashboard JSON - compatible with Grafana's API, file-based provisioning, and Kubernetes ConfigMaps.\n\nEach panel type, query type, and variable type lives in its own package. You import only what you need:\n\n```typescript\n// Each concern has its own import\nimport { DashboardBuilder, RowBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n```\n\n## Quick Start - TypeScript\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\nconst dashboard = new DashboardBuilder('My Service Overview')\n  .uid('my-service-overview')\n  .tags(['my-service'])\n  .editable()\n  .refresh('30s')\n  .time({ from: 'now-24h', to: 'now' })\n  .timezone('browser')\n  .withVariable(\n    new QueryVariableBuilder('service')\n      .label('Service')\n      .query('label_values(up{namespace=\"default\"}, job)')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .refresh(1)\n      .includeAll(true)\n      .allValue('.*')\n      .sort(1)\n  )\n  .withRow(new RowBuilder('Overview'))\n  .withPanel(\n    new StatBuilder()\n      .title('Request Rate')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('req/s')\n      )\n      .unit('reqps')\n      .decimals(1)\n      .height(4)\n      .span(6)\n      .colorMode(common.BigValueColorMode.Background)\n      .graphMode(common.BigValueGraphMode.Area)\n      .reduceOptions(\n        new common.ReduceDataOptionsBuilder().calcs(['lastNotNull'])\n      )\n  )\n  .withPanel(\n    new TimeseriesBuilder()\n      .title('Request Rate Over Time')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum by (job)(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('{{job}}')\n      )\n      .unit('reqps')\n      .fillOpacity(15)\n      .height(8)\n      .span(12)\n  );\n\n// Output the dashboard JSON\nconsole.log(JSON.stringify(dashboard.build(), null, 2));\n```\n\n## Key Patterns\n\n### 1. Helper functions for repeated panel configurations\n\nThe biggest win from using the SDK is creating reusable helpers that encode your team's conventions:\n\n```typescript\nfunction promDs() {\n  return { type: 'prometheus', uid: 'prometheus' } as const;\n}\n\nfunction lokiDs() {\n  return { type: 'loki', uid: 'loki' } as const;\n}\n\nfunction promQuery(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction statPanel(title: string, expr: string, opts?: { unit?: string; decimals?: number; color?: string }) {\n  const panel = new StatBuilder()\n    .title(title)\n    .datasource(promDs())\n    .withTarget(promQuery(expr))\n    .height(4)\n    .span(4)\n    .colorMode(common.BigValueColorMode.Background)\n    .graphMode(common.BigValueGraphMode.Area)\n    .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']));\n\n  if (opts?.unit) panel.unit(opts.unit);\n  if (opts?.decimals !== undefined) panel.decimals(opts.decimals);\n  // Thresholds can be set via .thresholds() if needed\n\n  return panel;\n}\n```\n\n### 2. Template variables\n\n```typescript\n// Query variable - populated from Prometheus labels\nnew QueryVariableBuilder('service')\n  .label('Service')\n  .query('label_values(http_server_duration_count{namespace=\"myapp\"}, job)')\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .refresh(2)  // 1=on dashboard load, 2=on time range change\n  .includeAll(true)\n  .allValue('.*')\n  .sort(1)  // 1=alphabetical asc\n\n// Custom variable - static key:value pairs\nnew CustomVariableBuilder('level')\n  .label('Log Level')\n  .query('All : .+, Error : error|fatal, Warning : warn, Info : info, Debug : debug')\n  .current({ text: 'All', value: '.+' })\n```\n\nReference variables in queries with standard Grafana syntax: `$service`, `$__range`, `$__rate_interval`, `$__auto`.\n\n### 3. Panel sizing\n\nPanels use `height(h)` (grid rows) and `span(w)` (out of 24 columns):\n- Full width: `.span(24)`\n- Half width: `.span(12)`\n- Third width: `.span(8)`\n- Quarter width: `.span(6)`\n- Typical stat panel: `.height(4).span(4)`\n- Typical timeseries: `.height(8).span(12)`\n\n### 4. Thresholds\n\n```typescript\nimport { ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// First step must have no value (it's the base)\nnew StatBuilder()\n  .thresholds(\n    new ThresholdsConfigBuilder()\n      .mode(common.ThresholdsMode.Absolute)\n      .steps([\n        { value: null as any, color: 'green' },\n        { value: 80, color: 'yellow' },\n        { value: 95, color: 'red' },\n      ])\n  )\n```\n\n### 5. Field overrides\n\n```typescript\nnew TimeseriesBuilder()\n  .overrideByName('Revenue', [\n    { id: 'color', value: { fixedColor: 'green', mode: 'fixed' } },\n  ])\n  .overrideByRegexp('.*5..', [\n    { id: 'color', value: { fixedColor: 'red', mode: 'fixed' } },\n  ])\n```\n\n### 6. Rows (including collapsed)\n\n```typescript\n// Regular row\n.withRow(new RowBuilder('Traffic'))\n\n// Collapsed row with nested panels\n.withRow(\n  new RowBuilder('Business Details')\n    .collapsed()\n    .withPanel(/* ... */)\n    .withPanel(/* ... */)\n)\n```\n\n### 7. Loki log and metric queries\n\n```typescript\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\n// Log query\nnew LokiQueryBuilder()\n  .expr('{namespace=\"myapp\", app=~\"$service\", level=~\"$level\"}')\n  .refId('A')\n\n// Metric query from logs\nnew LokiQueryBuilder()\n  .expr('sum by (buyer_wallet)(count_over_time({namespace=\"myapp\"} | event=\"request\" [$__range]))')\n  .legendFormat('{{buyer_wallet}}')\n  .refId('A')\n```\n\n### 8. Transformations\n\nTransformations are applied as raw objects since the SDK doesn't have typed builders for all transformation types:\n\n```typescript\nnew TableBuilder()\n  .withTransformation({\n    id: 'reduce',\n    options: {\n      reducers: ['lastNotNull'],\n      mode: 'seriesToRows',\n      includeTimeField: false,\n      labelsToFields: true,\n    },\n  })\n  .withTransformation({\n    id: 'organize',\n    options: {\n      excludeByName: { Field: true },\n      renameByName: { buyer_wallet: 'Buyer Wallet', 'Last not null': 'Requests' },\n    },\n  })\n  .withTransformation({\n    id: 'sortBy',\n    options: { sort: [{ field: 'Requests', desc: true }] },\n  })\n```\n\n## Generating Output\n\nThe `.build()` call returns a plain object matching Grafana's dashboard JSON schema. Serialize it however you need:\n\n```typescript\n// Standard JSON file (for provisioning or ConfigMaps)\nconst fs = require('fs');\nconst dashboard = builder.build();\nfs.writeFileSync('dashboard.json', JSON.stringify(dashboard, null, 2));\n\n// Kubernetes resource manifest (for Grafana's k8s API)\nconst manifest = {\n  apiVersion: 'dashboard.grafana.app/v1beta1',\n  kind: 'Dashboard',\n  metadata: { name: dashboard.uid },\n  spec: dashboard,\n};\nconsole.log(JSON.stringify(manifest, null, 2));\n```\n\n## Known Gotchas\n\nThese are sharp edges discovered from real usage and open issues on the SDK repo:\n\n1. **SDK is v0.0.x (public preview)** - Used by Grafana Labs in production but the API can change between releases. Pin your version explicitly. Best suited for Grafana >= 12, works with >= 10.\n\n2. **`instant()` and `range()` are mutually exclusive in Prometheus** - Calling `.instant()` sets `instant=true` AND `range=false`. Calling `.range()` does the opposite. Use `.rangeAndInstant()` if you need both.\n\n3. **Loki `range()`/`instant()` are deprecated** - Use `.queryType('range')` or `.queryType('instant')` instead. Similarly, `.resolution()` is deprecated in favor of `.step()`.\n\n4. **First threshold step must have `value: null`** - This is the base/default color. Omitting it produces invalid JSON.\n\n5. **Panel IDs are auto-assigned** - You don't set `id` on panels. Grafana assigns them at import time. Similarly, `gridPos.x/y` are computed from `height()` and `span()`.\n\n6. **Transformations are plain objects** - The SDK has no typed builders for transformations. Pass them as raw `{ id, options }` objects via `.withTransformation()`.\n\n7. **CustomVariable quirk** - When provisioning via Grafana's API, `CustomVariableBuilder` requires the `.query()` field with comma-separated key:value pairs (e.g., `'All : .+, Error : error'`) for options to persist, even when `.values()` is also used.\n\n8. **Go: `cog.ToPtr()` is essential** - Many struct fields are pointer types. Use `cog.ToPtr[T](value)` for nullable fields (thresholds, datasource refs). TypeScript doesn't have this issue.\n\n9. **Go: `Build()` returns error** - Always check it. TypeScript's `.build()` returns the object directly with compile-time type safety instead.\n\n10. **No typed query builders for plugin datasources** - Only core datasources (Prometheus, Loki, Tempo, Elasticsearch, CloudWatch, etc.) have builders. For third-party plugins, define custom query types by implementing the `Builder<Dataquery>` interface.\n\n11. **Dashboard schema v1 vs v2** - This skill targets the v1 dashboard (`@grafana/grafana-foundation-sdk/dashboard`, k8s apiVersion `dashboard.grafana.app/v1beta1`). A newer schema v2 ships as `dashboardv2beta1` (k8s apiVersion `dashboard.grafana.app/v2beta1`) with its own builders. v2beta1 is still stabilizing and has known sharp edges (e.g. transforms, annotation positioning, SQL expressions in Go) - prefer v1 unless you specifically need v2 layouts. Most query/panel builders are shared; some expose a `QueryV2Builder`/`VisualizationV2Builder` variant for v2.\n\n12. **Builders are only type-checked if wired into a tsconfig** - The SDK gives compile-time safety only when the generator file is actually type-checked. A generator sitting under a non-package directory (e.g. a Helm chart dir) that no `tsconfig` includes is silently unchecked, so type errors surface only at `.build()` runtime. Also: the SDK's output targets ES2024/`bundler` module resolution, which an older global `tsc` chokes on - run the project-local compiler (`npx tsc`), not a stale global one.\n\n13. **Regenerate JSON after every generator edit** - The deployed dashboard is the generated JSON, not the `.ts`/`.go` source. Edit the generator, re-run it, and commit the regenerated JSON together; never hand-edit the generated JSON (the next regen silently overwrites it). A repo rule (\"never edit the dashboard JSON directly\") is worth adding.\n\n## Reference Files\n\nFor detailed API reference and complete examples, see:\n- `references/typescript-api.md` - Full TypeScript API with all panel types, query builders, and configuration options\n- `references/patterns.md` - Common dashboard patterns, recipes, and a complete example converting this project's dashboard to SDK code\n\nFile v0.2.4:_meta.json\n\n{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"grafana-foundation-sdk\",\n  \"version\": \"0.2.4\",\n  \"publishedAt\": 1787313713832\n}\n\nFile v0.2.4:references/patterns.md\n\n# Common Dashboard Patterns\n\n## Table of Contents\n1. [Project Structure](#project-structure)\n2. [RED Method Dashboard](#red-method-dashboard)\n3. [Kubernetes Service Dashboard](#kubernetes-service-dashboard)\n4. [Loki Log Panels](#loki-log-panels)\n5. [Multi-Datasource Dashboard](#multi-datasource-dashboard)\n6. [Dynamic Dashboard Generation](#dynamic-dashboard-generation)\n7. [Converting Raw JSON to SDK](#converting-raw-json-to-sdk)\n8. [Integration with Helm/ConfigMaps](#integration-with-helmconfigmaps)\n\n---\n\n## Project Structure\n\nFor a monorepo with multiple dashboards, keep dashboard generators alongside the monitoring config:\n\n```\nops/\n  dashboards/\n    generate.ts          # entry point - imports and runs all generators\n    helpers.ts           # shared datasource refs, query helpers, panel defaults\n    overview.ts          # overview dashboard generator\n    per-service.ts       # per-service dashboard generator\n    package.json         # { \"type\": \"module\", \"scripts\": { \"generate\": \"tsx generate.ts\" } }\n    tsconfig.json\n  deploy/\n    dashboards/        # generated JSON files go here\n```\n\nOr for a single dashboard, a simple standalone script works fine:\n\n```\nops/dashboards/\n  generate-overview.ts\n  package.json\n```\n\n**package.json:**\n```json\n{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"generate\": \"tsx generate.ts\"\n  },\n  \"dependencies\": {\n    \"@grafana/grafana-foundation-sdk\": \"^0.0.16\"\n  },\n  \"devDependencies\": {\n    \"tsx\": \"^4.0.0\"\n  }\n}\n```\n\n---\n\n## RED Method Dashboard\n\nThe RED method (Rate, Errors, Duration) is the standard pattern for monitoring request-driven services. Here's a complete implementation:\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder, ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\n// --- Helpers ---\n\nconst PROM = { type: 'prometheus', uid: 'prometheus' } as const;\n\nfunction pq(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction defaultTimeseries(title: string) {\n  return new TimeseriesBuilder()\n    .title(title)\n    .datasource(PROM)\n    .height(8)\n    .span(12)\n    .fillOpacity(15)\n    .lineWidth(1)\n    .showPoints(common.VisibilityMode.Never)\n    .legend(\n      new common.VizLegendOptionsBuilder()\n        .showLegend(true)\n        .placement(common.LegendPlacement.Bottom)\n        .displayMode(common.LegendDisplayMode.List)\n    )\n    .tooltip(\n      new common.VizTooltipOptionsBuilder()\n        .mode(common.TooltipDisplayMode.Multi)\n        .sort(common.SortOrder.Descending)\n    );\n}\n\n// --- Dashboard ---\n\nfunction buildREDDashboard(namespace: string, metricPrefix: string) {\n  return new DashboardBuilder(`${namespace} RED`)\n    .uid(`${namespace}-red`)\n    .tags([namespace, 'red'])\n    .editable()\n    .refresh('30s')\n    .time({ from: 'now-1h', to: 'now' })\n    .withVariable(\n      new QueryVariableBuilder('service')\n        .label('Service')\n        .query(`label_values(${metricPrefix}_duration_count{namespace=\"${namespace}\"}, job)`)\n        .datasource(PROM)\n        .refresh(2)\n        .includeAll(true)\n        .allValue('.*')\n        .sort(1)\n    )\n\n    // --- Rate ---\n    .withRow(new RowBuilder('Rate'))\n    .withPanel(\n      defaultTimeseries('Request Rate')\n        .withTarget(pq(\n          `sum by (job)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m]))`,\n          '{{job}}'\n        ))\n        .unit('reqps')\n        .stacking(new common.StackingConfigBuilder().mode(common.StackingMode.Normal))\n    )\n    .withPanel(\n      defaultTimeseries('Request Rate by Status')\n        .withTarget(pq(\n          `sum by (http_status_code)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m]))`,\n          '{{http_status_code}}'\n        ))\n        .unit('reqps')\n    )\n\n    // --- Errors ---\n    .withRow(new RowBuilder('Errors'))\n    .withPanel(\n      defaultTimeseries('Error Rate (5xx)')\n        .withTarget(pq(\n          `sum by (job)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\",http_status_code=~\"5..\"}[5m]))`,\n          '{{job}}'\n        ))\n        .unit('reqps')\n    )\n    .withPanel(\n      defaultTimeseries('Error Percentage')\n        .withTarget(pq(\n          `sum(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\",http_status_code=~\"5..\"}[5m])) / sum(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m])) * 100`,\n          'error %'\n        ))\n        .unit('percent')\n    )\n\n    // --- Duration ---\n    .withRow(new RowBuilder('Duration'))\n    .withPanel(\n      defaultTimeseries('Latency Percentiles')\n        .withTarget(pq(\n          `histogram_quantile(0.50, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p50'\n        ))\n        .withTarget(pq(\n          `histogram_quantile(0.95, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p95'\n        ))\n        .withTarget(pq(\n          `histogram_quantile(0.99, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p99'\n        ))\n        .unit('ms')\n    )\n    .withPanel(\n      defaultTimeseries('Latency by Service')\n        .withTarget(pq(\n          `histogram_quantile(0.95, sum by (job, le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          '{{job}} p95'\n        ))\n        .unit('ms')\n    );\n}\n\n// Generate\nconst dashboard = buildREDDashboard('myapp', 'http_server');\nconsole.log(JSON.stringify(dashboard.build(), null, 2));\n```\n\n---\n\n## Kubernetes Service Dashboard\n\nPattern for monitoring a specific Kubernetes service with both Prometheus metrics and Loki logs:\n\n```typescript\nimport { PanelBuilder as LogsBuilder } from '@grafana/grafana-foundation-sdk/logs';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\nconst LOKI = { type: 'loki', uid: 'loki' } as const;\n\n// Log panel with filtering\nnew LogsBuilder()\n  .title('Application Logs')\n  .datasource(LOKI)\n  .withTarget(\n    new LokiQueryBuilder()\n      .expr('{namespace=\"myapp\", app=~\"$service\", level=~\"$level\"}')\n      .refId('A')\n  )\n  .showTime(true)\n  .wrapLogMessage(true)\n  .enableLogDetails(true)\n  .sortOrder(common.LogsSortOrder.Descending)\n  .dedupStrategy(common.LogsDedupStrategy.None)\n  .height(12)\n  .span(24)\n```\n\n---\n\n## Loki Log Panels\n\n### Metric from logs (count, sum, avg)\n\n```typescript\n// Count events over time\nnew LokiQueryBuilder()\n  .expr('sum by (status)(count_over_time({namespace=\"myapp\", app=~\"$service\"} | event=\"request\" [$__range]))')\n  .legendFormat('{{status}}')\n\n// Sum a numeric field extracted from logs\nnew LokiQueryBuilder()\n  .expr('sum by (network)(sum_over_time({namespace=\"myapp\"} | event=\"settlement\" | success=\"true\" | unwrap amount_usd [$__range]))')\n  .legendFormat('{{network}}')\n\n// Average a numeric field\nnew LokiQueryBuilder()\n  .expr('avg(avg_over_time({namespace=\"myapp\", app=~\"$service\"} | event=\"request\" | status=\"200\" | unwrap duration_ms | __error__=\"\" [$__auto]))')\n  .legendFormat('avg latency')\n\n// Top-K from logs\nnew LokiQueryBuilder()\n  .expr('topk(10, sum by (buyer_wallet)(count_over_time({namespace=\"myapp\"} | event=\"request\" | buyer_wallet!=\"\" [$__range])))')\n  .legendFormat('{{buyer_wallet}}')\n```\n\n### Table panel with Loki + transformations\n\n```typescript\nimport { PanelBuilder as TableBuilder } from '@grafana/grafana-foundation-sdk/table';\n\nnew TableBuilder()\n  .title('Top Buyer Wallets')\n  .datasource(LOKI)\n  .withTarget(\n    new LokiQueryBuilder()\n      .expr('topk(10, sum by (buyer_wallet)(count_over_time({namespace=\"myapp\"} | event=\"request\" | buyer_wallet!=\"\" [$__range])))')\n      .refId('A')\n  )\n  .withTransformation({\n    id: 'reduce',\n    options: { reducers: ['lastNotNull'], mode: 'seriesToRows', includeTimeField: false, labelsToFields: true },\n  })\n  .withTransformation({\n    id: 'organize',\n    options: {\n      excludeByName: { Field: true },\n      renameByName: { buyer_wallet: 'Buyer Wallet', 'Last not null': 'Requests' },\n    },\n  })\n  .withTransformation({\n    id: 'sortBy',\n    options: { sort: [{ field: 'Requests', desc: true }] },\n  })\n  .height(8)\n  .span(12)\n```\n\n---\n\n## Multi-Datasource Dashboard\n\nWhen combining Prometheus and Loki in the same dashboard, define datasource refs as constants and use them consistently:\n\n```typescript\nconst DS = {\n  prometheus: { type: 'prometheus', uid: 'prometheus' },\n  loki: { type: 'loki', uid: 'loki' },\n  tempo: { type: 'tempo', uid: 'tempo' },\n} as const;\n\n// Prometheus panel\nnew TimeseriesBuilder()\n  .datasource(DS.prometheus)\n  .withTarget(new PromQueryBuilder().expr('...'))\n\n// Loki panel\nnew LogsBuilder()\n  .datasource(DS.loki)\n  .withTarget(new LokiQueryBuilder().expr('...'))\n```\n\n---\n\n## Dynamic Dashboard Generation\n\nGenerate dashboards dynamically based on a list of services:\n\n```typescript\nconst services = ['inference', 'twitter', 'github', 'web'];\n\nfunction buildServiceDashboard(service: string) {\n  return new DashboardBuilder(`${service} - Detail`)\n    .uid(`${service}-detail`)\n    .tags(['myapp', service])\n    .editable()\n    .refresh('30s')\n    .time({ from: 'now-1h', to: 'now' })\n    .withRow(new RowBuilder('Overview'))\n    .withPanel(\n      new StatBuilder()\n        .title('Request Rate')\n        .datasource(PROM)\n        .withTarget(pq(`sum(rate(http_server_duration_count{namespace=\"myapp\",job=\"${service}\"}[5m]))`))\n        .unit('reqps')\n        .height(4).span(6)\n    )\n    // ... more panels\n    ;\n}\n\n// Generate all dashboards\nfor (const service of services) {\n  const dash = buildServiceDashboard(service);\n  fs.writeFileSync(`dashboards/${service}.json`, JSON.stringify(dash.build(), null, 2));\n}\n```\n\n---\n\n## Converting Raw JSON to SDK\n\nWhen converting existing hand-written dashboard JSON to SDK code, follow this mapping:\n\n| JSON Field | SDK Method |\n|---|---|\n| `\"uid\": \"x\"` | `.uid('x')` |\n| `\"title\": \"x\"` | `.title('x')` |\n| `\"tags\": [...]` | `.tags([...])` |\n| `\"editable\": true` | `.editable()` |\n| `\"refresh\": \"30s\"` | `.refresh('30s')` |\n| `\"time\": { \"from\": \"now-24h\", \"to\": \"now\" }` | `.time({ from: 'now-24h', to: 'now' })` |\n| `\"timezone\": \"browser\"` | `.timezone('browser')` |\n| `\"graphTooltip\": 1` | `.tooltip(DashboardCursorSync.Crosshair)` |\n| `\"type\": \"row\"` | `.withRow(new RowBuilder('title'))` |\n| `\"type\": \"stat\"` | `new StatBuilder()` |\n| `\"type\": \"timeseries\"` | `new TimeseriesBuilder()` |\n| `\"type\": \"logs\"` | `new LogsBuilder()` |\n| `\"type\": \"piechart\"` | `new PieChartBuilder()` |\n| `\"type\": \"bargauge\"` | `new BarGaugeBuilder()` |\n| `\"type\": \"barchart\"` | `new BarChartBuilder()` |\n| `\"type\": \"table\"` | `new TableBuilder()` |\n| `\"gridPos\": { \"h\": 8, \"w\": 12 }` | `.height(8).span(12)` |\n| `\"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" }` | `.datasource({ type: 'prometheus', uid: 'prometheus' })` |\n| `\"targets\": [{ \"expr\": \"...\" }]` | `.withTarget(new DataqueryBuilder().expr('...'))` |\n| `\"fieldConfig.defaults.unit\": \"ms\"` | `.unit('ms')` |\n| `\"fieldConfig.defaults.decimals\": 2` | `.decimals(2)` |\n| `\"fieldConfig.defaults.noValue\": \"N/A\"` | `.noValue('N/A')` |\n| `\"fieldConfig.defaults.custom.fillOpacity\": 15` | `.fillOpacity(15)` |\n| `\"fieldConfig.defaults.custom.stacking.mode\": \"normal\"` | `.stacking(new StackingConfigBuilder().mode(StackingMode.Normal))` |\n| `\"options.reduceOptions.calcs\": [\"lastNotNull\"]` | `.reduceOptions(new ReduceDataOptionsBuilder().calcs(['lastNotNull']))` |\n| `\"options.colorMode\": \"background\"` | `.colorMode(BigValueColorMode.Background)` |\n| `\"options.graphMode\": \"area\"` | `.graphMode(BigValueGraphMode.Area)` |\n| `\"collapsed\": true` | `.collapsed()` |\n\n**Tips for conversion:**\n1. Start with the dashboard-level config (uid, tags, time, refresh)\n2. Convert variables next (they're referenced by panels)\n3. Convert panels row by row, creating helper functions for repeated patterns\n4. Panel IDs are auto-assigned by Grafana - you don't need to set them\n5. `gridPos.x` and `gridPos.y` are computed automatically from `height()` and `span()` - you only need to specify width and height\n\n---\n\n## Integration with Helm/ConfigMaps\n\nIn this project, generated dashboard JSON files are provisioned via ConfigMaps:\n\n**Template (`templates/dashboards.yaml`):**\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: dashboard-overview\n  labels:\n    grafana_dashboard: \"1\"\ndata:\n  overview.json: |-\n{{ .Files.Get \"dashboards/overview.json\" | indent 4 }}\n```\n\n**Workflow:**\n1. Edit the TypeScript generator in `ops/dashboards/`\n2. Run `pnpm generate` (or `tsx generate.ts`) to produce JSON in `dashboards/`\n3. Commit the generated JSON alongside the generator code\n4. Deploy via your provisioning mechanism (e.g. sync the ConfigMap or drop into Grafana's provisioning directory)\n\nThe `grafana_dashboard: \"1\"` label tells the Grafana sidecar to load the ConfigMap as a dashboard automatically.\n\nFile v0.2.4:references/typescript-api.md\n\n# TypeScript API Reference\n\n## Table of Contents\n1. [Imports](#imports)\n2. [Dashboard Builder](#dashboard-builder)\n3. [Panel Types](#panel-types)\n4. [Query Builders](#query-builders)\n5. [Variable Builders](#variable-builders)\n6. [Field Configuration](#field-configuration)\n7. [Common Enums](#common-enums)\n8. [Go Quick Reference](#go-quick-reference)\n\n---\n\n## Imports\n\nEvery concept lives in its own subpath export. Import only what you need:\n\n```typescript\n// Dashboard structure\nimport {\n  DashboardBuilder,\n  RowBuilder,\n  QueryVariableBuilder,\n  CustomVariableBuilder,\n  DatasourceVariableBuilder,\n  DashboardLinkBuilder,\n  ThresholdsConfigBuilder,\n} from '@grafana/grafana-foundation-sdk/dashboard';\n\n// Panel types (each exports PanelBuilder)\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as GaugeBuilder } from '@grafana/grafana-foundation-sdk/gauge';\nimport { PanelBuilder as BarGaugeBuilder } from '@grafana/grafana-foundation-sdk/bargauge';\nimport { PanelBuilder as TableBuilder } from '@grafana/grafana-foundation-sdk/table';\nimport { PanelBuilder as PieChartBuilder } from '@grafana/grafana-foundation-sdk/piechart';\nimport { PanelBuilder as BarChartBuilder } from '@grafana/grafana-foundation-sdk/barchart';\nimport { PanelBuilder as HeatmapBuilder } from '@grafana/grafana-foundation-sdk/heatmap';\nimport { PanelBuilder as HistogramBuilder } from '@grafana/grafana-foundation-sdk/histogram';\nimport { PanelBuilder as LogsBuilder } from '@grafana/grafana-foundation-sdk/logs';\nimport { PanelBuilder as TextBuilder } from '@grafana/grafana-foundation-sdk/text';\nimport { PanelBuilder as StateTimelineBuilder } from '@grafana/grafana-foundation-sdk/statetimeline';\nimport { PanelBuilder as StatusHistoryBuilder } from '@grafana/grafana-foundation-sdk/statushistory';\nimport { PanelBuilder as NodeGraphBuilder } from '@grafana/grafana-foundation-sdk/nodegraph';\nimport { PanelBuilder as GeoMapBuilder } from '@grafana/grafana-foundation-sdk/geomap';\nimport { PanelBuilder as XYChartBuilder } from '@grafana/grafana-foundation-sdk/xychart';\nimport { PanelBuilder as TrendBuilder } from '@grafana/grafana-foundation-sdk/trend';\nimport { PanelBuilder as CandlestickBuilder } from '@grafana/grafana-foundation-sdk/candlestick';\nimport { PanelBuilder as CanvasBuilder } from '@grafana/grafana-foundation-sdk/canvas';\nimport { PanelBuilder as DatagridBuilder } from '@grafana/grafana-foundation-sdk/datagrid';\nimport { PanelBuilder as AnnotationsListBuilder } from '@grafana/grafana-foundation-sdk/annotationslist';\nimport { PanelBuilder as DashboardListBuilder } from '@grafana/grafana-foundation-sdk/dashboardlist';\nimport { PanelBuilder as NewsBuilder } from '@grafana/grafana-foundation-sdk/news';\n\n// Library panels (reference a shared, server-stored panel)\nimport { LibraryPanelBuilder } from '@grafana/grafana-foundation-sdk/librarypanel';\n\n// Query builders (each exports DataqueryBuilder)\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\nimport { DataqueryBuilder as TempoQueryBuilder } from '@grafana/grafana-foundation-sdk/tempo';\n// Other core datasource query builders (same DataqueryBuilder pattern):\n//   elasticsearch, cloudwatch, azuremonitor, googlecloudmonitoring, bigquery,\n//   athena, testdata, parca, grafanapyroscope (Pyroscope profiling)\nimport { DataqueryBuilder as TestDataQueryBuilder } from '@grafana/grafana-foundation-sdk/testdata';\n// Server-side / SQL expressions (math, reduce, resample, SQL across queries):\nimport { DataqueryBuilder as ExprBuilder } from '@grafana/grafana-foundation-sdk/expr';\n\n// Common types and sub-builders\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n// Typed unit constants (alternative to hardcoded strings like 'reqps'/'ms'):\nimport * as units from '@grafana/grafana-foundation-sdk/units';\n```\n\n---\n\n## Dashboard Builder\n\n```typescript\nnew DashboardBuilder(title: string)\n  // Identity\n  .uid(uid: string)\n  .tags(tags: string[])\n  .description(desc: string)\n\n  // Behavior\n  .editable()                    // allow editing in UI\n  .readonly()                    // read-only in UI\n  .refresh(interval: string)     // e.g. '30s', '1m', '5m'\n  .tooltip(mode: DashboardCursorSync)  // Off, Crosshair, Tooltip\n\n  // Time\n  .time({ from: string, to: string })  // e.g. { from: 'now-24h', to: 'now' }\n  .timezone(tz: string)                // 'browser', 'utc', or IANA tz\n\n  // Variables\n  .withVariable(builder)         // QueryVariableBuilder, CustomVariableBuilder, etc.\n\n  // Layout\n  .withRow(rowBuilder)           // RowBuilder\n  .withPanel(panelBuilder)       // any panel builder\n\n  // Links\n  .link(linkBuilder)             // DashboardLinkBuilder\n\n  // Annotations\n  .annotation(annotationBuilder)\n\n  // Build\n  .build()                       // returns the dashboard object\n```\n\n---\n\n## Panel Types\n\nAll panel builders share these common methods:\n\n```typescript\n// Every panel type\n.title(title: string)\n.description(desc: string)\n.transparent()                         // transparent background\n.datasource({ type: string, uid: string })\n.withTarget(queryBuilder)              // add a query\n.height(h: number)                     // grid height in rows\n.span(w: number)                       // grid width out of 24\n.unit(unit: string)                    // e.g. 'reqps', 'ms', 'percent', 'currencyUSD', 'short'\n.min(n: number)\n.max(n: number)\n.decimals(n: number)\n.noValue(text: string)                 // shown when no data\n.links(links: PanelLink[])\n.repeat(variableName: string)          // repeat panel for each variable value\n.maxPerRow(n: number)                  // max panels per row when repeating\n.thresholds(thresholdsConfigBuilder)\n.overrideByName(name, properties[])\n.overrideByRegexp(regexp, properties[])\n.withTransformation(transformation)\n```\n\n### Stat Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/stat';\n\nnew PanelBuilder()\n  .colorMode(common.BigValueColorMode.Background)  // None, Value, Background, BackgroundSolid\n  .graphMode(common.BigValueGraphMode.Area)         // None, Line, Area\n  .textMode(common.BigValueTextMode.Auto)           // Auto, Value, ValueAndName, Name, None\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .orientation(common.VizOrientation.Auto)          // Auto, Horizontal, Vertical\n```\n\n### Timeseries Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\n\nnew PanelBuilder()\n  .drawStyle(common.GraphDrawStyle.Line)             // Line, Bars, Points\n  .lineInterpolation(common.LineInterpolation.Linear) // Linear, Smooth, StepBefore, StepAfter\n  .lineWidth(n: number)                               // 0-10\n  .fillOpacity(n: number)                              // 0-100\n  .showPoints(common.VisibilityMode.Auto)             // Auto, Always, Never\n  .pointSize(n: number)\n  .gradientMode(common.GraphGradientMode.None)        // None, Opacity, Hue, Scheme\n  .spanNulls(bool | number)                           // connect null gaps (or max gap in ms)\n  .axisBorderShow(bool: boolean)\n  .stacking(new common.StackingConfigBuilder().mode(common.StackingMode.Normal))  // None, Normal, Percent\n  .legend(\n    new common.VizLegendOptionsBuilder()\n      .showLegend(true)\n      .placement(common.LegendPlacement.Bottom)       // Bottom, Right\n      .displayMode(common.LegendDisplayMode.List)      // List, Table, Hidden\n  )\n  .tooltip(\n    new common.VizTooltipOptionsBuilder()\n      .mode(common.TooltipDisplayMode.Multi)           // Single, Multi, None\n      .sort(common.SortOrder.Descending)               // Ascending, Descending, None\n  )\n  .thresholdsStyle(\n    new common.GraphThresholdsStyleConfigBuilder()\n      .mode(common.GraphThresholdsStyleMode.Off)       // Off, Line, Area, LineAndArea, Dashed, DashedAndArea\n  )\n```\n\n### Table Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/table';\n\nnew PanelBuilder()\n  .filterable(true)               // column filtering\n  .footer(footerBuilder)          // table footer with calculations\n```\n\n### Gauge Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/gauge';\n\nnew PanelBuilder()\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .orientation(common.VizOrientation.Auto)\n  .showThresholdLabels(true)\n  .showThresholdMarkers(true)\n```\n\n### BarGauge Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/bargauge';\n\nnew PanelBuilder()\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .orientation(common.VizOrientation.Horizontal)\n  .displayMode(common.BarGaugeDisplayMode.Gradient)  // Basic, Gradient, Lcd\n```\n\n### PieChart Panel\n\nPieChart uses its OWN legend builder (`PieChartLegendOptionsBuilder` from the `piechart` package), not the shared `common.VizLegendOptionsBuilder` - passing the common one is a type error because pie legends carry extra fields (`values`).\n\n```typescript\nimport { PanelBuilder, PieChartLegendOptionsBuilder } from '@grafana/grafana-foundation-sdk/piechart';\n\nnew PanelBuilder()\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .pieType(common.PieChartType.Pie)          // Pie, Donut\n  .legend(\n    new PieChartLegendOptionsBuilder()\n      .showLegend(true)\n      .placement(common.LegendPlacement.Right)\n  )\n```\n\n### BarChart Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/barchart';\n\nnew PanelBuilder()\n  .orientation(common.VizOrientation.Horizontal)\n```\n\n### Logs Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/logs';\n\nnew PanelBuilder()\n  .showTime(true)\n  .wrapLogMessage(true)\n  .enableLogDetails(true)\n  .prettifyLogMessage(false)\n  .showCommonLabels(false)\n  .showLabels(false)\n  .sortOrder(common.LogsSortOrder.Descending)  // Ascending, Descending\n  .dedupStrategy(common.LogsDedupStrategy.None) // None, Exact, Numbers, Signature\n```\n\n### Text Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/text';\n\nnew PanelBuilder()\n  .content('# Markdown content here')\n  .mode(common.TextMode.Markdown)  // Markdown, HTML, Code\n```\n\n---\n\n## Query Builders\n\n### Prometheus\n\n```typescript\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\n\nnew DataqueryBuilder()\n  .expr('sum(rate(http_requests_total{job=~\"$service\"}[$__rate_interval]))')\n  .legendFormat('{{job}}')\n  .refId('A')\n  .instant()           // instant query (single value)\n  .range()             // range query (time series) - default\n  .format(PromQueryFormat.Table)  // for table panels\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .hide(true)          // hide this query's results\n```\n\n### Loki\n\n```typescript\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\nnew DataqueryBuilder()\n  .expr('{namespace=\"myapp\", app=~\"$service\"}')\n  .legendFormat('{{app}}')\n  .refId('A')\n  .maxLines(100)\n  .queryType('range')       // 'range', 'instant', or 'stream'\n  .editorMode('code')       // 'code' or 'builder'\n  .datasource({ type: 'loki', uid: 'loki' })\n```\n\n**Loki query types:**\n- Log query: `{namespace=\"myapp\", app=\"my-app\"}` - returns log lines\n- Metric query: `count_over_time({namespace=\"myapp\"} [5m])` - returns time series\n- Use `| json` for JSON parsing, `| unwrap field_name` for numeric extraction\n\n---\n\n## Variable Builders\n\n### Query Variable\n\n```typescript\nimport { QueryVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\nnew QueryVariableBuilder('varName')\n  .label('Display Label')\n  .query('label_values(metric{filter=\"value\"}, label_name)')\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .refresh(2)              // 1=on load, 2=on time range change\n  .includeAll(true)        // add \"All\" option\n  .allValue('.*')          // regex for \"All\"\n  .multi(true)             // allow multiple selections\n  .sort(1)                 // 0=disabled, 1=alpha asc, 2=alpha desc, 3=num asc, etc.\n  .hide(0)                 // 0=visible, 1=hide label, 2=hide variable\n```\n\n### Custom Variable\n\n```typescript\nimport { CustomVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// Simple values\nnew CustomVariableBuilder('env')\n  .label('Environment')\n  .query('prod,staging,dev')\n  .current({ text: 'prod', value: 'prod' })\n\n// Key:value pairs (display text : actual value)\nnew CustomVariableBuilder('level')\n  .label('Log Level')\n  .query('All : .+, Error : error|fatal, Warning : warn')\n  .current({ text: 'All', value: '.+' })\n```\n\n### Datasource Variable\n\n```typescript\nimport { DatasourceVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\nnew DatasourceVariableBuilder('datasource')\n  .label('Data Source')\n  .type('prometheus')      // datasource plugin type\n  .regex('/^(?!.*test).*$/')  // filter datasources\n  .multi(false)\n```\n\n---\n\n## Field Configuration\n\n### Thresholds\n\n```typescript\nimport { ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// The first step MUST have value: null (it's the base/default color)\nnew ThresholdsConfigBuilder()\n  .mode(common.ThresholdsMode.Absolute)  // or Percentage\n  .steps([\n    { value: null as any, color: 'green' },\n    { value: 50, color: 'yellow' },\n    { value: 80, color: 'red' },\n  ])\n```\n\n### Value Mappings\n\n```typescript\n// Applied directly to panel as raw fieldConfig\n// The SDK doesn't have typed builders for all mapping types\n.withTransformation({\n  id: 'configFromData',\n  options: { /* ... */ }\n})\n```\n\n### Field Overrides\n\n```typescript\n// Override by field name\n.overrideByName('Revenue', [\n  { id: 'color', value: { fixedColor: 'green', mode: 'fixed' } },\n  { id: 'custom.fillOpacity', value: 10 },\n])\n\n// Override by regex\n.overrideByRegexp('.*5..', [\n  { id: 'color', value: { fixedColor: 'red', mode: 'fixed' } },\n])\n\n// Common override property IDs:\n// 'color'              - { fixedColor: 'green', mode: 'fixed' }\n// 'unit'               - 'ms', 'reqps', 'currencyUSD', etc.\n// 'decimals'           - number\n// 'custom.fillOpacity' - 0-100\n// 'custom.lineWidth'   - 0-10\n// 'custom.drawStyle'   - 'line', 'bars', 'points'\n// 'custom.lineStyle'   - { fill: 'dash', dash: [10, 10] }\n// 'custom.stacking'    - { mode: 'normal' }\n```\n\n---\n\n## Common Enums\n\n```typescript\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\n// BigValueColorMode: None, Value, Background, BackgroundSolid\n// BigValueGraphMode: None, Line, Area\n// BigValueTextMode: Auto, Value, ValueAndName, Name, None\n// GraphDrawStyle: Line, Bars, Points\n// LineInterpolation: Linear, Smooth, StepBefore, StepAfter\n// VisibilityMode: Auto, Always, Never\n// GraphGradientMode: None, Opacity, Hue, Scheme\n// StackingMode: None, Normal, Percent\n// LegendPlacement: Bottom, Right\n// LegendDisplayMode: List, Table, Hidden\n// TooltipDisplayMode: Single, Multi, None\n// SortOrder: Ascending, Descending, None\n// ThresholdsMode: Absolute, Percentage\n// VizOrientation: Auto, Horizontal, Vertical\n// LogsSortOrder: Ascending, Descending\n// LogsDedupStrategy: None, Exact, Numbers, Signature\n// BarGaugeDisplayMode: Basic, Gradient, Lcd\n// PieChartType: Pie, Donut\n// GraphThresholdsStyleMode: Off, Line, Area, LineAndArea, Dashed, DashedAndArea\n```\n\n---\n\n## Common Units\n\nThe SDK ships a typed `units` module (`@grafana/grafana-foundation-sdk/units`) with named constants for the strings below - use it to avoid typos, e.g. `.unit(units.RequestsPerSecond)` instead of `.unit('reqps')`. Plain strings still work and are fine for one-offs.\n\nFrequently used unit strings (pass to `.unit()`):\n\n| Unit | Description |\n|------|-------------|\n| `'short'` | Auto-scaled number |\n| `'none'` | Raw number |\n| `'percent'` | 0-100 percentage |\n| `'percentunit'` | 0.0-1.0 percentage |\n| `'reqps'` | Requests per second |\n| `'ms'` | Milliseconds |\n| `'s'` | Seconds |\n| `'bytes'` | Bytes (auto IEC) |\n| `'decbytes'` | Bytes (auto SI) |\n| `'currencyUSD'` | US Dollars |\n| `'ops'` | Operations per second |\n\n---\n\n## Go Quick Reference\n\nGo follows the same builder pattern but with Go conventions (uppercase methods, pointers for nullable values).\n\n```go\nimport (\n    \"github.com/grafana/grafana-foundation-sdk/go/cog\"\n    \"github.com/grafana/grafana-foundation-sdk/go/common\"\n    \"github.com/grafana/grafana-foundation-sdk/go/dashboard\"\n    \"github.com/grafana/grafana-foundation-sdk/go/prometheus\"\n    \"github.com/grafana/grafana-foundation-sdk/go/loki\"\n    \"github.com/grafana/grafana-foundation-sdk/go/stat\"\n    \"github.com/grafana/grafana-foundation-sdk/go/timeseries\"\n)\n\n// Dashboard\nbuilder := dashboard.NewDashboardBuilder(\"Title\").\n    Uid(\"my-uid\").\n    Tags([]string{\"tag\"}).\n    Editable().\n    Refresh(\"30s\").\n    Time(\"now-24h\", \"now\").\n    Timezone(common.TimeZoneBrowser)\n\n// Prometheus query\nprometheus.NewDataqueryBuilder().\n    Expr(`sum(rate(http_requests_total{job=~\"$service\"}[5m]))`).\n    LegendFormat(\"{{job}}\")\n\n// Stat panel\nstat.NewPanelBuilder().\n    Title(\"Requests\").\n    Datasource(dashboard.DataSourceRef{\n        Type: cog.ToPtr(\"prometheus\"),\n        Uid:  cog.ToPtr(\"prometheus\"),\n    }).\n    WithTarget(query).\n    Height(4).Span(6)\n\n// Thresholds (note: first step Value is nil)\ndashboard.NewThresholdsConfigBuilder().\n    Mode(dashboard.ThresholdsModeAbsolute).\n    Steps([]dashboard.Threshold{\n        {Value: nil, Color: \"green\"},\n        {Value: cog.ToPtr[float64](80), Color: \"red\"},\n    })\n\n// Build and serialize\ndash, err := builder.Build()\njsonBytes, _ := json.MarshalIndent(dash, \"\", \"  \")\n```\n\n**Go gotchas:**\n- Use `cog.ToPtr[T](value)` for nullable pointer fields (thresholds, datasource refs)\n- Some fields require wrapper types: `dashboard.StringOrMap{String: cog.ToPtr(val)}`\n- `Build()` returns `(Dashboard, error)` - always check the error\n- Variable `Current` uses `dashboard.VariableOption` with `dashboard.StringOrArrayOfString`\n\nFile v0.2.4:CHANGELOG.md\n\n# Changelog\n\nAll notable changes to this skill will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),\nand this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.2.4] - 2026-08-21\n\n### Changed\n\n- Declared ClawHub browse categories (`operations, development`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category.\n\n### Removed\n\n- `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub.\n\n## [0.2.3] - 2026-08-07\n\n### Removed\n\n- \"Project-Specific Context\" section that hardcoded one private repo's Helm chart layout and paths into the published skill.\n\n### Changed\n\n- Genericized example queries and provisioning workflow: `namespace=\"x402\"` -> `namespace=\"myapp\"`, private chart paths -> generic `deploy/dashboards/` layout, `helmfile sync` step -> provisioning-mechanism-agnostic wording.\n\n## [0.2.2] - 2026-07-22\n\n### Added\n\n- skill-card.md release record following NVIDIA's skill-card format\n- metadata.openclaw block (emoji, homepage) for ClawHub display\n\n## [0.2.1] - 2026-07-10\n\n### Changed\n- CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).\n\n## [0.2.0] - 2026-06-05\n\n### Fixed\n- Go install string corrected to `go get github.com/grafana/grafana-foundation-sdk/go@v0.0.16` (the canonical tag form per the official docs), replacing the stale `@next+cog-v0.0.x` branch ref.\n- PieChart legend example now uses `PieChartLegendOptionsBuilder` (from the `piechart` package) instead of `common.VizLegendOptionsBuilder`, which is a type error for piechart panels.\n\n### Added\n- `metadata.upstream` tracking established (pinned to v0.0.16).\n- Known gotcha on dashboard schema v1 vs v2: the skill targets v1; `dashboardv2beta1` (k8s apiVersion `dashboard.grafana.app/v2beta1`) exists and is still stabilizing.\n- Known gotchas on the type-checking gap (builders only checked when wired into a tsconfig; use project-local `npx tsc` for the SDK's ES2024/bundler output) and the regenerate-JSON-after-every-edit discipline.\n- `units` typed-constants module, `expr` server-side/SQL expressions, `testdata` datasource, and the broader set of core datasource query builders (elasticsearch, cloudwatch, azuremonitor, googlecloudmonitoring, bigquery, athena, parca, grafanapyroscope) in the TypeScript reference.\n- Additional panel types in the import reference: canvas, datagrid, annotationslist, dashboardlist, news; plus `librarypanel`.\n\n### Changed\n- Version pins updated to 0.0.16 across SKILL.md (install commands) and references/patterns.md (`package.json` example).\n\nVerified against: @grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\n\n## [0.1.0] - 2026-06-05\n\n### Added\n- Initial release: building Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Covers installation, core architecture, a TypeScript quick start, key patterns (helper functions, template variables, panel sizing, thresholds, field overrides, rows, Loki queries, transformations), output generation, and known gotchas, with `references/typescript-api.md` and `references/patterns.md` for depth.\n\nFile v0.2.4:skill-card.md\n\n## Description:\n\nBuild Grafana dashboards as code with typed TypeScript and Go builders that produce Grafana dashboard JSON.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[tenequm](https://clawhub.ai/user/tenequm)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and platform engineers use this skill to create, modify, or convert Grafana dashboards into typed dashboard-as-code generators for observability workflows.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The underlying Grafana Foundation SDK is pre-1.0, so APIs can change between releases.\n\nMitigation: Pin SDK and compiler versions in projects where reproducible dashboard generation matters.\n\nRisk: Generated Grafana dashboard JSON may encode incorrect queries, labels, thresholds, or provisioning details.\n\nMitigation: Review generated dashboard JSON before deploying it to Grafana.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/tenequm/skills/grafana-foundation-sdk)\n- [Publisher profile](https://clawhub.ai/user/tenequm)\n- [Project homepage](https://github.com/tenequm/skills/tree/main/skills/grafana-foundation-sdk)\n- [TypeScript API reference](references/typescript-api.md)\n- [Dashboard patterns](references/patterns.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with TypeScript, Go, shell, JSON, and configuration examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces guidance and code snippets for generating Grafana dashboard JSON; generated dashboards should be reviewed before deployment.]\n\n## Skill Version(s):\n\n0.2.4 (source: frontmatter, changelog, release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v0.2.4:LICENSE.txt\n\nApache License\nVersion 2.0, January 2004\nhttps://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the\ncopyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other\nentities that control, are controlled by, or are under common control with\nthat entity. For the purposes of this definition, \"control\" means (i) the\npower, direct or indirect, to cause the direction or management of such\nentity, whether by contract or otherwise, or (ii) ownership of fifty percent\n(50%) or more of the outstanding shares, or (iii) beneficial ownership of\nsuch entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications,\nincluding but not limited to software source code, documentation source, and\nconfiguration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object\ncode, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form,\nmade available under the License, as indicated by a copyright notice that is\nincluded in or attached to the work (an example is provided in the Appendix\nbelow).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form,\nthat is based on (or derived from) the Work and for which the editorial\nrevisions, annotations, elaborations, or other modifications represent, as a\nwhole, an original work of authorship. For the purposes of this License,\nDerivative Works shall not include works that remain separable from, or\nmerely link (or bind by name) to the interfaces of, the Work and Derivative\nWorks thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original\nversion of the Work and any modifications or additions to that Work or\nDerivative Works thereof, that is intentionally submitted to Licensor for\ninclusion in the Work by the copyright owner or by an individual or Legal\nEntity authorized to submit on behalf of the copyright owner. For the\npurposes of this definition, \"submitted\" means any form of electronic, verbal,\nor written communication sent to the Licensor or its representatives,\nincluding but not limited to communication on electronic mailing lists, source\ncode control systems, and issue tracking systems that are managed by, or on\nbehalf of, the Licensor for the purpose of discussing and improving the Work,\nbut excluding communication that is conspicuously marked or otherwise\ndesignated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on\nbehalf of whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this\nLicense, each Contributor hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable copyright license to\nreproduce, prepare Derivative Works of, publicly display, publicly perform,\nsublicense, and distribute the Work and such Derivative Works in Source or\nObject form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this\nLicense, each Contributor hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable (except as stated in this\nsection) patent license to make, have made, use, offer to sell, sell, import,\nand otherwise transfer the Work, where such license applies only to those\npatent claims licensable by such Contributor that are necessarily infringed by\ntheir Contribution(s) alone or by combination of their Contribution(s) with\nthe Work to which such Contribution(s) was submitted. If You institute patent\nlitigation against any entity (including a cross-claim or counterclaim in a\nlawsuit) alleging that the Work or a Contribution incorporated within the Work\nconstitutes direct or contributory patent infringement, then any patent\nlicenses granted to You under this License for that Work shall terminate as of\nthe date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or\nDerivative Works thereof in any medium, with or without modifications, and in\nSource or Object form, provided that You meet the following conditions:\n\n(a) You must give any other recipients of the Work or Derivative Works a copy\nof this License; and\n\n(b) You must cause any modified files to carry prominent notices stating that\nYou changed the files; and\n\n(c) You must retain, in the Source form of any Derivative Works that You\ndistribute, all copyright, patent, trademark, and attribution notices from\nthe Source form of the Work, excluding those notices that do not pertain to\nany part of the Derivative Works; and\n\n(d) If the Work includes a \"NOTICE\" text file as part of its distribution,\nthen any Derivative Works that You distribute must include a readable copy of\nthe attribution notices contained within such NOTICE file, excluding those\nnotices that do not pertain to any part of the Derivative Works, in at least\none of the following places: within a NOTICE text file distributed as part of\nthe Derivative Works; within the Source form or documentation, if provided\nalong with the Derivative Works; or, within a display generated by the\nDerivative Works, if and wherever such third-party notices normally appear.\nThe contents of the NOTICE file are for informational purposes only and do not\nmodify the License. You may add Your own attribution notices within Derivative\nWorks that You distribute, alongside or as an addendum to the NOTICE text from\nthe Work, provided that such additional attribution notices cannot be\nconstrued as modifying the License.\n\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a\nwhole, provided Your use, reproduction, and distribution of the Work otherwise\ncomplies with the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise, any\nContribution intentionally submitted for inclusion in the Work by You to the\nLicensor shall be under the terms and conditions of this License, without any\nadditional terms or conditions. Notwithstanding the above, nothing herein\nshall supersede or modify the terms of any separate license agreement you may\nhave executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names,\ntrademarks, service marks, or product names of the Licensor, except as\nrequired for reasonable and customary use in describing the origin of the Work\nand reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in\nwriting, Licensor provides the Work (and each Contributor provides its\nContributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied, including, without limitation, any warranties\nor conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\nPARTICULAR PURPOSE. You are solely responsible for determining the\nappropriateness of using or redistributing the Work and assume any risks\nassociated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in\ntort (including negligence), contract, or otherwise, unless required by\napplicable law (such as deliberate and grossly negligent acts) or agreed to in\nwriting, shall any Contributor be liable to You for damages, including any\ndirect, indirect, special, incidental, or consequential damages of any\ncharacter arising as a result of this License or out of the use or inability to\nuse the Work (including but not limited to damages for loss of goodwill, work\nstoppage, computer failure or malfunction, or any and all other commercial\ndamages or losses), even if such Contributor has been advised of the\npossibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work\nor Derivative Works thereof, You may choose to offer, and charge a fee for,\nacceptance of support, warranty, indemnity, or other liability obligations\nand/or rights consistent with this License. However, in accepting such\nobligations, You may act only on Your own behalf and on Your sole\nresponsibility, not on behalf of any other Contributor, and only if You agree\nto indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nArchive v0.2.3: 7 files, 22009 bytes\n\nFiles: CHANGELOG.md (2986b), LICENSE.txt (9157b), references/patterns.md (13365b), references/typescript-api.md (18174b), skill-card.md (2355b), SKILL.md (13573b), _meta.json (141b)\n\nFile v0.2.3:SKILL.md\n\n---\nname: grafana-foundation-sdk\ndescription: Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.\nmetadata:\n  version: \"0.2.3\"\n  upstream: \"@grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/grafana-foundation-sdk\n    emoji: \"📊\"\n---\n\n# Grafana Foundation SDK\n\nThe grafana-foundation-sdk provides strongly typed builder libraries for defining Grafana dashboards as code. Instead of writing raw JSON (which is error-prone and hard to review in diffs), you compose dashboards using chained builder calls that produce valid Grafana JSON.\n\nThe SDK is auto-generated from Grafana's internal CUE schemas via the `cog` tool. It supports Go, TypeScript, Python, PHP, and Java. This skill focuses on **TypeScript** (primary) and **Go** (secondary) since those are the most common choices for infrastructure teams.\n\n## When to use this skill\n\n- Creating new Grafana dashboards from scratch\n- Converting existing hand-written dashboard JSON to typed code\n- Adding panels, variables, or queries to dashboards\n- Building reusable dashboard components (helper functions for common panel patterns)\n- Generating dashboards dynamically based on service lists or configs\n\n## Installation\n\nThe SDK is published as concrete `v0.0.x` tags (latest: **v0.0.16**). Pin explicitly - it is pre-1.0 and the API churns between releases (see Known Gotchas).\n\n**TypeScript:**\n```bash\nnpm install '@grafana/grafana-foundation-sdk@~0.0.16'\n# or\npnpm add '@grafana/grafana-foundation-sdk@~0.0.16'\n```\n\n**Go:**\n```bash\ngo get github.com/grafana/grafana-foundation-sdk/go@v0.0.16\n```\n\n## Core Architecture\n\nEverything follows the **builder pattern**: create a builder, chain configuration methods, call `.build()` (TS) or `.Build()` (Go) to produce the final object. The output is standard Grafana dashboard JSON - compatible with Grafana's API, file-based provisioning, and Kubernetes ConfigMaps.\n\nEach panel type, query type, and variable type lives in its own package. You import only what you need:\n\n```typescript\n// Each concern has its own import\nimport { DashboardBuilder, RowBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n```\n\n## Quick Start - TypeScript\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\nconst dashboard = new DashboardBuilder('My Service Overview')\n  .uid('my-service-overview')\n  .tags(['my-service'])\n  .editable()\n  .refresh('30s')\n  .time({ from: 'now-24h', to: 'now' })\n  .timezone('browser')\n  .withVariable(\n    new QueryVariableBuilder('service')\n      .label('Service')\n      .query('label_values(up{namespace=\"default\"}, job)')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .refresh(1)\n      .includeAll(true)\n      .allValue('.*')\n      .sort(1)\n  )\n  .withRow(new RowBuilder('Overview'))\n  .withPanel(\n    new StatBuilder()\n      .title('Request Rate')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('req/s')\n      )\n      .unit('reqps')\n      .decimals(1)\n      .height(4)\n      .span(6)\n      .colorMode(common.BigValueColorMode.Background)\n      .graphMode(common.BigValueGraphMode.Area)\n      .reduceOptions(\n        new common.ReduceDataOptionsBuilder().calcs(['lastNotNull'])\n      )\n  )\n  .withPanel(\n    new TimeseriesBuilder()\n      .title('Request Rate Over Time')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum by (job)(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('{{job}}')\n      )\n      .unit('reqps')\n      .fillOpacity(15)\n      .height(8)\n      .span(12)\n  );\n\n// Output the dashboard JSON\nconsole.log(JSON.stringify(dashboard.build(), null, 2));\n```\n\n## Key Patterns\n\n### 1. Helper functions for repeated panel configurations\n\nThe biggest win from using the SDK is creating reusable helpers that encode your team's conventions:\n\n```typescript\nfunction promDs() {\n  return { type: 'prometheus', uid: 'prometheus' } as const;\n}\n\nfunction lokiDs() {\n  return { type: 'loki', uid: 'loki' } as const;\n}\n\nfunction promQuery(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction statPanel(title: string, expr: string, opts?: { unit?: string; decimals?: number; color?: string }) {\n  const panel = new StatBuilder()\n    .title(title)\n    .datasource(promDs())\n    .withTarget(promQuery(expr))\n    .height(4)\n    .span(4)\n    .colorMode(common.BigValueColorMode.Background)\n    .graphMode(common.BigValueGraphMode.Area)\n    .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']));\n\n  if (opts?.unit) panel.unit(opts.unit);\n  if (opts?.decimals !== undefined) panel.decimals(opts.decimals);\n  // Thresholds can be set via .thresholds() if needed\n\n  return panel;\n}\n```\n\n### 2. Template variables\n\n```typescript\n// Query variable - populated from Prometheus labels\nnew QueryVariableBuilder('service')\n  .label('Service')\n  .query('label_values(http_server_duration_count{namespace=\"myapp\"}, job)')\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .refresh(2)  // 1=on dashboard load, 2=on time range change\n  .includeAll(true)\n  .allValue('.*')\n  .sort(1)  // 1=alphabetical asc\n\n// Custom variable - static key:value pairs\nnew CustomVariableBuilder('level')\n  .label('Log Level')\n  .query('All : .+, Error : error|fatal, Warning : warn, Info : info, Debug : debug')\n  .current({ text: 'All', value: '.+' })\n```\n\nReference variables in queries with standard Grafana syntax: `$service`, `$__range`, `$__rate_interval`, `$__auto`.\n\n### 3. Panel sizing\n\nPanels use `height(h)` (grid rows) and `span(w)` (out of 24 columns):\n- Full width: `.span(24)`\n- Half width: `.span(12)`\n- Third width: `.span(8)`\n- Quarter width: `.span(6)`\n- Typical stat panel: `.height(4).span(4)`\n- Typical timeseries: `.height(8).span(12)`\n\n### 4. Thresholds\n\n```typescript\nimport { ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// First step must have no value (it's the base)\nnew StatBuilder()\n  .thresholds(\n    new ThresholdsConfigBuilder()\n      .mode(common.ThresholdsMode.Absolute)\n      .steps([\n        { value: null as any, color: 'green' },\n        { value: 80, color: 'yellow' },\n        { value: 95, color: 'red' },\n      ])\n  )\n```\n\n### 5. Field overrides\n\n```typescript\nnew TimeseriesBuilder()\n  .overrideByName('Revenue', [\n    { id: 'color', value: { fixedColor: 'green', mode: 'fixed' } },\n  ])\n  .overrideByRegexp('.*5..', [\n    { id: 'color', value: { fixedColor: 'red', mode: 'fixed' } },\n  ])\n```\n\n### 6. Rows (including collapsed)\n\n```typescript\n// Regular row\n.withRow(new RowBuilder('Traffic'))\n\n// Collapsed row with nested panels\n.withRow(\n  new RowBuilder('Business Details')\n    .collapsed()\n    .withPanel(/* ... */)\n    .withPanel(/* ... */)\n)\n```\n\n### 7. Loki log and metric queries\n\n```typescript\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\n// Log query\nnew LokiQueryBuilder()\n  .expr('{namespace=\"myapp\", app=~\"$service\", level=~\"$level\"}')\n  .refId('A')\n\n// Metric query from logs\nnew LokiQueryBuilder()\n  .expr('sum by (buyer_wallet)(count_over_time({namespace=\"myapp\"} | event=\"request\" [$__range]))')\n  .legendFormat('{{buyer_wallet}}')\n  .refId('A')\n```\n\n### 8. Transformations\n\nTransformations are applied as raw objects since the SDK doesn't have typed builders for all transformation types:\n\n```typescript\nnew TableBuilder()\n  .withTransformation({\n    id: 'reduce',\n    options: {\n      reducers: ['lastNotNull'],\n      mode: 'seriesToRows',\n      includeTimeField: false,\n      labelsToFields: true,\n    },\n  })\n  .withTransformation({\n    id: 'organize',\n    options: {\n      excludeByName: { Field: true },\n      renameByName: { buyer_wallet: 'Buyer Wallet', 'Last not null': 'Requests' },\n    },\n  })\n  .withTransformation({\n    id: 'sortBy',\n    options: { sort: [{ field: 'Requests', desc: true }] },\n  })\n```\n\n## Generating Output\n\nThe `.build()` call returns a plain object matching Grafana's dashboard JSON schema. Serialize it however you need:\n\n```typescript\n// Standard JSON file (for provisioning or ConfigMaps)\nconst fs = require('fs');\nconst dashboard = builder.build();\nfs.writeFileSync('dashboard.json', JSON.stringify(dashboard, null, 2));\n\n// Kubernetes resource manifest (for Grafana's k8s API)\nconst manifest = {\n  apiVersion: 'dashboard.grafana.app/v1beta1',\n  kind: 'Dashboard',\n  metadata: { name: dashboard.uid },\n  spec: dashboard,\n};\nconsole.log(JSON.stringify(manifest, null, 2));\n```\n\n## Known Gotchas\n\nThese are sharp edges discovered from real usage and open issues on the SDK repo:\n\n1. **SDK is v0.0.x (public preview)** - Used by Grafana Labs in production but the API can change between releases. Pin your version explicitly. Best suited for Grafana >= 12, works with >= 10.\n\n2. **`instant()` and `range()` are mutually exclusive in Prometheus** - Calling `.instant()` sets `instant=true` AND `range=false`. Calling `.range()` does the opposite. Use `.rangeAndInstant()` if you need both.\n\n3. **Loki `range()`/`instant()` are deprecated** - Use `.queryType('range')` or `.queryType('instant')` instead. Similarly, `.resolution()` is deprecated in favor of `.step()`.\n\n4. **First threshold step must have `value: null`** - This is the base/default color. Omitting it produces invalid JSON.\n\n5. **Panel IDs are auto-assigned** - You don't set `id` on panels. Grafana assigns them at import time. Similarly, `gridPos.x/y` are computed from `height()` and `span()`.\n\n6. **Transformations are plain objects** - The SDK has no typed builders for transformations. Pass them as raw `{ id, options }` objects via `.withTransformation()`.\n\n7. **CustomVariable quirk** - When provisioning via Grafana's API, `CustomVariableBuilder` requires the `.query()` field with comma-separated key:value pairs (e.g., `'All : .+, Error : error'`) for options to persist, even when `.values()` is also used.\n\n8. **Go: `cog.ToPtr()` is essential** - Many struct fields are pointer types. Use `cog.ToPtr[T](value)` for nullable fields (thresholds, datasource refs). TypeScript doesn't have this issue.\n\n9. **Go: `Build()` returns error** - Always check it. TypeScript's `.build()` returns the object directly with compile-time type safety instead.\n\n10. **No typed query builders for plugin datasources** - Only core datasources (Prometheus, Loki, Tempo, Elasticsearch, CloudWatch, etc.) have builders. For third-party plugins, define custom query types by implementing the `Builder<Dataquery>` interface.\n\n11. **Dashboard schema v1 vs v2** - This skill targets the v1 dashboard (`@grafana/grafana-foundation-sdk/dashboard`, k8s apiVersion `dashboard.grafana.app/v1beta1`). A newer schema v2 ships as `dashboardv2beta1` (k8s apiVersion `dashboard.grafana.app/v2beta1`) with its own builders. v2beta1 is still stabilizing and has known sharp edges (e.g. transforms, annotation positioning, SQL expressions in Go) - prefer v1 unless you specifically need v2 layouts. Most query/panel builders are shared; some expose a `QueryV2Builder`/`VisualizationV2Builder` variant for v2.\n\n12. **Builders are only type-checked if wired into a tsconfig** - The SDK gives compile-time safety only when the generator file is actually type-checked. A generator sitting under a non-package directory (e.g. a Helm chart dir) that no `tsconfig` includes is silently unchecked, so type errors surface only at `.build()` runtime. Also: the SDK's output targets ES2024/`bundler` module resolution, which an older global `tsc` chokes on - run the project-local compiler (`npx tsc`), not a stale global one.\n\n13. **Regenerate JSON after every generator edit** - The deployed dashboard is the generated JSON, not the `.ts`/`.go` source. Edit the generator, re-run it, and commit the regenerated JSON together; never hand-edit the generated JSON (the next regen silently overwrites it). A repo rule (\"never edit the dashboard JSON directly\") is worth adding.\n\n## Reference Files\n\nFor detailed API reference and complete examples, see:\n- `references/typescript-api.md` - Full TypeScript API with all panel types, query builders, and configuration options\n- `references/patterns.md` - Common dashboard patterns, recipes, and a complete example converting this project's dashboard to SDK code\n\nFile v0.2.3:_meta.json\n\n{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"grafana-foundation-sdk\",\n  \"version\": \"0.2.3\",\n  \"publishedAt\": 1786109809974\n}\n\nFile v0.2.3:references/patterns.md\n\n# Common Dashboard Patterns\n\n## Table of Contents\n1. [Project Structure](#project-structure)\n2. [RED Method Dashboard](#red-method-dashboard)\n3. [Kubernetes Service Dashboard](#kubernetes-service-dashboard)\n4. [Loki Log Panels](#loki-log-panels)\n5. [Multi-Datasource Dashboard](#multi-datasource-dashboard)\n6. [Dynamic Dashboard Generation](#dynamic-dashboard-generation)\n7. [Converting Raw JSON to SDK](#converting-raw-json-to-sdk)\n8. [Integration with Helm/ConfigMaps](#integration-with-helmconfigmaps)\n\n---\n\n## Project Structure\n\nFor a monorepo with multiple dashboards, keep dashboard generators alongside the monitoring config:\n\n```\nops/\n  dashboards/\n    generate.ts          # entry point - imports and runs all generators\n    helpers.ts           # shared datasource refs, query helpers, panel defaults\n    overview.ts          # overview dashboard generator\n    per-service.ts       # per-service dashboard generator\n    package.json         # { \"type\": \"module\", \"scripts\": { \"generate\": \"tsx generate.ts\" } }\n    tsconfig.json\n  deploy/\n    dashboards/        # generated JSON files go here\n```\n\nOr for a single dashboard, a simple standalone script works fine:\n\n```\nops/dashboards/\n  generate-overview.ts\n  package.json\n```\n\n**package.json:**\n```json\n{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"generate\": \"tsx generate.ts\"\n  },\n  \"dependencies\": {\n    \"@grafana/grafana-foundation-sdk\": \"^0.0.16\"\n  },\n  \"devDependencies\": {\n    \"tsx\": \"^4.0.0\"\n  }\n}\n```\n\n---\n\n## RED Method Dashboard\n\nThe RED method (Rate, Errors, Duration) is the standard pattern for monitoring request-driven services. Here's a complete implementation:\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder, ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\n// --- Helpers ---\n\nconst PROM = { type: 'prometheus', uid: 'prometheus' } as const;\n\nfunction pq(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction defaultTimeseries(title: string) {\n  return new TimeseriesBuilder()\n    .title(title)\n    .datasource(PROM)\n    .height(8)\n    .span(12)\n    .fillOpacity(15)\n    .lineWidth(1)\n    .showPoints(common.VisibilityMode.Never)\n    .legend(\n      new common.VizLegendOptionsBuilder()\n        .showLegend(true)\n        .placement(common.LegendPlacement.Bottom)\n        .displayMode(common.LegendDisplayMode.List)\n    )\n    .tooltip(\n      new common.VizTooltipOptionsBuilder()\n        .mode(common.TooltipDisplayMode.Multi)\n        .sort(common.SortOrder.Descending)\n    );\n}\n\n// --- Dashboard ---\n\nfunction buildREDDashboard(namespace: string, metricPrefix: string) {\n  return new DashboardBuilder(`${namespace} RED`)\n    .uid(`${namespace}-red`)\n    .tags([namespace, 'red'])\n    .editable()\n    .refresh('30s')\n    .time({ from: 'now-1h', to: 'now' })\n    .withVariable(\n      new QueryVariableBuilder('service')\n        .label('Service')\n        .query(`label_values(${metricPrefix}_duration_count{namespace=\"${namespace}\"}, job)`)\n        .datasource(PROM)\n        .refresh(2)\n        .includeAll(true)\n        .allValue('.*')\n        .sort(1)\n    )\n\n    // --- Rate ---\n    .withRow(new RowBuilder('Rate'))\n    .withPanel(\n      defaultTimeseries('Request Rate')\n        .withTarget(pq(\n          `sum by (job)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m]))`,\n          '{{job}}'\n        ))\n        .unit('reqps')\n        .stacking(new common.StackingConfigBuilder().mode(common.StackingMode.Normal))\n    )\n    .withPanel(\n      defaultTimeseries('Request Rate by Status')\n        .withTarget(pq(\n          `sum by (http_status_code)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m]))`,\n          '{{http_status_code}}'\n        ))\n        .unit('reqps')\n    )\n\n    // --- Errors ---\n    .withRow(new RowBuilder('Errors'))\n    .withPanel(\n      defaultTimeseries('Error Rate (5xx)')\n        .withTarget(pq(\n          `sum by (job)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\",http_status_code=~\"5..\"}[5m]))`,\n          '{{job}}'\n        ))\n        .unit('reqps')\n    )\n    .withPanel(\n      defaultTimeseries('Error Percentage')\n        .withTarget(pq(\n          `sum(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\",http_status_code=~\"5..\"}[5m])) / sum(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m])) * 100`,\n          'error %'\n        ))\n        .unit('percent')\n    )\n\n    // --- Duration ---\n    .withRow(new RowBuilder('Duration'))\n    .withPanel(\n      defaultTimeseries('Latency Percentiles')\n        .withTarget(pq(\n          `histogram_quantile(0.50, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p50'\n        ))\n        .withTarget(pq(\n          `histogram_quantile(0.95, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p95'\n        ))\n        .withTarget(pq(\n          `histogram_quantile(0.99, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p99'\n        ))\n        .unit('ms')\n    )\n    .withPanel(\n      defaultTimeseries('Latency by Service')\n        .withTarget(pq(\n          `histogram_quantile(0.95, sum by (job, le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          '{{job}} p95'\n        ))\n        .unit('ms')\n    );\n}\n\n// Generate\nconst dashboard = buildREDDashboard('myapp', 'http_server');\nconsole.log(JSON.stringify(dashboard.build(), null, 2));\n```\n\n---\n\n## Kubernetes Service Dashboard\n\nPattern for monitoring a specific Kubernetes service with both Prometheus metrics and Loki logs:\n\n```typescript\nimport { PanelBuilder as LogsBuilder } from '@grafana/grafana-foundation-sdk/logs';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\nconst LOKI = { type: 'loki', uid: 'loki' } as const;\n\n// Log panel with filtering\nnew LogsBuilder()\n  .title('Application Logs')\n  .datasource(LOKI)\n  .withTarget(\n    new LokiQueryBuilder()\n      .expr('{namespace=\"myapp\", app=~\"$service\", level=~\"$level\"}')\n      .refId('A')\n  )\n  .showTime(true)\n  .wrapLogMessage(true)\n  .enableLogDetails(true)\n  .sortOrder(common.LogsSortOrder.Descending)\n  .dedupStrategy(common.LogsDedupStrategy.None)\n  .height(12)\n  .span(24)\n```\n\n---\n\n## Loki Log Panels\n\n### Metric from logs (count, sum, avg)\n\n```typescript\n// Count events over time\nnew LokiQueryBuilder()\n  .expr('sum by (status)(count_over_time({namespace=\"myapp\", app=~\"$service\"} | event=\"request\" [$__range]))')\n  .legendFormat('{{status}}')\n\n// Sum a numeric field extracted from logs\nnew LokiQueryBuilder()\n  .expr('sum by (network)(sum_over_time({namespace=\"myapp\"} | event=\"settlement\" | success=\"true\" | unwrap amount_usd [$__range]))')\n  .legendFormat('{{network}}')\n\n// Average a numeric field\nnew LokiQueryBuilder()\n  .expr('avg(avg_over_time({namespace=\"myapp\", app=~\"$service\"} | event=\"request\" | status=\"200\" | unwrap duration_ms | __error__=\"\" [$__auto]))')\n  .legendFormat('avg latency')\n\n// Top-K from logs\nnew LokiQueryBuilder()\n  .expr('topk(10, sum by (buyer_wallet)(count_over_time({namespace=\"myapp\"} | event=\"request\" | buyer_wallet!=\"\" [$__range])))')\n  .legendFormat('{{buyer_wallet}}')\n```\n\n### Table panel with Loki + transformations\n\n```typescript\nimport { PanelBuilder as TableBuilder } from '@grafana/grafana-foundation-sdk/table';\n\nnew TableBuilder()\n  .title('Top Buyer Wallets')\n  .datasource(LOKI)\n  .withTarget(\n    new LokiQueryBuilder()\n      .expr('topk(10, sum by (buyer_wallet)(count_over_time({namespace=\"myapp\"} | event=\"request\" | buyer_wallet!=\"\" [$__range])))')\n      .refId('A')\n  )\n  .withTransformation({\n    id: 'reduce',\n    options: { reducers: ['lastNotNull'], mode: 'seriesToRows', includeTimeField: false, labelsToFields: true },\n  })\n  .withTransformation({\n    id: 'organize',\n    options: {\n      excludeByName: { Field: true },\n      renameByName: { buyer_wallet: 'Buyer Wallet', 'Last not null': 'Requests' },\n    },\n  })\n  .withTransformation({\n    id: 'sortBy',\n    options: { sort: [{ field: 'Requests', desc: true }] },\n  })\n  .height(8)\n  .span(12)\n```\n\n---\n\n## Multi-Datasource Dashboard\n\nWhen combining Prometheus and Loki in the same dashboard, define datasource refs as constants and use them consistently:\n\n```typescript\nconst DS = {\n  prometheus: { type: 'prometheus', uid: 'prometheus' },\n  loki: { type: 'loki', uid: 'loki' },\n  tempo: { type: 'tempo', uid: 'tempo' },\n} as const;\n\n// Prometheus panel\nnew TimeseriesBuilder()\n  .datasource(DS.prometheus)\n  .withTarget(new PromQueryBuilder().expr('...'))\n\n// Loki panel\nnew LogsBuilder()\n  .datasource(DS.loki)\n  .withTarget(new LokiQueryBuilder().expr('...'))\n```\n\n---\n\n## Dynamic Dashboard Generation\n\nGenerate dashboards dynamically based on a list of services:\n\n```typescript\nconst services = ['inference', 'twitter', 'github', 'web'];\n\nfunction buildServiceDashboard(service: string) {\n  return new DashboardBuilder(`${service} - Detail`)\n    .uid(`${service}-detail`)\n    .tags(['myapp', service])\n    .editable()\n    .refresh('30s')\n    .time({ from: 'now-1h', to: 'now' })\n    .withRow(new RowBuilder('Overview'))\n    .withPanel(\n      new StatBuilder()\n        .title('Request Rate')\n        .datasource(PROM)\n        .withTarget(pq(`sum(rate(http_server_duration_count{namespace=\"myapp\",job=\"${service}\"}[5m]))`))\n        .unit('reqps')\n        .height(4).span(6)\n    )\n    // ... more panels\n    ;\n}\n\n// Generate all dashboards\nfor (const service of services) {\n  const dash = buildServiceDashboard(service);\n  fs.writeFileSync(`dashboards/${service}.json`, JSON.stringify(dash.build(), null, 2));\n}\n```\n\n---\n\n## Converting Raw JSON to SDK\n\nWhen converting existing hand-written dashboard JSON to SDK code, follow this mapping:\n\n| JSON Field | SDK Method |\n|---|---|\n| `\"uid\": \"x\"` | `.uid('x')` |\n| `\"title\": \"x\"` | `.title('x')` |\n| `\"tags\": [...]` | `.tags([...])` |\n| `\"editable\": true` | `.editable()` |\n| `\"refresh\": \"30s\"` | `.refresh('30s')` |\n| `\"time\": { \"from\": \"now-24h\", \"to\": \"now\" }` | `.time({ from: 'now-24h', to: 'now' })` |\n| `\"timezone\": \"browser\"` | `.timezone('browser')` |\n| `\"graphTooltip\": 1` | `.tooltip(DashboardCursorSync.Crosshair)` |\n| `\"type\": \"row\"` | `.withRow(new RowBuilder('title'))` |\n| `\"type\": \"stat\"` | `new StatBuilder()` |\n| `\"type\": \"timeseries\"` | `new TimeseriesBuilder()` |\n| `\"type\": \"logs\"` | `new LogsBuilder()` |\n| `\"type\": \"piechart\"` | `new PieChartBuilder()` |\n| `\"type\": \"bargauge\"` | `new BarGaugeBuilder()` |\n| `\"type\": \"barchart\"` | `new BarChartBuilder()` |\n| `\"type\": \"table\"` | `new TableBuilder()` |\n| `\"gridPos\": { \"h\": 8, \"w\": 12 }` | `.height(8).span(12)` |\n| `\"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" }` | `.datasource({ type: 'prometheus', uid: 'prometheus' })` |\n| `\"targets\": [{ \"expr\": \"...\" }]` | `.withTarget(new DataqueryBuilder().expr('...'))` |\n| `\"fieldConfig.defaults.unit\": \"ms\"` | `.unit('ms')` |\n| `\"fieldConfig.defaults.decimals\": 2` | `.decimals(2)` |\n| `\"fieldConfig.defaults.noValue\": \"N/A\"` | `.noValue('N/A')` |\n| `\"fieldConfig.defaults.custom.fillOpacity\": 15` | `.fillOpacity(15)` |\n| `\"fieldConfig.defaults.custom.stacking.mode\": \"normal\"` | `.stacking(new StackingConfigBuilder().mode(StackingMode.Normal))` |\n| `\"options.reduceOptions.calcs\": [\"lastNotNull\"]` | `.reduceOptions(new ReduceDataOptionsBuilder().calcs(['lastNotNull']))` |\n| `\"options.colorMode\": \"background\"` | `.colorMode(BigValueColorMode.Background)` |\n| `\"options.graphMode\": \"area\"` | `.graphMode(BigValueGraphMode.Area)` |\n| `\"collapsed\": true` | `.collapsed()` |\n\n**Tips for conversion:**\n1. Start with the dashboard-level config (uid, tags, time, refresh)\n2. Convert variables next (they're referenced by panels)\n3. Convert panels row by row, creating helper functions for repeated patterns\n4. Panel IDs are auto-assigned by Grafana - you don't need to set them\n5. `gridPos.x` and `gridPos.y` are computed automatically from `height()` and `span()` - you only need to specify width and height\n\n---\n\n## Integration with Helm/ConfigMaps\n\nIn this project, generated dashboard JSON files are provisioned via ConfigMaps:\n\n**Template (`templates/dashboards.yaml`):**\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: dashboard-overview\n  labels:\n    grafana_dashboard: \"1\"\ndata:\n  overview.json: |-\n{{ .Files.Get \"dashboards/overview.json\" | indent 4 }}\n```\n\n**Workflow:**\n1. Edit the TypeScript generator in `ops/dashboards/`\n2. Run `pnpm generate` (or `tsx generate.ts`) to produce JSON in `dashboards/`\n3. Commit the generated JSON alongside the generator code\n4. Deploy via your provisioning mechanism (e.g. sync the ConfigMap or drop into Grafana's provisioning directory)\n\nThe `grafana_dashboard: \"1\"` label tells the Grafana sidecar to load the ConfigMap as a dashboard automatically.\n\nFile v0.2.3:references/typescript-api.md\n\n# TypeScript API Reference\n\n## Table of Contents\n1. [Imports](#imports)\n2. [Dashboard Builder](#dashboard-builder)\n3. [Panel Types](#panel-types)\n4. [Query Builders](#query-builders)\n5. [Variable Builders](#variable-builders)\n6. [Field Configuration](#field-configuration)\n7. [Common Enums](#common-enums)\n8. [Go Quick Reference](#go-quick-reference)\n\n---\n\n## Imports\n\nEvery concept lives in its own subpath export. Import only what you need:\n\n```typescript\n// Dashboard structure\nimport {\n  DashboardBuilder,\n  RowBuilder,\n  QueryVariableBuilder,\n  CustomVariableBuilder,\n  DatasourceVariableBuilder,\n  DashboardLinkBuilder,\n  ThresholdsConfigBuilder,\n} from '@grafana/grafana-foundation-sdk/dashboard';\n\n// Panel types (each exports PanelBuilder)\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as GaugeBuilder } from '@grafana/grafana-foundation-sdk/gauge';\nimport { PanelBuilder as BarGaugeBuilder } from '@grafana/grafana-foundation-sdk/bargauge';\nimport { PanelBuilder as TableBuilder } from '@grafana/grafana-foundation-sdk/table';\nimport { PanelBuilder as PieChartBuilder } from '@grafana/grafana-foundation-sdk/piechart';\nimport { PanelBuilder as BarChartBuilder } from '@grafana/grafana-foundation-sdk/barchart';\nimport { PanelBuilder as HeatmapBuilder } from '@grafana/grafana-foundation-sdk/heatmap';\nimport { PanelBuilder as HistogramBuilder } from '@grafana/grafana-foundation-sdk/histogram';\nimport { PanelBuilder as LogsBuilder } from '@grafana/grafana-foundation-sdk/logs';\nimport { PanelBuilder as TextBuilder } from '@grafana/grafana-foundation-sdk/text';\nimport { PanelBuilder as StateTimelineBuilder } from '@grafana/grafana-foundation-sdk/statetimeline';\nimport { PanelBuilder as StatusHistoryBuilder } from '@grafana/grafana-foundation-sdk/statushistory';\nimport { PanelBuilder as NodeGraphBuilder } from '@grafana/grafana-foundation-sdk/nodegraph';\nimport { PanelBuilder as GeoMapBuilder } from '@grafana/grafana-foundation-sdk/geomap';\nimport { PanelBuilder as XYChartBuilder } from '@grafana/grafana-foundation-sdk/xychart';\nimport { PanelBuilder as TrendBuilder } from '@grafana/grafana-foundation-sdk/trend';\nimport { PanelBuilder as CandlestickBuilder } from '@grafana/grafana-foundation-sdk/candlestick';\nimport { PanelBuilder as CanvasBuilder } from '@grafana/grafana-foundation-sdk/canvas';\nimport { PanelBuilder as DatagridBuilder } from '@grafana/grafana-foundation-sdk/datagrid';\nimport { PanelBuilder as AnnotationsListBuilder } from '@grafana/grafana-foundation-sdk/annotationslist';\nimport { PanelBuilder as DashboardListBuilder } from '@grafana/grafana-foundation-sdk/dashboardlist';\nimport { PanelBuilder as NewsBuilder } from '@grafana/grafana-foundation-sdk/news';\n\n// Library panels (reference a shared, server-stored panel)\nimport { LibraryPanelBuilder } from '@grafana/grafana-foundation-sdk/librarypanel';\n\n// Query builders (each exports DataqueryBuilder)\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\nimport { DataqueryBuilder as TempoQueryBuilder } from '@grafana/grafana-foundation-sdk/tempo';\n// Other core datasource query builders (same DataqueryBuilder pattern):\n//   elasticsearch, cloudwatch, azuremonitor, googlecloudmonitoring, bigquery,\n//   athena, testdata, parca, grafanapyroscope (Pyroscope profiling)\nimport { DataqueryBuilder as TestDataQueryBuilder } from '@grafana/grafana-foundation-sdk/testdata';\n// Server-side / SQL expressions (math, reduce, resample, SQL across queries):\nimport { DataqueryBuilder as ExprBuilder } from '@grafana/grafana-foundation-sdk/expr';\n\n// Common types and sub-builders\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n// Typed unit constants (alternative to hardcoded strings like 'reqps'/'ms'):\nimport * as units from '@grafana/grafana-foundation-sdk/units';\n```\n\n---\n\n## Dashboard Builder\n\n```typescript\nnew DashboardBuilder(title: string)\n  // Identity\n  .uid(uid: string)\n  .tags(tags: string[])\n  .description(desc: string)\n\n  // Behavior\n  .editable()                    // allow editing in UI\n  .readonly()                    // read-only in UI\n  .refresh(interval: string)     // e.g. '30s', '1m', '5m'\n  .tooltip(mode: DashboardCursorSync)  // Off, Crosshair, Tooltip\n\n  // Time\n  .time({ from: string, to: string })  // e.g. { from: 'now-24h', to: 'now' }\n  .timezone(tz: string)                // 'browser', 'utc', or IANA tz\n\n  // Variables\n  .withVariable(builder)         // QueryVariableBuilder, CustomVariableBuilder, etc.\n\n  // Layout\n  .withRow(rowBuilder)           // RowBuilder\n  .withPanel(panelBuilder)       // any panel builder\n\n  // Links\n  .link(linkBuilder)             // DashboardLinkBuilder\n\n  // Annotations\n  .annotation(annotationBuilder)\n\n  // Build\n  .build()                       // returns the dashboard object\n```\n\n---\n\n## Panel Types\n\nAll panel builders share these common methods:\n\n```typescript\n// Every panel type\n.title(title: string)\n.description(desc: string)\n.transparent()                         // transparent background\n.datasource({ type: string, uid: string })\n.withTarget(queryBuilder)              // add a query\n.height(h: number)                     // grid height in rows\n.span(w: number)                       // grid width out of 24\n.unit(unit: string)                    // e.g. 'reqps', 'ms', 'percent', 'currencyUSD', 'short'\n.min(n: number)\n.max(n: number)\n.decimals(n: number)\n.noValue(text: string)                 // shown when no data\n.links(links: PanelLink[])\n.repeat(variableName: string)          // repeat panel for each variable value\n.maxPerRow(n: number)                  // max panels per row when repeating\n.thresholds(thresholdsConfigBuilder)\n.overrideByName(name, properties[])\n.overrideByRegexp(regexp, properties[])\n.withTransformation(transformation)\n```\n\n### Stat Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/stat';\n\nnew PanelBuilder()\n  .colorMode(common.BigValueColorMode.Background)  // None, Value, Background, BackgroundSolid\n  .graphMode(common.BigValueGraphMode.Area)         // None, Line, Area\n  .textMode(common.BigValueTextMode.Auto)           // Auto, Value, ValueAndName, Name, None\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .orientation(common.VizOrientation.Auto)          // Auto, Horizontal, Vertical\n```\n\n### Timeseries Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\n\nnew PanelBuilder()\n  .drawStyle(common.GraphDrawStyle.Line)             // Line, Bars, Points\n  .lineInterpolation(common.LineInterpolation.Linear) // Linear, Smooth, StepBefore, StepAfter\n  .lineWidth(n: number)                               // 0-10\n  .fillOpacity(n: number)                              // 0-100\n  .showPoints(common.VisibilityMode.Auto)             // Auto, Always, Never\n  .pointSize(n: number)\n  .gradientMode(common.GraphGradientMode.None)        // None, Opacity, Hue, Scheme\n  .spanNulls(bool | number)                           // connect null gaps (or max gap in ms)\n  .axisBorderShow(bool: boolean)\n  .stacking(new common.StackingConfigBuilder().mode(common.StackingMode.Normal))  // None, Normal, Percent\n  .legend(\n    new common.VizLegendOptionsBuilder()\n      .showLegend(true)\n      .placement(common.LegendPlacement.Bottom)       // Bottom, Right\n      .displayMode(common.LegendDisplayMode.List)      // List, Table, Hidden\n  )\n  .tooltip(\n    new common.VizTooltipOptionsBuilder()\n      .mode(common.TooltipDisplayMode.Multi)           // Single, Multi, None\n      .sort(common.SortOrder.Descending)               // Ascending, Descending, None\n  )\n  .thresholdsStyle(\n    new common.GraphThresholdsStyleConfigBuilder()\n      .mode(common.GraphThresholdsStyleMode.Off)       // Off, Line, Area, LineAndArea, Dashed, DashedAndArea\n  )\n```\n\n### Table Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/table';\n\nnew PanelBuilder()\n  .filterable(true)               // column filtering\n  .footer(footerBuilder)          // table footer with calculations\n```\n\n### Gauge Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/gauge';\n\nnew PanelBuilder()\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .orientation(common.VizOrientation.Auto)\n  .showThresholdLabels(true)\n  .showThresholdMarkers(true)\n```\n\n### BarGauge Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/bargauge';\n\nnew PanelBuilder()\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .orientation(common.VizOrientation.Horizontal)\n  .displayMode(common.BarGaugeDisplayMode.Gradient)  // Basic, Gradient, Lcd\n```\n\n### PieChart Panel\n\nPieChart uses its OWN legend builder (`PieChartLegendOptionsBuilder` from the `piechart` package), not the shared `common.VizLegendOptionsBuilder` - passing the common one is a type error because pie legends carry extra fields (`values`).\n\n```typescript\nimport { PanelBuilder, PieChartLegendOptionsBuilder } from '@grafana/grafana-foundation-sdk/piechart';\n\nnew PanelBuilder()\n  .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']))\n  .pieType(common.PieChartType.Pie)          // Pie, Donut\n  .legend(\n    new PieChartLegendOptionsBuilder()\n      .showLegend(true)\n      .placement(common.LegendPlacement.Right)\n  )\n```\n\n### BarChart Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/barchart';\n\nnew PanelBuilder()\n  .orientation(common.VizOrientation.Horizontal)\n```\n\n### Logs Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/logs';\n\nnew PanelBuilder()\n  .showTime(true)\n  .wrapLogMessage(true)\n  .enableLogDetails(true)\n  .prettifyLogMessage(false)\n  .showCommonLabels(false)\n  .showLabels(false)\n  .sortOrder(common.LogsSortOrder.Descending)  // Ascending, Descending\n  .dedupStrategy(common.LogsDedupStrategy.None) // None, Exact, Numbers, Signature\n```\n\n### Text Panel\n\n```typescript\nimport { PanelBuilder } from '@grafana/grafana-foundation-sdk/text';\n\nnew PanelBuilder()\n  .content('# Markdown content here')\n  .mode(common.TextMode.Markdown)  // Markdown, HTML, Code\n```\n\n---\n\n## Query Builders\n\n### Prometheus\n\n```typescript\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\n\nnew DataqueryBuilder()\n  .expr('sum(rate(http_requests_total{job=~\"$service\"}[$__rate_interval]))')\n  .legendFormat('{{job}}')\n  .refId('A')\n  .instant()           // instant query (single value)\n  .range()             // range query (time series) - default\n  .format(PromQueryFormat.Table)  // for table panels\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .hide(true)          // hide this query's results\n```\n\n### Loki\n\n```typescript\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\nnew DataqueryBuilder()\n  .expr('{namespace=\"myapp\", app=~\"$service\"}')\n  .legendFormat('{{app}}')\n  .refId('A')\n  .maxLines(100)\n  .queryType('range')       // 'range', 'instant', or 'stream'\n  .editorMode('code')       // 'code' or 'builder'\n  .datasource({ type: 'loki', uid: 'loki' })\n```\n\n**Loki query types:**\n- Log query: `{namespace=\"myapp\", app=\"my-app\"}` - returns log lines\n- Metric query: `count_over_time({namespace=\"myapp\"} [5m])` - returns time series\n- Use `| json` for JSON parsing, `| unwrap field_name` for numeric extraction\n\n---\n\n## Variable Builders\n\n### Query Variable\n\n```typescript\nimport { QueryVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\nnew QueryVariableBuilder('varName')\n  .label('Display Label')\n  .query('label_values(metric{filter=\"value\"}, label_name)')\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .refresh(2)              // 1=on load, 2=on time range change\n  .includeAll(true)        // add \"All\" option\n  .allValue('.*')          // regex for \"All\"\n  .multi(true)             // allow multiple selections\n  .sort(1)                 // 0=disabled, 1=alpha asc, 2=alpha desc, 3=num asc, etc.\n  .hide(0)                 // 0=visible, 1=hide label, 2=hide variable\n```\n\n### Custom Variable\n\n```typescript\nimport { CustomVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// Simple values\nnew CustomVariableBuilder('env')\n  .label('Environment')\n  .query('prod,staging,dev')\n  .current({ text: 'prod', value: 'prod' })\n\n// Key:value pairs (display text : actual value)\nnew CustomVariableBuilder('level')\n  .label('Log Level')\n  .query('All : .+, Error : error|fatal, Warning : warn')\n  .current({ text: 'All', value: '.+' })\n```\n\n### Datasource Variable\n\n```typescript\nimport { DatasourceVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\nnew DatasourceVariableBuilder('datasource')\n  .label('Data Source')\n  .type('prometheus')      // datasource plugin type\n  .regex('/^(?!.*test).*$/')  // filter datasources\n  .multi(false)\n```\n\n---\n\n## Field Configuration\n\n### Thresholds\n\n```typescript\nimport { ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// The first step MUST have value: null (it's the base/default color)\nnew ThresholdsConfigBuilder()\n  .mode(common.ThresholdsMode.Absolute)  // or Percentage\n  .steps([\n    { value: null as any, color: 'green' },\n    { value: 50, color: 'yellow' },\n    { value: 80, color: 'red' },\n  ])\n```\n\n### Value Mappings\n\n```typescript\n// Applied directly to panel as raw fieldConfig\n// The SDK doesn't have typed builders for all mapping types\n.withTransformation({\n  id: 'configFromData',\n  options: { /* ... */ }\n})\n```\n\n### Field Overrides\n\n```typescript\n// Override by field name\n.overrideByName('Revenue', [\n  { id: 'color', value: { fixedColor: 'green', mode: 'fixed' } },\n  { id: 'custom.fillOpacity', value: 10 },\n])\n\n// Override by regex\n.overrideByRegexp('.*5..', [\n  { id: 'color', value: { fixedColor: 'red', mode: 'fixed' } },\n])\n\n// Common override property IDs:\n// 'color'              - { fixedColor: 'green', mode: 'fixed' }\n// 'unit'               - 'ms', 'reqps', 'currencyUSD', etc.\n// 'decimals'           - number\n// 'custom.fillOpacity' - 0-100\n// 'custom.lineWidth'   - 0-10\n// 'custom.drawStyle'   - 'line', 'bars', 'points'\n// 'custom.lineStyle'   - { fill: 'dash', dash: [10, 10] }\n// 'custom.stacking'    - { mode: 'normal' }\n```\n\n---\n\n## Common Enums\n\n```typescript\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\n// BigValueColorMode: None, Value, Background, BackgroundSolid\n// BigValueGraphMode: None, Line, Area\n// BigValueTextMode: Auto, Value, ValueAndName, Name, None\n// GraphDrawStyle: Line, Bars, Points\n// LineInterpolation: Linear, Smooth, StepBefore, StepAfter\n// VisibilityMode: Auto, Always, Never\n// GraphGradientMode: None, Opacity, Hue, Scheme\n// StackingMode: None, Normal, Percent\n// LegendPlacement: Bottom, Right\n// LegendDisplayMode: List, Table, Hidden\n// TooltipDisplayMode: Single, Multi, None\n// SortOrder: Ascending, Descending, None\n// ThresholdsMode: Absolute, Percentage\n// VizOrientation: Auto, Horizontal, Vertical\n// LogsSortOrder: Ascending, Descending\n// LogsDedupStrategy: None, Exact, Numbers, Signature\n// BarGaugeDisplayMode: Basic, Gradient, Lcd\n// PieChartType: Pie, Donut\n// GraphThresholdsStyleMode: Off, Line, Area, LineAndArea, Dashed, DashedAndArea\n```\n\n---\n\n## Common Units\n\nThe SDK ships a typed `units` module (`@grafana/grafana-foundation-sdk/units`) with named constants for the strings below - use it to avoid typos, e.g. `.unit(units.RequestsPerSecond)` instead of `.unit('reqps')`. Plain strings still work and are fine for one-offs.\n\nFrequently used unit strings (pass to `.unit()`):\n\n| Unit | Description |\n|------|-------------|\n| `'short'` | Auto-scaled number |\n| `'none'` | Raw number |\n| `'percent'` | 0-100 percentage |\n| `'percentunit'` | 0.0-1.0 percentage |\n| `'reqps'` | Requests per second |\n| `'ms'` | Milliseconds |\n| `'s'` | Seconds |\n| `'bytes'` | Bytes (auto IEC) |\n| `'decbytes'` | Bytes (auto SI) |\n| `'currencyUSD'` | US Dollars |\n| `'ops'` | Operations per second |\n\n---\n\n## Go Quick Reference\n\nGo follows the same builder pattern but with Go conventions (uppercase methods, pointers for nullable values).\n\n```go\nimport (\n    \"github.com/grafana/grafana-foundation-sdk/go/cog\"\n    \"github.com/grafana/grafana-foundation-sdk/go/common\"\n    \"github.com/grafana/grafana-foundation-sdk/go/dashboard\"\n    \"github.com/grafana/grafana-foundation-sdk/go/prometheus\"\n    \"github.com/grafana/grafana-foundation-sdk/go/loki\"\n    \"github.com/grafana/grafana-foundation-sdk/go/stat\"\n    \"github.com/grafana/grafana-foundation-sdk/go/timeseries\"\n)\n\n// Dashboard\nbuilder := dashboard.NewDashboardBuilder(\"Title\").\n    Uid(\"my-uid\").\n    Tags([]string{\"tag\"}).\n    Editable().\n    Refresh(\"30s\").\n    Time(\"now-24h\", \"now\").\n    Timezone(common.TimeZoneBrowser)\n\n// Prometheus query\nprometheus.NewDataqueryBuilder().\n    Expr(`sum(rate(http_requests_total{job=~\"$service\"}[5m]))`).\n    LegendFormat(\"{{job}}\")\n\n// Stat panel\nstat.NewPanelBuilder().\n    Title(\"Requests\").\n    Datasource(dashboard.DataSourceRef{\n        Type: cog.ToPtr(\"prometheus\"),\n        Uid:  cog.ToPtr(\"prometheus\"),\n    }).\n    WithTarget(query).\n    Height(4).Span(6)\n\n// Thresholds (note: first step Value is nil)\ndashboard.NewThresholdsConfigBuilder().\n    Mode(dashboard.ThresholdsModeAbsolute).\n    Steps([]dashboard.Threshold{\n        {Value: nil, Color: \"green\"},\n        {Value: cog.ToPtr[float64](80), Color: \"red\"},\n    })\n\n// Build and serialize\ndash, err := builder.Build()\njsonBytes, _ := json.MarshalIndent(dash, \"\", \"  \")\n```\n\n**Go gotchas:**\n- Use `cog.ToPtr[T](value)` for nullable pointer fields (thresholds, datasource refs)\n- Some fields require wrapper types: `dashboard.StringOrMap{String: cog.ToPtr(val)}`\n- `Build()` returns `(Dashboard, error)` - always check the error\n- Variable `Current` uses `dashboard.VariableOption` with `dashboard.StringOrArrayOfString`\n\nFile v0.2.3:CHANGELOG.md\n\n# Changelog\n\nAll notable changes to this skill will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),\nand this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.2.3] - 2026-08-07\n\n### Removed\n\n- \"Project-Specific Context\" section that hardcoded one private repo's Helm chart layout and paths into the published skill.\n\n### Changed\n\n- Genericized example queries and provisioning workflow: `namespace=\"x402\"` -> `namespace=\"myapp\"`, private chart paths -> generic `deploy/dashboards/` layout, `helmfile sync` step -> provisioning-mechanism-agnostic wording.\n\n## [0.2.2] - 2026-07-22\n\n### Added\n\n- skill-card.md release record following NVIDIA's skill-card format\n- metadata.openclaw block (emoji, homepage) for ClawHub display\n\n## [0.2.1] - 2026-07-10\n\n### Changed\n- CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).\n\n## [0.2.0] - 2026-06-05\n\n### Fixed\n- Go install string corrected to `go get github.com/grafana/grafana-foundation-sdk/go@v0.0.16` (the canonical tag form per the official docs), replacing the stale `@next+cog-v0.0.x` branch ref.\n- PieChart legend example now uses `PieChartLegendOptionsBuilder` (from the `piechart` package) instead of `common.VizLegendOptionsBuilder`, which is a type error for piechart panels.\n\n### Added\n- `metadata.upstream` tracking established (pinned to v0.0.16).\n- Known gotcha on dashboard schema v1 vs v2: the skill targets v1; `dashboardv2beta1` (k8s apiVersion `dashboard.grafana.app/v2beta1`) exists and is still stabilizing.\n- Known gotchas on the type-checking gap (builders only checked when wired into a tsconfig; use project-local `npx tsc` for the SDK's ES2024/bundler output) and the regenerate-JSON-after-every-edit discipline.\n- `units` typed-constants module, `expr` server-side/SQL expressions, `testdata` datasource, and the broader set of core datasource query builders (elasticsearch, cloudwatch, azuremonitor, googlecloudmonitoring, bigquery, athena, parca, grafanapyroscope) in the TypeScript reference.\n- Additional panel types in the import reference: canvas, datagrid, annotationslist, dashboardlist, news; plus `librarypanel`.\n\n### Changed\n- Version pins updated to 0.0.16 across SKILL.md (install commands) and references/patterns.md (`package.json` example).\n\nVerified against: @grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\n\n## [0.1.0] - 2026-06-05\n\n### Added\n- Initial release: building Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Covers installation, core architecture, a TypeScript quick start, key patterns (helper functions, template variables, panel sizing, thresholds, field overrides, rows, Loki queries, transformations), output generation, and known gotchas, with `references/typescript-api.md` and `references/patterns.md` for depth.\n\nFile v0.2.3:skill-card.md\n\n## Description:\n\nBuild Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[tenequm](https://clawhub.ai/user/tenequm)\n\n### License/Terms of Use:\n\nApache 2.0\n\n## Use Case:\n\nDevelopers and infrastructure engineers use this skill to create, modify, convert, and generate Grafana dashboards as typed TypeScript or Go code, then serialize them to Grafana dashboard JSON or deployment manifests.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Generated dashboards and Prometheus or Loki queries may expose sensitive operational data to dashboard viewers.\n\nMitigation: Review generated dashboards, datasource scopes, variables, and queries before deploying or sharing them.\n\nRisk: Generated dashboard code can rely on pre-1.0 SDK behavior, unchecked TypeScript, or stale generated JSON.\n\nMitigation: Pin the SDK version, run project-local type checks or builds, regenerate dashboard JSON after generator edits, and review the generated output.\n\n## Reference(s):\n\n- [ClawHub Skill Page](https://clawhub.ai/tenequm/skills/grafana-foundation-sdk)\n- [OpenClaw Homepage](https://github.com/tenequm/skills/tree/main/skills/grafana-foundation-sdk)\n- [TypeScript API Reference](references/typescript-api.md)\n- [Common Dashboard Patterns](references/patterns.md)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Code, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with TypeScript, Go, shell, and JSON snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May produce dashboard JSON or Kubernetes dashboard/provisioning manifests for review before deployment.]\n\n## Skill Version(s):\n\n0.2.3 (source: evidence release, frontmatter, changelog released 2026-08-07)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v0.2.3:LICENSE.txt\n\nApache License\nVersion 2.0, January 2004\nhttps://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the\ncopyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other\nentities that control, are controlled by, or are under common control with\nthat entity. For the purposes of this definition, \"control\" means (i) the\npower, direct or indirect, to cause the direction or management of such\nentity, whether by contract or otherwise, or (ii) ownership of fifty percent\n(50%) or more of the outstanding shares, or (iii) beneficial ownership of\nsuch entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications,\nincluding but not limited to software source code, documentation source, and\nconfiguration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object\ncode, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form,\nmade available under the License, as indicated by a copyright notice that is\nincluded in or attached to the work (an example is provided in the Appendix\nbelow).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form,\nthat is based on (or derived from) the Work and for which the editorial\nrevisions, annotations, elaborations, or other modifications represent, as a\nwhole, an original work of authorship. For the purposes of this License,\nDerivative Works shall not include works that remain separable from, or\nmerely link (or bind by name) to the interfaces of, the Work and Derivative\nWorks thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original\nversion of the Work and any modifications or additions to that Work or\nDerivative Works thereof, that is intentionally submitted to Licensor for\ninclusion in the Work by the copyright owner or by an individual or Legal\nEntity authorized to submit on behalf of the copyright owner. For the\npurposes of this definition, \"submitted\" means any form of electronic, verbal,\nor written communication sent to the Licensor or its representatives,\nincluding but not limited to communication on electronic mailing lists, source\ncode control systems, and issue tracking systems that are managed by, or on\nbehalf of, the Licensor for the purpose of discussing and improving the Work,\nbut excluding communication that is conspicuously marked or otherwise\ndesignated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on\nbehalf of whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this\nLicense, each Contributor hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable copyright license to\nreproduce, prepare Derivative Works of, publicly display, publicly perform,\nsublicense, and distribute the Work and such Derivative Works in Source or\nObject form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this\nLicense, each Contributor hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable (except as stated in this\nsection) patent license to make, have made, use, offer to sell, sell, import,\nand otherwise transfer the Work, where such license applies only to those\npatent claims licensable by such Contributor that are necessarily infringed by\ntheir Contribution(s) alone or by combination of their Contribution(s) with\nthe Work to which such Contribution(s) was submitted. If You institute patent\nlitigation against any entity (including a cross-claim or counterclaim in a\nlawsuit) alleging that the Work or a Contribution incorporated within the Work\nconstitutes direct or contributory patent infringement, then any patent\nlicenses granted to You under this License for that Work shall terminate as of\nthe date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or\nDerivative Works thereof in any medium, with or without modifications, and in\nSource or Object form, provided that You meet the following conditions:\n\n(a) You must give any other recipients of the Work or Derivative Works a copy\nof this License; and\n\n(b) You must cause any modified files to carry prominent notices stating that\nYou changed the files; and\n\n(c) You must retain, in the Source form of any Derivative Works that You\ndistribute, all copyright, patent, trademark, and attribution notices from\nthe Source form of the Work, excluding those notices that do not pertain to\nany part of the Derivative Works; and\n\n(d) If the Work includes a \"NOTICE\" text file as part of its distribution,\nthen any Derivative Works that You distribute must include a readable copy of\nthe attribution notices contained within such NOTICE file, excluding those\nnotices that do not pertain to any part of the Derivative Works, in at least\none of the following places: within a NOTICE text file distributed as part of\nthe Derivative Works; within the Source form or documentation, if provided\nalong with the Derivative Works; or, within a display generated by the\nDerivative Works, if and wherever such third-party notices normally appear.\nThe contents of the NOTICE file are for informational purposes only and do not\nmodify the License. You may add Your own attribution notices within Derivative\nWorks that You distribute, alongside or as an addendum to the NOTICE text from\nthe Work, provided that such additional attribution notices cannot be\nconstrued as modifying the License.\n\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a\nwhole, provided Your use, reproduction, and distribution of the Work otherwise\ncomplies with the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise, any\nContribution intentionally submitted for inclusion in the Work by You to the\nLicensor shall be under the terms and conditions of this License, without any\nadditional terms or conditions. Notwithstanding the above, nothing herein\nshall supersede or modify the terms of any separate license agreement you may\nhave executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names,\ntrademarks, service marks, or product names of the Licensor, except as\nrequired for reasonable and customary use in describing the origin of the Work\nand reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in\nwriting, Licensor provides the Work (and each Contributor provides its\nContributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied, including, without limitation, any warranties\nor conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\nPARTICULAR PURPOSE. You are solely responsible for determining the\nappropriateness of using or redistributing the Work and assume any risks\nassociated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in\ntort (including negligence), contract, or otherwise, unless required by\napplicable law (such as deliberate and grossly negligent acts) or agreed to in\nwriting, shall any Contributor be liable to You for damages, including any\ndirect, indirect, special, incidental, or consequential damages of any\ncharacter arising as a result of this License or out of the use or inability to\nuse the Work (including but not limited to damages for loss of goodwill, work\nstoppage, computer failure or malfunction, or any and all other commercial\ndamages or losses), even if such Contributor has been advised of the\npossibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work\nor Derivative Works thereof, You may choose to offer, and charge a fee for,\nacceptance of support, warranty, indemnity, or other liability obligations\nand/or rights consistent with this License. However, in accepting such\nobligations, You may act only on Your own behalf and on Your sole\nresponsibility, not on behalf of any other Contributor, and only if You agree\nto indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nArchive v0.2.2: 7 files, 21982 bytes\n\nFiles: CHANGELOG.md (2583b), LICENSE.txt (9157b), references/patterns.md (13343b), references/typescript-api.md (18171b), skill-card.md (2259b), SKILL.md (14234b), _meta.json (141b)\n\nFile v0.2.2:SKILL.md\n\n---\nname: grafana-foundation-sdk\ndescription: Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.\nmetadata:\n  version: \"0.2.2\"\n  upstream: \"@grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/grafana-foundation-sdk\n    emoji: \"📊\"\n---\n\n# Grafana Foundation SDK\n\nThe grafana-foundation-sdk provides strongly typed builder libraries for defining Grafana dashboards as code. Instead of writing raw JSON (which is error-prone and hard to review in diffs), you compose dashboards using chained builder calls that produce valid Grafana JSON.\n\nThe SDK is auto-generated from Grafana's internal CUE schemas via the `cog` tool. It supports Go, TypeScript, Python, PHP, and Java. This skill focuses on **TypeScript** (primary) and **Go** (secondary) since those are the most common choices for infrastructure teams.\n\n## When to use this skill\n\n- Creating new Grafana dashboards from scratch\n- Converting existing hand-written dashboard JSON to typed code\n- Adding panels, variables, or queries to dashboards\n- Building reusable dashboard components (helper functions for common panel patterns)\n- Generating dashboards dynamically based on service lists or configs\n\n## Installation\n\nThe SDK is published as concrete `v0.0.x` tags (latest: **v0.0.16**). Pin explicitly - it is pre-1.0 and the API churns between releases (see Known Gotchas).\n\n**TypeScript:**\n```bash\nnpm install '@grafana/grafana-foundation-sdk@~0.0.16'\n# or\npnpm add '@grafana/grafana-foundation-sdk@~0.0.16'\n```\n\n**Go:**\n```bash\ngo get github.com/grafana/grafana-foundation-sdk/go@v0.0.16\n```\n\n## Core Architecture\n\nEverything follows the **builder pattern**: create a builder, chain configuration methods, call `.build()` (TS) or `.Build()` (Go) to produce the final object. The output is standard Grafana dashboard JSON - compatible with Grafana's API, file-based provisioning, and Kubernetes ConfigMaps.\n\nEach panel type, query type, and variable type lives in its own package. You import only what you need:\n\n```typescript\n// Each concern has its own import\nimport { DashboardBuilder, RowBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n```\n\n## Quick Start - TypeScript\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\nconst dashboard = new DashboardBuilder('My Service Overview')\n  .uid('my-service-overview')\n  .tags(['my-service'])\n  .editable()\n  .refresh('30s')\n  .time({ from: 'now-24h', to: 'now' })\n  .timezone('browser')\n  .withVariable(\n    new QueryVariableBuilder('service')\n      .label('Service')\n      .query('label_values(up{namespace=\"default\"}, job)')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .refresh(1)\n      .includeAll(true)\n      .allValue('.*')\n      .sort(1)\n  )\n  .withRow(new RowBuilder('Overview'))\n  .withPanel(\n    new StatBuilder()\n      .title('Request Rate')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('req/s')\n      )\n      .unit('reqps')\n      .decimals(1)\n      .height(4)\n      .span(6)\n      .colorMode(common.BigValueColorMode.Background)\n      .graphMode(common.BigValueGraphMode.Area)\n      .reduceOptions(\n        new common.ReduceDataOptionsBuilder().calcs(['lastNotNull'])\n      )\n  )\n  .withPanel(\n    new TimeseriesBuilder()\n      .title('Request Rate Over Time')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum by (job)(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('{{job}}')\n      )\n      .unit('reqps')\n      .fillOpacity(15)\n      .height(8)\n      .span(12)\n  );\n\n// Output the dashboard JSON\nconsole.log(JSON.stringify(dashboard.build(), null, 2));\n```\n\n## Key Patterns\n\n### 1. Helper functions for repeated panel configurations\n\nThe biggest win from using the SDK is creating reusable helpers that encode your team's conventions:\n\n```typescript\nfunction promDs() {\n  return { type: 'prometheus', uid: 'prometheus' } as const;\n}\n\nfunction lokiDs() {\n  return { type: 'loki', uid: 'loki' } as const;\n}\n\nfunction promQuery(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction statPanel(title: string, expr: string, opts?: { unit?: string; decimals?: number; color?: string }) {\n  const panel = new StatBuilder()\n    .title(title)\n    .datasource(promDs())\n    .withTarget(promQuery(expr))\n    .height(4)\n    .span(4)\n    .colorMode(common.BigValueColorMode.Background)\n    .graphMode(common.BigValueGraphMode.Area)\n    .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']));\n\n  if (opts?.unit) panel.unit(opts.unit);\n  if (opts?.decimals !== undefined) panel.decimals(opts.decimals);\n  // Thresholds can be set via .thresholds() if needed\n\n  return panel;\n}\n```\n\n### 2. Template variables\n\n```typescript\n// Query variable - populated from Prometheus labels\nnew QueryVariableBuilder('service')\n  .label('Service')\n  .query('label_values(http_server_duration_count{namespace=\"x402\"}, job)')\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .refresh(2)  // 1=on dashboard load, 2=on time range change\n  .includeAll(true)\n  .allValue('.*')\n  .sort(1)  // 1=alphabetical asc\n\n// Custom variable - static key:value pairs\nnew CustomVariableBuilder('level')\n  .label('Log Level')\n  .query('All : .+, Error : error|fatal, Warning : warn, Info : info, Debug : debug')\n  .current({ text: 'All', value: '.+' })\n```\n\nReference variables in queries with standard Grafana syntax: `$service`, `$__range`, `$__rate_interval`, `$__auto`.\n\n### 3. Panel sizing\n\nPanels use `height(h)` (grid rows) and `span(w)` (out of 24 columns):\n- Full width: `.span(24)`\n- Half width: `.span(12)`\n- Third width: `.span(8)`\n- Quarter width: `.span(6)`\n- Typical stat panel: `.height(4).span(4)`\n- Typical timeseries: `.height(8).span(12)`\n\n### 4. Thresholds\n\n```typescript\nimport { ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\n\n// First step must have no value (it's the base)\nnew StatBuilder()\n  .thresholds(\n    new ThresholdsConfigBuilder()\n      .mode(common.ThresholdsMode.Absolute)\n      .steps([\n        { value: null as any, color: 'green' },\n        { value: 80, color: 'yellow' },\n        { value: 95, color: 'red' },\n      ])\n  )\n```\n\n### 5. Field overrides\n\n```typescript\nnew TimeseriesBuilder()\n  .overrideByName('Revenue', [\n    { id: 'color', value: { fixedColor: 'green', mode: 'fixed' } },\n  ])\n  .overrideByRegexp('.*5..', [\n    { id: 'color', value: { fixedColor: 'red', mode: 'fixed' } },\n  ])\n```\n\n### 6. Rows (including collapsed)\n\n```typescript\n// Regular row\n.withRow(new RowBuilder('Traffic'))\n\n// Collapsed row with nested panels\n.withRow(\n  new RowBuilder('Business Details')\n    .collapsed()\n    .withPanel(/* ... */)\n    .withPanel(/* ... */)\n)\n```\n\n### 7. Loki log and metric queries\n\n```typescript\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n\n// Log query\nnew LokiQueryBuilder()\n  .expr('{namespace=\"x402\", app=~\"$service\", level=~\"$level\"}')\n  .refId('A')\n\n// Metric query from logs\nnew LokiQueryBuilder()\n  .expr('sum by (buyer_wallet)(count_over_time({namespace=\"x402\"} | event=\"request\" [$__range]))')\n  .legendFormat('{{buyer_wallet}}')\n  .refId('A')\n```\n\n### 8. Transformations\n\nTransformations are applied as raw objects since the SDK doesn't have typed builders for all transformation types:\n\n```typescript\nnew TableBuilder()\n  .withTransformation({\n    id: 'reduce',\n    options: {\n      reducers: ['lastNotNull'],\n      mode: 'seriesToRows',\n      includeTimeField: false,\n      labelsToFields: true,\n    },\n  })\n  .withTransformation({\n    id: 'organize',\n    options: {\n      excludeByName: { Field: true },\n      renameByName: { buyer_wallet: 'Buyer Wallet', 'Last not null': 'Requests' },\n    },\n  })\n  .withTransformation({\n    id: 'sortBy',\n    options: { sort: [{ field: 'Requests', desc: true }] },\n  })\n```\n\n## Generating Output\n\nThe `.build()` call returns a plain object matching Grafana's dashboard JSON schema. Serialize it however you need:\n\n```typescript\n// Standard JSON file (for provisioning or ConfigMaps)\nconst fs = require('fs');\nconst dashboard = builder.build();\nfs.writeFileSync('dashboard.json', JSON.stringify(dashboard, null, 2));\n\n// Kubernetes resource manifest (for Grafana's k8s API)\nconst manifest = {\n  apiVersion: 'dashboard.grafana.app/v1beta1',\n  kind: 'Dashboard',\n  metadata: { name: dashboard.uid },\n  spec: dashboard,\n};\nconsole.log(JSON.stringify(manifest, null, 2));\n```\n\n## Known Gotchas\n\nThese are sharp edges discovered from real usage and open issues on the SDK repo:\n\n1. **SDK is v0.0.x (public preview)** - Used by Grafana Labs in production but the API can change between releases. Pin your version explicitly. Best suited for Grafana >= 12, works with >= 10.\n\n2. **`instant()` and `range()` are mutually exclusive in Prometheus** - Calling `.instant()` sets `instant=true` AND `range=false`. Calling `.range()` does the opposite. Use `.rangeAndInstant()` if you need both.\n\n3. **Loki `range()`/`instant()` are deprecated** - Use `.queryType('range')` or `.queryType('instant')` instead. Similarly, `.resolution()` is deprecated in favor of `.step()`.\n\n4. **First threshold step must have `value: null`** - This is the base/default color. Omitting it produces invalid JSON.\n\n5. **Panel IDs are auto-assigned** - You don't set `id` on panels. Grafana assigns them at import time. Similarly, `gridPos.x/y` are computed from `height()` and `span()`.\n\n6. **Transformations are plain objects** - The SDK has no typed builders for transformations. Pass them as raw `{ id, options }` objects via `.withTransformation()`.\n\n7. **CustomVariable quirk** - When provisioning via Grafana's API, `CustomVariableBuilder` requires the `.query()` field with comma-separated key:value pairs (e.g., `'All : .+, Error : error'`) for options to persist, even when `.values()` is also used.\n\n8. **Go: `cog.ToPtr()` is essential** - Many struct fields are pointer types. Use `cog.ToPtr[T](value)` for nullable fields (thresholds, datasource refs). TypeScript doesn't have this issue.\n\n9. **Go: `Build()` returns error** - Always check it. TypeScript's `.build()` returns the object directly with compile-time type safety instead.\n\n10. **No typed query builders for plugin datasources** - Only core datasources (Prometheus, Loki, Tempo, Elasticsearch, CloudWatch, etc.) have builders. For third-party plugins, define custom query types by implementing the `Builder<Dataquery>` interface.\n\n11. **Dashboard schema v1 vs v2** - This skill targets the v1 dashboard (`@grafana/grafana-foundation-sdk/dashboard`, k8s apiVersion `dashboard.grafana.app/v1beta1`). A newer schema v2 ships as `dashboardv2beta1` (k8s apiVersion `dashboard.grafana.app/v2beta1`) with its own builders. v2beta1 is still stabilizing and has known sharp edges (e.g. transforms, annotation positioning, SQL expressions in Go) - prefer v1 unless you specifically need v2 layouts. Most query/panel builders are shared; some expose a `QueryV2Builder`/`VisualizationV2Builder` variant for v2.\n\n12. **Builders are only type-checked if wired into a tsconfig** - The SDK gives compile-time safety only when the generator file is actually type-checked. A generator sitting under a non-package directory (e.g. a Helm chart dir) that no `tsconfig` includes is silently unchecked, so type errors surface only at `.build()` runtime. Also: the SDK's output targets ES2024/`bundler` module resolution, which an older global `tsc` chokes on - run the project-local compiler (`npx tsc`), not a stale global one.\n\n13. **Regenerate JSON after every generator edit** - The deployed dashboard is the generated JSON, not the `.ts`/`.go` source. Edit the generator, re-run it, and commit the regenerated JSON together; never hand-edit the generated JSON (the next regen silently overwrites it). A repo rule (\"never edit the dashboard JSON directly\") is worth adding.\n\n## Project-Specific Context\n\nIn this project, dashboards are provisioned as Kubernetes ConfigMaps via the monitoring-deps Helm chart:\n- Dashboard JSON files live in `ops/helmfile-infra/charts/monitoring-deps/dashboards/`\n- `templates/dashboards.yaml` wraps each JSON file into a ConfigMap with `grafana_dashboard: \"1\"` label\n- Data sources: Prometheus (`uid: \"prometheus\"`) and Loki (`uid: \"loki\"`)\n- Standard namespace filter: `namespace=\"x402\"`\n- Common template variables: `$service` (job selector), `$level` (log level)\n\nWhen generating dashboards for this project, output the JSON to the dashboards directory and ensure the ConfigMap template references it.\n\n## Reference Files\n\nFor detailed API reference and complete examples, see:\n- `references/typescript-api.md` - Full TypeScript API with all panel types, query builders, and configuration options\n- `references/patterns.md` - Common dashboard patterns, recipes, and a complete example converting this project's dashboard to SDK code\n\nFile v0.2.2:_meta.json\n\n{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"grafana-foundation-sdk\",\n  \"version\": \"0.2.2\",\n  \"publishedAt\": 1784745862066\n}\n\nFile v0.2.2:references/patterns.md\n\n# Common Dashboard Patterns\n\n## Table of Contents\n1. [Project Structure](#project-structure)\n2. [RED Method Dashboard](#red-method-dashboard)\n3. [Kubernetes Service Dashboard](#kubernetes-service-dashboard)\n4. [Loki Log Panels](#loki-log-panels)\n5. [Multi-Datasource Dashboard](#multi-datasource-dashboard)\n6. [Dynamic Dashboard Generation](#dynamic-dashboard-generation)\n7. [Converting Raw JSON to SDK](#converting-raw-json-to-sdk)\n8. [Integration with Helm/ConfigMaps](#integration-with-helmconfigmaps)\n\n---\n\n## Project Structure\n\nFor a monorepo with multiple dashboards, keep dashboard generators alongside the monitoring config:\n\n```\nops/\n  dashboards/\n    generate.ts          # entry point - imports and runs all generators\n    helpers.ts           # shared datasource refs, query helpers, panel defaults\n    overview.ts          # overview dashboard generator\n    per-service.ts       # per-service dashboard generator\n    package.json         # { \"type\": \"module\", \"scripts\": { \"generate\": \"tsx generate.ts\" } }\n    tsconfig.json\n  helmfile/\n    charts/monitoring-deps/\n      dashboards/        # generated JSON files go here\n```\n\nOr for a single dashboard, a simple standalone script works fine:\n\n```\nops/dashboards/\n  generate-overview.ts\n  package.json\n```\n\n**package.json:**\n```json\n{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"generate\": \"tsx generate.ts\"\n  },\n  \"dependencies\": {\n    \"@grafana/grafana-foundation-sdk\": \"^0.0.16\"\n  },\n  \"devDependencies\": {\n    \"tsx\": \"^4.0.0\"\n  }\n}\n```\n\n---\n\n## RED Method Dashboard\n\nThe RED method (Rate, Errors, Duration) is the standard pattern for monitoring request-driven services. Here's a complete implementation:\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder, ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\n// --- Helpers ---\n\nconst PROM = { type: 'prometheus', uid: 'prometheus' } as const;\n\nfunction pq(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction defaultTimeseries(title: string) {\n  return new TimeseriesBuilder()\n    .title(title)\n    .datasource(PROM)\n    .height(8)\n    .span(12)\n    .fillOpacity(15)\n    .lineWidth(1)\n    .showPoints(common.VisibilityMode.Never)\n    .legend(\n      new common.VizLegendOptionsBuilder()\n        .showLegend(true)\n        .placement(common.LegendPlacement.Bottom)\n        .displayMode(common.LegendDisplayMode.List)\n    )\n    .tooltip(\n      new common.VizTooltipOptionsBuilder()\n        .mode(common.TooltipDisplayMode.Multi)\n        .sort(common.SortOrder.Descending)\n    );\n}\n\n// --- Dashboard ---\n\nfunction buildREDDashboard(namespace: string, metricPrefix: string) {\n  return new DashboardBuilder(`${namespace} RED`)\n    .uid(`${namespace}-red`)\n    .tags([namespace, 'red'])\n    .editable()\n    .refresh('30s')\n    .time({ from: 'now-1h', to: 'now' })\n    .withVariable(\n      new QueryVariableBuilder('service')\n        .label('Service')\n        .query(`label_values(${metricPrefix}_duration_count{namespace=\"${namespace}\"}, job)`)\n        .datasource(PROM)\n        .refresh(2)\n        .includeAll(true)\n        .allValue('.*')\n        .sort(1)\n    )\n\n    // --- Rate ---\n    .withRow(new RowBuilder('Rate'))\n    .withPanel(\n      defaultTimeseries('Request Rate')\n        .withTarget(pq(\n          `sum by (job)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m]))`,\n          '{{job}}'\n        ))\n        .unit('reqps')\n        .stacking(new common.StackingConfigBuilder().mode(common.StackingMode.Normal))\n    )\n    .withPanel(\n      defaultTimeseries('Request Rate by Status')\n        .withTarget(pq(\n          `sum by (http_status_code)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m]))`,\n          '{{http_status_code}}'\n        ))\n        .unit('reqps')\n    )\n\n    // --- Errors ---\n    .withRow(new RowBuilder('Errors'))\n    .withPanel(\n      defaultTimeseries('Error Rate (5xx)')\n        .withTarget(pq(\n          `sum by (job)(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\",http_status_code=~\"5..\"}[5m]))`,\n          '{{job}}'\n        ))\n        .unit('reqps')\n    )\n    .withPanel(\n      defaultTimeseries('Error Percentage')\n        .withTarget(pq(\n          `sum(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\",http_status_code=~\"5..\"}[5m])) / sum(rate(${metricPrefix}_duration_count{namespace=\"${namespace}\",job=~\"$service\"}[5m])) * 100`,\n          'error %'\n        ))\n        .unit('percent')\n    )\n\n    // --- Duration ---\n    .withRow(new RowBuilder('Duration'))\n    .withPanel(\n      defaultTimeseries('Latency Percentiles')\n        .withTarget(pq(\n          `histogram_quantile(0.50, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p50'\n        ))\n        .withTarget(pq(\n          `histogram_quantile(0.95, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p95'\n        ))\n        .withTarget(pq(\n          `histogram_quantile(0.99, sum by (le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          'p99'\n        ))\n        .unit('ms')\n    )\n    .withPanel(\n      defaultTimeseries('Latency by Service')\n        .withTarget(pq(\n          `histogram_quantile(0.95, sum by (job, le)(rate(${metricPrefix}_duration_bucket{namespace=\"${namespace}\",job=~\"$service\"}[5m])))`,\n          '{{job}} p95'\n        ))\n        .unit('ms')\n    );\n}\n\n// Gener\n\nArchive v0.2.1: 7 files, 21887 bytes\n\nFiles: CHANGELOG.md (2414b), LICENSE.txt (9157b), references/patterns.md (13343b), references/typescript-api.md (18171b), skill-card.md (2377b), SKILL.md (14116b), _meta.json (141b)\n\nArchive v0.2.0: 7 files, 21667 bytes\n\nFiles: CHANGELOG.md (2259b), LICENSE.txt (9157b), references/patterns.md (13343b), references/typescript-api.md (18171b), skill-card.md (2046b), SKILL.md (14116b), _meta.json (141b)\n\nArchive v0.1.0: 7 files, 19770 bytes\n\nFiles: CHANGELOG.md (736b), LICENSE.txt (9157b), references/patterns.md (13343b), references/typescript-api.md (16388b), skill-card.md (2289b), SKILL.md (12414b), _meta.json (141b)","readmeExcerpt":"Skill: grafana-foundation-sdk Owner: tenequm Summary: Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards. Tags: latest:0.2.4 Version history: v0.2","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"npm install '@grafana/grafana-foundation-sdk@~0.0.16'\n# or\npnpm add '@grafana/grafana-foundation-sdk@~0.0.16'"},{"language":"bash","snippet":"go get github.com/grafana/grafana-foundation-sdk/go@v0.0.16"},{"language":"typescript","snippet":"// Each concern has its own import\nimport { DashboardBuilder, RowBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';"},{"language":"typescript","snippet":"import { DashboardBuilder, RowBuilder, QueryVariableBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\nconst dashboard = new DashboardBuilder('My Service Overview')\n  .uid('my-service-overview')\n  .tags(['my-service'])\n  .editable()\n  .refresh('30s')\n  .time({ from: 'now-24h', to: 'now' })\n  .timezone('browser')\n  .withVariable(\n    new QueryVariableBuilder('service')\n      .label('Service')\n      .query('label_values(up{namespace=\"default\"}, job)')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .refresh(1)\n      .includeAll(true)\n      .allValue('.*')\n      .sort(1)\n  )\n  .withRow(new RowBuilder('Overview'))\n  .withPanel(\n    new StatBuilder()\n      .title('Request Rate')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('req/s')\n      )\n      .unit('reqps')\n      .decimals(1)\n      .height(4)\n      .span(6)\n      .colorMode(common.BigValueColorMode.Background)\n      .graphMode(common.BigValueGraphMode.Area)\n      .reduceOptions(\n        new common.ReduceDataOptionsBuilder().calcs(['lastNotNull'])\n      )\n  )\n  .withPanel(\n    new TimeseriesBuilder()\n      .title('Request Rate Over Time')\n      .datasource({ type: 'prometheus', uid: 'prometheus' })\n      .withTarget(\n        new DataqueryBuilder()\n          .expr('sum by (job)(rate(http_requests_total{job=~\"$service\"}[5m]))')\n          .legendFormat('{{job}}')\n      )\n      .unit('reqps')\n      .fillOpacity(15)\n      .height(8)\n      .span(12)\n  );\n\n// Output the dashboard JSON\nconsole.log(JSON.stringify(dashboard"},{"language":"typescript","snippet":"function promDs() {\n  return { type: 'prometheus', uid: 'prometheus' } as const;\n}\n\nfunction lokiDs() {\n  return { type: 'loki', uid: 'loki' } as const;\n}\n\nfunction promQuery(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction statPanel(title: string, expr: string, opts?: { unit?: string; decimals?: number; color?: string }) {\n  const panel = new StatBuilder()\n    .title(title)\n    .datasource(promDs())\n    .withTarget(promQuery(expr))\n    .height(4)\n    .span(4)\n    .colorMode(common.BigValueColorMode.Background)\n    .graphMode(common.BigValueGraphMode.Area)\n    .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']));\n\n  if (opts?.unit) panel.unit(opts.unit);\n  if (opts?.decimals !== undefined) panel.decimals(opts.decimals);\n  // Thresholds can be set via .thresholds() if needed\n\n  return panel;\n}"},{"language":"typescript","snippet":"// Query variable - populated from Prometheus labels\nnew QueryVariableBuilder('service')\n  .label('Service')\n  .query('label_values(http_server_duration_count{namespace=\"myapp\"}, job)')\n  .datasource({ type: 'prometheus', uid: 'prometheus' })\n  .refresh(2)  // 1=on dashboard load, 2=on time range change\n  .includeAll(true)\n  .allValue('.*')\n  .sort(1)  // 1=alphabetical asc\n\n// Custom variable - static key:value pairs\nnew CustomVariableBuilder('level')\n  .label('Log Level')\n  .query('All : .+, Error : error|fatal, Warning : warn, Info : info, Debug : debug')\n  .current({ text: 'All', value: '.+' })"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: grafana-foundation-sdk\ndescription: Build Grafana dashboards as code with the grafana-foundation-sdk typed builders (TypeScript or Go). Use when creating, modifying, or generating Grafana dashboard JSON programmatically, converting hand-written dashboard JSON to typed code, building monitoring dashboards, or working with Prometheus/Loki queries in dashboards.\nmetadata:\n  version: \"0.2.4\"\n  categories: \"operations, development\"\n  topics: \"grafana, dashboards, observability, dashboards-as-code, typescript\"\n  upstream: \"@grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/grafana-foundation-sdk\n    emoji: \"📊\"\n---\n\n# Grafana Foundation SDK\n\nThe grafana-foundation-sdk provides strongly typed builder libraries for defining Grafana dashboards as code. Instead of writing raw JSON (which is error-prone and hard to review in diffs), you compose dashboards using chained builder calls that produce valid Grafana JSON.\n\nThe SDK is auto-generated from Grafana's internal CUE schemas via the `cog` tool. It supports Go, TypeScript, Python, PHP, and Java. This skill focuses on **TypeScript** (primary) and **Go** (secondary) since those are the most common choices for infrastructure teams.\n\n## When to use this skill\n\n- Creating new Grafana dashboards from scratch\n- Converting existing hand-written dashboard JSON to typed code\n- Adding panels, variables, or queries to dashboards\n- Building reusable dashboard components (helper functions for common panel patterns)\n- Generating dashboards dynamically based on service lists or configs\n\n## Installation\n\nThe SDK is published as concrete `v0.0.x` tags (latest: **v0.0.16**). Pin explicitly - it is pre-1.0 and the API churns between releases (see Known Gotchas).\n\n**TypeScript:**\n```bash\nnpm install '@grafana/grafana-foundation-sdk@~0.0.16'\n# or\npnpm add '@grafana/grafana-foundation-sdk@~0.0.16'\n```\n\n**Go:**\n```bash\ngo get github.com/grafana/grafana-foundation-sdk/go@v0.0.16\n```\n\n## Core Architecture\n\nEverything follows the **builder pattern**: create a builder, chain configuration methods, call `.build()` (TS) or `.Build()` (Go) to produce the final object. The output is standard Grafana dashboard JSON - compatible with Grafana's API, file-based provisioning, and Kubernetes ConfigMaps.\n\nEach panel type, query type, and variable type lives in its own package. You import only what you need:\n\n```typescript\n// Each concern has its own import\nimport { DashboardBuilder, RowBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { DataqueryBuilder as PromQueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport { DataqueryBuilder as LokiQueryBuilder } from '@grafana/grafana-foundation-sdk/loki';\n```\n\n## "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"grafana-foundation-sdk\",\n  \"version\": \"0.2.4\",\n  \"publishedAt\": 1787313713832\n}"},{"path":"references/patterns.md","content":"# Common Dashboard Patterns\n\n## Table of Contents\n1. [Project Structure](#project-structure)\n2. [RED Method Dashboard](#red-method-dashboard)\n3. [Kubernetes Service Dashboard](#kubernetes-service-dashboard)\n4. [Loki Log Panels](#loki-log-panels)\n5. [Multi-Datasource Dashboard](#multi-datasource-dashboard)\n6. [Dynamic Dashboard Generation](#dynamic-dashboard-generation)\n7. [Converting Raw JSON to SDK](#converting-raw-json-to-sdk)\n8. [Integration with Helm/ConfigMaps](#integration-with-helmconfigmaps)\n\n---\n\n## Project Structure\n\nFor a monorepo with multiple dashboards, keep dashboard generators alongside the monitoring config:\n\n```\nops/\n  dashboards/\n    generate.ts          # entry point - imports and runs all generators\n    helpers.ts           # shared datasource refs, query helpers, panel defaults\n    overview.ts          # overview dashboard generator\n    per-service.ts       # per-service dashboard generator\n    package.json         # { \"type\": \"module\", \"scripts\": { \"generate\": \"tsx generate.ts\" } }\n    tsconfig.json\n  deploy/\n    dashboards/        # generated JSON files go here\n```\n\nOr for a single dashboard, a simple standalone script works fine:\n\n```\nops/dashboards/\n  generate-overview.ts\n  package.json\n```\n\n**package.json:**\n```json\n{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"generate\": \"tsx generate.ts\"\n  },\n  \"dependencies\": {\n    \"@grafana/grafana-foundation-sdk\": \"^0.0.16\"\n  },\n  \"devDependencies\": {\n    \"tsx\": \"^4.0.0\"\n  }\n}\n```\n\n---\n\n## RED Method Dashboard\n\nThe RED method (Rate, Errors, Duration) is the standard pattern for monitoring request-driven services. Here's a complete implementation:\n\n```typescript\nimport { DashboardBuilder, RowBuilder, QueryVariableBuilder, ThresholdsConfigBuilder } from '@grafana/grafana-foundation-sdk/dashboard';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus';\nimport * as common from '@grafana/grafana-foundation-sdk/common';\n\n// --- Helpers ---\n\nconst PROM = { type: 'prometheus', uid: 'prometheus' } as const;\n\nfunction pq(expr: string, legend?: string) {\n  const q = new DataqueryBuilder().expr(expr);\n  if (legend) q.legendFormat(legend);\n  return q;\n}\n\nfunction defaultTimeseries(title: string) {\n  return new TimeseriesBuilder()\n    .title(title)\n    .datasource(PROM)\n    .height(8)\n    .span(12)\n    .fillOpacity(15)\n    .lineWidth(1)\n    .showPoints(common.VisibilityMode.Never)\n    .legend(\n      new common.VizLegendOptionsBuilder()\n        .showLegend(true)\n        .placement(common.LegendPlacement.Bottom)\n        .displayMode(common.LegendDisplayMode.List)\n    )\n    .tooltip(\n      new common.VizTooltipOptionsBuilder()\n        .mode(common.TooltipDisplayMode.Multi)\n        .sort(common.SortOrder.Descending)\n    );\n}\n\n// --- Dashboard ---\n\nfunction buildREDDashboard(namespace: string, metricPref"},{"path":"references/typescript-api.md","content":"# TypeScript API Reference\n\n## Table of Contents\n1. [Imports](#imports)\n2. [Dashboard Builder](#dashboard-builder)\n3. [Panel Types](#panel-types)\n4. [Query Builders](#query-builders)\n5. [Variable Builders](#variable-builders)\n6. [Field Configuration](#field-configuration)\n7. [Common Enums](#common-enums)\n8. [Go Quick Reference](#go-quick-reference)\n\n---\n\n## Imports\n\nEvery concept lives in its own subpath export. Import only what you need:\n\n```typescript\n// Dashboard structure\nimport {\n  DashboardBuilder,\n  RowBuilder,\n  QueryVariableBuilder,\n  CustomVariableBuilder,\n  DatasourceVariableBuilder,\n  DashboardLinkBuilder,\n  ThresholdsConfigBuilder,\n} from '@grafana/grafana-foundation-sdk/dashboard';\n\n// Panel types (each exports PanelBuilder)\nimport { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';\nimport { PanelBuilder as StatBuilder } from '@grafana/grafana-foundation-sdk/stat';\nimport { PanelBuilder as GaugeBuilder } from '@grafana/grafana-foundation-sdk/gauge';\nimport { PanelBuilder as BarGaugeBuilder } from '@grafana/grafana-foundation-sdk/bargauge';\nimport { PanelBuilder as TableBuilder } from '@grafana/grafana-foundation-sdk/table';\nimport { PanelBuilder as PieChartBuilder } from '@grafana/grafana-foundation-sdk/piechart';\nimport { PanelBuilder as BarChartBuilder } from '@grafana/grafana-foundation-sdk/barchart';\nimport { PanelBuilder as HeatmapBuilder } from '@grafana/grafana-foundation-sdk/heatmap';\nimport { PanelBuilder as HistogramBuilder } from '@grafana/grafana-foundation-sdk/histogram';\nimport { PanelBuilder as LogsBuilder } from '@grafana/grafana-foundation-sdk/logs';\nimport { PanelBuilder as TextBuilder } from '@grafana/grafana-foundation-sdk/text';\nimport { PanelBuilder as StateTimelineBuilder } from '@grafana/grafana-foundation-sdk/statetimeline';\nimport { PanelBuilder as StatusHistoryBuilder } from '@grafana/grafana-foundation-sdk/statushistory';\nimport { PanelBuilder as NodeGraphBuilder } from '@grafana/grafana-foundation-sdk/nodegraph';\nimport { PanelBuilder as GeoMapBuilder } from '@grafana/grafana-foundation-sdk/geomap';\nimport { PanelBuilder as XYChartBuilder } from '@grafana/grafana-foundation-sdk/xychart';\nimport { PanelBuilder as TrendBuilder } from '@grafana/grafana-foundation-sdk/trend';\nimport { PanelBuilder as CandlestickBuilder } from '@grafana/grafana-foundation-sdk/candlestick';\nimport { PanelBuilder as CanvasBuilder } from '@grafana/grafana-foundation-sdk/canvas';\nimport { PanelBuilder as DatagridBuilder } from '@grafana/grafana-foundation-sdk/datagrid';\nimport { PanelBuilder as AnnotationsListBuilder } from '@grafana/grafana-foundation-sdk/annotationslist';\nimport { PanelBuilder as DashboardListBuilder } from '@grafana/grafana-foundation-sdk/dashboardlist';\nimport { PanelBuilder as NewsBuilder } from '@grafana/grafana-foundation-sdk/news';\n\n// Library panels (reference a shared, server-stored panel)\nimport { LibraryPanelBuilder } from '@grafana/grafana-foundation-sdk/librarypa"},{"path":"CHANGELOG.md","content":"# Changelog\n\nAll notable changes to this skill will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),\nand this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.2.4] - 2026-08-21\n\n### Changed\n\n- Declared ClawHub browse categories (`operations, development`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category.\n\n### Removed\n\n- `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub.\n\n## [0.2.3] - 2026-08-07\n\n### Removed\n\n- \"Project-Specific Context\" section that hardcoded one private repo's Helm chart layout and paths into the published skill.\n\n### Changed\n\n- Genericized example queries and provisioning workflow: `namespace=\"x402\"` -> `namespace=\"myapp\"`, private chart paths -> generic `deploy/dashboards/` layout, `helmfile sync` step -> provisioning-mechanism-agnostic wording.\n\n## [0.2.2] - 2026-07-22\n\n### Added\n\n- skill-card.md release record following NVIDIA's skill-card format\n- metadata.openclaw block (emoji, homepage) for ClawHub display\n\n## [0.2.1] - 2026-07-10\n\n### Changed\n- CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).\n\n## [0.2.0] - 2026-06-05\n\n### Fixed\n- Go install string corrected to `go get github.com/grafana/grafana-foundation-sdk/go@v0.0.16` (the canonical tag form per the official docs), replacing the stale `@next+cog-v0.0.x` branch ref.\n- PieChart legend example now uses `PieChartLegendOptionsBuilder` (from the `piechart` package) instead of `common.VizLegendOptionsBuilder`, which is a type error for piechart panels.\n\n### Added\n- `metadata.upstream` tracking established (pinned to v0.0.16).\n- Known gotcha on dashboard schema v1 vs v2: the skill targets v1; `dashboardv2beta1` (k8s apiVersion `dashboard.grafana.app/v2beta1`) exists and is still stabilizing.\n- Known gotchas on the type-checking gap (builders only checked when wired into a tsconfig; use project-local `npx tsc` for the SDK's ES2024/bundler output) and the regenerate-JSON-after-every-edit discipline.\n- `units` typed-constants module, `expr` server-side/SQL expressions, `testdata` datasource, and the broader set of core datasource query builders (elasticsearch, cloudwatch, azuremonitor, googlecloudmonitoring, bigquery, athena, parca, grafanapyroscope) in the TypeScript reference.\n- Additional panel types in the import reference: canvas, datagrid, annotationslist, dashboardlist, news; plus `librarypanel`.\n\n### Changed\n- Version pins updated to 0.0.16 across SKILL.md (install commands) and references/patterns.md (`package.json` example).\n\nVerified against: @grafana/grafana-foundation-sdk@0.0.16, github.com/grafana/grafana-foundation-sdk/go@0.0.16\n\n## [0.1.0] - 2026-06-05\n\n### Added\n- Initial release: building Grafana dashboa"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1589,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T20:42:44.996Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T23:48:06.949Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}