Skip to content
Ryan M. edited this page Sep 17, 2026 · 2 revisions

Hooks

Hook extensions subscribe to events and POST JSON to a webhook URL configured on the site, project, or member destination. Declare each event as { "on": "<event>", "label": "…" }. Duplicate on values are rejected. The UI uses label; templates and APIs still use the event name.

Hook extensions also need delivery, at least one setting for url_from, and usually templates.

When a hook fires

A project hook fires only when all of these are true:

  1. The extension is enabled for the site.
  2. The extension is enabled for the project (or the member destination is enabled).
  3. The event is listed in hooks.
  4. The destination included it in Triggers (hook_select). Empty project/member trigger lists mean none. A trigger of * matches every declared hook.
  5. A template exists for that event (destination override, else manifest default, else templates["*"] on the destination). An empty template skips the send.
  6. Declared filters also apply: skip_self, claimed_only, claimed_is_me, min_priority, tag_ids, status_ids, status_exclude_ids, quiet hours, digest, field_filter, status_only. See Settings and controls.

join.* events are site-level. They use a site-scoped webhook URL (Admin → Extensions) and do not appear on project Extensions tabs. Empty site triggers for join hooks mean all declared join events.

The live UI (SSE) still refreshes on task.updated / project.updated / task.commented. Outbound hooks use the specific event.

Event catalog

These names are accepted by Ordryn (host_api 1). Example manifests may subscribe to a subset. webhook is a large set, not the complete catalog.

Task

Event Typical meaning
task.created A project task was created.
task.updated Residual field edits (title, description, priority, parent, estimate, custom fields). Not double-fired with the specific hooks below.
task.status_changed Kanban/list column changed. Subscribe here instead of relying on task.updated.
task.deleted A project task was deleted.
task.commented A comment was added. {comment} is truncated. Edit/delete/restore do not fire this outbound.
task.comment_edited A comment was edited.
task.comment_deleted A comment was deleted.
task.comment_restored A deleted comment was restored.
task.claimed Someone claimed the task. {claimed_by} is the claim holder.
task.unclaimed The claim was cleared.
task.due_changed Due date set, changed, or cleared. Empty {due_date} means cleared.
task.due_soon Due tomorrow. Hourly worker; once per task per due date.
task.overdue Due date passed. Hourly worker; once per task per due date.
task.moved Task moved (including project or sprint). Still fires when the split hooks below also fire.
task.project_changed Task moved to another project. Fires in addition to task.moved.
task.sprint_changed Task sprint changed. Fires in addition to task.moved.
task.tagged Tags changed. {tags} is the current tag list.
task.mentioned Someone was @-mentioned in a comment. {mentions} is the named usernames.
task.completed Task marked completed.
task.reopened Task reopened.
task.archived Task archived.
task.restored Task restored from archive.
task.reordered Board order changed. One project-level POST with {count}, not one per card.

With Only notify when status changes (status_only) on, task.updated is skipped unless the kanban/list status actually changed. Prefer task.status_changed for column moves.

Project, sprint, import

Event Typical meaning
project.created A project was created.
project.updated Project name, members, workflow, sprints, or GitHub link changed.
project.deleted A project was deleted (not project.updated).
project.archived A project was archived.
project.restored A project was restored.
project.member_joined A member joined. {member} is set.
project.member_left A member left. {member} is set.
project.member_role_changed A member’s role changed.
project.invite_sent A project invite was sent.
project.invite_declined A project invite was declined.
sprint.created A sprint was created. {sprint} is the name.
sprint.updated A sprint was edited.
sprint.deleted A sprint was deleted.
sprint.started Sprint start (worker + create/update paths; idempotent).
sprint.ended Sprint end (worker + create/update paths; idempotent).
import.completed CSV import finished. {count} is set.

Site

Event Typical meaning
join.request Site join request. {join_email}, {join_message}. {url} is Admin → Join requests when PUBLIC_URL is set.
join.approved Join request approved.
join.denied Join request denied.

Narrower examples:

  • comments — task.commented and task.mentioned
  • activity — board/audit events, skips routine task.updated
  • lifecycle — split moves, archive/restore, membership, sprints
  • due-dates — task.due_changed, task.due_soon, task.overdue
  • claimed — claim / unclaim / overdue
  • mentions — task.mentioned only
  • join-requests — join.request, join.approved, join.denied

join.* on the generic webhook example is site-level only. It is never posted into every project channel; prefer the dedicated join-requests extension for those events.

Templates

templates is a map of hook name → message string. Manifest keys must be known hook names.

Project/member owners can override them. If a destination sets a template to blank, that trigger is skipped. If the destination leaves a template unset, the manifest default is used. Destination overrides may set * as the fallback message when an event has no specific template.

Placeholders are case-insensitive. Unknown tokens become empty. Interpolated message text is trimmed to 2000 characters; @everyone / @here are stripped.

Markup is passed through unchanged — use Discord **bold**, Slack/Google Chat *bold*, or plain text depending on the destination.

Placeholders

Token Value
{task}, {name} Task title (max 120 characters; newlines flattened). Aliases of each other.
{status} Current status name.
{old_status} Previous status.
{project} Project name.
{project_id} Project id.
{actor} Username or email of the person who caused the event. May be replaced by a chat mention when mention_map matches.
{actor_id} Actor user id.
{url} Task link from PUBLIC_URL (or a URL-like BASE_PATH) + /tasks/{id}. Project-level events use the project URL. For join.*, Admin → Join requests.
{id} Task id.
{priority} None, Low, Medium, or High.
{old_priority} Previous priority when it changed.
{description} Task description (truncated).
{parent_id} Parent task id.
{estimate} Estimate points.
{comment} Comment body. Truncated.
{claimed_by} Claim holder.
{due_date} Due date; empty if cleared.
{sprint} Sprint name when present.
{tags} Current tags.
{count} Count for task.reordered or import.completed.
{mentions} Comma-separated @-mentioned usernames.
{member} Member name on membership events.
{event} Event name (for example task.created).
{event_id} Event id.
{occurred_at} RFC3339 timestamp.
{changed} Comma-separated changed field keys.
{fields}, {fields_json} Custom fields JSON object.
{changes_json} JSON array of { "field", "old", "new" }.
{config_json} JSON object of generic setting values (select / status / user / string / int) when present.
{join_email} Join-request email.
{join_message} Join-request message.

Set PUBLIC_URL in Ordryn’s .env (for example https://todo.example.com) so {url} is a clickable link.

Clone this wiki locally