Skip to content

Dynamic Dashboards

A dashboard here is not a saved query over a database. It is a natural-language brief handed to an agent, which goes and collects the data with whatever tools it has — files, shell, git, MCP servers — and then decides how to show it, emitting a validated JSON document the UI renders as widgets.

The same machine produces a morning briefing, a communications board, and a system-status view. Only the prompt and the tool allowlist differ.

A dashboard rendered from an agent-authored document

Metrics, a list, a chart, and a written summary — chosen by the agent that gathered them.

+ in the Dashboards sidebar (or New Dashboard in the command palette) asks one question: what do you want to see, and how often? Answer in your own words. The description goes to an authoring model that returns a complete definition, which is written to <home>/dashboards/<id>.md and opened.

Every morning tell me what happened overnight — how many runs, how many failed, what needs my attention, and a short summary of what you’d do first.

Name exact numbers and charts and you get named slots that are pinned on every build; describe an outcome and you get no slots and a dynamic layout, leaving the agent to pick a presentation from whatever the data turns out to be. It is one completion, not a tool loop — a transformation, not a task — and it retries once if the reply isn’t usable JSON.

The same design endpoint both creates and revises. Pass an existing id and an instruction like “drop the token count and make the list full width” and the model rewrites that definition, preserving what the instruction didn’t touch — the scope of a revision is whatever you asked for, and the previous version is snapshotted into version history first.

The structured fields in the editor are refinement, not entry: they sit behind an Advanced disclosure, and the prose brief is the first thing you see.

The dashboard editor

The brief up front; slots, tools, and schedule behind Advanced.

The tab toggles Rendered ⇄ Definition the way a workflow toggles Canvas ⇄ JSON. Applying a plain-English change is destructive of unsaved edits in the form below it, and the editor says so — prose is the source of truth, not the machinery.

What Where
Definition — the brief <home>/dashboards/<id>.md
Widget catalog — what the agent may draw with <home>/widgets/<type>.md
Document — one run’s output <home>/runs/dashboards/<id>/<ts>.json
Source archive — every tool call’s full payload <home>/runs/dashboards/<id>/<ts>.sources/
Vault export <home>/workspace/dashboards/<date>-<id>.md

All of the authored kinds are plain files: git-able, editable in LatchAI’s own editor, versioned, and readable by agents. The source archive exists so a number on a dashboard can be traced back to the exact tool output it came from; it is never loaded wholesale into a model’s context.

The twenty most recent documents per dashboard are kept, each with its own source archive; older ones are pruned after a build.

---
name: Morning Briefing
icon:
description: What happened overnight and what needs me today.
refresh: 0 7 * * * # cron expression; omit for manual-only
agent: dashboard-analyst # optional agent def for persona/model
layout: hybrid # pinned | dynamic | hybrid
tools: [read_file, list_dir, run_shell, latchai_activity]
widgets: [metric, list, line, markdown]
slots:
- overnight | metric | Runs overnight | 3
- attention | list | Needs your attention | 9
- spend | line | Token spend, 7 days | 6
---
Summarise what happened while I was asleep…

Slot entries are id | type | title | span, where type names an entry in the widget catalog and span is 1–12 grid columns. Slots are the consistency mechanism: they are what stops a daily briefing from rearranging itself every morning. tools is an allowlist over the whole tool registry — omit it and the run gets every tool. widgets narrows the catalog the agent may draw from; omit it and it gets all of it.

Widgets are data, not code. Each entry is <home>/widgets/<type>.md:

---
name: metric
renderer: metric
label: Metric tile
span: 3
---
A single headline number, optionally with a change indicator and a sparkline.
Use for "how many" / "how much" answers you want readable at a glance.
data:
{ "value": 14, "unit": "runs", "delta": 3, "deltaLabel": "vs yesterday",
"trend": [9, 11, 8, 14], "tone": "ok" }

The body is the text the agent reads — editing it changes how every dashboard is built, which is where “always use a line chart for time series” is really configured. Twelve types are seeded on first boot: metric, line, bar, area, donut, table, list, links, markdown, timeline, status and mermaid. Settings → Widgets edits them.

renderer must name one of the renderers that exist in code — the same twelve names. That is the one hard constraint: a type naming a renderer that doesn’t exist is skipped with a console error rather than letting an agent publish into a void. You can add as many named types as you like (incidents, standup) as long as each binds to a real renderer, and its data will be validated against that renderer’s schema.

pinned requires exactly the declared slots and rejects extras — which can leave a hole. dynamic lets the agent choose everything. hybrid forces every declared slot but tolerates an extra widget, and it is what authored definitions are given, because a briefing that also noticed something is better than one that dropped it. (An authored definition with no slots is called dynamic outright, since “hybrid with zero slots” is just dynamic with a misleading name. pinned stays available explicitly in the editor.)

publish_dashboard validates every widget before it lands, and returns the failures to the model as the tool result — so a wrong shape gets fixed on the next turn instead of silently vanishing. A widget is rejected when:

  • it doesn’t parse as a widget at all (no id, no type, no data);
  • the dashboard is pinned and the id isn’t a declared slot;
  • it fills a declared slot with the wrong type;
  • its type isn’t in the catalog;
  • its data doesn’t match the renderer’s schema.

Everything that survives is stamped with its resolved renderer, the slot’s title and span when it didn’t supply its own, and a timestamp. Stamping the renderer onto the stored document is what lets an old board still paint correctly after you edit or delete a catalog entry.

Two escape hatches keep one failure from taking the board down: a widget may be published with status: "error" and an error string (its data isn’t schema-checked, because a placeholder has none), and widgets merge by id across calls, so an agent is told to publish each section the moment its data is in hand rather than gathering everything first.

During a dashboard run, every tool result is archived in full before it is truncated for the model, and the model sees a [source: tN] marker appended to it. The prompt tells it to carry those refs onto the widget they fed:

{
"id": "overnight",
"type": "metric",
"title": "Runs overnight",
"summary": "14 runs, 3 failed — all three in build-pipeline.",
"data": { "value": 14, "unit": "runs", "tone": "warn" },
"sources": [{ "ref": "t3", "label": "latchai_activity · last 12h", "tool": "latchai_activity" }]
}

Those refs render as a click-through footer, and dashboard_source fetches the full payload on demand — including from chat, which is how “where did that 14 come from?” gets a real answer rather than a re-derivation.

Every widget also carries a one-to-three-line summary saying what it shows. That is what a follow-up conversation reads, so it is written to carry the finding rather than describe the chart.

A dashboard build drives the same tool loop everything else does, so it appears in the Run Monitor, the run log, and the token report for free — under the run id dash-<id>-<timestamp> and the workflow id dashboard:<id>. Widgets land via dashboard.published events as the agent produces them, so an open board fills in live.

The publish tool is force-added to every dashboard run’s allowlist — an agent that gathers everything and then has no way to show it is a failure mode worth designing out.

Builds start from an empty board, so a run must republish everything it wants shown. A run that publishes nothing at all fails rather than blanking the dashboard, which is what stops a broken brief from quietly erasing this morning’s briefing.

refresh: arms a cron schedule alongside your workflow triggers, re-armed whenever the dashboards/ directory changes. in the sidebar builds on demand.

Only one build of a dashboard runs at a time: a scheduled refresh that comes due while the previous build is still going is skipped and logged, and a manual POST /api/dashboards/<id>/run returns 409 rather than starting a second one. The UI treats that 409 as “already building” and just keeps showing the spinner.