Octonode Playbook

Workspace task management architecture

Canonical Octonode repository documentation.

Workspace task management architecture

Date: 2026-08-03

Status: Core milestone implemented; advanced administration remains optional

Engineering plan: Task management implementation plan

Decision

Octonode adds Tasks as a top-level workspace capability. A Task Space is a planning container inside the active personal, organization, or team workspace. It is deliberately not an Octonode project: projects own code, workflows, runs, and repository links, while Task Spaces own planning views, types, fields, statuses, sprints, releases, and work items. A work item may link to several projects, workflows, or pull requests without changing any source or .octonode authority.

The developer-only apps/tasks, TASKS.json, and BUGS.json are unrelated. Product Tasks must not read them, import them, migrate them, replace their UI, alter their build, or delete them. They remain an independent repository-maintainer tool outside this architecture.

Implemented milestone

The current milestone ships the cloud-backed task_management capability, the top-level Studio Tasks workspace, generated OpenAPI client, and workspace-scoped Durable Object persistence. Users can create simple, Kanban, or Scrum spaces and Initiative, Epic, Story, Task, Bug, or Subtask items; search, update, and move work; use Board, List, Calendar, Timeline, sprint/backlog, and release projections; create tags, sprints, and releases; comment and inspect activity; attach workflow or pull-request links; and share a task permalink in Chat. Social schema v8 includes task data in logical backup, restore, and workspace deletion.

The rest of this document is the target architecture. Type/field/view administration, My Work and Initiatives roll-ups, contextual Calendar/Timeline editing, workflow execution from a task, live pull-request resolution, rich Chat cards, and expanded operational/visual acceptance evidence remain optional follow-up work. The generated OpenAPI document is the authority for the routes implemented by the current milestone.

The product combines a simple default surface with Jira-like depth through progressive disclosure:

  • every Task Space starts with Board, List, and Calendar views and three statuses;
  • Scrum spaces additionally expose Backlog, Active Sprint, and sprint planning;
  • hierarchy, releases, estimates, dependencies, Timeline, saved views, typed columns, and integrations remain available but do not crowd the default create/edit flow;
  • every view offers inline creation with a full work-type picker and inherits the clicked row/group's status, sprint, release, date, parent, or tag context;
  • all work item types use one shared status model rather than a workflow designer per type.

The fixed hierarchy levels and built-in types are:

Initiative
  └─ Epic
       └─ Story | Task | Bug
            └─ Subtask

Every item belongs to exactly one Task Space. An Initiative may parent Epics from other Task Spaces in the same workspace; every other parent/child pair must remain in one Task Space. This supplies a workspace portfolio without making Task Spaces another authorization boundary.

Research basis and limits

Jira's public APIs expose product resources, not Atlassian's private implementation architecture. This design therefore copies useful semantics, not undisclosed internals:

  • Jira separates platform work-item operations (items, comments, links, transitions) from Jira Software planning resources (boards, backlogs, epics, and sprints). Octonode keeps one Tasks API module but preserves the same conceptual split between durable work and query-based views.
  • Jira defines the default hierarchy as Epic, standard items such as Story/Task/Bug, and Subtask; premium plans may add Initiative-like levels. Octonode fixes the useful four-level hierarchy and does not build a hierarchy editor.
  • Jira's backlog is derived from incomplete items outside future or active sprints, and its sprint resource has future, active, and completed lifecycle states. Octonode uses the same derivation and lifecycle with one active sprint per Task Space.
  • Jira Calendar and Timeline are projections over item dates, hierarchy, and dependencies. They are not separate copies of the work.
  • Jira permits work-item creation directly in List, Board, Backlog, and Timeline contexts, with the selected type and grouping values inherited by the new item. Octonode uses the same contextual quick-create rule in every view.
  • Jira versions are exposed to software teams as Releases, including progress and an explicit choice for unresolved work when a version is released. Octonode models a Release as Task Space planning data rather than a Git tag or deployment.
  • Jira supports custom work types and many field types. Octonode allows custom names at four fixed hierarchy levels and a bounded set of typed columns, without schemes, formulas, or plugins.
  • GitHub Projects supports table, board, and roadmap views over the same items and typed fields. Octonode similarly persists saved view definitions, never card membership or a second board copy.

Primary references:

