Authoring
Authoring briefs
A brief is a single self-contained HTML page you push to a human's inbox. It can carry clickable choices and a notes box; their answers flow straight back to you as events. This is how an agent asks a question and gets a decision.
Anatomy of a brief
A brief is just HTML: inline CSS and JS, one file (or a small bundle). Include the Brief SDK and the page can record choices and notes that come straight back to the producing session:
<script src="/lib/brief.js"></script>
Here's a complete, pushable brief (three choices plus a note box, zero hand-written wiring for the buttons):
hello.html
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Q3 ad directions</title>
<style>
body{ font:16px/1.6 system-ui; max-width:620px; margin:40px auto; padding:0 20px; }
.opt{ display:block; width:100%; text-align:left; padding:14px 16px; margin:8px 0;
border:1px solid #ddd; border-radius:10px; background:#fff; cursor:pointer; }
.opt.is-selected{ border-color:#6B6BD8; box-shadow:0 0 0 2px rgba(107,107,216,.2); }
textarea{ width:100%; min-height:80px; }
</style></head>
<body>
<h1>Pick a direction for Q3</h1>
<!-- zero-JS choices: a click records a selection + toggles .is-selected -->
<button class="opt" data-brief-select="direction" data-brief-value="bold">A — Bold rebrand</button>
<button class="opt" data-brief-select="direction" data-brief-value="iterate">B — Iterate on current</button>
<button class="opt" data-brief-select="direction" data-brief-value="pause">C — Pause and research</button>
<h3>Notes</h3>
<textarea id="n" placeholder="anything else…"></textarea>
<button onclick="Brief.note(n.value)">Send note</button>
<script src="/lib/brief.js"></script>
</body></html>
Push it with the CLI (see the mb CLI) and hand over the deeplink:
mb brief push ./hello.html --title "Q3 ad directions — pick one" --kind options
# → ✓ br_8f3k… v1 → https://meltbox.ai/#/brief/br_8f3k…
Zero-JS choices
Any element with data-brief-select="FIELD" and data-brief-value="V" becomes a clickable choice. On click, brief.js records a selection for that field, clears .is-selected from the others in the group, and marks the clicked one, so you can style the selected state however you like. The latest selection per field wins.
<button data-brief-select="plan" data-brief-value="a">Plan A</button>
<button data-brief-select="plan" data-brief-value="b">Plan B</button>
The Brief SDK
For anything beyond a button, brief.js exposes window.Brief:
| Call | Records |
|---|---|
Brief.id | The current brief id (read from the /briefs/<id>/ path). |
Brief.select(field, value, {label}) | A selection: same as a data-attribute button, but programmatic. |
Brief.note(text) | A free-form note. |
Brief.signal(name, data?) | An arbitrary structured signal datapoint. |
Brief.emit(kind, data) | Escape hatch: emit any event kind directly. |
document.getElementById("send").onclick = () => {
Brief.select("direction", "bold", { label: "Bold rebrand" });
Brief.note(document.getElementById("n").value);
};
CSP & authoring constraints
Briefs are served same-origin under a strict Content-Security-Policy:
default-src 'self'; script-src 'self' 'unsafe-inline';
img-src 'self' data: blob:; connect-src 'self'
So, concretely:
- ✓ Inline
<script>/<style>and/lib/brief.jswork. - ✓ Network calls only to same-origin (which is exactly what
brief.jsuses to post events). - ✓ Images inline as
data:URIs, or shipped in the bundle and referenced by relative path. - ✗ No external images, fonts, scripts, or CDNs. Everything is in the file or same-origin.
Visual register
Briefs default to a light (cream) register. They read like a clean document, independent of the app's chrome (brief.js sets data-theme="light" unless you set your own). Two easy paths to the brand look, or roll your own:
<link rel="stylesheet" href="/lib/cc.css"> <!-- inherit Meltbox tokens: the indigo accent over warm-cream neutrals -->
Depart from the default as much as a different treatment helps: a dark data viz, a bold split layout. The light default is a starting point, not a constraint.
How feedback comes back
Every selection, note, and signal is appended to the brief's event stream, keyed by a monotonic cursor. The producing agent watches it live, or pulls it later. Keep the monitor running so you don't miss the reply:
mb brief watch br_8f3k --since 0
# watching brief br_8f3k (every 3s, Ctrl-C to stop)…
# #1 2026-06-30T18:02Z selection {"field":"direction","value":"bold"}
# #2 2026-06-30T18:02Z note {"text":"go bold, but keep the wordmark"}
# #3 2026-06-30T18:03Z status {"status":"archived"} ← reviewed & decided
A status: archived event is the human signalling they've reviewed the brief and made their decisions. It's a clean exit for the watch loop, not a stop signal. The agent acts on the selections and notes, keeps working, and ships the next round as a new brief: the archive is the human's history of what they reviewed, so agents don't edit an already-reviewed brief in place. The raw form is GET /api/briefs/:id/events?since=<cursor> → {events, cursor}; poll with the returned cursor. GET /api/briefs/:id also returns selections already reduced to the latest value per field.