Skip to content
Codeberg

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 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)
  • let bindings — extracts subtitle/priority logic from inline ternaries into named values
  • extends — three snooze variants inherit from a shared snooze_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 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-Signature with each webhook
  • let bindings — extracts due-date detection, subtitle, and priority into named values
  • extends with inherited fetches — snooze variants inherit the fetch-mutate-PUT pattern from snooze_base
  • Signal mode for multiple events — both task.updated (when done == true) and task.deleted clear the notification
  • merge() and map() — 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"