Skip to content

Watch and schedule updates

Keep reviewed sensors and routines current with enabled and refresh. Assistants declare their own inputs, post channel, schedules, gates and wakes in bf.yaml. Timing and health says when each program is due and how failures retry. Start inside your brain.

You want to… Use
Run due programs while a terminal is open bf watch
Observe another collector bf status --watch; it executes nothing.
Run unattended on a running machine bf watch --json as a user service you install.

BF installs no background service. First run your programs once by hand.

Check before scheduling

bf update --dry-run    # list due work; run nothing
bf update              # run due sensors, then routines, then assistants
bf status --check

The dry run lists due programs with their windows. Under manual, it names the enabled programs that updates skip: sensors and routines with refresh: 0, and assistants without schedules or inputs, which only bf run starts. A healthy bf status --check exits 0.

  • Set enabled: false to stop a program, or refresh: 0 to keep it manual, as sensors and routines are by default.
  • Keep CI for offline bf validate and bf eval. For a shared source, designate one collecting laptop.

Watch collection

bf watch

Watch runs due sensors and routines immediately, then checks again 60 seconds after each completed cycle; --interval SECONDS changes that. It can contact providers. Closing watch stops all owned process groups.

Beside collection, watch runs the assistant dispatcher: persistent streams, due batch reads, named schedules, queued jobs and deliveries. The dashboard marks assistants with @. Assistant timing and recovery gives the dispatcher's rules.

  • Assistant work that cannot start, such as for a missing prompt or a full queue, never stops watch. Neither does a failing read, a disconnected stream or a failed delivery. The message line says why, and watch retries work that waits.
  • Only a stopped stream worker ends watch.
  • Restart watch after adding or renaming an assistant, or changing channel commands, stream subscriptions or the watch preferences.

The dashboard shows each program's state, last success, next due time, returned items and record changes. Use bf status --check for full health.

  • never means no success on this machine, such as a sensor that only backfilled older windows.
  • failed means the last attempt failed.
  • Items counts the records the last committed collection returned, even a backfill, not the stored total.
  • Manual and disabled programs stay visible without running.

Watch dashboard for the fictional offline demo: five sensors sorted by state, with a failed sensor's details beside their last success, next due time, item count and record changes.

This is the fictional offline watch demo, sorted by state: unavailable failed, calendar updated its record, git added one, and the manual and disabled sensors stay idle.

Key Effect
j / k, arrows Select a program and show its details, including its log and latest action.
Tab Show only programs needing attention: failed, never collected or due.
f (or u) Reload bf.yaml and check due work now; queues one check during an update.
Space Pause or resume future checks; an active update finishes.
? Toggle the guide: every key, sort field and count explained.
q Cancel any active update and quit. Ctrl-C or closing the terminal exits 130.

Press f after adding a reviewed sensor or routine to bf.yaml: its row appears and it runs if due. A new assistant's row appears too, but it runs only after you restart watch. Refresh respects selectors, pause, refresh and failure backoff.

When bf.yaml becomes invalid, or stops declaring a program you selected, the message line shows BF's diagnostic, such as unknown sensor git; check names in bf.yaml. Nothing runs until you fix it.

Sort the dashboard

s cycles the sort field, n, t and i sort by name, last success and items, and r reverses. The panel title shows the field and direction, such as Programs · 1-5/5 · items ↓.

  • Unknown values sort last, and ties stay alphabetical.
  • The selected program stays selected as rows move.
  • Sorting lasts for the session and changes nothing in bf.yaml.
  • Sorting also works in bf status --watch.

Terminal and observation

Observe another collector without running anything, or run due work and stream its state as JSON for a service:

bf status --watch   # observe only; never runs programs
bf watch --json     # run due work and stream JSON Lines

The interactive dashboards, bf watch and bf status --watch, need a color terminal of at least 80 columns by 24 rows; from 120 columns, details appear beside the table. Colors follow NO_COLOR, and labels stay meaningful without color.

Observation shows local history alongside a service or timer. It offers u to reread and has no pause. Without an interactive terminal, it fails and suggests bf status.

