Assist — prompts, messaging, events, queues, and watch¶
Coordinate agent prompt jobs, inbound/outbound messaging, event subscriptions, gate: checks, bf wake follow-ups, failure incidents, durable queues, and continuous bf watch.
Prompt jobs (prompt: + harness:)¶
Prompt jobs load a Markdown template from prompts/ (expanded prompt cap: 120,000 bytes), expand placeholders once, and run your external CLI harness: (or defaults.harness):
defaults:
timezone: Europe/Paris
post: owner-chat
harness:
[
claude,
--print,
--output-format,
json,
--tools,
"Read,Grep,Glob,Bash",
--allowedTools,
"Bash(bf read *),Bash(bf search *)",
--permission-mode,
dontAsk,
]
jobs:
triage:
prompt: prompts/triage.md
extract: /result
emit: message
idempotent: true
retries: 3
on:
- { at: "08:00", days: [mon, tue, wed, thu, fri] }
- { event: { kind: record, source: mail, cooldown: 300 } }
- Prompt inputs: BF passes the expanded prompt on stdin by default. Put
{{prompt}}in argv to pass on argv with stdin closed, or{{context}}to pass a private JSON file path in.bf/state/JOB/(job,run,kind,start,end,reason,hook,args,events,messages,stdin). - Routing (
extract:+emit:):extract: /pointerreads a JSON string value;NO_REPLYor blank output stays quiet (quiet: true).emit: messagedelivers throughpost/defaults.post;emit: actionvalidatesdraftACTION.mdoutput and createsactions/YYYY-MM-DD_JOB-SUFFIX/ACTION.md;emit: logwriteslogs/JOB.log. - Safe permissions & privilege split: restrict harness behavior via harness flags (
--allowedTools,--permission-mode dontAsk,--setting-sources userin shared repos — never--dangerously-skip-permissions). Keep scheduled triage read-only (emit: actiondrafts, orscout/actorsplit inexamples/prompts/including the disabledcompileassistant recipe).
Channels and messaging (emit: messages + send:)¶
- Inbound (
emit: messages): runs either bounded polls ({"messages": [...], "cursor": "..."}) or continuouson: { daemon: true }NDJSON line streams; admitted messages persist in.bf/runtime.sqlite3and emitmessage.receivedevents (<= 1,000messages/batch). - Outbound (
send:+bf post):send:jobs read an outgoing envelope (id,text,conversation,target,reply_to) on stdin or expand{{text}},{{target}},{{thread}}in argv:
bf post "Launch approved." --to owner-chat --thread t1 --run "$RUN_ID"
- Sender trust boundary: only attach prompt harnesses to
emit: messageschannels whose adapter authenticates trusted senders (see the channel DM allowlist recipe using{{lines:...}}inexamples/channels/); collect multi-sender mail or group rooms viaemit: records.
Triggers, canonical events, and gate: checks¶
Each entry in on: (up to 32 rules per job) sets one of:
- Schedule:
every(seconds/duration such as15m) orat: "HH:MM"with optionaldays(mon..sun) andtimezone(defaults.timezone), never both. - Hook:
hook: EVENTinvoked bybf run --hook EVENT(exits0cleanly when no enabled job matches). - Daemon:
daemon: truealone onemit: messagesjobs underbf watch. - Event (
on: { event: ... }): reactive subscription with fields: kind: one or a list (up to 16) of"record.added","record.changed","record.removed","note.changed","message.received","job.failed"("record"expands to all threerecord.*kinds).source: omitted/""(any), one job/source name, or a list of up to 32 names.match: optional offline FTS5 search query string (up to 1,024 chars — not regex). Evaluated against.bf/cache.sqlite3forrecord.added,record.changed, andnote.changedrefs, and against normalized event text (summary,ref,source,data) forrecord.removed,message.received, andjob.failed. Non-matches advance the cursor with zero subprocesses.cooldown(0s–365dholds the event cursor to coalesce bursts),max(1–10,000total firings), andexpires(ISO timestamp cutoff).
Events stay strictly decoupled from record collection/ingest: bf collect and bf ingest touch .bf/wakeup, and runtime.sync_item_events diffs .bf/cache.sqlite3 watermarks inside bf update / bf watch to emit record.* and note.changed.
Attach an optional pre-flight gate: script (examples/routines/records-gate.py) when you need scalar attribute filtering (--where status=open): exit 0 proceeds; non-zero skips as gated cleanly.
Wakes, failure incidents, and durable queues¶
bf wake --job triage --in 30m --reason "Recheck CI status"
bf wake --at 2026-09-28T08:00:00Z --run "$RUN_ID"
bf queue
bf queue ENTRY_ID --retry
bf queue ENTRY_ID --cancel
- Bounded wakes: at most
16active pending wakes total across.bf/runtime.sqlite3and recursion depth<= 8when chained via--run. - Failure incidents: failed runs open or increment a per-job incident row in
.bf/runtime.sqlite3and emitjob.failed. Whendefaults.postnames a different enabledsend:job, BF enqueues an opening alert ([brain] JOB failed: ...), repeats reminders every6h(21,600s) while still failing, and sends one[brain] JOB recoveredmessage on the next success. Active counts appear underqueue.incidentsinbf statusand onbf watchrows. - Explicit recovery: transient
EX_TEMPFAIL(75) oridempotent: truejobs/deliveries retry cleanly up toretries(0–20, capped at 300s backoff). Non-idempotent failures enteruncertain/failedin.bf/runtime.sqlite3: inspectbf queue ID(listing hides message bodies) and confirm--retryor--cancelexplicitly. Queue state never blocks collection or retrieval.
Run continuously (bf update and bf watch)¶
bf update --dry-run
bf update --job mail
bf watch --interval 60 --notify failure --json
bf status --watch
bf updateruns due record jobs (concurrently), then due scheduled jobs, refreshes the cache, syncs item events, dispatches matching event jobs, runs due wakes, and flushes pending deliveries.bf watchruns continuousdaemon: truejobs, reacts immediately to.bf/wakeup, and renders an interactive Rich dashboard (j/kselect,g/Gjump,Tabattention filter,f/ureload, Space pause,s/n/t/i/rsort,?help,qquit) or JSON Lines (--json) for services. A secondbf watchobserves read-only (same asbf status --watch). Configure defaults underwatch:(interval,poll_interval,notifications,notification_cooldown).
Run bf watch --json as a user service (Linux systemd / macOS launchd)
Set explicit PATH including bf and provider CLIs, keep credentials in CLI logins or mode-600 files, and keep service units outside shared Git repos:
Linux (~/.config/systemd/user/bf-watch.service, verify with systemd-analyze --user verify and systemctl --user enable --now bf-watch.service):
[Unit]
Description=Brain Framework watch
[Service]
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=%h/.local/bin/bf watch --json --brain %h/brain
StandardOutput=null
Restart=on-failure
RestartSec=60
SuccessExitStatus=130
[Install]
WantedBy=default.target
macOS (~/Library/LaunchAgents/bf-watch.plist, check with plutil -lint and launchctl bootstrap "gui/$(id -u)" ...):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>bf-watch</string>
<key>ProgramArguments</key>
<array>
<string>/Users/me/.local/bin/bf</string><string>watch</string><string>--json</string><string>--brain</string><string>/Users/me/brain</string>
</array>
<key>EnvironmentVariables</key><dict><key>PATH</key><string>/Users/me/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string></dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
<key>ThrottleInterval</key><integer>60</integer>
<key>ExitTimeOut</key><integer>60</integer>
<key>StandardOutPath</key><string>/dev/null</string>
<key>StandardErrorPath</key><string>/Users/me/Library/Logs/bf-watch.log</string>
</dict>
</plist>