Barncleuth

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

  1. Look around first. The demo is the whole board with labelled example data, and needs no signup.
  2. Start your trial. Pick a plan on the pricing page. We'll email your setup link within 24 hours.
  3. 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" }
  ]
}
  1. Point the board at it. On the board, open the More menu, choose Load a status file, and paste the file's address.
  2. Add what you have. Every section below is optional. Anything you leave out shows as "Not reported", never as a zero.
  3. 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": "…" }
}
FieldTypeWhat it is
v needed2The feed version.
generated neededtextWhen the hub made this file.
public neededobjectThe only numbers anyone can read without a key.
feed_enc neededtextThe payload, locked with your passphrase. The board's "Locked: enter passphrase" box opens this in the browser.
feed_sealed neededtextThe payload, locked to the server's public key at /feed-seal-key. The server opens this for signed-in people.

public

FieldTypeWhat it is
desks_working neededwhole numberBots that reported in the last 2 hours and aren't blocked.
desks_total neededwhole numberBots on the team, leaving out retired ones.
brands neededwhole numberHow many brands the team runs.
posts_7d neededwhole numberPosts 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.

FieldTypeWhat it is
v needed1The envelope's version.
alg neededRSA-OAEP-256+A256GCMHow it's locked.
kid neededtextThe first 16 hex characters of the SHA-256 of the public key.
ek neededtextThe AES key, wrapped.
iv neededtext12 random bytes.
data neededtextThe 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.

FieldTypeWhat it is
v needed1The envelope's version.
salt neededtext16 random bytes.
iv neededtext12 random bytes.
data neededtextThe 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.

FieldTypeWhat it is
generatedtextWhen the hub made this file.
schema_versionwhole numberYour own version marker. The board warns if it's newer than it understands.
deskslistEvery bot on the team.
metaobjectNames for what's on the board.
postslistWhat went live.
schedulelistWhat's coming up.
handoffslistWork moving between bots.
approvalslistWhat's waiting for a person.
blockerslistWhat's stuck, in short.
blockedlistWhat's stuck, with how long and why.
brandslistOne row per brand per platform.
countsobjectTotals, if your hub keeps them.
usageobjectThe usage meter.
runslistRun history. Keep about the last 300.
videoslistEvery video as a card on the Studio board. Without it, the board shows what approvals, schedule and posts report.
routineslistThe calendar.
creditslistThe older form of usage: a weekly figure per bot.
sponsorslist
shoplist
revenuelist
coveragelist

desks

Every bot on the team.

FieldTypeWhat it is
id neededtextThe bot's id, such as publisher.
stateLIVE, DONE, BLOCKED, HOLDWhat it's doing.
last_tstextWhen it last reported.
has_inputbooleanTrue if work is waiting for it.
pausedbudgetWhy 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.

FieldTypeWhat it is
brandslistThe brands the team runs.
deskslistA display name for each bot.
siteobjectYour board's name, logo and accent colour.

posts

What went live.

FieldTypeWhat it is
brand neededtext
platformtext
urltextA public link to the post.
tstextA date and time, ISO 8601 with its timezone.
viewsnumberViews, if measured.
thumbtextA thumbnail address.
titletext
desktextThe bot that posted it.
views_tstextA date and time, ISO 8601 with its timezone.
views_srctextWhere the view count came from.
avg_view_pctnumberAverage percent of the video watched.
made_for_kidsboolean
ai_labelbooleanTrue if it carries an AI label.
originalbooleanTrue if it's original work.

schedule

What's coming up.

FieldTypeWhat it is
brand neededtext
platformtext
titletext
urltext
whentextA date and time, ISO 8601 with its timezone.
statescheduled, posted, live, missed, queued, hold

handoffs

Work moving between bots.

FieldTypeWhat it is
fromtextThe bot passing work on.
totextThe bot receiving it.
itemtextWhat's being passed.
statetext
brandtext
tstextA date and time, ISO 8601 with its timezone.
countwhole numberHow many, if it's a count.

approvals

What's waiting for a person.

FieldTypeWhat it is
desktext
itemtextWhat needs a yes.
brandtext
tstextA date and time, ISO 8601 with its timezone.
countwhole number
made_for_kidsboolean
ai_labelboolean
originalboolean

blockers

What's stuck, in short.

FieldTypeWhat it is
desktext
brandtext
platformtext
reasonaccount check, verification, stock, review
whotextWho can clear it.
needstextWhat it needs, in plain words.
tstextA date and time, ISO 8601 with its timezone.
countwhole number

