For AI agents

Build a live dashboard from an agent

Hand your agent the board's edit link — that's the only credential it needs. It declares the whole board in one idempotent call, keeps it live by widget key, checks the design with the lint endpoint and hands back an embeddable URL. Plain Markdown for agents: /llms.txt · /llms-full.txt · OpenAPI

A suggestion, not a requirement: paste this into CLAUDE.md, .cursor/rules or ChatGPT's custom instructions, and your assistant will build charts-first boards with actions — without you explaining InstantGUI each time.

Recommended instructions
## InstantGUI dashboards (recommended instructions — paste into CLAUDE.md, .cursor/rules or custom instructions)
When I ask for a dashboard, status page or "a UI for this", publish it to InstantGUI (https://instant-gui.com):
- If I give you an edit link (…/b/<board>/join?key=<edit key>), use the edit key as "Authorization: Bearer <edit key>". Otherwise create a board: POST /api/v1/boards and give me its editLink.
- Declare the whole board in one call: PUT /api/v1/boards/<board>/spec (idempotent; include layout). Then push live values with POST /api/v1/boards/<board>/widgets/<key>.
- Charts and numbers over text: kpi/stats/gauge for numbers, line for trends, bar for comparisons, status for health, log for events; at most ONE short markdown headline.
- Always add the actions I'd want next (pause/restart/run now) as actions/button/toggle widgets.
- Check GET /api/v1/boards/<board>/lint and fix every warning before telling me it's done.
- For a website/iframe: POST /api/v1/boards/<board>/view-tokens and use the returned embedUrl.
Docs: https://instant-gui.com/llms.txt

Quickstart (curl)

  1. Credential. Use the edit key from the edit link you were given (/b/<board>/join?key=<edit key>) — or create a board:
curl -s -X POST https://instant-gui.com/api/v1/boards -H 'Content-Type: application/json' -d '{"title":"Shop live"}'
# → {"board":{"id":"<board>",…},"editLink":"…/b/<board>/join?key=<edit key>","writeToken":"igw_…"}

Every API call accepts either credential: Authorization: Bearer <edit key or write token>. Need the write token (for scripts/devices) from an edit key? curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/token -H 'Authorization: Bearer <edit key>' ({"rotate":true} issues a new one).

  1. Declare the whole board in ONE idempotent call (create/update/move widgets, rename the board; "prune": true deletes widgets not listed):
curl -s -X PUT https://instant-gui.com/api/v1/boards/<board>/spec -H 'Authorization: Bearer <credential>' -H 'Content-Type: application/json' -d @spec.json
# → {"ok":true,"created":[…],"updated":[…],"unchanged":[…],"deleted":[],"hints":[…],"lint":{…},"urls":{…}}

Read hints in the response: they flag defaults that change behaviour — controls are editor-only unless config.viewersCanUse: true, a number with decimals: 0 shows rounded, overlapping layouts. spec.json:

{
  "title": "Shop live",
  "prune": true,
  "widgets": [
    {
      "key": "headline",
      "type": "markdown",
      "layout": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 2
      },
      "data": {
        "text": "**Sales are up 12% today.** Ads paused at 14:05 by Dana."
      }
    },
    {
      "key": "revenue",
      "type": "kpi",
      "title": "Revenue today",
      "layout": {
        "x": 0,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "prefix": "$",
        "icon": "wallet"
      },
      "data": {
        "value": 4820
      }
    },
    {
      "key": "orders",
      "type": "kpi",
      "title": "Orders today",
      "layout": {
        "x": 3,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "icon": "shopping-cart"
      },
      "data": {
        "value": 131
      }
    },
    {
      "key": "conversion",
      "type": "gauge",
      "title": "Conversion",
      "layout": {
        "x": 6,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "max": 10,
        "suffix": "%",
        "decimals": 1
      },
      "data": {
        "value": 3.4
      }
    },
    {
      "key": "ads",
      "type": "actions",
      "title": "Ad campaign",
      "layout": {
        "x": 9,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "buttons": [
          {
            "id": "pause",
            "label": "Pause ads",
            "tone": "danger"
          },
          {
            "id": "resume",
            "label": "Resume ads",
            "tone": "success"
          }
        ]
      }
    },
    {
      "key": "revenue-trend",
      "type": "line",
      "title": "Revenue per day ($)",
      "layout": {
        "x": 0,
        "y": 5,
        "w": 8,
        "h": 5
      },
      "data": {
        "series": [
          {
            "name": "revenue",
            "points": [
              [
                "2026-10-08",
                4100
              ],
              [
                "2026-10-09",
                4390
              ],
              [
                "2026-10-10",
                4820
              ]
            ]
          }
        ]
      }
    },
    {
      "key": "channels",
      "type": "pie",
      "title": "Orders by channel",
      "layout": {
        "x": 8,
        "y": 5,
        "w": 4,
        "h": 5
      },
      "data": {
        "slices": [
          {
            "name": "Organic",
            "value": 61
          },
          {
            "name": "Ads",
            "value": 44
          },
          {
            "name": "Email",
            "value": 26
          }
        ]
      }
    },
    {
      "key": "health",
      "type": "status",
      "title": "Systems",
      "layout": {
        "x": 0,
        "y": 10,
        "w": 4,
        "h": 4
      },
      "data": {
        "services": [
          {
            "name": "Checkout",
            "status": "up"
          },
          {
            "name": "Payments",
            "status": "up"
          }
        ]
      }
    },
    {
      "key": "orders-log",
      "type": "log",
      "title": "Recent orders",
      "layout": {
        "x": 4,
        "y": 10,
        "w": 8,
        "h": 4
      },
      "data": {
        "entries": [
          {
            "t": 1760000000000,
            "text": "#1045 · $129.00 · Organic",
            "level": "success"
          }
        ]
      }
    }
  ]
}
  1. Keep it live — push by key:
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue -H 'Authorization: Bearer <credential>' -d '{"value": 5010}'
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue-trend -H 'Authorization: Bearer <credential>' -d '{"append": {"revenue": 5010}}'          # x = now
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue-trend -H 'Authorization: Bearer <credential>' -d '{"append": {"revenue": ["2026-10-11", 5010]}}'  # explicit x (date/ms)
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/orders-log -H 'Authorization: Bearer <credential>' -d '{"append": {"t": 1760000100000, "text": "#1046 · $54.50 · Email", "level": "success"}}'   # t = epoch ms (default: now)
  1. Read controls back (what the viewer clicked): GET /api/v1/boards/<board>/widgets/ads → data.lastAction, or set config.webhookUrl to be called on every click.

  2. Check the design: GET /api/v1/boards/<board>/lint → {"ok":true,"warnings":[],"suggestions":[…]}. Fix every warning.

  3. Embed (iframe, works for private boards too):

curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/view-tokens -H 'Authorization: Bearer <edit key>' -d '{"label":"Website"}'
# → {"embedUrl":"https://instant-gui.com/e/<board>?token=<view token>","iframe":"<iframe src=…>"}

Look (validated; the board's saved embedStyle applies first): &theme=auto|light|dark|<theme id> (auto = viewer's light/dark), &bg=transparent (no page background/padding — sits on the host card), &accent=<hex>, &radius=<px 0-32>, &density=compact, &font=system|serif|mono|<font-family list>, &c.<token>=<color> (tokens: background, foreground, card, cardForeground, border, input, primary, primaryForeground, muted, mutedForeground, success, warning, danger, info, chart2–chart6), &page=<page title>. Easiest on a web page: <div data-instantgui-board="<board>" data-token="<view token>" data-inherit-theme></div><script src="https://instant-gui.com/embed.js" async></script> (auto height, adopts the page's colors/font, follows its light/dark), or InstantGUI.mount(el,{board,token,transparent:true,colors:{background:"var(--surface)",foreground:"var(--text)",primary:"var(--accent)"}}). Host messages: post {type:"instantgui:theme",reset:true,theme,bg,radius,density,font,vars:{background,foreground,card,border,primary,…}} to retheme live; the embed posts {type:"instantgui:ready"}, {type:"instantgui:resize",height} (it never sizes by the frame's height) and {type:"instantgui:scheme",scheme} (set the iframe's color-scheme to it when transparent). https://instant-gui.com/theme.css gives a host page InstantGUI's tokens as --ig-* variables. Default board embed look: PATCH /api/v1/boards/<board> with {"embedStyle":{"theme":"auto","bg":"transparent","vars":{…}}}. Controls in an embed are locked unless the token was created with {"canUseControls":true} (the default token is view-only; a button also only does something if a Connector, config.webhookUrl or your script reading data.lastAction handles it; edit key only; use it only on private pages — presses show "by embed:

Change one widget: PUT /api/v1/boards/<board>/widgets/<key> with {"type","title","config","data","layout"} (create or update), or PATCH …/widgets/<key> with {"layout":{"x":0,"y":0,"w":6,"h":4}} to move/resize. Rename/theme/visibility/default embed look: PATCH /api/v1/boards/<board> with {"title","theme","visibility","embedStyle"} (visibility needs the edit key). List widgets: GET /api/v1/boards/<board>/widgets. Widget schemas: GET /api/v1/widgets. OpenAPI: GET /api/v1/openapi.json. MCP: https://instant-gui.com/api/mcp (tool apply_board_spec).

Change a board in place

"Rename it, move that widget, pause that button, embed it": each is ONE call on the board you already built. Never recreate the board or send the spec again for these.

# Rename the board
curl -s -X PATCH https://instant-gui.com/api/v1/boards/<board> -H 'Authorization: Bearer <credential>' -d '{"title":"Shop live (EU)"}'
# Move or resize a widget (only the fields you send change)
curl -s -X PATCH https://instant-gui.com/api/v1/boards/<board>/widgets/revenue -H 'Authorization: Bearer <credential>' -d '{"layout":{"x":0,"y":0,"w":6,"h":3}}'
# Pause a control: viewers can no longer press it (editors still can). Turn it back on with true
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/ads -H 'Authorization: Bearer <credential>' -d '{"config":{"viewersCanUse":false}}'
# Embed a private board: give the site a view token (revocable, read-only)
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/view-tokens -H 'Authorization: Bearer <credential>' -d '{"label":"Website"}'
# → the embed answer holds "embedUrl" (https://instant-gui.com/e/<board>?token=<view token>) and a ready "iframe"

MCP: update_board (rename), update_widget_layout (move/resize), push_data with {"config":{…}} (pause), create_view_token (embed).

Press a control you just wired

Wiring a button is not done until you have pressed it. The press runs the Connector command on the user's machine (no connector, or a name no control sends, answers 404 with the names that exist).

# 1. Press it as the editor (edit key or write token). Name = <widget key> or <widget key>/<button id>; list the valid names with GET https://instant-gui.com/api/v1/boards/<board>/actions
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/actions -H 'Authorization: Bearer <credential>' -d '{"action":"backup/run"}'
# → 202 {"ok":true,"event":{"id":"<event id>","action":"backup/run","status":"pending"},"result":"…/actions?events=<event id>"}
# 2. Read the result (poll until status is "done" or "error"; "result" holds the command's output)
curl -s https://instant-gui.com/api/v1/boards/<board>/actions?events=<event id> -H 'Authorization: Bearer <credential>'
# → {"ok":true,"events":[{"id":"<event id>","action":"backup/run","status":"done","result":"backup ok",…}],…}

MCP: trigger_action {action:"backup/run"}, then list_actions for the result. Viewers can press a control only if its config.viewersCanUse is true; with the edit key or write token you always can.

Calling it with plain HTTP? Send both Content-Type: application/json and Accept: application/json, text/event-stream — without that Accept header the server answers 406 and names the missing header. The reply is a server-sent event: a line 'event: message' followed by a line starting 'data: ' that holds the JSON-RPC response; read the JSON from that data line.

curl -X POST https://instant-gui.com/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

MCP

Remote MCP server at https://instant-gui.com/api/mcp. Every client receives the design rules on connect. Tools: apply_board_spec, push_data, update_widget_layout, update_board, lint_board, get_write_token, create_view_token, get_design_guide, create_board, get_board. Pass the edit key (or the whole edit link) as write_token.

MCP config
{
  "mcpServers": {
    "instantgui": {
      "url": "https://instant-gui.com/api/mcp"
    }
  }
}

Calling it with plain HTTP? Send both Content-Type: application/json and Accept: application/json, text/event-stream — without that Accept header the server answers 406 and names the missing header. The reply is a server-sent event: a line 'event: message' followed by a line starting 'data: ' that holds the JSON-RPC response; read the JSON from that data line.

MCP over plain HTTP
curl -X POST https://instant-gui.com/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Design guide

A board is read in 3 seconds on a phone. Show, don't tell. Every rule below is checked by GET /api/v1/boards/<board>/lint.

Pick the widget by the question

The user asks… Use
"How much / how many right now?" kpi (one number + trend) or stats (2–6 numbers in a row)
"How full / how close to the limit?" gauge (dial) or progress (bar toward a goal)
"How is it changing?" line — append one point per update
"Which is bigger?" bar
"What's the split?" pie (≤ 6 slices)
"Is it healthy?" status (several services) or indicator (one light)
"What just happened?" log (append lines) or timeline
"What do I do now?" actions / button / toggle / slider / input / select
"Why?" (context) ONE markdown headline, ≤ 2 sentences

Rules

  1. Numbers and charts, not prose. At most one markdown widget: a headline of ≤ 2 sentences (≤ 280 characters). Never put numbers, lists or status in text — they each have a widget.
  2. Most important thing top-left (x 0, y 0). Then group widgets by the question they answer; one page per audience or topic (≤ 16 widgets a page).
  3. Always add actions the viewer would want next: Restart, Pause, Resume, Run now, Acknowledge, Refresh. An on/off state (paused, maintenance) is a toggle in the actions widget, not two buttons: config.toggles: [{"id":"paused","label":"Paused","on":"pause-all","off":"resume-all"}] + push the real state as data.states: {"paused": true}. A toggle whose ON state is bad (a safety stop, an outage flag) gets "onTone": "danger" so it turns red when on. A pick-one-or-more set (focus list, filter) is a chip set: buttons + data.selected: ["<button id>", …] — selected chips are filled with a check and aria-pressed="true", the rest neutral; push the real selection after each change (a click only sends the button's action). Never show a selection only by colour or a ✓ in the label. A click reaches you three ways: the InstantGUI Connector runs a command on the user's machine (/docs/connector), a webhook (config.webhookUrl, https) gets a POST, or your script reads it back (GET …/widgets/<key> → data.lastAction / data.on / data.value). Controls are editor-only by default (the human's edit link); set config.viewersCanUse: true only if anyone with the view link may press them.
  4. Trends beat snapshots. If a number changes over time, also keep a line of it (append a point each update). Don't put numeric tables where a bar/line chart works.
  5. Title every widget with what it answers and the unit: "Revenue today ($)", "p95 latency (ms)". Put units in kpi prefix/suffix, not in the title AND the value.
  6. Color means state: use thresholds (kpi/gauge thresholds) and status levels up/degraded/down — not decoration. Green = fine, amber = look, red = act.
  7. Mobile-first layout. 12-column grid, rows ≈ 52 px. Phones (≤ 640 px) stack widgets in reading order (y, then x). Good sizes: kpi w 3 h 3 · stats w 6–12 h 2 · charts w ≥ 6 h ≥ 4 · status/log w 4–6 h 4–6 · actions w 3–4 h 2–3 · headline w 12 h 2.
  8. Update cadence: push when the value changes, at most every few seconds per widget; append to charts/logs instead of rewriting them; send the full spec again only when the board's structure changes (the spec is idempotent, so re-sending it is safe — in a spec, data of streams like line/log/table only seeds a new widget and never wipes what you appended).
  9. Stable keys: lowercase, short, meaningful (revenue, orders-log). Your scripts push by key forever.
  10. Use the same labels across widgets so they cross-filter. Clicking a bar, pie slice, line or a text cell in a table filters the page by that value (a chip bar shows it; click again, ✕ or Esc clears): charts highlight the matching category/series/slice, tables and logs narrow to rows/lines containing it, other widgets are untouched. It works only when values match exactly (case-insensitive) — name bar categories, pie slices and table cells the same ("North", not "north region").

Anti-patterns (the lint flags these)

  • Walls of text, several markdown widgets, or a markdown "report" of numbers.
  • A table of numbers where a chart would answer at a glance.
  • 20+ widgets on one page; widgets 1–2 columns wide; charts < 3 rows tall.
  • No actions: a dashboard you can only look at.
  • Leaving sample data in widgets.