bf watch --json writes a snapshot every poll_interval with brain, running, assisting (assistant work is active), message and programs, one row per program. The Row class defines every row field.

  • A row's status is the dashboard state: failed, never, due, fresh, manual or disabled.
  • Times show your local offset, and absent values are empty strings or null.
  • A JSON watcher always collects, and fails while another watcher owns the brain.
  • Each new reason assistant work waits appears as one bf: line on stderr.

Watch preferences

The watch section is optional, and an empty one keeps the defaults. By default, watch checks due work every 60 seconds, rereads local history every 2 seconds and alerts on failures and recovery. For a quieter session:

bf watch --interval 300 --notify off

To keep those preferences, add a watch section to bf.yaml:

# https://fmind.github.io/brain-framework/docs/schedule/#watch-preferences
watch:
  interval: 300
  notifications: off
Setting Default Accepted values
interval 60 Seconds between checks for due work, 5–86400.
poll_interval 2 Seconds between history reads and JSON snapshots, 0.2–60.
notifications failure off, failure, success or all.
notification_cooldown 300 Seconds between alerts, 0–86400.

Command-line options override bf.yaml, then defaults apply. Every command loading bf.yaml validates the section, even when an option overrides it.

  • failure alerts on new failures and recoveries.
  • success alerts on recoveries and on cycles that completed work, even while another program keeps failing.
  • all alerts on all three.
  • Alerts never name sources or quote provider text.
  • Desktop alerts need osascript on macOS, or notify-send or gdbus on Linux. BF finds them through an absolute PATH entry and runs them without the variables BF removes. A delivery failure warns once, and collection continues.

Select programs

Repeat --sensor, --routine and --assistant on update or watch:

bf update --sensor git-commits --dry-run
bf watch --sensor git-commits --routine weekly-review
  • With no selector, every configured program is eligible, including sensors and routines added later. Assistants added later need a restart.
  • With any selector, only the named programs are.
  • bf update --kind KIND, repeatable, keeps only sensors, routines or assistants.
  • An unknown name fails before anything runs and suggests a close one.
  • Selectors never force a disabled or manual program to run.

Run watch as a service

To keep collecting without an open terminal, run bf watch --json as a user service that restarts after a failure. BF installs nothing: write the file, then start it with your system's commands. In the service file:

  • Set PATH: a service does not read your shell profile. List the folders of bf and of every provider CLI your programs call, as command -v bf and command -v gh print them. Prefer stable folders, such as mise or asdf shims, to version-specific ones.
  • Copy XDG_CONFIG_HOME and XDG_STATE_HOME when your shell sets them, so the service shares registration, run history and the assistant queue with your terminal.
  • Keep it current and private: edit the file by hand when bf, the brain or a provider CLI moves. Keep it out of a shared brain's Git history, since it holds your paths.

On Linux, save a systemd user service as ~/.config/systemd/user/bf-watch.service:

# https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
[Unit]
Description=Brain Framework watch

[Service]
# %h is your home folder. List every folder holding bf and the provider CLIs your programs call.
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=%h/.local/bin/bf watch --json --brain %h/brain
# Watch prints a JSON snapshot every poll_interval; its bf: diagnostics on stderr reach the journal.
StandardOutput=null
Restart=on-failure
RestartSec=60
# A stop request ends watch with status 130 once its programs have stopped.
SuccessExitStatus=130

[Install]
WantedBy=default.target
systemd-analyze --user verify ~/.config/systemd/user/bf-watch.service
systemctl --user daemon-reload
systemctl --user enable --now bf-watch.service
journalctl --user -u bf-watch.service -n 30 --no-pager

On macOS, save a LaunchAgent as ~/Library/LaunchAgents/bf-watch.plist. Replace /Users/me with your home folder: launchd expands neither ~ nor variables.

<?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>
plutil -lint ~/Library/LaunchAgents/bf-watch.plist
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/bf-watch.plist
launchctl print "gui/$(id -u)/bf-watch"

Check the service with bf status --check, or follow it with bf status --watch. To stop and remove it, run systemctl --user disable --now bf-watch.service or launchctl bootout "gui/$(id -u)/bf-watch", then delete its file.

A plain timer also works: a systemd timer, a launchd StartCalendarInterval job or a cron line that runs bf update --brain BRAIN every few minutes. A timer never starts a channel stream.