Jira Cloud is not a runtime dependency or initial synchronization authority. A one-time importer can be added later against the public APIs, but continuous Jira synchronization would create two writers and is outside this architecture.

Product model

Workspace

The existing workspace remains the tenant, authorization boundary, backup unit, realtime audience, and billing scope. Task data never crosses user:<id>, org:<id>, or team:<id> boundaries. Task Spaces do not have separate memberships. If private team boards are later required, use a Team workspace rather than adding per-space ACLs.

Task Space

A Task Space has a UUID, immutable uppercase key, name, optional description, sprint-enabled flag, and archive state. Creation offers three templates that only seed types, statuses, and views:

TemplateSeeded behavior
SimpleTo do, Doing, Done; Board, List, Calendar; sprints hidden
KanbanBacklog, Ready, In progress, Review, Done; Board/List/Calendar; sprints hidden
ScrumBacklog, Ready, In progress, Review, Done; Backlog/Active Sprint plus Board/List/Calendar

There are not three engines. After creation, templates disappear into the same statuses, views, and sprintsEnabled flag. A space key becomes permanently immutable as soon as the space is created so human links such as OPS-142 stay valid.

Archiving hides a Task Space but preserves all item links and history. User-facing hard deletion is not provided; complete workspace deletion remains the destructive erasure path.

Work item types

Every Task Space starts with Initiative, Epic, Story, Task, Bug, and Subtask. Space managers may add, rename, reorder, recolor, or archive types at one of four fixed levels:

initiative | epic | standard | subtask

Examples of custom standard types are Feature, Chore, Incident, or Research. A type's level cannot change after creation, and a type in use cannot be deleted; archiving removes it from future create menus while preserving existing items. There are no global type schemes. The four levels supply all parent validation, roll-ups, sprint rules, and view behavior.

Every create entry point—global Create, Board column, List row/group, Backlog position, sprint, Calendar date, Timeline parent row, and Release detail—shows all active types valid for that context. Quick create asks only for type and title, inherits the surrounding context, and opens the full form only when a required parent or field is missing.

Work item

Every item has a stable UUID and a human key formed from its immutable Task Space key and monotonic number. The first release supports only fields with demonstrated use:

type WorkItem = {
  id: string;
  key: string;
  spaceId: string;
  type: {
    id: string;
    name: string;
    level: "initiative" | "epic" | "standard" | "subtask";
    builtin: "initiative" | "epic" | "story" | "task" | "bug" | "subtask" | null;
  };
  title: string;
  description: string | null;       // Markdown, raw HTML disabled when rendered
  statusId: string;
  priority: "highest" | "high" | "medium" | "low" | "lowest";
  reporterId: string;
  assigneeId: string | null;
  parentId: string | null;
  sprintId: string | null;          // standard-level items only; subtasks inherit
  storyPoints: number | null;       // standard items only
  startAt: number | null;
  dueAt: number | null;
  rank: number;
  version: number;
  createdAt: number;
  updatedAt: number;
  archivedAt: number | null;
};

Tags and releases are normalized many-to-many values rather than JSON inside the item. Description and comments use the existing safe Markdown renderer. Type conversion is allowed only between types at the same hierarchy level. Cross-level conversion, moving an item to another Task Space, and hard deletion are excluded initially because each requires key aliases and parent/sprint/release repair.

Tags and typed fields

Tags belong to a Task Space and have a stable ID, unique normalized name, color, and archive state. Items may carry several tags. Tags appear as first-class List columns and Board card chips and are available in quick create, grouping, filters, search, Calendar, Timeline, Releases, and reporting.

Space managers may add up to a bounded number of lightweight custom fields. Supported value types are text, number, single_select, multi_select, date, checkbox, user, and url. A field has a name, type, description, required flag, options where relevant, display order, and archive state. Its type cannot change after values exist. Values are schema-validated against the definition and stored in work_item_field_values; no arbitrary object value crosses the API.

List views can add, remove, and reorder built-in or custom columns without changing the Task Space schema. Inline cell editing uses native controls. Board grouping additionally accepts a single-select custom field; Calendar/Timeline may use a configured custom date column as their date source. Formula, rollup, rich-object, cross-workspace relation, and plugin field types are not supported.

Statuses and board movement

