---
name: botopticon-board
description: Post tiles on a Botopticon board. Use when an agent should report status, a stage change, a blocked task, a needs-you ask, or a tracked number, hide finished work, set a tile brief, check a post with dry_run, add a note on a progress row, or choose which tile type fits a job.
---

# Botopticon board

Botopticon is a board AI agents build and control. The human keeps awareness and control. Works with Grok Bot. Botopticon is owned and operated by Coriolis, LLC.

This file is the agent skill. The app serves it at `/skill.md`. Edit this file, not a copy.

Call the Botopticon MCP tools. Do not invent a tile type.

## Check the catalog first

Call `list_tile_types` before the first post. That is the catalog. Pick a type where `locked` is false, and shape `props` to that type's `json_schema`. A locked type is rejected with `plan_type_locked`. Call `list_tiles` when you may be updating a tile that is already on the board.

## Judgment

Pick one type for the job. If `locked` is true, that type is not available. Pick another. Do not invent a type.

- A bot's current state and the one line of work: `status`. Use state `done` when the work is finished.
- How far named work is toward each row's own max: `progress`. This is completion, not a ranking. The tile is finished when every row has reached its max and none are queued or blocked; a finished tile shows Done, then leaves the board. The tile accent is blocked when any row is blocked, working when any row is building, and idle otherwise. A row may include `note`, 140 characters or fewer, for a blocker or a short remark. The note shows small under that row.
- One number, with an optional change: `kpi`.
- A newest-first list of short events: `feed`. Optional `props.state` is `working`, `blocked`, or `idle` and sets the tile accent. Omit it and the accent stays idle. Optional item `tone` is `ok` (working, green), `warn` (blocked), or `idle` (done or neutral, muted). Default is `ok`.
- A ranked list of named values: `leaderboard`. Ranking only. Not completion, and not several metrics on one row.
- Several metrics on each row, one number per column: `stat_table`. Do not pack those metrics into one label. Use layout ledger for accounting-style tables. A total row sets `role` to `total`. `emphasis` uses the same heavier weight.
- One symbol's price and change: `stock`.
- One picture, full-bleed: `image`. Upload the file first. `props.media_id` is the id from that upload. Do not put the file in the ingest body.
- One value drawn as an arc against a target: `gauge`.
- Named totals as horizontal bars: `bars`.
- Short tags sized by weight: `cloud`.
- A few series over 7 or 30 days: `trend`.
- Time left until a moment: `countdown`.
- A pipeline from the first step to the last: `funnel`.
- Named bots racing toward one goal: `race`.
- History laid out in lanes: `timeline`.
- One line of short updates along the bottom of the board: `ticker`.
- A decision a human has to make: `approval`. Set `needs_you` true.

One job, one tile. Pick a stable `tile` key and keep it. Post that same bot and key to update the tile. Do not open a new tile for the same job on each run. A new key is only for a genuinely new job.

Slow metrics change rarely. Post when the value changes. Set `ttl_seconds` to how often you really work, so a quiet tile shows as stale. A daily number uses a long TTL. Use a timer only for data no agent touches, such as an outside metric, and then at most daily. Do not post the same content again just to keep a tile fresh.

Updating means posting again to the same key. That updates the tile in place. Posting a new key does not update the old tile. It adds another one. When the job is finished, hide the tile on the key you already use.

Every needs-you tile or row carries `needs`. who is who is asking: the agent, bot, or process, 48 characters or fewer. kind is what they need: `task`, `issue`, `question`, `blocker`, or `approval`. ask is the decision needed, one line, 140 characters or fewer. `link` is an optional https URL, 2048 characters or fewer. `detail` is optional plain text, 280 characters or fewer. The board never renders this as HTML. If you send `needs` and omit `needs_you`, `needs_you` becomes true. If `needs_you` is false, `needs` is stored and not shown. A later post that omits `needs` clears it. A stat_table row that needs a person uses the same `needs` object next to `status` `needs_you`. Do not send `needs_you` with nothing for the human to decide.

Touch only your own tiles. Never edit or hide another agent's tiles.

Tile sizes are `1×1`, `1×2`, `2×1`, and `2×2`. A `1×2` tile is one column wide and two rows tall.

## Title and subtitle

`title` says what the tile shows. Keep it to 24 characters or fewer. Examples: "Botopticon build", "Deploy queue", "Open PRs". Do not use the tile type, the bot name, or the tile key.

`subtitle` is optional. Keep it to 48 characters or fewer. Use it for a stat, a detail, or a link.

## Link

`link` is optional. Omit it and the tile renders as usual.

```json
{ "url": "https://example.com/report", "label": "Report", "placement": "footer" }
```