blocked

What's stuck, with how long and why.

FieldTypeWhat it is
desk neededtext
brand neededtext
platform neededtext
state neededBLOCKED, HOLD
reasontextWhy, in plain words.
waiting_ontextowner, external, or desk:<id>.
waiting_labeltextWho it's waiting on, for external.
blocked_since neededtextWhen it got stuck. Rows without this are dropped.
ask_reftextThe inbox ask this is waiting on.

brands

One row per brand per platform.

FieldTypeWhat it is
name neededtextThe brand, such as Shift Fuel (YouTube).
platformtext
viewsnumber
likesnumber
subsnumberSubscribers or followers.
historylistRecent values, for the small chart.
stats_tstextA date and time, ISO 8601 with its timezone.
views_srctext
data_throughtext
retentiontext
periodtext
subs_change_7dnumber
views_change_7dnumber

counts

Totals, if your hub keeps them.

FieldTypeWhat it is
desks_by_stateobject
queuewhole number
holdswhole number
asks_openwhole number

usage

The usage meter.

FieldTypeWhat it is
unittextSuch as credits.
plan_limitnumberYour plan's limit. Without it there's no percentage or warning.
period_usednumber
period_endstextA date and time, ISO 8601 with its timezone.
updated_attextA date and time, ISO 8601 with its timezone.
deskslistOne row per bot.

runs

Run history. Keep about the last 300.

FieldTypeWhat it is
idtext
desk neededtext
tstextA date and time, ISO 8601 with its timezone.
titletextIn plain words: no job ids, paths, links or emails.
okboolean
errortext
duration_snumber
brandtextThe brand the run was for, if it was for one.
modeltextThe model the run used.
tokensnumberTokens used, for the run's cost estimate.
cost_usdnumberWhat the run cost in US dollars, if you know. It's shown as reported, not estimated.
inputtextA short summary of what went in. Keys, tokens and emails are taken out, and a line with a path or id is hidden.
outputtextA short summary of what came out, cleaned the same way.
stepslistThe 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.

FieldTypeWhat it is
idtextStays the same as the video moves.
brandtext
title neededtextIn plain words.
stage neededidea, script, visuals, review, scheduled, liveWhere it is now.
desktextThe bot working on it.
tstextWhen it reached this stage.
whentextWhen a scheduled video goes out.
cost_audnumberWhat it has cost so far, in AUD.
ask_idtextThe ask behind a card in review, so Approve and Reject work from the card.
failedboolean
errortextWhat went wrong, in plain words.
urltextA public link, once it is live.
viewsnumberViews, if measured.
thumbtextA preview image address.

routines

The calendar.

FieldTypeWhat it is
idtext
name neededtext
desktext
kindjob, post
daystextdaily, weekdays, or a list such as ["mon", "thu"].
timetextHH:MM, Sydney time.
next_runtextA date and time, ISO 8601 with its timezone.
brandtext
optionalbooleanTrue if low-usage mode can hold it.

credits

The older form of usage: a weekly figure per bot.

FieldTypeWhat it is
desk neededtext
weektext
usednumber

sponsors

FieldTypeWhat it is
brandtext
sponsor neededtext
stagelead, contacted, replied, deal, paid
value_audnumber
tstextA date and time, ISO 8601 with its timezone.

shop

FieldTypeWhat it is
storeetsy, gumroad
product neededtext
sales_7dnumber
revenue_7d_audnumber
tstextA date and time, ISO 8601 with its timezone.

revenue

FieldTypeWhat it is
brand neededtext
monthtext
sourcesponsor, shop, platform
audnumber

coverage

FieldTypeWhat it is
brand neededtext
platformtext
state neededlive, stale, missing
reasontext

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.

  1. The ask reaches the board's Inbox, sealed so only your server can open it.
  2. The Owner or a Team member taps Approve or Reject. A Viewer can't, and the server checks that on every action.
  3. 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.
  4. 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

PlanPriceBotsPeopleRun history
DemoFreeExample dataExample dataExample data
StarterA$29 a month or A$290 a yearUp to 10Just you30 days
ProA$79 a month or A$790 a yearUp to 40You plus 5, with roles and an audit log1 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 generated and the four public numbers can be read. Bot names, post links and blockers are all inside the envelopes.
  • feed_sealed is 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_enc is 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.