# Allow integrations to declare their own dashboard widgets (JSON schema)

**URL:** <https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641>\
**Category:** Demandes livrées\
**Tags:** in-progress\
**Created:** [August 17, 2026, 10:18am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641 "2026-08-17T10:18:22Z")\
**Posts on this page:** 14\
**Page:** 1

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [August 17, 2026, 10:18am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/1 "2026-08-17T10:18:23Z")

</div>

**The need**

Today, all dashboard widgets are developed in the core of Gladys. An integration (especially an external one, via the SDK) cannot propose a widget specific to its domain, although this is often where the data makes the most sense: solar production tracking, status of a robot vacuum, charging schedule of an electric car, etc.

**The proposal**

Allow an integration to declare one or more widgets via a JSON schema, on the same principle as the configuration pages of external integrations. The integration describes _what_ to display (values, graphs, buttons, states, gauges…), and it’s Gladys that decides _how_ to display it.

Deliberately, no custom HTML or iframe: it’s a philosophical choice. By remaining declarative, the core of Gladys ensures for all widgets, including third-party ones: visual consistency, dark mode, mobile/tablet responsiveness, translations, performance, and non-regression when the interface evolves. It’s the same model as iOS widgets: purely declarative, and no one finds the ecosystem lacking in richness 🙂

**The benefits**

- Integrations become complete: their data finally have a real place on the dashboard
- The experience remains clean and unified, regardless of the widget’s author
- The JSON being validatable, an AI will be able to generate or repair these widgets reliably (and eventually, build an entire dashboard on demand)
- The vocabulary of components can be gradually enriched in the core, without ever breaking anything, and each addition benefits all integrations at once

**Areas for reflection**

- Define the initial vocabulary: what basic components? (value + unit, historical graph, action button, list of states…)
- How the widget retrieves its data: existing device features, or endpoint exposed by the integration?
- Version the schema so that widgets remain compatible with updates

---

<div class="post-metadata">

