Events

A hook runs its commands each time its event fires. The command gets the event in its context as event.name and event.data; tab and agent describe the tab the event is about.

Event list

EventFires whenevent.data
agent.foundAn agent is detected in a tab, or changes kind{}
agent.workingAn agent's status becomes working{"from": <previous status>}
agent.blocked…becomes blocked (waiting on you){"from"}
agent.done…becomes done (finished, not yet seen){"from"}
agent.idle…becomes idle (finished and seen){"from"}
tab.openedA tab is created{}
tab.closedA tab is gone{"id": <tab id>} — no tab in the context, it no longer exists
tab.renamedA tab's title changes{"from": <old title>}
tab.focusedThe focused tab changes{}
workspace.openedA workspace is created{"workspace", "path"}
workspace.closedA workspace is removed{"workspace", "path"}
workspace.renamedA workspace is renamed{"workspace", "from", "path"}
workspace.focusedThe focused workspace changes{"workspace", "path"}
worktree.addedA worktree is added{"workspace", "worktree", "path"}
worktree.removedA worktree is removed{"workspace", "worktree", "path"}
tui.startedA TUI starts on the session, before its first frame{}

An agent whose status becomes unknown fires nothing. A hook key ending in .*, like "agent.*" or "tab.*", matches the whole group.

How events fire

  • Hooks only run while a TUI is open on the session. tui.started fires each time one starts, not once per session.
  • Nothing fires for what already exists when the TUI starts. To learn the starting state, hook tui.started and query BeSwarm.
  • Hooks run in parallel. Events are dispatched in order, but they can finish in any order.
  • A disabled plugin's hooks, and hooks whose command is for another OS, are skipped silently.
  • There are deliberately no events for high-volume changes such as screen output.

Example: notify when an agent needs you

A macOS notification when an agent blocks or finishes. notify has no label, so it stays out of the Plugins panel; ping is there to test the setup.

beswarm.toml:

[plugin]
id = "examples.agent-notify"
name = "Agent notify"
version = "0.1.0"
requires = "0.1.0"
about = "Notify when an agent needs you or finishes"
os = ["macos"]
 
[commands.notify]
run = ["sh", "notify.sh"]
timeout = "10s"
 
[commands.ping]
label = "Send a test notification"
run = ["sh", "ping.sh"]
 
[hooks]
"agent.blocked" = "notify"
"agent.done" = "notify"

notify.sh:

#!/bin/sh
# Reads the run context on stdin and posts a macOS notification
# naming the tab and what its agent is doing.
ctx=$(cat)
field() { printf '%s' "$ctx" | /usr/bin/python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; }
event=$(field 'd.get("event",{}).get("name","test")')
title=$(field 'd.get("tab",{}).get("title","a tab")')
case "$event" in
  agent.blocked) msg="needs you" ;;
  agent.done) msg="is done" ;;
  *) msg="$event" ;;
esac
# The text goes in as an argument, never into the script source: tab
# titles are set by users and agents.
osascript -e 'on run argv' -e 'display notification (item 1 of argv) with title "beswarm"' \
  -e 'end run' "$title $msg"

ping.sh:

#!/bin/sh
cat >/dev/null
osascript -e 'display notification "agent-notify works" with title "beswarm"'
echo "sent"
beswarm plugin add ./agent-notify
beswarm plugin run ping