Service behavior and host lifecycle

A systemd user service runs while your user manager does, from your first login to your last logout, unless loginctl enable-linger keeps it running. A LaunchAgent runs while you are logged in. After a failure, both start watch again a minute later; a stop request leaves watch at least a minute to roll back an interrupted record write and stop its programs. Watch catches up due work after sleep or power-off, up to 30 days. Each program's timeout bounds its run.

ChromeOS stops its Linux environment at logout, and WSL needs a running distribution. See the ChromeOS lifecycle, WSL systemd, systemd lingering and Apple launch agent guides.

Collector ownership

Use one execution owner per program: a watch, a service or a timer.

  • A second interactive bf watch on the same brain observes the first instead of collecting. It does not take over when the first exits.
  • Watch cannot detect timers: use bf status --watch while a timer owns collection.
  • Updates of one brain run one at a time. A second update waits up to 10 minutes, then computes its own due list, or fails with another update is still active.
  • bf collect keeps its own lock per sensor.
  • Stopping a systemd or launchd service or timer also stops an active update. Removing a cron line stops future runs only.

Timing and health

A program is due when it is enabled, has a nonzero refresh and has not succeeded on this machine within that many seconds.

  • refresh defaults to 0 for sensors and routines, which keeps them manual: only bf collect or bf run starts them.
  • Assistants have no implicit schedule: declare schedules: {review: {every: 1800}} for a 30-minute review. Their input and schedule windows are independent.
  • Choose a watch interval, or a timer's, shorter than the smallest nonzero refresh. When the interval reaches it, bf watch warns and names that program, in the dashboard header and guide, or as one bf: line on stderr with --json. With an hourly sensor and a 15-minute timer, a check 59 minutes after the last success skips the sensor; the next check, at 74 minutes, runs it.
  • A success timestamp in the future, after a clock correction, makes the program due at once.
  • Long pauses catch up at most 30 days.

When a sensor or routine fails:

  • The other programs continue, and bf update exits 1.
  • The failed program retries 1 minute later, then after 2, 4, 8… minutes per consecutive failure, never waiting longer than its refresh. A success resets the delay.
  • bf collect and bf run always run at once. A direct assistant run leaves the schedule unchanged.

An assistant failure instead stays queued for explicit inspection and retry: uncertain model effects are never replayed automatically. Each failing assistant read, schedule, job or delivery is its own failed row and makes bf update exit 1, while the other assistants still run.

Run history is local, even when records are shared, so check health on the collecting machine. Each program has one freshness state:

State Meaning
fresh Scheduled and succeeded on this machine within twice its refresh.
overdue Scheduled and has succeeded, but not within twice its refresh.
never Scheduled with no local success.
manual refresh: 0.
unknown A disabled program or historical source.

A sensor succeeds only with a collection reaching the moment it runs: one that only backfilled older windows stays never.

bf status --check exits 1 for:

  • an enabled scheduled program that is overdue, never succeeded, last failed or, for a source, fell silent;
  • an assistant with failed or uncertain jobs, or schedules that cannot start;
  • a channel with an unresolved delivery that failed or is uncertain, or a stream or read error.

See messaging for the assistant and channel fields.

Silent sources

Freshness proves that a sensor succeeded, not that it found anything. After a changed filter, an expired account or a provider change, a sensor can keep succeeding with nothing new. Give a source that always brings news within a known delay a silent_after, in seconds:

# https://fmind.github.io/brain-framework/docs/sensors/
sensors:
  mail:
    command: [sensors/gmail-headers.py, "{{start}}", "{{end}}"]
    refresh: 3600
    silent_after: 172800 # new mail arrives at least every two days

bf status gives each source's changed: the newest time this brain gained a new or changed record of it, whatever the records' event times. An enabled scheduled source whose changed is older than its silent_after:

  • is "silent": true in bf status, memories pages and search coverage;
  • joins attention and the home page;
  • fails bf status --check, while its freshness stays as it is.

Collecting a new or changed record ends the silence. A source without any record is never silent: its count already shows it. Set silent_after above the longest quiet spell the source really has, such as a weekend for a daily feed.