Example Adapters
These are production adapters for two self-hosted task managers. They demonstrate most of the features covered in Agent Adapters — auth, fetches, let bindings, extends, signal mode, user mapping, and the fetch-mutate-PUT pattern. For the complete field reference, see Adapter Schema.
Donetick
Section titled “Donetick”Donetick is a self-hosted chore/task manager. Its webhook system fires task.reminder events on a scheduler cadence and task.completed when a task is marked done. The REST API supports completing and rescheduling tasks.
Key patterns demonstrated:
- Path-mode bearer auth — Donetick doesn’t sign webhooks, so the secret is the trailing URL segment
- Shared webhook fetch — enriches the reminder with the full chore description (not included in the webhook payload)
letbindings — extracts subtitle/priority logic from inline ternaries into named valuesextends— three snooze variants inherit from a sharedsnooze_base- Signal mode — clears the notification when a task is completed via the Donetick UI
owner: user_abc
webhook: auth: bearer: path: true secret: "${DONETICK_WEBHOOK_SECRET}"
fetches: - name: chore method: GET url: "${DONETICK_URL}/api/v1/chores/{{ payload.data.id }}" headers: secretkey: "${DONETICK_API_KEY}" optional: true
notifications: - if: payload.type == "task.reminder" id: string(payload.data.id) actions: [done, snooze_30, snooze_60, snooze_1d] let: - name: is_overdue expr: payload.data.type == "overdue" - name: subtitle expr: | is_overdue ? "Overdue!" : payload.data.type == "due" ? "Due now" : "Upcoming" - name: priority expr: 'is_overdue ? "high" : "normal"' - name: description expr: | (fetched.chore != nil && fetched.chore.data.res != nil && fetched.chore.data.res.description != "") ? fetched.chore.data.res.description : "" body: | { to: vars.users["1"], title: payload.data.name, subtitle: subtitle, body: description, priority: priority, source: { adapter: "donetick", name: "Donetick", icon_url: "https://raw.githubusercontent.com/donetick/donetick/main/assets/logo.svg" }, click_url: vars.base_url + "/chores/" + id, state: { adapter_id: "donetick", task_id: payload.data.id } }
# Task completed elsewhere — clear the notification. - if: payload.type == "task.completed" id: string(payload.data.chore.id) signal: "clear"
actions: done: title: Done traits: [confirm] request: | { method: "POST", url: "${DONETICK_URL}/api/v1/chores/" + string(state.task_id) + "/do", headers: { secretkey: "${DONETICK_API_KEY}" } }
snooze_base: title: Snooze traits: [defer] request: | { method: "PUT", url: "${DONETICK_URL}/api/v1/chores/" + string(state.task_id) + "/dueDate", headers: { secretkey: "${DONETICK_API_KEY}", "Content-Type": "application/json" }, body: { dueDate: rfc3339(addMinutes(now, action.minutes)), updatedAt: rfc3339(now) } }
snooze_30: title: Snooze 30m extends: snooze_base params: minutes: 30
snooze_60: title: Snooze 1h extends: snooze_base params: minutes: 60
snooze_1d: title: Remind Tomorrow extends: snooze_base params: minutes: 1440
vars: base_url: "${DONETICK_URL}" users: "1": "user_abc" "2": "user_def"Vikunja
Section titled “Vikunja”Vikunja is an open-source task management tool. Unlike Donetick, Vikunja signs its webhooks with HMAC-SHA256 and its REST API has no PATCH endpoint — to update one field you must GET the full task, mutate the object, and PUT the whole thing back.
Key patterns demonstrated:
- HMAC signature auth — Vikunja sends
X-Vikunja-Signaturewith each webhook letbindings — extracts due-date detection, subtitle, and priority into named valuesextendswith inherited fetches — snooze variants inherit the fetch-mutate-PUT pattern fromsnooze_base- Signal mode for multiple events — both
task.updated(whendone == true) andtask.deletedclear the notification merge()andmap()— functional transforms to update nested arrays without destructuring
owner: user_abc
webhook: auth: signature: algorithm: hmac-sha256 header: X-Vikunja-Signature secret: "${VIKUNJA_WEBHOOK_SECRET}"
notifications: - if: payload.event_name == "task.reminder.fired" id: string(payload.data.task.id) actions: [done, snooze_30, snooze_60, snooze_1d] let: - name: has_due_date expr: payload.data.task.due_date != nil && payload.data.task.due_date != "0001-01-01T00:00:00Z" - name: is_overdue expr: has_due_date && payload.data.task.due_date < payload.data.reminder.reminder - name: subtitle expr: | !has_due_date ? "Reminder" : is_overdue ? "Overdue" : "Due soon" - name: priority expr: 'is_overdue ? "high" : "normal"' body: | { to: vars.users[string(payload.data.user.id)], title: payload.data.task.title, subtitle: subtitle, body: payload.data.task.description, priority: priority, source: { adapter: "vikunja", name: "Vikunja", icon_url: "https://raw.githubusercontent.com/go-vikunja/website/main/public/favicon.svg" }, click_url: vars.base_url + "/tasks/" + string(payload.data.task.id), state: { adapter_id: "vikunja", task_id: payload.data.task.id, reminder_time: payload.data.reminder.reminder } }
# Task completed elsewhere — clear the notification. - if: payload.event_name == "task.updated" && payload.data.task.done == true id: string(payload.data.task.id) signal: "clear"
# Task deleted — clear the notification. - if: payload.event_name == "task.deleted" id: string(payload.data.task.id) signal: "clear"
actions: done: title: Done traits: [confirm] request: | { method: "POST", url: "${VIKUNJA_URL}/api/v1/tasks/" + string(state.task_id), headers: { Authorization: "Bearer ${VIKUNJA_TOKEN}", "Content-Type": "application/json" }, body: { done: true } }
snooze_base: title: Snooze traits: [defer] fetches: - name: task method: GET url: "${VIKUNJA_URL}/api/v1/tasks/{{ state.task_id }}" headers: Authorization: "Bearer ${VIKUNJA_TOKEN}" request: | { method: "POST", url: "${VIKUNJA_URL}/api/v1/tasks/" + string(state.task_id), headers: { Authorization: "Bearer ${VIKUNJA_TOKEN}", "Content-Type": "application/json" }, body: merge(fetched.task.data, { reminders: map(fetched.task.data.reminders, #.reminder == state.reminder_time ? merge(#, { reminder: rfc3339(addMinutes(now, action.minutes)) }) : # ) }) }
snooze_30: title: Snooze 30m extends: snooze_base params: minutes: 30
snooze_60: title: Snooze 1h extends: snooze_base params: minutes: 60
snooze_1d: title: Remind Tomorrow extends: snooze_base params: minutes: 1440
vars: base_url: "${VIKUNJA_URL}" users: "1": "user_abc" "2": "user_def"