**Author:** ![guim31](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/guim31/32/4840_2.png) [@guim31](https://community.gladysassistant.com/u/guim31)\
**Post date:** [August 17, 2026, 11:16am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/2 "2026-08-17T11:16:33Z")

</div>

Very very good idea, it’s similar to the principle of Android apps that can offer widgets 👍

---

<div class="post-metadata">

**Author:** ![cursor-bot](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/cursor-bot/32/22252_2.png) [@cursor-bot](https://community.gladysassistant.com/u/cursor-bot)\
**Post date:** [September 18, 2026, 7:56am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/4 "2026-09-18T07:56:40Z")

</div>

Hi everyone!

This topic is now **in development**.

A PR has been opened to specify the dashboard widgets declared by integrations (JSON schema):

> <https://github.com/GladysAssistant/Gladys/pull/3109>
>
> \### Description
> 
> Spec-first PR (no code): adds the living specification of the "…dashboard widgets declared by integrations" capability, following the spec-first process of \`AGENTS.md\` and the layout rules of the external-integrations spec (a new capability is a new self-contained file in \`capabilities/\`).
> 
> New file \`docs/specs/external-integrations/capabilities/dashboard-widgets.md\`:
> 
> \- \*\*Context\*\*: the community request, the PR #3061 pilot (cinema widget closed on scope: one integration type per domain is the bottleneck the external-integration system was built to remove) and the related topics (custom screens, "create my own widgets").
> \- \*\*Design principles\*\*: the integration describes \*what\*, Gladys decides \*how\* (no HTML / iframe / script / free styling); the widget is \*\*declared in the manifest\*\* (identity, icon, per-instance \`settings\` in the \`config\_schema\` grammar) and its \*\*content is produced at runtime\*\* as a component tree in a core vocabulary (the iOS/Android widget model, no data-binding language to maintain); the payload is never trusted (normalized and bounded); "trigger, not data" (pull + stateless refresh nudge); orthogonal to the integration \`type\`; additive-only vocabulary.
> \- \*\*Manifest \`widgets\` field\*\* (≤ 5, key / label / description / icon / settings / \`action\_timeout\_seconds\`) with its validation rules.
> \- \*\*One core box type \`external-widget\`\*\* (\`{ integration, widget, name?, settings? }\`), the picker tiles from a new \`GET /api/v1/external\_integration/widget\` open to every authenticated user, the edit form reusing the shared config-schema engine, uninstall behavior.
> \- \*\*Data path\*\*: \`GET .../widget/:key/content\` → settings validation (defaults, dynamic \`source: "devices"\`) → in-memory cache keyed by settings with a content-declared \`ttl\_seconds\` → \`external-integration.widget.get\` (15 s ack, coalesced) → \`normalizeWidgetContent\`; the \`widget.refresh\` nudge (rate-limited) and the \`widget-updated\` push to the frontend; error translation to \`REQUEST\_TO\_THIRD\_PARTY\_FAILED\`.
> \- \*\*Content vocabulary v1\*\*: \`text\`, \`value\`, \`gauge\`, \`status\`, \`chart\` (inline series), \`card-list\` (the poster grid with a core detail panel), \`image\`, and \`button\` (phase 2); per-field bounds, semantic \`color\` enum, ISO dates and numbers formatted by the core, \`https\`-only links with the domain displayed, drop rules for unknown types/fields, envelope \`version\` and the published \`widget-content.schema.json\`.
> \- \*\*Images\*\* served through the core (integration serves bytes on demand, magic-number validation, ≤ 300 KB, 1 h cache, allowlist of declared keys, concurrency cap) — never a third-party URL loaded by the browser, never a core-side fetch (SSRF).
> \- \*\*Phase 2\*\*: widget actions (allowlisted from the last content, rate-limited, cache invalidation), device-feature buttons and live \`device\_feature\` / \`device\_features\` bindings resolved within the tenant, reusing the chips / chart box paths.
> \- Management API routes, WebSocket messages, SDK handlers (\`onWidgetGet\`, \`onWidgetGetImage\`, \`onWidgetAction\`, \`requestWidgetRefresh\`), frontend rendering states, security and accepted risks, tests, manual verification, phases, alternatives considered, out of scope.
> 
> Also adds a short section I to \`docs/specs/dashboard-flexible-layout-and-widgets.md\` recording the new box type and pointing to the capability file that owns its contract.
> 
> \### Related request
> 
> Forum: \`\`https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json\`\`/10641
> 
> \### Checklist
> 
> \- \[x\] If a forum topic or GitHub issue exists, the description links it (\`Forum: https://community.gladysassistant.com/t/...\` or \`Closes #...\`)
> \- \[x\] Tests pass: \`cd server && npm run coverage\` (Codecov requires 100% coverage on changed lines) and Cypress (\`npm run cypress:run\`) if the UI changed — not applicable, documentation-only change (no code touched)
> \- \[x\] Linter and prettier pass on both front and server (\`npm run eslint\`, \`npm run prettier\`) — not applicable, \`docs/\` is outside both prettier scopes
> \- \[x\] No undocumented breaking change
> 
> 🤖 Generated with \[Claude Code\](https://claude.com/claude-code)
> 
> https://claude.ai/code/session\_01Mym9VS4QcYR7ZxvJVS2oGh
> 
> \---
> \_Generated by \[Claude Code\](https://claude.ai/code/session\_01Mym9VS4QcYR7ZxvJVS2oGh)\_

Feel free to follow the PR, test (optional, especially for small requests) and give your feedback here if needed.

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [September 18, 2026, 11:09am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/5 "2026-09-18T11:09:40Z")

</div>

Hi everyone! 👋

Development is ready to be tested, and I would love to get feedback from several integration developers to check that the API meets real use cases before release.

Here’s a preview of the result:

 ![01-editeur-choix-du-type-de-box](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/5/f/5f1d1945bc214f1cd803c15e643ca839a4e54fa7.jpeg)  
 ![01c-editeur-recherche](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/b/f/bf94ce16c33e44a5b577763d8a35eed35ff20ec3.jpeg)  
 ![02-editeur-reglages-du-widget](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/5/8/58eedad1d04e12c201b5d7cf4881fcd5cd432fde.jpeg)  
 ![03-dashboard-clair](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/9/b/9ba432bd2f99a0dab123cdf42b74886c6e73af8b.jpeg)  
 ![04-detail-d-une-carte](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/4/9/49a81326a48c06c774bacb6faccf3219c277e60f.jpeg)

The PR contains the API specification (you can also give it to Claude to develop an integration while waiting for it to be merged): [https://github.com/GladysAssistant/Gladys/pull/3109](https://github.com/GladysAssistant/Gladys/pull/3109)

The Docker image to test:

```auto
ghcr.io/gladysassistant/gladys-preview:claude-exciting-babbage-waa7zm

```

@spenceur Could you redo your movie integration as an external integration with this API and let me know if it meets your needs? 🙂

The schedule is quite tight, so I’m open to all your feedback as soon as possible. The goal is to publish this as early as Monday if everything goes well! 🚀

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [September 18, 2026, 11:25am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/6 "2026-09-18T11:25:15Z")

</div>

I’ll test this over the weekend, but based on the screenshots, I like it 👍

---

<div class="post-metadata">

**Author:** ![spenceur](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/spenceur/32/1961_2.png) [@spenceur](https://community.gladysassistant.com/u/spenceur)\
**Post date:** [September 18, 2026, 12:56pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/7 "2026-09-18T12:56:21Z")

</div>

Arfffff thanks @pierre-gilles I’m not very available before Sunday 😅😅 I promise I’ll look on Sunday evening (if it can wait until then?)

Ps: I’ll check tonight as soon as I can, I saw the end of the message 🥲

---

<div class="post-metadata">

**Author:** ![spenceur](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/spenceur/32/1961_2.png) [@spenceur](https://community.gladysassistant.com/u/spenceur)\
**Post date:** [September 18, 2026, 4:07pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/8 "2026-09-18T16:07:35Z")

</div>

@pierre-gilles  
Widget :

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/3/f/3f2693c07e55b4484a9fc540b8d5bf82eac1fb07.png)  
 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/d/0/d0627bd1ccffa7c50684e7f0ec318c91568b8cd7.jpeg)  
 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/4/6/462fbc1a965e263a323d49b15d16c3429e782783.jpeg)

Trigger

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/f/5/f5e7045d37dda0e6380ae15e6411ccc7d9038970.png)  
 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/5/c/5cccb8b3ff346db2bee7d40f00b72dd9afb6f277.jpeg)

Case where the plugin is crashed :

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/b/9/b9d439ec0f0cc876d1506646d0d2693edbb3da6f.jpeg)  
the trigger is still displayed but gladys works correctly 🙂

Everything is functional.  
Can I ask for a «&nbsp;contains&nbsp;» filter function that is case-insensitive? 😃

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [September 18, 2026, 6:09pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/9 "2026-09-18T18:09:46Z")

</div>

Excellent, thanks for the test! It looks great on movies 😁

> [@spenceur](#):
>
> Could you add a case-insensitive «&nbsp;contains&nbsp;» filter function in the triggers? 😅? 😃

That can be added!

---

<div class="post-metadata">

**Author:** ![spenceur](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/spenceur/32/1961_2.png) [@spenceur](https://community.gladysassistant.com/u/spenceur)\
**Post date:** [September 18, 2026, 8:59pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/10 "2026-09-18T20:59:51Z")

</div>

I have an issue with certain images that exceed the size limit imposed by gladys

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/6/e/6e02c4bd2c6eeed00852924441171b5d00933029.jpeg)

If it helps, here’s what claude says:  
**Technical details:**

- The core limits widget images to **300 KB** (`MAX_WIDGET_IMAGE_BYTES = 300 * 1024` in `externalIntegration.normalizeWidgetImage.js`), an intentional limit to prevent a widget from loading a huge image in the browser.
- I checked by retrieving the posters directly from the AlloCiné CDN (`all.web.img.acsta.net`): those that display correctly are 182-241 KB, the two broken ones are **341 KB and 378 KB** — above the threshold.
- When `onWidgetGetImage` returns an image that is too heavy, the core rejects the response on the server side and the front end simply displays the broken image icon, with a generic error message (`REQUEST_TO_THIRD_PARTY_FAILED`) that does not explicitly say «&nbsp;too heavy&nbsp;» — it took me a while to trace.

**This is not a bug in the integration code** : `widget.js` does exactly the same thing for all images, it’s just that the AlloCiné CDN sometimes serves posters heavier than Gladys’ limit for certain films. There is no resizing parameter in the CGR poster URL to request a lighter version, so nothing to correct on the integration side.

---

<div class="post-metadata">

**Author:** ![cicoub13](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/cicoub13/32/4482_2.png) [@cicoub13](https://community.gladysassistant.com/u/cicoub13)\
**Post date:** [September 19, 2026, 12:29pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/11 "2026-09-19T12:29:49Z")

</div>

Tested with [Intégration externe - Prochains passages de l'ISS](https://community.gladysassistant.com/t/integration-externe-prochains-passages-de-liss/10884) 🚀  
No issues, either during development or testing (it’s a custom widget but without configuration)

 ![Capture d’écran 2026-09-19 à 14.22.20](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/b/5/b5427e7efa3a6c26d1cf2c6eb0018b4bb31f4c4a.jpeg)

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [September 19, 2026, 1:10pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/12 "2026-09-19T13:10:53Z")

</div>

I tested this PR end-to-end with a fake integration provider that serves a tide widget speaking raw WebSocket protocol (no Docker nor SDK — SDK 0.13.0 does not yet expose widget handlers). The mechanism works: the widget appears in the picker, the settings are validated, the content is normalized, the action round-trip and the nudge widget.refresh behave as specified. The plumbing is solid.

I then tried a concrete case: the tide widget from #3028. The result is a useful data point, because the gap is not cosmetic.

First, the core widget of #3028; then, what I was able to do:

 ![Screenshot of 2026-09-19 14-47-59](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/7/2/72a1e71d49e8d72d7e8bfba350f16fa1f82352c7.png)  
 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/f/9/f9a5a781c7d998d779865fffbec73affcde9cb18.png)

Three things are structurally impossible, and not just less pretty:

1. The tide clock — a dial with a hand indicating the position in the cycle and the time remaining before the stand. There is no dial component, and neither value, nor gauge, nor chart approximates it: a gauge is a radial arc for a ratio, not a dial with two labeled poles PM/BM.

2. The annotations on the curve — the markers for high and low tide with their time and height, the coefficient badges, the dotted reference of the present moment. Chart only takes point series, so the curve loses exactly the information for which it is read. You don’t look at a tide curve for its shape, but to read «&nbsp;07:48, 10.69 m, coef. 74&nbsp;».

3. The day tabs (today + 6 days). I understand that this one is deliberate — «&nbsp;a widget is read and touched, it’s not a page&nbsp;» — but a tide without the next day loses its purpose: you consult a tide to prepare an outing, so in advance. A card-list of 7 items would make a good focal, but it would take up the space of the curve, which is the heart of the widget.

This does not call into question. The capability remains the right answer for the TMDB pilot case and for the domain widgets cited in the spec. My point is narrower: the dividing line «&nbsp;domain widgets vs generic control widgets&nbsp;» is not enough to classify a widget like the tide. It’s not a control widget, so the «&nbsp;out of scope&nbsp;» section does not exclude it; but its rendering relies on a domain-specific representation (a dial, an annotated curve) that the vocabulary cannot describe by construction. The spec says «&nbsp;the worst possible third-party widget looks like a slightly loaded core widget&nbsp;» — here, the opposite happens: the best widget that the vocabulary allows is significantly below what the core already knows how to do.

Two suggestions, in the additive spirit of principle 7:

- A dial component (value, min, max, two pole labels, central label) would join gauge as the second radial component. It serves the tide, but also a wind rose, a position in a load cycle, a lunar phase…
- Optional annotations on chart: a bounded array of { t, label, value, color } that the core renders as markers, plus a now\_marker boolean. Additive, ignored by an older core, and useful well beyond the tide (solar production peaks, tariff slots, expected end of charge).

---

<div class="post-metadata">

**Author:** ![prohand](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/prohand/32/15713_2.png) [@prohand](https://community.gladysassistant.com/u/prohand)\
**Post date:** [September 19, 2026, 10:20pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/13 "2026-09-19T22:20:59Z")

</div>

@pierre-gilles The fuel price integration is okay:

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/c/3/c3c0586af0ad4a3a25cac832544cf867626edd5e.jpeg)

Edit: I wanted to set the chart to 30 days, but Claude gave me this feedback:

> **Over 30 days** , to be clear: the title remains “Last 30 days” and the axis extends to the left as readings are taken until it covers a month. I can’t display a 30-day axis from the first day without making up prices we’ve never measured — the front end doesn’t allow forcing a min/max axis, and fabricating points would be false data on a price chart. If you still prefer a fixed 30-day axis, the only honest way would be a patch on the core side (exposing xaxis.min/max in the chart component)

@pierre-gilles Do you think this is relevant?

---

<div class="post-metadata">

**Author:** ![mutmut](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/mutmut/32/14064_2.png) [@mutmut](https://community.gladysassistant.com/u/mutmut)\
**Post date:** [September 20, 2026, 7:38am UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/14 "2026-09-20T07:38:47Z")

</div>

For the 30 days that are not « filled in », I’m not shocked, and it’s already what happens with other sensors when you only have a few days of data and you display them over 30 days. The graph only shows what it has and starts on the left at the first date of the data in the database.

---

<div class="post-metadata">

**Author:** ![cursor-bot](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/cursor-bot/32/22252_2.png) [@cursor-bot](https://community.gladysassistant.com/u/cursor-bot)\
**Post date:** [September 21, 2026, 6:40pm UTC](https://community.gladysassistant.com/t/permettre-aux-integrations-de-declarer-leurs-propres-widgets-de-dashboard-schema-json/10641/15 "2026-09-21T18:40:57Z")

</div>


