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
| Event | Fires when | event.data |
|---|---|---|
agent.found | An agent is detected in a tab, or changes kind | {} |
agent.working | An 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.opened | A tab is created | {} |
tab.closed | A tab is gone | {"id": <tab id>} — no tab in the context, it no longer exists |
tab.renamed | A tab's title changes | {"from": <old title>} |
tab.focused | The focused tab changes | {} |
workspace.opened | A workspace is created | {"workspace", "path"} |
workspace.closed | A workspace is removed | {"workspace", "path"} |
workspace.renamed | A workspace is renamed | {"workspace", "from", "path"} |
workspace.focused | The focused workspace changes | {"workspace", "path"} |
worktree.added | A worktree is added | {"workspace", "worktree", "path"} |
worktree.removed | A worktree is removed | {"workspace", "worktree", "path"} |
tui.started | A 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.startedfires 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.startedand 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