Actions¶
Actions are the script-runner half of the notification subsystem. A
notify: rule on an event or root may reference a named action; when
the rule fires, the daemon spawns the action's command with curated
event metadata in the environment.
The actions file is local to the laptop. It is not stored in the synced vault. This split is the trust boundary — see the threat model for the full reasoning. The short version: if the synced vault could declare script paths directly, a compromised sync peer (phone, tablet, replica) becomes a remote-code-execution primitive on the laptop. Decoupling action names (vault, synced) from action commands (laptop, unsynced) means the worst the vault can do is fire a wrong-time action you already blessed.
Where the file lives¶
By default calemdar reads from ~/.config/calemdar/actions.yaml.
You can override the path via notifications.actions.config_path in the
main config.
The file is optional — when missing, the runner returns an empty registry and any rule that names an action is logged and skipped.
Format¶
actions:
pre-meeting:
cmd: /home/me/bin/join-meeting.sh
timeout: 10s
morning-bell:
cmd: ["/usr/bin/notify-send", "-u", "low", "morning"]
pomo-log:
shell: "echo done >> ~/.local/state/pomo.log"
Per-action keys¶
cmd— either a single path string or an array of strings (argv). Spawned via directexec, no shell. Shell metacharacters in either form are passed literally to the program.shell— a string passed tosh -c. Use this when you need pipes, redirects, or other shell features. Mutually exclusive withcmd.timeout— Go duration string (30s,2m). Default 30s. Long-running actions are killed when this expires.
Exactly one of cmd or shell must be set.
Action names¶
Action names match ^[a-z][a-z0-9-]{0,47}$ — lowercase, alpha-first,
hyphens permitted, max 48 chars. The same regex is enforced on the vault
side, so what one half accepts the other half accepts.
Environment passed to the script¶
The runner does not inherit the daemon's environment. The child sees:
PATH=/usr/local/bin:/usr/bin:/binHOME(from the parent)USER(from the parent)CALEMDAR_TITLE— event titleCALEMDAR_DATE— event date (YYYY-MM-DD)CALEMDAR_START— event start time (HH:MM)CALEMDAR_END— event end time (HH:MM, may be empty)CALEMDAR_PATH— absolute path to the event markdown fileCALEMDAR_CALENDAR— the events// folder name CALEMDAR_LEAD— the lead string from the rule (5m,0, …)
This blocks accidental leakage of daemon-side secrets (e.g. a
NTFY_TOKEN env var) into action subprocesses.
Wiring it to a notify rule¶
In any event or recurring root frontmatter:
notify:
- lead: 5m
via: [system] # also fire a desktop notification
action: pre-meeting
- lead: 0
action: pre-meeting
When the rule fires:
- The scheduler dispatches the message to every backend in
via. - If
action:is set, the named action is spawned with the env above. - The fire is recorded in
notify_firedso a daemon restart doesn't replay it.
Concurrency¶
notifications.max_concurrent_spawns (default 4) caps how many actions
can run at once. Beyond that they queue.
Testing¶
calemdar notify actions lists what's registered. To dry-run a fire,
trigger an event with a notify: [{lead: 0, action: yourname}] rule and
watch the daemon log.