Statuses belong to one Task Space and have a display name, stable ID, category todo | in_progress | done, order, optional WIP limit, and archive state. Every item type shares the same status set. There is no transition graph or per-type screen scheme.

A Board is a saved query grouped by status. Moving a card updates status and rank in one Durable Object transaction using If-Match item version. Ranks use spaced integers; the affected column is renumbered only when no midpoint remains.

ponytail: column renumbering is intentionally bounded and simple. Replace it with fractional ranks only if measured large-column moves make the mutation SLO fail.

Sprints and backlog

A sprint belongs to one sprint-enabled Task Space and has future | active | completed state, name, goal, start/end dates, and version. Only one sprint may be active in a Task Space. Only standard-level records are assigned directly; a Subtask inherits its parent's sprint. Initiative- and Epic-level types span sprints.

Backlog is derived, not persisted:

not archived
AND status.category != done
AND type.level = standard
AND sprint_id IS NULL

Starting a sprint validates dates and the one-active-sprint invariant. Completing it atomically marks it completed and moves incomplete items to either a selected future sprint or the backlog.

Releases

A Release belongs to one Task Space and has a stable ID, name, optional description, status unreleased | released | archived, optional start date, required target/release date when released, and version. Work items may belong to multiple releases through work_item_releases, matching the common need for one change to be backported or delivered in several versions.

Release progress is derived from linked items by status category and story points; it is never stored as another counter. Release detail shows linked items, status/assignee/type/tag breakdowns, and the current linked pull request state already available on each item. Calendar shows release dates and Timeline shows release markers.

Releasing is an explicit version-checked operation. It records the release date and requires a choice for unresolved items: keep them on the released version, move them to one selected unreleased version, or remove the released version assignment. It does not create Git tags, GitHub Releases, deployments, or package versions; those remain separate source/deployment authorities and may be linked later.

Parenthood is distinct from work-item links. The initial directional link types are blocks, duplicates, and relates; the UI supplies the inverse wording. Duplicate/self links are rejected.

Each mutation writes one compact append-only activity event in the same transaction as the item. Events record actor, event kind, changed field names, bounded before/after scalars, and timestamp. Full description bodies are not copied into history. Comments are versioned records with edit and delete tombstones, author/moderator rules, Markdown, and workspace notifications.

The first release has no arbitrary file uploads. Links to workflows and pull requests cover the requested engineering attachments without adding malware scanning, retention, and R2 quota policy.

Saved views

A view stores a name, layout, typed filter, grouping, sort, and revision:

type TaskView = {
  layout: "board" | "list" | "calendar" | "timeline";
  filter: {
    query?: string;
    typeIds?: string[];
    statusIds?: string[];
    priorities?: WorkItem["priority"][];
    assigneeIds?: string[];
    tagIds?: string[];
    sprintId?: string | null;
    releaseIds?: string[];
    parentId?: string;
    projectIds?: string[];
    customFields?: Array<{
      fieldId: string;
      operator: "is" | "is_not" | "contains" | "before" | "after";
      value: unknown;
    }>;
    startFrom?: number;
    dueThrough?: number;
    hideDone?: boolean;
  };
  groupBy:
    | "status"
    | "assignee"
    | "priority"
    | "type"
    | "tag"
    | "sprint"
    | "release"
    | "custom_single_select"
    | "none";
  sort: "rank" | "updated" | "due" | "priority";
  columns: Array<{ field: string; width?: number }>;
};

Filters are AND-only and schema-validated. This covers the common Monday/Jira views without a JQL parser, arbitrary formulas, or user-authored SQL. Board membership, Calendar entries, Timeline bars, and My Work are projections over the same work items.

Integration behavior

Workflows and executions

work_item_workflow_links stores only (workItemId, projectId, workflowId) plus creator and time. Before writing, the service resolves the project and workflow inside the same workspace using the existing authorities. Reads batch-resolve current names and availability; a deleted or inaccessible workflow renders as unavailable without destroying the historical link.

Phase 1 offers Open workflow. A later phase may offer Run through the existing schema-driven workflow input dialog and record the successful execution ID. Task fields are never injected into a workflow implicitly, and the browser never receives an execution credential it did not already have.

GitHub pull requests

work_item_pull_links stores (workItemId, repositoryId, pullNumber) and audit fields. The Cloud API validates that the existing architecture_repository_links record belongs to the active workspace before writing. Pull request title, URL, author, state, and head SHA remain GitHub-owned data mirrored in D1; they are composed into task responses and are not copied into the workspace object.

