Run it every night¶
A nightly job runs sb run for you at a time you choose, so the queue is worked through while you sleep and the morning index is waiting. It is optional: sb run by hand does the same.
The job runs sb run --if-due. --if-due does real work only when a scheduled batch is due. The first batch of the day is due after nightly_time (default 03:00, set under [run]); once it has run, a check with nothing to do prints Not due: the nightly run already happened. and stops, which is why it is safe to ask for it every hour. A run stopped by a model outage, or interrupted (Ctrl-C, SIGTERM, a shutdown), does not count as the nightly run, so the next check retries it; manual sb runs never count.
The hourly tick drains the queue. A run stops at [run].max_sources_per_run sources (20 by default), at max_tokens_per_run tokens or, when you set it, at max_usd_per_run. Once the nightly run has happened, whenever sources are queued the next hourly tick is due again and runs the next batch, one batch per tick, whatever ended the last batch: one of those limits, or an empty queue that filled again later (links you sb add during the day). The check counts what is queued and the files waiting in inbox/ that a run can read (an image without an OCR model does not count). New mail is fetched by the nightly run, so on its own it waits for the next night. Before 0.4.0 only a batch that stopped at the source limit was continued, so a batch that stopped on its token budget (common with long PDFs) waited for the next night. At most [run].max_batches_per_day scheduled batches run a day (a whole number of at least 1, default 6), counted from nightly_time and including the nightly run itself; this cap is what bounds a day's work and spend, so a large backlog cannot keep the model busy all day: with the defaults, up to 6 batches of 20 sources, then the rest waits for the next night. A run stopped by an outage or an interruption does not count towards it, and neither does a manual sb run. The hourly tick never continues after a scheduled run that stopped for an outage or an interruption: that waits for the next night (when it was the nightly run itself, the next check retries it, as above), and it never starts while another run is active. To cap what a day can cost, set max_usd_per_run (see [run]): the scheduled batches of a day then cost about max_batches_per_day times it at most (the check happens between sources, so the source being read finishes first).
The job is supported on macOS (launchd). On other systems, which are not supported, you can try cron.
macOS (launchd)¶
Install it once:
sb schedule install
Installed ~/Library/LaunchAgents/com.esbi-cli.nightly.plist
Runs every day at 03:00 (or at the next wake). Log: ~/Documents/Obsidian/esbi/.esbi/logs/nightly.log
(Or answer yes to the nightly question in sb init, or run sb init --nightly 03:00 on a new config.) Check it:
sb schedule status
installed: yes
loaded: yes
What it installs is a launchd agent that runs sb run --if-due at the time you set, at login, and every hour as a safety net. If the Mac is asleep at the set time, launchd starts the job at the next wake (unlike cron, which skips it). The run is wrapped in caffeinate -i, so the Mac cannot idle-sleep in the middle of it. The hourly timer alone would not be enough, because launchd drops interval ticks that fall while the Mac sleeps.
First run: macOS asks for permission¶
The job reads ~/Documents (your vault, the config). The first time, macOS's privacy protection asks whether the Python that runs sb may access files in your Documents folder. Click Allow. Until you do, the job waits silently and looks "running" forever. If you missed the dialog, print which Python that is:
realpath "$(head -1 "$(command -v sb)" | cut -c3-)"
With uv it is a python3.x under ~/.local/share/uv/python/; a Python inside a Python.framework is listed as Python. In System Settings, Privacy & Security, Files & Folders (or Full Disk Access), allow it (add it with + if it is not listed), then restart the job:
launchctl kickstart -k gui/$(id -u)/com.esbi-cli.nightly
An agent or script cannot click that dialog for you.
Choose the time¶
Set nightly_time = "HH:MM" (24 hours) under [run] in config.toml, then install again so launchd is set for the new time:
sb schedule install
The due check follows the config at once, but launchd only fires at the time it was installed for. sb doctor warns when the two differ:
WARN nightly job: installed for 03:00 but config.toml says 04:30
fix: sb schedule install
Wake the Mac for it¶
The Mac only works while awake. To have it awake for the run, wake it at the same time or a minute later. For 04:30:
sudo pmset repeat wakeorpoweron MTWRFSU 04:30:00
It needs sudo, so it is yours to run. Never wake it earlier than nightly_time: a run is only due from that moment, so an earlier wake finds nothing to do and the Mac goes back to sleep. Check it with pmset -g sched. Ollama must also be running (the Homebrew service starts it at login).
Check that it works¶
sb doctor
ok last run: 2026-10-03 03:00: 2 ingested, 0 failed
ok nightly job: installed and loaded, runs at 03:00
WARN last run: no scheduled run yet: the job has not run since you installed it.WARN last run: 2026-10-03 03:00: the model server was unreachable (nothing was lost; the sources stay queued): the latest run stopped because the model could not be reached. Start Ollama (brew services start ollama) or check themodelline, then runsb run; the hourly check retries by itself too. The next run that reaches the model clears the warning.- The log is
<vault>/.esbi/logs/nightly.log; it is trimmed by itself. A healthy hourly check with nothing queued (or with the day's batches used up) ends inNot due: the nightly run already happened. - Every run, manual or scheduled, is listed under Runs (Ejecuciones in a Spanish vault) in the daily note.
launchctl print gui/$(id -u)/com.esbi-cli.nightly | grep -E "state|last exit"shows launchd's own view.
Remove it¶
sb schedule uninstall
Removed.
After you update or move sb¶
The job remembers where sb lives and which config it uses. After updating, reinstalling or moving sb, or moving the config, run sb schedule install again.
Other systems: cron (not supported)¶
macOS is the only supported platform for now; this section is for people who try another system anyway, and nothing here is promised. There is no launchd there, so use cron (or any scheduler that can run a command every hour). sb init --nightly 03:00 prints the line to use, and so do sb schedule install, status and uninstall. install and uninstall stop with exit code 1 and error: the nightly job uses launchd, which only macOS has, because they cannot do what was asked; status has nothing to query, so it prints the line and exits 0:
Nightly job: this system has no launchd. Add this to cron (every hour: the first tick after [run].nightly_time runs the nightly batch, later ones drain what is queued, up to [run].max_batches_per_day batches a day):
0 * * * * /home/you/.local/share/uv/tools/esbi-cli/bin/sb run --if-due --config /home/you/.config/esbi-cli/config.toml
The line names sb by its absolute path (the one inside the environment it was installed in, here a uv tool one), quotes a path that has spaces and writes a % as \% (in a crontab a bare % means a new line). Copy it as printed.
Add it with:
crontab -e
Two details that cron needs and a terminal does not:
sbmust be named by its full path. cron'sPATHis only/usr/bin:/bin, which has no~/.local/bin, whereuv toolandpipxputsb; a baresbis "not found" every hour and the nightly job never runs, with nothing to tell you. The printed line already has the path. An older version printed a baresb: if you copied that line, replacesbwith the output ofcommand -v sb.- Give it what the model needs. For Ollama running as a service there is nothing to add. For your subscription,
claudemust be on cron'sPATH; for an API key, the key must be set in the crontab (a variable line above the job), which stores it in plain text.
A full line, with a log:
0 * * * * /home/you/.local/share/uv/tools/esbi-cli/bin/sb run --if-due --config /home/you/.config/esbi-cli/config.toml >> /home/you/esbi-cli.log 2>&1
Hourly is intentional: --if-due means only the first check after nightly_time does the first batch (and, while sources are queued, one more batch per hourly check, up to max_batches_per_day), and if the machine was off at 03:00 the first check after it comes back does it. nightly_time still decides when a run counts as due. It was tried on Ubuntu 22.04 and 24.04 in containers; launchd on macOS is the only supported way (see Other systems).
WSL and native Windows were not tried (WSL does not run cron unless systemd is enabled, to name one thing); they are not supported.
Does the job need my machine to be on?¶
Yes: it runs on your machine, with your models. With a subscription or an API the model is elsewhere, but the worker still runs here. See Use it offline for what works without a network.