A plugin is a self-contained directory that reacts to task events. Drop it
into ~/.config/task/plugins/ and it's live — no rebuild, no config edits, and
no collisions with other plugins. This is the easy on-ramp for community
integrations (notifications, proxies, trackers, chat bridges, …).
It builds on TaskYou's existing event hooks. The
difference: the legacy hooks dir allows one script per event, so two
integrations that both want task.done fight over the same file. A plugin
namespaces its scripts in its own directory and declares what it handles in a
manifest, so any number of plugins can handle the same event and all of them
run.
The community collection lives at github.com/taskyou/plugins — browse it, review a plugin, then install the whole collection (or a single plugin) with:
ty plugins add https://github.com/taskyou/plugins # clone & install the collection
ty plugins list # see what it providesty plugins add clones a repo into your plugins dir; a repo can hold one plugin at
its root or many nested in subdirectories, and every plugin inside becomes active.
Re-running add on the same source updates it in place (git pull). You can also
just drop a directory into ~/.config/task/plugins/ by hand — see Examples.
ty plugins remove <name> # aliases: rm, uninstallremove deletes the named plugin's directory (the name is the one from
ty plugins list, even if it differs from the directory name). If the plugin was
installed as part of a multi-plugin collection checkout, only its own subdirectory
is removed — the shared checkout and its sibling plugins stay, and re-running
ty plugins add on the collection source may restore it.
~/.config/task/plugins/
└── my-plugin/
├── plugin.yaml # manifest (required)
└── on-done.sh # your script(s)
name: my-plugin # required, unique
version: 0.1.0 # optional
description: What it does. # optional
hooks: # event -> script path (relative to this dir)
task.done: on-done.sh
task.blocked: on-blocked.sh
actions: # user-invoked commands (optional)
- id: sync
label: Sync to tracker
command: sync.sh
services: # long-running processes (optional)
- name: index
command: ./search-server # sh -c, run from the plugin dir
cwd: "" # optional, relative to the plugin dir
env: ["FOO=bar"] # optional extra envScript paths are resolved relative to the plugin directory. Scripts must be
executable (chmod +x). A plugin needs at least one usable hook, action,
workflow (workflows/*.yaml), or service.
A service is a process the daemon supervises for its whole lifetime: it starts
when the daemon comes up and is stopped (SIGTERM, then SIGKILL) when the daemon exits.
Each service runs as sh -c <command> from the plugin dir (or cwd), in its own
process group. See examples/plugins/heartbeat/ for
a runnable example.
A hook is fired by the daemon on one discrete event (task.done), runs, and
exits — it's stateless and short-lived, and it can only ever react to a single event
in isolation. A service is the opposite: it owns its own loop, and it's for the
work a hook structurally can't do:
- Hold a persistent connection — a Slack socket-mode client, an IMAP IDLE connection, an MCP stdio server other tools attach to.
- Run on its own schedule — poll an inbox every 30s, reindex every 5min — rather than waiting for a ty event to fire it.
- Serve a port — a metrics endpoint, a small web dashboard, a search server.
- Batch or debounce across many events — a hook fires once per event and keeps no state between fires; a service can accumulate and flush.
- Watch an external source and create tasks — turn emails, GitHub webhooks, or a file watcher into ty tasks.
Rule of thumb: react to one ty event → hook. Hold state, a connection, a schedule, or a port → service.
A service is just a process — it has no special in-process access to ty. The daemon hands it, via the environment, a stable way to find the running instance:
| env var | value | use it for |
|---|---|---|
TY_API_URL |
http://127.0.0.1:8080 (the daemon's HTTP API; absent if the API is disabled) |
Read/write tasks over HTTP — the decoupled, recommended path. |
TY_DB_PATH |
the SQLite file (respects WORKTREE_DB_PATH) |
Open the DB read-only for queries the API doesn't expose yet. Couples you to the schema — prefer the API. |
Beyond those, a service can subscribe to ty's event feed (the SSE stream on the
HTTP API) to react to task changes in real time, or simply shell out to the ty
CLI, which is often the least-effort option for a small shell service.
- Email/chat bridge — hold an IMAP IDLE or Slack socket connection; turn incoming
messages into tasks and post task updates back out. (This is the shape of the
existing
ty-emailextension — the kind of thing that used to run and be babysat on its own.) - Semantic search / index server — keep an embeddings index warm and answer
queries over a port or via MCP. (The shape of
ty-qmd.) - Webhook listener — bind a port, accept GitHub/Linear webhooks, and create tasks from them.
- Metrics / dashboard — serve board stats read from
TY_API_URLon a small HTTP endpoint for a status display. - Reactive reindexer / cache warmer — subscribe to the event feed and rebuild a derived view whenever tasks change.
Note: declaring a service does not migrate any of the existing in-repo extensions — they stay where they are. This is simply the capability so a new plugin can ship a long-running helper if it wants one.
Plugins handle the same events as the event hooks system. The ones dispatched today:
| Event | When |
|---|---|
task.started |
Execution begins |
task.done |
Agent finished successfully |
task.blocked |
Task needs input |
task.failed |
Agent execution failed |
task.auth_required |
Executor session needs re-authentication |
A plugin may declare any event string; it only runs for events TaskYou actually emits, so unknown events are harmless.
Every hook receives the standard task variables:
TASK_ID TASK_TITLE TASK_STATUS TASK_PROJECT TASK_TYPE
TASK_MESSAGE TASK_EVENT WORKTREE_PATH
Plugin hooks additionally receive:
TASK_PLUGIN_NAME # this plugin's name
TASK_PLUGIN_DIR # absolute path to this plugin's directory
The script's working directory is set to the plugin directory, so it can read its own bundled files (config, templates, helper binaries) with relative paths.
Hooks fire automatically on events. Actions are commands the user triggers on
demand. Each action has an id, an optional label, and a command (a script
path relative to the plugin dir).
Run one from the CLI, optionally against a task:
ty plugins run my-plugin sync # no task context
ty plugins run my-plugin sync 42 # with task #42's envAn action script receives TASK_PLUGIN_NAME / TASK_PLUGIN_DIR always, and the
TASK_* variables (TASK_ID, TASK_TITLE, TASK_STATUS, TASK_PROJECT,
TASK_TYPE, WORKTREE_PATH) when a task is in context. Unlike hooks, actions
run synchronously (up to 60s) and their output is shown to the caller (the CLI
prints it; the TUI shows the first line in the notification banner).
Actions are reachable from every surface, all running the same command:
- CLI:
ty plugins run <plugin> <action> [task-id]. - TUI — detail view: press
Aon a task to open a picker of that task's plugin actions; the chosen one runs with the task's env. - TUI — command palette: open it (
p/Ctrl+P) and type a leading>to switch from task search to action search. - GUI / API:
GET /api/plugins/actionslists them;POST /api/plugins/actions/run({plugin, action, task_id?}) runs one. The desktop app and any agent use these.
Two capabilities need no manifest entry — a plugin ships them by convention, just by having the right subdirectory:
workflows/*.yaml— each file is a workflow definition, resolvable byty pipeline -d <name>once the plugin is installed.routines/<name>/prompt.md— each subdirectory is a routine: a named, unattended agent run, resolvable byty run <name>and listed byty routines(frontmatter formodel/project/timeout/… works exactly as it does for a user routine under~/.config/task/routines/).
my-plugin/
├── plugin.yaml
├── workflows/
│ └── review.yaml # ty pipeline -d review "<goal>"
└── routines/
└── linear-poll/
└── prompt.md # ty run linear-poll (+ ty routine schedule --every 2m)
A user routine of the same name shadows a plugin's (your
~/.config/task/routines/ wins), so a plugin routine is a sensible default you can
override. Scheduling stays a separate step (ty routine schedule <name> --every … | --cron …) — the plugin ships the what, you choose the cadence.
Routine vs. service: ship a routine for periodic, run-to-completion work (a
poller, a digest, a nightly sweep) — the daemon records each run, enforces a timeout,
and keeps cross-run state in ROUTINE_STATE_DIR. Ship a service (where available)
only for a process that must stay up (a socket connection, a listening port).
- Fan-out: for each event, the legacy single-script hook and every plugin that declares the event run, concurrently and in the background. A slow or failing plugin never blocks task execution or the other plugins.
- Isolation: a malformed manifest, a missing script, or a plugin with no
name is skipped (surfaced via
ty plugins listand the daemon log) — one bad community plugin can't break your pipeline. - Timeout: each hook is given 30s before it's killed.
- Deterministic order: plugins run sorted by name.
ty plugins list # what's installed and which events each handles
ty plugins dir # the plugins directory pathSet TY_PLUGINS_DIR to use a directory other than ~/.config/task/plugins/.
Complete, copy-pasteable plugins live in examples/plugins/:
| Plugin | Kind | What it shows |
|---|---|---|
desktop-notify |
hooks + action | native notifications; a test action |
slack |
hooks | webhook integration; bundled config.env |
worktree |
actions | task-scoped diff / test using WORKTREE_PATH |
heartbeat |
service | a daemon-supervised long-running process |
cp -R examples/plugins/desktop-notify ~/.config/task/plugins/
ty plugins listFor ready-made, reviewable plugins, see the community collection at github.com/taskyou/plugins (see Installing). For more ideas, see the plugin idea gallery.