spec: "https://dadl.ai/spec/dadl-spec-v0.1.md" credits: - "Dunkel Cloud GmbH" source_name: "Vikunja REST API" source_url: "https://vikunja.io/docs/api/" date: "2026-03-29" backend: name: vikunja type: rest # base_url is intentionally omitted — must be provided via backends.yaml url field, # because each deployment has its own Vikunja instance URL. description: "Vikunja open-source task management — project planning, kanban boards, and task tracking" coverage: endpoints: 19 total_endpoints: 80 percentage: 24 focus: "projects, tasks, views, buckets, labels, comments, positions" missing: "teams, users, shares, attachments, notifications, webhooks, subscriptions, filters, saved filters" last_reviewed: "2026-03-31" setup: credential_steps: - "Navigate to Vikunja → User Settings → API Tokens" - "Click 'Create a token', enter a name and select permissions" - "Copy the generated token (starts with tk_)" env_var: CREDENTIAL_VIKUNJA_API_TOKEN backends_yaml: | - name: vikunja transport: rest dadl: /app/dadl/vikunja.dadl url: "https://your-vikunja-instance.com/api/v1" required_scopes: - "read/write access to projects and tasks" docs_url: "https://vikunja.io/docs/api-tokens" notes: "Vikunja is self-hosted. Replace the URL with your instance address." auth: type: bearer credential: vikunja_api_token inject_into: header header_name: Authorization prefix: "Bearer " defaults: headers: Content-Type: application/json Accept: application/json pagination: &default-pagination strategy: page request: page_param: page limit_param: per_page limit_default: 50 response: total_pages_header: x-pagination-total-pages behavior: auto max_pages: 20 errors: &standard-errors format: json message_path: "$.message" code_path: "$.code" retry_on: [429, 502, 503, 504] terminal: [400, 401, 403, 404, 409] retry_strategy: max_retries: 3 backoff: exponential initial_delay: 1s # ────────────────────────────────────────────────────────────── # Domain notes (for LLM consumers of these tools) # # Views & Positions: # Every project has multiple views (list, kanban, gantt, table). # Each view has its own independent task positions (float64). # Always call list_views first to get the view_id, then use it # in list_project_tasks and set_task_position. # # Kanban structure: # Kanban views group tasks into buckets (e.g. Backlog, Doing, Done). # list_project_tasks on a kanban view returns buckets with nested tasks, # NOT a flat task list. Use list_buckets to get bucket metadata. # Moving a task between buckets requires move_task_to_bucket # (NOT update_task — bucket_id in update_task has no effect). # # Position values: # Positions are float64 values. To insert a task between two others, # use the midpoint: (pos_above + pos_below) / 2. # set_task_position requires the project_view_id to know which # view's ordering to update. # # Priority values: # 0 = unset, 1 = low, 2 = medium, 3 = high, 4 = urgent, 5 = critical # # Update semantics: # update_task is a composite that does GET → merge → POST. # Vikunja's raw POST /tasks/{id} resets missing fields to zero, # so the composite reads the current state first and only overwrites # fields that were explicitly provided. Safe for partial updates. # # Typical workflow: # 1. list_projects → pick project_id # 2. list_views(project_id) → pick view_id (e.g. kanban view) # 3. list_project_tasks(project_id, view_id) → see tasks in order # 4. set_task_position(id, position, project_view_id) → reorder # ────────────────────────────────────────────────────────────── tools: set_task_position: method: POST path: /tasks/{id}/position access: write description: "Set task position (sort order) within a project view. The position is a float64 value — use values between existing tasks to insert." params: id: { type: integer, in: path, required: true } position: { type: number, in: body, required: true } project_view_id: { type: integer, in: body, required: true } pagination: none list_project_tasks: method: GET path: /projects/{project_id}/views/{view_id}/tasks access: read description: "List all tasks in a project view. Use view_id from list_views. Supports sorting by position, due_date, created, etc. Kanban views return buckets with nested tasks, not a flat list." params: project_id: { type: integer, in: path, required: true } view_id: { type: integer, in: path, required: true } sort_by: { type: string, in: query, default: "position" } order_by: { type: string, in: query, default: "asc" } page: { type: integer, in: query } per_page: { type: integer, in: query, default: 50 } filter: { type: string, in: query } s: { type: string, in: query } response: transform: | [.[] | if .tasks then {bucket_id: .id, title: .title, tasks: [.tasks[]? | {id: .id, identifier: .identifier, title: .title, priority: .priority, done: .done, due_date: .due_date, labels: [.labels[]?.title]}]} else {id: .id, identifier: .identifier, title: .title, priority: .priority, done: .done, due_date: .due_date, labels: [.labels[]?.title]} end] allow_jq_override: true get_task: method: GET path: /tasks/{id} access: read description: "Get a single task by its ID, including all details, assignees, labels, and relations." params: id: { type: integer, in: path, required: true } response: transform: | {id, identifier, title, description, done, priority, due_date, project_id, bucket_id, position, percent_done, labels: [.labels[]?.title], assignees: [.assignees[]?.username], related_tasks} allow_jq_override: true pagination: none create_task: method: PUT path: /projects/{project_id}/tasks access: write description: "Create a new task in a project. Priority: 0=unset, 1=low, 2=medium, 3=high, 4=urgent, 5=critical." params: project_id: { type: integer, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } done: { type: boolean, in: body } priority: { type: integer, in: body } due_date: { type: string, in: body } labels: { type: array, in: body } assignees: { type: array, in: body } bucket_id: { type: integer, in: body } position: { type: number, in: body } pagination: none _update_task_raw: method: POST path: /tasks/{id} access: write description: "Internal: raw task update (sends full object). Use the update_task composite instead." params: id: { type: integer, in: path, required: true } title: { type: string, in: body } description: { type: string, in: body } done: { type: boolean, in: body } priority: { type: integer, in: body } due_date: { type: string, in: body } position: { type: number, in: body } pagination: none move_task_to_bucket: method: POST path: /projects/{project_id}/views/{view_id}/buckets/{bucket_id}/tasks access: write description: "Move a task to a different kanban bucket. Use list_buckets to get bucket IDs. This is the only way to move tasks between kanban columns." params: project_id: { type: integer, in: path, required: true } view_id: { type: integer, in: path, required: true } bucket_id: { type: integer, in: path, required: true } task_id: { type: integer, in: body, required: true } pagination: none delete_task: method: DELETE path: /tasks/{id} access: dangerous description: "Permanently delete a task." params: id: { type: integer, in: path, required: true } pagination: none list_projects: method: GET path: /projects access: read description: "List all projects accessible to the authenticated user." params: page: { type: integer, in: query } per_page: { type: integer, in: query, default: 50 } s: { type: string, in: query } is_archived: { type: boolean, in: query } response: transform: | [.[] | {id: .id, title: .title, description: .description, is_archived: .is_archived, views: [.views[]? | {id: .id, title: .title, view_kind: .view_kind}]}] get_project: method: GET path: /projects/{id} access: read description: "Get a single project by ID." params: id: { type: integer, in: path, required: true } pagination: none list_views: method: GET path: /projects/{project_id}/views access: read description: "List all views (list, kanban, gantt, table) for a project. Each view has its own task positions. Call this first to get view_id for list_project_tasks." params: project_id: { type: integer, in: path, required: true } pagination: none list_buckets: method: GET path: /projects/{project_id}/views/{view_id}/buckets access: read description: "List all kanban buckets for a project view. Returns bucket id, title, and task count. Use move_task_to_bucket to move tasks between columns." params: project_id: { type: integer, in: path, required: true } view_id: { type: integer, in: path, required: true } pagination: none create_bucket: method: PUT path: /projects/{project_id}/views/{view_id}/buckets access: write description: "Create a new kanban bucket in a project view." params: project_id: { type: integer, in: path, required: true } view_id: { type: integer, in: path, required: true } title: { type: string, in: body, required: true } pagination: none list_labels: method: GET path: /labels access: read description: "List all labels." params: page: { type: integer, in: query } per_page: { type: integer, in: query, default: 50 } s: { type: string, in: query } add_label_to_task: method: PUT path: /tasks/{task_id}/labels access: write description: "Add a label to a task." params: task_id: { type: integer, in: path, required: true } label_id: { type: integer, in: body, required: true } pagination: none list_task_comments: method: GET path: /tasks/{task_id}/comments access: read description: "List all comments on a task." params: task_id: { type: integer, in: path, required: true } create_task_comment: method: PUT path: /tasks/{task_id}/comments access: write description: "Add a comment to a task." params: task_id: { type: integer, in: path, required: true } comment: { type: string, in: body, required: true } pagination: none composites: update_task: description: "Update an existing task. Only include fields you want to change — unchanged fields are preserved. To move between kanban buckets, use move_task_to_bucket instead." params: id: type: number required: true description: "Task ID" title: type: string description: "New title" description: type: string description: "New description" done: type: boolean description: "Mark as done/undone" priority: type: number description: "Priority: 0=unset, 1=low, 2=medium, 3=high, 4=urgent, 5=critical" due_date: type: string description: "Due date (ISO 8601)" position: type: number description: "Sort position (float64)" timeout: 15s depends_on: [get_task, _update_task_raw] code: | const current = await api.get_task({ id: params.id }); const merged = { id: params.id, title: params.title !== undefined ? params.title : current.title, description: params.description !== undefined ? params.description : current.description, done: params.done !== undefined ? params.done : current.done, priority: params.priority !== undefined ? params.priority : current.priority, due_date: params.due_date !== undefined ? params.due_date : current.due_date, position: params.position !== undefined ? params.position : current.position, }; return await api._update_task_raw(merged);