The first release links and unlinks an existing pull request explicitly. It reuses the current GitHub App, cached pull request records, webhook verification/deduplication, and realtime invalidation. Octonode tasks are not mirrored to GitHub Issues, so the App does not request broader Issues permissions. A later convenience may recognize an exact Octonode task permalink in a pull request body, but branch/title heuristics do not silently create links.

Chat sharing

Every work item has a canonical Studio permalink. Sharing a task in Chat sends that permalink as ordinary message Markdown; no second mention table or chat/task transaction is required. The chat controller recognizes same-origin task links and batch-resolves the visible page's keys through the Tasks API for compact cards. It rechecks tasks:read; an inaccessible, archived, or removed target renders a neutral unavailable link rather than leaking title, assignee, or status.

Notifications

Assignment, mention, comment reply, and sprint lifecycle notifications reuse the existing workspace notification table and WebSocket. Notifications carry item ID/key plus bounded display metadata, not full descriptions or comment bodies.

Persistence authority

Hosted Tasks extends the existing workspace collaboration Durable Object instead of creating a second task database. The object is already deterministically keyed by workspace, owns strongly consistent collaboration writes, hibernating WebSockets, generations, write fencing, backup, and deletion. Cloudflare recommends one Durable Object per logical coordination unit such as a tenant workspace and SQLite for relational state; this workspace is the correct atom. See Cloudflare's Durable Object design rules and SQLite storage API.

DataAuthorityProjection/cache
Workspace membership and task permissionsExisting control D1Request authorization context
Task Spaces, types, fields, statuses, sprints, releases, tags, viewsWorkspaceSocialObject SQLiteReact Query
Work items, tags, hierarchy, links, comments, activityWorkspaceSocialObject SQLiteReact Query + WebSocket invalidation
Project/workflow definitions and executionsExisting project/repository/store authorityBatch-resolved task link cards
GitHub installation, repository, and pull request stateGitHub mirrored in control D1Composed pull request card
Chat message containing a task permalinkExisting social chat tablesRich card resolved at read/render time

Social schema v8 adds these tables; names are contract-level and may be split across nearby migration files:

task_spaces                 task_space_statuses
work_item_types             task_space_fields
task_field_options          task_space_views
task_tags                   task_releases
task_sprints                work_items
work_item_field_values      work_item_tags
work_item_releases          work_item_links
work_item_comments          work_item_watchers
work_item_events            work_item_workflow_links
work_item_pull_links

Important constraints and indexes:

  • unique Task Space key and unique (space_id, item_number);
  • partial unique index for one active sprint per Task Space;
  • parent and linked item foreign keys with restricted deletion;
  • unique normalized type/tag/release names per Task Space and unique relation/link tuples;
  • field values reference a same-space field definition and match its immutable value type;
  • list indexes beginning with space_id for status/rank, sprint/rank, assignee/updated, parent/rank, release/date, tags, and archived state;
  • keyset cursors over stable (sort_value, id) pairs; never offset-scan an unbounded workspace;
  • every mutable aggregate has a positive version and every client mutation has an idempotency key.

Task tables join freely with collaboration tables because they share one workspace object. Cross- authority project, workflow, and GitHub checks happen before the synchronous task transaction; the transaction stores identifiers only. No external I/O occurs inside a related SQLite write sequence.

The existing logical export, generation restore, PITR, and workspace deletion lists must include all task tables. No R2 binding is added because arbitrary attachments are excluded.

ponytail: one collaboration/task Durable Object per workspace remains the initial ceiling. Split tasks by Task Space only if measured request saturation or storage growth approaches platform limits; the public API and stable UUIDs allow that later without changing the product model.

Configured local-cloud and hosted modes use this authority. A server with no cloud collaboration configuration returns a truthful unavailable capability instead of writing task state into a second filesystem store.

Authorization and trust boundaries

Three workspace actions in the shared schema catalog govern Tasks:

ActionAllowsDefault
tasks:readRead spaces, items, views, comments, links, and activityowner/admin/member/team lead/team member/viewer
tasks:writeCreate/edit/archive items; comment; link; move cards; plan sprintsowner/admin/member/team lead/team member
tasks:manageConfigure/archive spaces, types, fields, tags, statuses, releases, shared views, and sprint settingsowner/admin/team lead

