Docs
How it all works.
Set up the board, publish your feed, and know exactly what's stored and who can read it.
Quick start
- Look around first. The demo is the whole board with labelled example data, and needs no signup.
- Start your trial. Pick a plan on the pricing page. We'll email your setup link within 24 hours.
- Write a status file. Your bots don't talk to the board. They report to a small hub script, and the hub publishes one JSON file to your own GitHub account, only when something changes. This is all the board needs to show two bots:
{
"generated": "2026-11-02T09:00:00+11:00",
"desks": [
{ "id": "writer", "state": "DONE", "last_ts": "2026-11-02T08:40:00+11:00" },
{ "id": "publisher", "state": "LIVE", "last_ts": "2026-11-02T08:55:00+11:00" }
]
}- Point the board at it. On the board, open the More menu, choose Load a status file, and paste the file's address.
- Add what you have. Every section below is optional. Anything you leave out shows as "Not reported", never as a zero.
- Lock it. A file in a public repo can be read by anyone. When your board is working, publish version 2, below, so only you can read it.
See what works with what on the integrations page.
Feed format
Version 2 of the feed locks everything except the date and four numbers. The full definition is at /schema/feed-v2.json. The board still reads version 1, which is the same contents with no envelopes, so you can move over whenever you're ready.
What anyone can read
{
"v": 2,
"generated": "2026-11-02T09:00:00+11:00",
"public": { "desks_working": 1, "desks_total": 3, "brands": 1, "posts_7d": 1 },
"feed_enc": { "v": 1, "salt": "…", "iv": "…", "data": "…" },
"feed_sealed": { "v": 1, "alg": "RSA-OAEP-256+A256GCM", "kid": "0123456789abcdef", "ek": "…", "iv": "…", "data": "…" }
}| Field | Type | What it is |
|---|---|---|
v needed | 2 | The feed version. |
generated needed | text | When the hub made this file. |
public needed | object | The only numbers anyone can read without a key. |
feed_enc needed | text | The payload, locked with your passphrase. The board's "Locked: enter passphrase" box opens this in the browser. |
feed_sealed needed | text | The payload, locked to the server's public key at /feed-seal-key. The server opens this for signed-in people. |
public
| Field | Type | What it is |
|---|---|---|
desks_working needed | whole number | Bots that reported in the last 2 hours and aren't blocked. |
desks_total needed | whole number | Bots on the team, leaving out retired ones. |
brands needed | whole number | How many brands the team runs. |
posts_7d needed | whole number | Posts in the last 7 days. |
The two envelopes
Both hold the same JSON. You need both, so the board always has a way in.
feed_sealed
A fresh AES-256-GCM key, wrapped with RSA-OAEP-SHA256. Every value is base64 except alg and kid.
| Field | Type | What it is |
|---|---|---|
v needed | 1 | The envelope's version. |
alg needed | RSA-OAEP-256+A256GCM | How it's locked. |
kid needed | text | The first 16 hex characters of the SHA-256 of the public key. |
ek needed | text | The AES key, wrapped. |
iv needed | text | 12 random bytes. |
data needed | text | The JSON, encrypted, with its tag at the end. |
feed_enc
PBKDF2-SHA256 with 310,000 rounds gives an AES-256-GCM key. Every value is base64.
| Field | Type | What it is |
|---|---|---|
v needed | 1 | The envelope's version. |
salt needed | text | 16 random bytes. |
iv needed | text | 12 random bytes. |
data needed | text | The JSON, encrypted, with its tag at the end. |
Sections that were already locked on their own, the inbox and the trading book, stay outside the envelopes.
What's inside
What's inside both envelopes. A version 1 feed is this same object, published without the envelopes. Every section is optional, and anything you leave out shows as "Not reported", never as zero.
| Field | Type | What it is |
|---|---|---|
generated | text | When the hub made this file. |
schema_version | whole number | Your own version marker. The board warns if it's newer than it understands. |
desks | list | Every bot on the team. |
meta | object | Names for what's on the board. |
posts | list | What went live. |
schedule | list | What's coming up. |
handoffs | list | Work moving between bots. |
approvals | list | What's waiting for a person. |
blockers | list | What's stuck, in short. |
blocked | list | What's stuck, with how long and why. |
brands | list | One row per brand per platform. |
counts | object | Totals, if your hub keeps them. |
usage | object | The usage meter. |
runs | list | Run history. Keep about the last 300. |
videos | list | Every video as a card on the Studio board. Without it, the board shows what approvals, schedule and posts report. |
routines | list | The calendar. |
credits | list | The older form of usage: a weekly figure per bot. |
sponsors | list | |
shop | list | |
revenue | list | |
coverage | list |
desks
Every bot on the team.
| Field | Type | What it is |
|---|---|---|
id needed | text | The bot's id, such as publisher. |
state | LIVE, DONE, BLOCKED, HOLD | What it's doing. |
last_ts | text | When it last reported. |
has_input | boolean | True if work is waiting for it. |
paused | budget | Why the desk is paused, when it wasn't a person: budget means auto-pause stopped it at 100% of its budget. Send state HOLD with it, and the board shows Paused: budget. |
meta
Names for what's on the board.
| Field | Type | What it is |
|---|---|---|
brands | list | The brands the team runs. |
desks | list | A display name for each bot. |
site | object | Your board's name, logo and accent colour. |
posts
What went live.
| Field | Type | What it is |
|---|---|---|
brand needed | text | |
platform | text | |
url | text | A public link to the post. |
ts | text | A date and time, ISO 8601 with its timezone. |
views | number | Views, if measured. |
thumb | text | A thumbnail address. |
title | text | |
desk | text | The bot that posted it. |
views_ts | text | A date and time, ISO 8601 with its timezone. |
views_src | text | Where the view count came from. |
avg_view_pct | number | Average percent of the video watched. |
made_for_kids | boolean | |
ai_label | boolean | True if it carries an AI label. |
original | boolean | True if it's original work. |
schedule
What's coming up.
| Field | Type | What it is |
|---|---|---|
brand needed | text | |
platform | text | |
title | text | |
url | text | |
when | text | A date and time, ISO 8601 with its timezone. |
state | scheduled, posted, live, missed, queued, hold |
handoffs
Work moving between bots.
| Field | Type | What it is |
|---|---|---|
from | text | The bot passing work on. |
to | text | The bot receiving it. |
item | text | What's being passed. |
state | text | |
brand | text | |
ts | text | A date and time, ISO 8601 with its timezone. |
count | whole number | How many, if it's a count. |
approvals
What's waiting for a person.
| Field | Type | What it is |
|---|---|---|
desk | text | |
item | text | What needs a yes. |
brand | text | |
ts | text | A date and time, ISO 8601 with its timezone. |
count | whole number | |
made_for_kids | boolean | |
ai_label | boolean | |
original | boolean |
blockers
What's stuck, in short.
| Field | Type | What it is |
|---|---|---|
desk | text | |
brand | text | |
platform | text | |
reason | account check, verification, stock, review | |
who | text | Who can clear it. |
needs | text | What it needs, in plain words. |
ts | text | A date and time, ISO 8601 with its timezone. |
count | whole number |
blocked
What's stuck, with how long and why.
| Field | Type | What it is |
|---|---|---|
desk needed | text | |
brand needed | text | |
platform needed | text | |
state needed | BLOCKED, HOLD | |
reason | text | Why, in plain words. |
waiting_on | text | owner, external, or desk:<id>. |
waiting_label | text | Who it's waiting on, for external. |
blocked_since needed | text | When it got stuck. Rows without this are dropped. |
ask_ref | text | The inbox ask this is waiting on. |
brands
One row per brand per platform.
| Field | Type | What it is |
|---|---|---|
name needed | text | The brand, such as Shift Fuel (YouTube). |
platform | text | |
views | number | |
likes | number | |
subs | number | Subscribers or followers. |
history | list | Recent values, for the small chart. |
stats_ts | text | A date and time, ISO 8601 with its timezone. |
views_src | text | |
data_through | text | |
retention | text | |
period | text | |
subs_change_7d | number | |
views_change_7d | number |
counts
Totals, if your hub keeps them.
| Field | Type | What it is |
|---|---|---|
desks_by_state | object | |
queue | whole number | |
holds | whole number | |
asks_open | whole number |
usage
The usage meter.
| Field | Type | What it is |
|---|---|---|
unit | text | Such as credits. |
plan_limit | number | Your plan's limit. Without it there's no percentage or warning. |
period_used | number | |
period_ends | text | A date and time, ISO 8601 with its timezone. |
updated_at | text | A date and time, ISO 8601 with its timezone. |
desks | list | One row per bot. |
runs
Run history. Keep about the last 300.
| Field | Type | What it is |
|---|---|---|
id | text | |
desk needed | text | |
ts | text | A date and time, ISO 8601 with its timezone. |
title | text | In plain words: no job ids, paths, links or emails. |
ok | boolean | |
error | text | |
duration_s | number | |
brand | text | The brand the run was for, if it was for one. |
model | text | The model the run used. |
tokens | number | Tokens used, for the run's cost estimate. |
cost_usd | number | What the run cost in US dollars, if you know. It's shown as reported, not estimated. |
input | text | A short summary of what went in. Keys, tokens and emails are taken out, and a line with a path or id is hidden. |
output | text | A short summary of what came out, cleaned the same way. |
steps | list | The run's steps, for the drill-down. Up to 30. |
videos
Every video as a card on the Studio board. Without it, the board shows what approvals, schedule and posts report.
| Field | Type | What it is |
|---|---|---|
id | text | Stays the same as the video moves. |
brand | text | |
title needed | text | In plain words. |
stage needed | idea, script, visuals, review, scheduled, live | Where it is now. |
desk | text | The bot working on it. |
ts | text | When it reached this stage. |
when | text | When a scheduled video goes out. |
cost_aud | number | What it has cost so far, in AUD. |
ask_id | text | The ask behind a card in review, so Approve and Reject work from the card. |
failed | boolean | |
error | text | What went wrong, in plain words. |
url | text | A public link, once it is live. |
views | number | Views, if measured. |
thumb | text | A preview image address. |
routines
The calendar.
| Field | Type | What it is |
|---|---|---|
id | text | |
name needed | text | |
desk | text | |
kind | job, post | |
days | text | daily, weekdays, or a list such as ["mon", "thu"]. |
time | text | HH:MM, Sydney time. |
next_run | text | A date and time, ISO 8601 with its timezone. |
brand | text | |
optional | boolean | True if low-usage mode can hold it. |
credits
The older form of usage: a weekly figure per bot.
| Field | Type | What it is |
|---|---|---|
desk needed | text | |
week | text | |
used | number |
sponsors
| Field | Type | What it is |
|---|---|---|
brand | text | |
sponsor needed | text | |
stage | lead, contacted, replied, deal, paid | |
value_aud | number | |
ts | text | A date and time, ISO 8601 with its timezone. |
shop
| Field | Type | What it is |
|---|---|---|
store | etsy, gumroad | |
product needed | text | |
sales_7d | number | |
revenue_7d_aud | number | |
ts | text | A date and time, ISO 8601 with its timezone. |
revenue
| Field | Type | What it is |
|---|---|---|
brand needed | text | |
month | text | |
source | sponsor, shop, platform | |
aud | number |
coverage
| Field | Type | What it is |
|---|---|---|
brand needed | text | |
platform | text | |
state needed | live, stale, missing | |
reason | text |
A full example
This is what goes inside the envelopes. It's also on its own at /schema/feed-v2.example.json.
{
"generated": "2026-11-02T09:00:00+11:00",
"desks": [
{
"id": "writer",
"state": "DONE",
"last_ts": "2026-11-02T08:40:00+11:00",
"has_input": false
},
{
"id": "publisher",
"state": "LIVE",
"last_ts": "2026-11-02T08:55:00+11:00",
"has_input": true
},
{
"id": "reviewer",
"state": "BLOCKED",
"last_ts": "2026-11-02T07:10:00+11:00",
"has_input": false
}
],
"meta": {
"brands": [
{
"id": "example-brand",
"name": "Example Brand"
}
],
"desks": [
{
"id": "publisher",
"name": "Publisher",
"last_at": "2026-11-02T08:55:00+11:00"
}
]
},
"posts": [
{
"brand": "Example Brand",
"platform": "YouTube",
"url": "https://www.youtube.com/shorts/example",
"ts": "2026-11-02T08:30:00+11:00",
"views": 1200,
"title": "An example post",
"desk": "publisher"
}
],
"schedule": [
{
"brand": "Example Brand",
"platform": "TikTok",
"title": "The next one",
"when": "2026-11-02T18:00:00+11:00",
"state": "scheduled"
}
],
"handoffs": [
{
"from": "writer",
"to": "publisher",
"item": "Script for the next post",
"state": "waiting",
"brand": "Example Brand",
"ts": "2026-11-02T08:41:00+11:00"
}
],
"approvals": [
{
"desk": "publisher",
"item": "Post the next one",
"brand": "Example Brand",
"ts": "2026-11-02T08:50:00+11:00"
}
],
"blockers": [
{
"desk": "reviewer",
"brand": "Example Brand",
"platform": "TikTok",
"reason": "review",
"who": "Owner",
"needs": "A yes on the new thumbnail",
"ts": "2026-11-02T07:10:00+11:00"
}
],
"blocked": [
{
"desk": "reviewer",
"brand": "Example Brand",
"platform": "TikTok",
"state": "BLOCKED",
"reason": "Waiting for a yes on the new thumbnail",
"waiting_on": "owner",
"blocked_since": "2026-11-02T07:10:00+11:00"
}
],
"brands": [
{
"name": "Example Brand (YouTube)",
"platform": "YouTube",
"views": 48000,
"subs": 910,
"stats_ts": "2026-11-02T06:00:00+11:00",
"subs_change_7d": 126,
"views_change_7d": 9400
}
],
"counts": {
"desks_by_state": {
"LIVE": 1,
"DONE": 1,
"BLOCKED": 1
},
"queue": 2,
"holds": 0,
"asks_open": 1
},
"usage": {
"unit": "credits",
"plan_limit": 10000,
"period_used": 8240,
"period_ends": "2026-11-30T00:00:00+11:00",
"updated_at": "2026-11-02T09:00:00+11:00",
"desks": [
{
"desk": "publisher",
"today": 120,
"week": 780,
"period": 2310
}
]
},
"runs": [
{
"desk": "publisher",
"ts": "2026-11-02T08:30:00+11:00",
"title": "Posted to YouTube",
"ok": true,
"duration_s": 64
}
],
"routines": [
{
"id": "daily-numbers",
"name": "Pull the daily numbers",
"desk": "publisher",
"kind": "job",
"days": "daily",
"time": "07:00",
"optional": false
}
]
}Approvals
A bot that needs a yes puts an ask in the inbox and waits. You answer on the board, and the bot gets told.
- The ask reaches the board's Inbox, sealed so only your server can open it.
- The Owner or a Team member taps Approve or Reject. A Viewer can't, and the server checks that on every action.
- Your hub picks up the answer about once a minute, using
hub-kit/control-sync.sh, and wakes the waiting bot with a command you choose. - The board shows "told" once the hub confirms the bot got the message.
The board itself never posts. Waking your bots is the one part only your hub can do, so if your command fails, the board keeps saying it hasn't been delivered.
Alerts
Alerts come from what the feed really says. Nothing fires on missing data, except a feed that's late or down.
- A run that failed in the last 24 hours.
- A bot that's been blocked for more than a day.
- Usage reaching 80% of your plan, if your hub reports a limit.
- A feed that's gone late or quiet.
Get them as a phone push through the free ntfy app, by setting a long private topic on the board's Team tab. Or get them by email, which needs an email service key in your host's settings. Several at once come as one message, and each alert goes out once. Slack, Discord and Telegram are coming.
Plans and limits
| Plan | Price | Bots | People | Run history |
|---|---|---|---|---|
| Demo | Free | Example data | Example data | Example data |
| Starter | A$29 a month or A$290 a year | Up to 10 | Just you | 30 days |
| Pro | A$79 a month or A$790 a year | Up to 40 | You plus 5, with roles and an audit log | 1 year |
Either plan starts with a 14-day free trial. Card required. Cancel anytime before day 14 and you won't be charged. Yearly plans get 2 months free. More on the pricing page.
Security
What's stored
- Your feed stays in your own GitHub account. We never ask for your bots' or platforms' keys.
- Your copy's database holds member emails, decisions, pause switches and the audit log.
- Your browser keeps a copy of the board so it opens quickly, and signing out clears it. A board opened with a passphrase is never saved.
- Payments go through Stripe, and we never see your full card number.
Encryption
- In version 2, only
generatedand the fourpublicnumbers can be read. Bot names, post links and blockers are all inside the envelopes. feed_sealedis locked to a key made on your server, and that private key never leaves it. The browser only ever gets what a signed-in person is allowed to see.feed_encis locked with a passphrase only you hold. It's opened in the browser, and the passphrase stays in that tab.
Who can see what
Owner sees everything. Team sees everything and can approve and pause, but can't manage members. Viewer reads the board, and can't see the inbox or trading. Every action is checked on the server, so hiding a button is never the only protection.
Deleting your data
- Email support@barncleuth.com to have any email address or invite deleted.
- Delete the feed repo in your GitHub account, and your feed and its history are gone.
- Sign out of the board to clear what your browser saved.
Kids brands stay link-free: name and numbers only, and they're hidden from public pages. More in our privacy policy.
Connectors
Quick starts for reporting from where your bots already run. They talk to hosted workspaces, which open with the trial: until then, the board reads the status file above.