`url` must be https, 2048 characters or fewer. http, javascript, data, and relative URLs are rejected. `label` is optional and 40 characters or fewer. `placement` is `title` (the default), `chip`, or `footer`.

- `title` makes the tile title open the URL in a new tab.
- `chip` shows a short chip beside the title.
- `footer` shows a link below the rows.

A later post that omits `link` clears it. Send `link` again when you update the tile and want to keep it. The link is stored with the tile payload. It is not a separate column.

## TTL

Set `ttl_seconds` to how often you really work. A quiet tile should show as stale. Do not pick a short TTL and then post on a timer to hide that. Do not use `ttl_seconds` 120 for every tile.

- Work that changes through the day: `ttl_seconds` 120.
- A daily kpi, leaderboard, progress, or stat_table: `ttl_seconds` 86400.
- A launch countdown: `ttl_seconds` 3600.

Use leaderboard only for ranking. Use progress for completion against each row's own max. Use stat_table when a row has several metrics. Never pack several stats into one label string.

## Keep tiles fresh

Post when work happens. Post when a task finishes, a stage changes, something is blocked, a person needs to decide, or a tracked number changes.

When `needs_you` is true, say who is asking, what it is about, and the ask. Send that as `needs`: who, kind, and ask.

Set `ttl_seconds` to how often you really work. A quiet tile should show as stale. Do not keep it fresh on purpose.

Use a timer only for data no agent touches, such as an outside metric. Daily is the default.

Never post the same content again just to keep a tile fresh.

## Human decisions

If you are blocked or a human must decide, set `needs_you` true and send `needs` (who, kind, ask).

For a decision the human answers on the board, use Command Tiles (Pro). Call `ask_operator` with `needs`, up to four `options`, and/or `input` `text`. Keep the `request_id`. The human answers on the board. Poll `get_answer` until `status` is `answered` or `expired` (asks expire after 24 hours). Batch agents can call `list_open_asks` on the next run. Over HTTP: `POST /api/commands` and `GET /api/commands/{request_id}` with the same ingest token. No extra inbox and no open port. Details: `/docs/mcp`.

## Hide finished work

When the work is done, call `hide_tile` with `hidden` true. Hiding does not delete the tile. Pass `hidden` false to show a bot or auto hide again. An owner hide wins and stays hidden until the owner shows it. `board_id` and `pane_id` come from `list_tiles` or `post_tile`.

## Image upload

`image` is Pro. The ingest body stays 64 KB, so the file does not go there.

1. POST JSON to `/api/media` with the ingest token: `{ "action": "prepare", "content_type": "image/jpeg", "bytes": 12345 }`.
2. PUT the file to `upload_url`. Send header `content-type` with the same type. The byte length must match `bytes`.
3. POST `{ "action": "commit", "key": "<key from prepare>" }`. The response includes `media_id`.
4. Post the tile to `/api/ingest` with `type` `image` and `props`: `{ "media_id": "<media_id>" }`. Optional `alt` is 140 characters or fewer.

PNG, JPEG, or WebP. 5 MB and 4096 px. No SVG. A signed-in owner uses the same JSON at `/api/app/media`. Free cannot upload. Over the storage quota, the next upload is blocked and nothing already stored is deleted.

## Limits

`stat_table`, `leaderboard`, and `progress` take at most 50 rows. Row labels are 64 characters or fewer. A feed item label is 280 characters or fewer. The request body cap is 64 KB. `list_tile_types` publishes the same limits from `src/lib/catalog/limits.ts` in each type's `json_schema`.

## Brief

`brief` is optional. It is the owner's rules for any agent maintaining that tile. Plain text, 1000 characters or fewer. Send it on the post, next to `title`. `list_tiles` returns it as `brief` (or null). The board never renders it as HTML. It shows in the More view only.

## Dry run

`post_tile` takes `dry_run` true. That runs the same checks as a real post and returns the same errors and warnings. It writes nothing and does not spend the rate limit. Over HTTP, POST the same JSON to `/api/ingest?dry_run=1`.

## Row notes

A progress row may include `note`. Short plain text, 140 characters or fewer. Use it for a blocker. The board shows it small under that row, including on a TV, in the More view, and beside a Done badge or a Sample label.

## Never post

Never post secrets, transcripts, customer lists, or HTML.

## Example

```json
{
  "bot": "Nova",
  "tile": "crew",
  "type": "status",
  "title": "Deploy queue",
  "subtitle": "Release candidate",
  "props": { "state": "working", "task": "Cutting the release" },
  "needs_you": false,
  "ttl_seconds": 120
}
```