Every Tasks request resolves the active workspace principal first. Cross-workspace IDs, project IDs, workflow IDs, repository IDs, item IDs, and comment IDs return 404, not authorization detail. Agents may receive these actions explicitly and remain intersected with workspace role and optional project binding. A project-bound agent can link only its bound project/workflows and cannot manage a workspace Task Space.

Mutations use Zod boundary validation, bounded UTF-8 sizes, idempotency keys, optimistic versions, and synchronous relational checks. Markdown raw HTML remains disabled. Search text is normalized and bounded. Resource limits begin conservative: 100 active Task Spaces per workspace; 20 statuses, 50 work types, 50 typed fields, 500 tags, and 50 saved views per space; 20 tags, 20 releases, and 100 links per item; 240 title characters; and 32 KiB each for description or comment. List endpoints return at most 100 records.

Target API shape

Routes are Hono + Zod documented, generated into the Studio client, and gated by workspace capability. The target surface is:

GET/POST     /api/task-spaces
GET/PATCH    /api/task-spaces/:spaceId
POST         /api/task-spaces/:spaceId/archive
GET/POST     /api/task-spaces/:spaceId/statuses
PATCH        /api/task-statuses/:statusId
GET/POST     /api/task-spaces/:spaceId/types
PATCH        /api/work-item-types/:typeId
GET/POST     /api/task-spaces/:spaceId/fields
PATCH        /api/task-fields/:fieldId
GET/POST     /api/task-spaces/:spaceId/tags
PATCH        /api/task-tags/:tagId
GET/POST     /api/task-spaces/:spaceId/views
PATCH/DELETE /api/task-views/:viewId

GET          /api/work-items
GET          /api/work-items/previews
POST         /api/task-spaces/:spaceId/work-items
GET/PATCH    /api/work-items/:itemKey
POST         /api/work-items/:itemKey/archive
POST         /api/work-items/:itemKey/move
GET          /api/work-items/:itemKey/activity
GET/POST     /api/work-items/:itemKey/comments
PATCH/DELETE /api/work-item-comments/:commentId
PUT/DELETE   /api/work-items/:itemKey/links/:targetKey
PUT/DELETE   /api/work-items/:itemKey/workflows/:projectId/:workflowId
PUT/DELETE   /api/work-items/:itemKey/pull-requests/:repositoryId/:pullNumber

GET/POST     /api/task-spaces/:spaceId/sprints
POST         /api/task-sprints/:sprintId/start
POST         /api/task-sprints/:sprintId/complete
GET/POST     /api/task-spaces/:spaceId/releases
GET/PATCH    /api/task-releases/:releaseId
POST         /api/task-releases/:releaseId/release

GET /api/work-items accepts the typed view filter and a keyset cursor. POST .../move is a domain operation because status and rank must change atomically. Start/complete sprint and release are operations because they enforce lifecycle and bulk movement. Ordinary scalar changes use PATCH with expectedVersion.

The server registers the complete route surface for OpenAPI and configured forwarding. Hosted Gateway routing sends control-plane task routes directly to the Cloud API so opening a board does not wake the Sandbox. Workflow resolution/run and repository source operations remain Sandbox-bound; GitHub pull metadata stays control-plane-bound.

Target Studio information architecture

Tasks belongs in the top-level Left Rail, not under one Octonode project.

Tasks
├─ My Work                 assigned to me across the active workspace
├─ Initiatives             workspace roll-up
└─ Task Spaces
   └─ OPS
      ├─ Board
      ├─ List
      ├─ Calendar
      ├─ Timeline          progressive disclosure
      ├─ Backlog           sprint-enabled spaces only
      ├─ Active Sprint     sprint-enabled spaces only
      ├─ Releases
      └─ Settings          tasks:manage only

The main route is /studio/tasks. Space/view/item state is path-backed so task links survive refresh, back/forward, and chat sharing. The existing small URL helper is extended; a routing dependency is not added solely for this feature.

