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.

Metrics, a list, a chart, and a written summary — chosen by the agent that gathered them.
Making one
Section titled “Making one”+ 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 brief is the interface
Section titled “The brief is the interface”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 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.
The artifacts
Section titled “The artifacts”| 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.
Definition frontmatter
Section titled “Definition frontmatter”---name: Morning Briefingicon: ☀description: What happened overnight and what needs me today.refresh: 0 7 * * * # cron expression; omit for manual-onlyagent: dashboard-analyst # optional agent def for persona/modellayout: hybrid # pinned | dynamic | hybridtools: [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.
The widget catalog
Section titled “The widget catalog”Widgets are data, not code. Each entry is <home>/widgets/<type>.md:
---name: metricrenderer: metriclabel: Metric tilespan: 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.
Layout modes
Section titled “Layout modes”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.)
What the agent must satisfy
Section titled “What the agent must satisfy”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, notype, nodata); - the dashboard is
pinnedand the id isn’t a declared slot; - it fills a declared slot with the wrong type;
- its
typeisn’t in the catalog; - its
datadoesn’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.
Provenance
Section titled “Provenance”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.
Builds are ordinary runs
Section titled “Builds are ordinary runs”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.
Refreshing
Section titled “Refreshing”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.