About this proposal
I’m floating this as an RFC to check fit with your vision for Vikunja before writing any code — not asking you to take on work. I know this is on the larger side. If the direction aligns, I’ll implement it and open the PR(s), and I’m happy to ship it in the phases in §7 rather than as one large change.
If it’s promising but you’d want the detail before signing off on a direction, I’m glad to follow up with a full design doc — concern-grouped read schema, field/endpoint specs, where the computed fields resolve, validation rules, and migration steps — for your review before any code lands.
Summary
Vikunja links tasks with a single relation_kind enum whose eleven kinds actually span six unrelated concerns — hierarchy, dependency, sequence, duplication, lineage, association. As one flat tag, none of them can carry the validation, cardinality, or behavior that makes a relation useful, and none can be reliably targeted by an importer or an AI assistant.
This proposes modeling the six concerns explicitly — as concern-scoped fields in the API and UI — in an additive, non-breaking way (the existing table and endpoint stay). It matters for two reasons: it makes relations machine-actionable for AI assistants, and it makes import/sync from the simpler tools Vikunja’s users come from deterministic and lossless. Mature suites (Jira, Linear, GitLab, Asana) appear only as prior art, not as competitors.
1. Current model
Relations live in one table (pkg/models/task_relation.go): TaskID, OtherTaskID, RelationKind (direction base→other), plus audit fields. Each link is persisted in both directions — two rows — with the inverse computed at write time by getInverseRelation(). That pairing already groups the enum into six concern-pairs:
| Concern | Kinds (canonical / inverse) | Nature |
|---|---|---|
| Hierarchy | subtask / parenttask |
child is part of the parent’s scope |
| Dependency | blocking / blocked |
one must finish before the other starts/completes |
| Sequence | precedes / follows |
suggested order, no gating |
| Duplication | duplicateof / duplicates |
same work; one canonical |
| Lineage | copiedfrom / copiedto |
historical copy origin |
| Association | related (self-inverse) |
unspecified “see also” |
Because the concern is invisible to the schema, Vikunja cannot: attach concern-specific behavior (the kind is a bare label — nothing behaves differently for a blocking row than for a related one); enforce most concern-specific rules (subtask/parenttask links are already cycle-checked via checkTaskRelationCycle, but there is no single-parent constraint, dependency links are not kept acyclic, and no state rolls up); or offer an unambiguous target to importers and assistants. The picker also surfaces both halves of every auto-managed pair, showing ~11 options for 6 real choices.
2. Proposal
Principles: (1) each concern is first-class, with its own cardinality, validation, and behavior; (2) the user/assistant asserts one canonical edge and Vikunja derives the inverse (it already computes it — this just stops offering both halves as choices); (3) additive first — no destructive migration needed to land the benefits.
| Concern | Rule | Behavior it enables |
|---|---|---|
| Hierarchy | ≤1 parent → tree; no cycles | subtask progress rolls up to a computed done_percent; optional cascade-close / inherited due date |
| Dependency | acyclic; cross-project OK | computed is_blocked; optional start-gate / completion warning; critical path; “just unblocked” signals |
| Sequence | — | roadmap ordering only. Recommended: fold into Dependency as a soft type (vs hard), rather than a seventh field — see §5 |
| Duplication | directed to canonical | merge / close-as-duplicate. Open: model as a resolution action, not a persistent relation — see §5 |
| Lineage | immutable | traceability only. Recommended: system-set and read-only; drop from the user picker |
| Association | none | none — the safe bucket when no stronger concern can be asserted |
In the UI, one “select a relation kind” control becomes concern-scoped controls (Parent/Sub-tasks, Dependencies with hard/soft, Duplicate of, Related), each offering only the canonical direction; Lineage shows read-only.
3. API changes
The AI benefit only materializes if the API exposes concerns explicitly. Additively:
- Group relations by concern on the task read model (
parent,subtasks[],dependencies{blocking[],blocked_by[]},duplicate_of,related[],lineage{…}). Todayrelated_tasksis a map keyed by all eleven raw kinds; this regroups it into the six named concerns, collapses the auto-managed inverse halves so only the canonical direction surfaces, and (per below) adds computed state. - Canonical-write / derived-inverse contract: accept one direction; the inverse is server-managed.
- Computed state:
is_blocked,blocking_count, rolled-updone_percent— so a client reads graph state rather than rebuilding it. - Server-side validation: enforce single-parent trees and acyclic dependencies, so even an autonomous agent can’t write an invalid graph.
Existing flat reads and endpoints keep working.
4. Why it matters
(The two drivers below; each behavior itself is specified once, in §2.)
AI-assistant enablement. Assistants increasingly manage tasks on their owner’s behalf (a Vikunja MCP server already exists). Typed, behavior-bearing concerns plus the computed fields in §3 let an assistant act deterministically and safely — e.g. “what can I start now?” filters on is_blocked; progress and unblock signals come from the server — instead of string-matching an opaque enum and inferring intent. This is why the §3 API work is in scope, not optional.
Clean ingestion from simpler tools. Vikunja’s realistic adopters migrate from simpler personal/project tools, not enterprise suites. A concern-typed schema gives an importer or sync engine exactly one unambiguous target per source concept — a lossless mapping and a stable sync target (e.g. Google Tasks’ “parent”), with the other concerns available to grow into later. Those simpler tools implement only hierarchy (§5), which is itself evidence the concerns are separable and that a clean Hierarchy field is the highest-value first target.
5. Ingestion mapping (primary target set)
All rows verified against vendor docs. They split into two implementations of the same hierarchy concern — a first-class subtask tree vs checklist-style sub-items that aren’t standalone tasks — but every source implements hierarchy and nothing else, which is why a clean Hierarchy field is the highest-value first target.
| Source | Maps to | Kind |
|---|---|---|
| Google Tasks | Hierarchy — parent, 1 level only | first-class |
| Remember the Milk | Hierarchy — subtasks | first-class |
| Things 3 | Hierarchy — Project→Heading→to-do grouping | first-class |
| Todoist | Hierarchy — nested sub-tasks, up to 4 levels | first-class |
| TickTick | Hierarchy — “Task Nesting” subtasks, up to 5 levels (its lightweight “Check Items” are a separate in-description checklist) | first-class |
| Microsoft To Do | Hierarchy — “Steps” are single-level sub-items, not standalone tasks | checklist-style |
| Trello | Hierarchy — checklist items within a card, not first-class cards (card-to-card only via automation) | checklist-style |
Traps: Google Tasks’ position is a manual sort-order (not Sequence) and its links[] are external refs (not task-to-task).
Prior art (not competitors): Jira, Linear, GitHub/GitLab, and Asana all separate hierarchy from dependency from duplication from association rather than using one flat list, and predecessor/successor typing is the established basis for the hard/soft dependency type above — evidence the split is proven, not that Vikunja competes with them.
6. Open questions
- Sequence as a Dependency
softtype (recommended) or a separate concern? - Duplication as a persistent relation or a close-as-duplicate resolution action?
- Lineage demoted to read-only system metadata (recommended)?
- Stop at the additive layer, or later normalize storage to one canonical row per link?
- Anything you’d want addressed up front re: how concern-grouping meets the CalDAV
RELATED-TOmapping (pkg/caldavtests/relations_test.go) or the in-flight v2 API read models — or is additive layering on top of both acceptable?
7. Migration
- Phase 1 (non-breaking): derive concern from
RelationKind; add grouped reads, computed fields, concern validation, and the split UI. Storage and existing endpoints unchanged. - Phase 2 (optional): Lineage read-only; hard/soft dependency type; revisit Duplication.
- Phase 3 (optional): normalize to one canonical row per link, deriving the inverse on read.
Non-goals: adding PM surface area (boards, sprints), competing on feature breadth, or touching unrelated task fields.
Authored by: Kent Gale (kentonium3/kg-automation) & Claude Code (Claude Opus 4.8), 2026-07-24.
Submission approved by: Kent Gale (kentonium3/kg-automation), 2026-07-24.
Local tracking: kentonium3/kg-automation#840.