The default Board keeps Monday-like simplicity: inline type-aware add, drag cards, search, assignee/type/tag/release filters, and a details side panel. The panel progressively reveals hierarchy, estimates, dates, typed fields, dependencies, releases, workflow links, pull requests, comments, and activity. List offers dense inline edits and configurable columns. Calendar derives entries from start/due or selected date fields and supports contextual create/date drag. Timeline displays hierarchy, releases, and blocking links; it is read-mostly in the first release, with date-bar movement only after Board/List editing is stable.

Mobile uses a status selector plus cards instead of requiring horizontal drag, List becomes stacked rows, and Calendar defaults to agenda. Every drag action has keyboard buttons/menu equivalents and screen-reader announcements.

Studio follows existing boundaries:

server task route -> OpenAPI -> generated SDK -> api/tasks.ts
  -> hooks/tasks/* (React Query) -> TaskWorkspace components
  -> existing social WebSocket invalidates workspace-scoped task query keys

Reuse MarkdownText, WorkspaceAvatar, Dialog primitives, member search, pull request summaries, workflow selectors, query-key workspace scoping, and safe error patterns. Every new component has a colocated Storybook story and atomic folder/barrel structure.

Search and performance

Initial task search normalizes item key, title, tags, and searchable text fields and uses a bounded SQLite substring scan within the already-filtered workspace/space set. It returns at most 100 results and never loads all comments or descriptions. Board/List/Backlog queries use indexed keyset pagination; comment and activity streams paginate independently.

ponytail: substring search is the deliberate ceiling. Add a proven full-text index or external search service only after measured workspace data makes bounded search miss its latency target.

The Studio uses optimistic card movement with rollback on 409, lazy-loads item detail/comments, and invalidates by Task Space or item key from realtime events. It does not stream entire boards.

Explicit non-goals

  • Any dependency on, migration from, integration with, replacement of, or cleanup of apps/tasks, TASKS.json, or BUGS.json.
  • Jira-compatible JQL, workflow designers, per-type screens, work-type schemes, field formulas, rollups, plugin-defined field types, or per-space permission schemes.
  • Jira Service Management, deployment automation, time sheets, capacity forecasting, burndown reports, forms, SLAs, automation-rule builders, or app marketplace compatibility.
  • Multiple active sprints in one Task Space.
  • Arbitrary file attachments, email ingestion, or public task sharing.
  • Per-Task-Space membership or cross-workspace hierarchy/links.
  • Continuous two-way Jira or GitHub Issues synchronization.
  • Persisting Board, Calendar, or Timeline membership separately from work items and saved views.

These are extension points, not scaffolding tasks. Add one only when real use demonstrates the need.

Acceptance criteria

  1. A member can create a simple, Kanban, or Scrum Task Space inside the active workspace and cannot read it from another workspace.
  2. Users can create Initiative/Epic/Story/Task/Bug/Subtask items only in valid hierarchy relationships. Space managers can add custom types at those fixed levels, and every view can create any valid type inline while inheriting its row/group context.
  3. Board, List, Calendar, Timeline, Backlog, Active Sprint, My Work, and Initiatives are projections over one item authority and converge after a realtime mutation.
  4. Card movement is atomic, keyboard accessible, version checked, and rank stable after concurrent edits and a column rebalance.
  5. A Scrum space can have one active sprint; completion moves incomplete items exactly once to the selected destination.
  6. Space managers can define tags and bounded typed fields; List columns edit them inline and all supported views filter/group them without accepting arbitrary JSON or SQL.
  7. Releases show derived work and pull-request progress, appear on Calendar/Timeline, and release unresolved items according to one explicit version-checked choice.
  8. Comments, activity, assignment, and mentions retain actor identity, version history/tombstones, and workspace notification behavior.
  9. A work item can link to an accessible Octonode workflow and existing GitHub pull request; stale targets render unavailable without leaking or deleting history.
  10. Sharing a task link in Chat renders a card only for readers who still have tasks:read.
  11. Backup, PITR, generation restore, rollback, and workspace deletion include every task table and pass foreign-key/count validation.
  12. Hosted task list/board routes do not start a Sandbox; workflow/repository operations retain their existing authorities.
  13. The OpenAPI spec and generated Studio SDK are current, all task components have stories, and the Linux screenshot gate covers populated, empty, loading, error, conflict, archived, and mobile states.
  14. Product Tasks has no import, runtime, build, UI, or cleanup relationship with apps/tasks, TASKS.json, or BUGS.json.

On this page