Skip to content

Work core

This describes how work is tracked in Qren: one list of things to do, shared by the people in a workspace and by the agents helping them. You will not see an ID again — items are known by their titles, and the short code behind each one exists only so that a link, a command or a chat message can point at exactly one thing. Every item has exactly one person accountable for it and at most one agent acting on that person’s behalf; taking the work back from an agent is one click, and nothing the agent did is lost when you do. Nothing is allowed to rot: an item nobody accepts expires, an item nobody has touched in months closes itself, and every one of those automatic moves is written down as a visible event you can undo. What it deliberately does not do: it keeps no goals, areas or key results, no separate inbox and no threads, and it never creates work on its own — every item traces back to a person, or to a proposal a person approved.

The work core is the core system that answers “what is being done, by whom, and where does it stand” for one workspace. It is the spine the other core systems hang off: companion proposes items from meetings, chat carries the conversation, knowledge is what an item produced, drive holds its files, agents are delegates on it. It replaces the AOS work engine outright (decision record §4, “replace, not evolve”). Evidence is cited throughout to work-backend-landscape-2026-08-20 (landscape), work-engine-audit-2026-08-20 (audit), qren-agent-teamwork-2026-08-20 (teamwork) and runrec-study-2026-08-20. Each non-goal below is something the current engine has and this one will not; the numbers are the reason.

Non-goal Evidence
No automatic task creation. No session hook, comms trigger, runner or importer writes an item directly; every creator emits a command landing in triage with its source, or goes through the approval queue 470 tasks created on one day, 444 ownerless; 88% of tasks have no activity past creation (audit §1, TL;DR 2)
No goals, areas or key results 4 goals, 0 areas, 0 key results against full schema support (audit §4); no leader models them on the task (landscape §1)
No separate inbox table — triage is a status category 37 inbox items, 0 ever triaged (audit §4)
No threads table — conversation is chat, exploration is knowledge 4,061 rows, nearly 2× the task count; 4,059 auto-generated per worktree checkout, 2 ever promoted (audit §4)
No narrative or briefing generation inside the core — the morning brief is an agent routine reading these tables the briefing layer is 3,949 lines, 22.8% of the engine and nearly double the task model (audit §7)
No dotted sub-IDs, no counters shown by default, no t# in 15 live rows the prefix and the real parent disagree; two ID generators, no source-of-truth column (audit §4, §9.1)
No multi-assignee, free-text status, EAV custom fields, stored progress percentage, or blocked/waiting statuses derived or refused by every leader (landscape §1)
No project-directory bureaucracy — manifests, worktree reconciliation, project detection 2,892 lines that are not a work concern (audit §7)

One workspace.db per workspace (design/target-tree §1.2). Principals, the approval queue and the audit log live in instance.db, so assignee_id, delegate_id, actor and approval_id are ids into that database and not foreign keys — the workspace folder has to stay liftable on its own.

Entity What it is Required
item One piece of work. The only work object. id, short_code, title, status_id, category, assignee_id, source, created_by, timestamps
status A workspace-named status inside a fixed category id, name, category, rank
project Optional container, one level. No cycles, no milestones in v1. id, name
label / item_label Flat, many-to-many; carries RunRec’s domain facet id, name
relation blocks · duplicate_of · related. Three kinds, no more. src, dst, kind
comment A note on an item; may point at a chat thread id, item_id, author_id, body_md, created_at
events Append-only. Activity feed, audit trail and sync unit at once. seq, ts, actor, kind, item, payload
commands Idempotent write intents, client-generated id id, actor, kind, payload
view A saved filter/sort/group/layout, as data id, owner_id, spec_json
item_rank Saved manual order per view — the board rank view_id, item_id, rank
agent_run / agent_activity One delegation session and its turns see §9
approval_link Join from an item or run to an approval in instance.db approval_id, item_id
legacy_key Old identifiers that must keep resolving key, item_id

Derived, never stored: blocked (an open item is the src of a blocks relation pointing here), waiting (snooze_until set, or the run is awaiting_input/awaiting_approval), stale (last_activity_at past the workspace threshold) — landscape §1, “no leader has blocked as a status.” Provenance is required: source on every item, plus source_meeting_refs and confidence when a companion or agent proposed it, all three named hard requirements by runrec-study §5.

-- workspace.db — the work core. Ids naming principals or approvals are
-- instance.db ids, unconstrained here so the workspace folder stays liftable.
PRAGMA foreign_keys = ON;
CREATE TABLE status ( -- names configurable per workspace, categories are not
id TEXT PRIMARY KEY, name TEXT NOT NULL, rank TEXT NOT NULL,
category TEXT NOT NULL CHECK (category IN
('triage','backlog','unstarted','started','review','done','cancelled')),
is_default INTEGER NOT NULL DEFAULT 0, reserved TEXT); -- 'duplicate' | NULL
CREATE TABLE project (
id TEXT PRIMARY KEY, name TEXT NOT NULL, rank TEXT NOT NULL, archived_at TEXT,
key TEXT UNIQUE, show_keys INTEGER NOT NULL DEFAULT 0, -- opt-in, off by default
next_number INTEGER NOT NULL DEFAULT 1, target_on TEXT);
CREATE TABLE item (
id TEXT PRIMARY KEY, -- UUIDv7, client-generated
short_code TEXT NOT NULL UNIQUE, -- 7-char Crockford base32
title TEXT NOT NULL, body_md TEXT NOT NULL DEFAULT '',
status_id TEXT NOT NULL REFERENCES status(id),
category TEXT NOT NULL, -- denormalised from status, trigger-maintained
priority INTEGER NOT NULL DEFAULT 3 CHECK (priority BETWEEN 0 AND 4),
assignee_id TEXT NOT NULL, delegate_id TEXT, -- one human; at most one agent
project_id TEXT REFERENCES project(id), project_number INTEGER, -- number only if show_keys
parent_id TEXT REFERENCES item(id), -- one parent, acyclic
duplicate_of TEXT REFERENCES item(id),
due_on TEXT, start_on TEXT, snooze_until TEXT,
snooze_on_activity INTEGER NOT NULL DEFAULT 1,
handoff_count INTEGER NOT NULL DEFAULT 0, -- hard handoffs; 3 forces human triage
source TEXT NOT NULL, -- app|cli|chat|companion|connection|agent|gardener|import
source_meeting_refs JSON, confidence REAL, -- provenance; confidence NULL if a human typed it
attachments JSON, -- Drive-relative paths (see Open questions)
created_by TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
last_activity_at TEXT NOT NULL, completed_at TEXT, cancelled_at TEXT,
archived_at TEXT, trashed_at TEXT,
CHECK (duplicate_of IS NULL OR category = 'cancelled'));
CREATE INDEX item_live ON item(category, last_activity_at)
WHERE archived_at IS NULL AND trashed_at IS NULL;
CREATE INDEX item_owner ON item(assignee_id, category);
CREATE INDEX item_parent ON item(parent_id);
CREATE TABLE label (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, colour TEXT);
CREATE TABLE item_label (item_id TEXT REFERENCES item(id), label_id TEXT REFERENCES label(id),
PRIMARY KEY (item_id, label_id));
CREATE TABLE relation (src TEXT NOT NULL REFERENCES item(id), dst TEXT NOT NULL REFERENCES item(id),
kind TEXT NOT NULL CHECK (kind IN ('blocks','duplicate_of','related')),
created_by TEXT NOT NULL, created_at TEXT NOT NULL,
PRIMARY KEY (src, dst, kind), CHECK (src <> dst));
CREATE TABLE comment (id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES item(id),
author_id TEXT NOT NULL, body_md TEXT NOT NULL, chat_ref TEXT,
created_at TEXT NOT NULL, edited_at TEXT);
CREATE TABLE commands ( -- idempotent write intents (Todoist shape, landscape §4)
id TEXT PRIMARY KEY, -- client-generated uuid = idempotency key
actor TEXT NOT NULL, kind TEXT NOT NULL, payload JSON NOT NULL,
base_seq INTEGER, -- events.seq the client had seen — the CAS input
received_at TEXT NOT NULL, applied_at TEXT, rejected_reason TEXT);
CREATE TABLE events ( -- append-only: activity feed + audit trail + sync unit
seq INTEGER PRIMARY KEY AUTOINCREMENT, -- the global lastSyncId
ts TEXT NOT NULL, actor TEXT NOT NULL, kind TEXT NOT NULL,
-- kind: item.created | item.transitioned | assigned | delegated | handed_off | taken_back
-- | commented | linked | merged | snoozed | auto_declined | auto_closed | archived | trashed
item TEXT REFERENCES item(id), payload JSON NOT NULL,
command_id TEXT REFERENCES commands(id));
CREATE INDEX events_item ON events(item, seq);
CREATE TABLE view (id TEXT PRIMARY KEY, owner_id TEXT, scope TEXT NOT NULL,
name TEXT NOT NULL, spec_json JSON NOT NULL, rank TEXT NOT NULL);
CREATE TABLE item_rank (view_id TEXT REFERENCES view(id), item_id TEXT REFERENCES item(id),
rank TEXT NOT NULL, PRIMARY KEY (view_id, item_id)); -- board order
CREATE TABLE agent_run (
id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES item(id),
agent_id TEXT NOT NULL, requested_by TEXT NOT NULL,
trigger TEXT NOT NULL CHECK (trigger IN ('delegate','mention','automation','schedule')),
state TEXT NOT NULL CHECK (state IN ('pending','active','awaiting_input',
'awaiting_approval','error','complete','stale','cancelled')),
engine TEXT, model TEXT, session_ref TEXT, outcome TEXT,
started_at TEXT, last_activity_at TEXT, ended_at TEXT,
cost_usd REAL, tokens_in INTEGER, tokens_out INTEGER, tool_calls INTEGER,
artifacts JSON, handoff_packet JSON);
CREATE TABLE agent_activity ( -- immutable; the transcript and the audit trail
run_id TEXT NOT NULL REFERENCES agent_run(id), seq INTEGER NOT NULL, ts TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('thought','action','elicitation','response','error','prompt')),
body TEXT, action TEXT, parameter TEXT, result TEXT,
ephemeral INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (run_id, seq));
CREATE TABLE approval_link ( -- the approval itself lives in instance.db
approval_id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES item(id),
run_id TEXT REFERENCES agent_run(id), capability TEXT NOT NULL,
trust_level TEXT NOT NULL, -- the level in force when the action was attempted
requested_at TEXT NOT NULL, decided_at TEXT, decision TEXT);
CREATE TABLE legacy_key (key TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES item(id));
CREATE VIRTUAL TABLE item_fts USING fts5(title, body_md, content='item', content_rowid='rowid');
-- derived state is a view, never a column
CREATE VIEW item_state AS SELECT i.id,
EXISTS (SELECT 1 FROM relation r JOIN item b ON b.id = r.src
WHERE r.dst = i.id AND r.kind = 'blocks'
AND b.category NOT IN ('done','cancelled')) AS blocked,
(i.snooze_until IS NOT NULL
OR EXISTS (SELECT 1 FROM agent_run ar WHERE ar.item_id = i.id
AND ar.state IN ('awaiting_input','awaiting_approval'))) AS waiting,
(julianday('now') - julianday(i.last_activity_at)) AS days_idle
FROM item i;

Invariants, enforced in the command layer and never by the UI: exactly one human assignee_id, always present; at most one delegate_id, always an agent; category follows status_id; parent_id acyclic; a parent cannot close while it has open children; duplicate_of implies category cancelled (landscape §8; teamwork §3.3).

Seven categories, fixed in code; their display names are configurable per workspace, and a workspace may define several statuses inside one category (landscape §1: “small fixed category enum, names configurable”).

Category Default name Meaning
triage Triage Arrived from somewhere; nobody has accepted it
backlog Backlog Accepted, not scheduled
unstarted Todo Scheduled, not begun
started In progress Someone or something is working on it
review In review Work done, awaiting a human’s read (runrec-study §5)
done Done Finished
cancelled Cancelled Not happening. The reserved Duplicate status sits here

Transitions are unrestricted — any category to any other, as every leader allows, because gating them only produces workarounds (landscape §8). The record is not optional: each writes an item.transitioned event with actor, from, to and reason. Two guards only — a parent with open children cannot close, and only the triage verbs move an item out of triage.

The four triage verbs (Linear’s exactly, landscape §3): accept → the default backlog/unstarted status, assignee set if absent; declinecancelled with a reason; duplicate <item>cancelled with the reserved Duplicate status, duplicate_of set, merge performed (§7); snooze <until> → hidden until that date or any new activity, whichever comes first. Snooze works outside triage too: with snooze_on_activity = 1 (the default) a comment, transition or agent turn un-snoozes immediately, and snooze_until IS NULL with the flag set is “Someday” — in the weekly review view, never in Today.

The invariant (teamwork §3.3): exactly one human assignee at all times, at most one agent delegate, ownership changing only through an atomic handoff event. An agent is never an assignee; a non-owner may read and be asked, only the owner writes.

Operation Ownership effect Event
assign(human) assignee_id changes; a human→human handoff is this plus a packet assigned
delegate(agent) delegate_id set, assignee unchanged; opens an agent_run; moves the item to the first started status when a human triggered it, leaves it in triage when an automation did delegated
handoff(to) Hard handoff; the previous owner releases and handoff_count increments handed_off
escalate() Handoff up the ladder — owner → Lead → human assignee → workspace owner — with blockers[] populated handed_off
takeback One click: delegate_id cleared, run cancelled, packet and activity preserved taken_back

A fourth hard handoff is refused: at handoff_count = 3 the item goes to human triage (teamwork §3.4). ask is not a work-core operation — consulting a peer agent changes no ownership, creates no item and writes nothing here; it belongs to the chat/agent model and is specified there. If the answer requires the peer to act, the asker must delegate, creating a child item (teamwork §7). The HandoffPacket is generated, never typed — assembled from events and agent_activity at handoff time, stored on agent_run.handoff_packet and in the handed_off payload, editable before sending. If to is empty the item returns to the Lead’s triage rather than a guessed recipient (teamwork §3.2).

{ "item": "7g3k2pq", "from": "prn_hisham", "to": "prn_bookkeeper",
"reason": "context exhausted", "objective": "one sentence",
"state": {"done": ["…"], "verified": ["…"], "assumed": ["…"]},
"next_step": "the single concrete next action, not a plan",
"decisions": [{"decision": "…", "why": "…", "at": "2026-08-20T14:02Z"}],
"blockers": [{"type": "needs_human|needs_peer|needs_capability|external_wait", "what": "…"}],
"files": ["Nuchay/Files/contract-v3.pdf"], "open_approvals": ["apr_01J…"],
"recent_events": [{"seq": 8812, "kind": "item.transitioned"}],
"constraints": {"scope_out": ["…"], "approval_level": "propose_only"},
"budget_remaining": {"usd": 4.10, "wall_clock_s": 900} }
Element Rule
Primary key UUIDv7, generated by the client — time-sortable, merge-safe, and it removes the temp-id problem for offline creates (landscape §2)
Short code 7 characters, Crockford base32, derived from the UUID’s random segment, not a counter, so it never renumbers and needs no per-project sequence. Collision policy: UNIQUE on the column; on conflict the writer re-derives from the next 35 bits and retries three times, then fails the command. At 2³⁵ values a workspace needs ~260,000 items for a 1% chance of one collision
What a human sees Titles. Lists, boards, chat and briefs show titles truncated at ~60 characters; the short code appears on hover, on copy-link, and in dim type at the end of a CLI row. No ID is ever printed into a chat or Telegram reply — that is exactly how Created t#187 reached the operator’s phone (audit §6)
Per-project key KEY-12 from project_number, when a project sets key and show_keys = 1. Off by default. On a move the old key goes to legacy_key and keeps resolving in search, URLs and show (Linear’s behaviour, landscape §2)
URLs qren://w/<workspace>/i/7g3k2pq in the app; https://<appliance>/w/<workspace>/i/7g3k2pq/<title-slug> through the portal. The slug is decorative, the code resolves
Resolution order short_codelegacy_key → UUID → KEY-N → fuzzy title. An ambiguous fuzzy match lists candidates and exits non-zero rather than guess

Every timer is a workspace setting and every automatic action writes an event and appears in a daily digest card — nothing changes silently. Defaults from decision record §4; exclusions copied from Linear (landscape §3).

Rule Default Exclusions
Triage expiry 14 d idle → snoozed 14 d once → cancelled, reason triage_expired none
Stale auto-close, human-owned 90 d idle in backlog/unstartedcancelled, reason stale future due_on, open children, active project with a target date, snooze_until set
Stale auto-close, agent-owned 30 d, same rule as above
Archive after done 30 d → archived_at none
Archive after cancelled 7 d → archived_at none
Trash 30 d after trashed_at → hard delete none

Archived is hidden from live views and the client’s partial bootstrap, still searchable, restorable in one action; trashed is an explicit human delete with a 30-day grace. Both exist and they are different states (landscape §1). A seven-day warning card precedes every stale auto-close, and all rules run dry for two weeks after a migration — the one documented case of an automation closing 2,000 tickets is in landscape §8. Duplicates at creation: FTS5 title similarity, plus embeddings once the knowledge index exists, suggests a canonical item and never auto-merges; when the creator is an automation the suggestion becomes a comment on the canonical item instead of a new item. Merge is one-directional from the duplicate — relations, labels, attachments and subscribers move to the canonical item, comments stay on the duplicate behind a banner linking forward, duplicate_of is set.

Rule What it means
Every mutation is a command commands(id, actor, kind, payload, base_seq); the client-generated id is the idempotency key, so re-sending is a no-op
Applying one is one transaction it mutates rows and appends to events. events.seq is the workspace’s global sync cursor and the same number that drives the activity feed
The appliance is server-authoritative one writer; the edge holds no authority (landscape §4; ADR 0002)
v1 clients talk to the Supervisor’s Control API over control.sock locally, through the portal or tunnel remotely — the way members reach runrec.org today (decision record §4, “appliance-only in v1”). The qren CLI is one caller among several (ADR 0004); no work operation exists only as a button
Compare-and-swap on assignee_id, delegate_id, status_id a command whose base_seq predates a change to one of those is rejected with a reason that surfaces as a card. Title, body, labels, dates and priority are last-command-wins. No CRDTs — per-column LWW cannot enforce the one-owner invariant (landscape §4)
Not built in v1 the edge read-replica, the offline queue, mobile clients, CRDT co-editing of body_md, cross-workspace queries, and any sync path that does not pass through the appliance
What the edge replica reads later GET /sync?since=<seq>, a cacheable read-only events delta, plus a relay inbox of queued commands drained in arrival order when the appliance returns. The command log makes both additive

One agent_run per delegation or mention, with Linear’s state vocabulary plus the one it lacks, awaiting_approval (landscape §6). agent_activity rows are immutable, frozen-in-time snapshots — the transcript and the audit trail, for which comments are not a substitute. Liveness (teamwork §3.5): acknowledge within 10 s with a thought or the run shows unresponsive; 30 minutes idle marks it stale, recoverable by any activity; the Supervisor marks error at 900 s per item and hands it to Lead triage. Fields no leading tool records and Qren does: cost_usd, tokens_in, tokens_out, tool_calls, engine, model, artifacts[], outcome — an owner paying per token wants the cost on the work item.

Approvals. Every agent capability starts propose-only and graduates per capability (decision record §3). When an agent attempts an action above its trust level it emits an elicitation, the run moves to awaiting_approval, and an approval_link row points at an approval in instance.db. The card renders that row joined to its item: title, what the agent wants to do, the diff or message, and the run’s cost so far; deciding it writes decision/decided_at and emits the event the run resumes from. trust_level is recorded per action, at the moment it was attempted, so the audit answers “what was it allowed to do then”. An agent message can never satisfy an approval (teamwork §7).

qren work <verb> — engine-agnostic, reachable by any agent through the shell (ADR 0004), human-readable by default, --json for agents. Every verb accepts short_code | KEY-N | uuid | "fuzzy title".

Verb Example
add qren work add "Reconcile July invoices" --project nuchay --label finance
show qren work show "reconcile july"
list qren work list --mine --category started · --view "Needs a decision"
start qren work start "reconcile july"
done qren work done "reconcile july"
cancel qren work cancel 7g3k2pq --reason "client withdrew"
triage qren work triage accept 7g3k2pq · decline · duplicate <item> · snooze 2026-09-01
assign qren work assign "reconcile july" zeeshan
delegate qren work delegate "reconcile july" bookkeeper
handoff qren work handoff "reconcile july" eddeb --reason "crossing to design"
takeback qren work takeback "reconcile july"
comment qren work comment 7g3k2pq "waiting on the bank export"
link qren work link 7g3k2pq blocks 4m8ptz2 · related · duplicate_of

Output prints titles with the short code following in dim type; list prints no codes at all unless --codes is passed. Control API: each verb is one command kind through the Supervisor — item.create, item.update, item.transition, item.triage, item.assign, item.delegate, item.handoff, item.takeback, comment.create, relation.create, view.upsert, rank.set — and reads are GET /work/items?view=… and GET /sync?since=<seq>.

Views are data. Board and list are the same rows: view.spec_json holds {filters, group_by, sort, layout}, and manual board order lives in item_rank as a string rank per (view, item), so a drag is one row update and never rewrites another view’s order (landscape §5). Seeded per workspace: Triage, Mine, Today, In review, Delegated to agents, Waiting on approval, Rotting (open items by last_activity_at, with age badges). The thin slice renders one board with the seven categories as columns, the triage view with its four verbs, an item detail carrying description, comments, agent activity and the handoff card, and the shared approval queue — nothing else (decision record §1).

The work core ships empty. At install the Gardener reads the AOS work.db and proposes an import to the approval queue; the operator approves, amends or rejects (decision record §2). Against today’s 2,111 tasks (audit §1):

Rows Proposal
898 todo + 22 active Import. The 93 idle over 90 d land in backlog and are listed on a review card; the rest keep their status
done/cancelled within 90 d Import, already archived
done/cancelled older (of 1,173 total) Not imported. The old database is kept read-only in the workspace’s Drive
368+ test-fixture titles (audit §4) Never imported, matched by literal title against the known fixture list
4,059 exploring threads Never imported — 2 were ever promoted
37 inbox rows, 16 stuck islah-import tasks Into triage, where the 14-day expiry applies
739 dotted subtasks Become parent_id; the 15 rows whose prefix and real parent disagree follow parent_id, with the discrepancy on the card
1,003 project-less t# tasks project_id NULL, source = import; no bucket project is invented

ID mapping: every old id (t#187, aos#3, uc#1.1) goes to legacy_key, so old links, handoff notes and vault documents keep resolving; new short codes are minted at import; project prefixes become project.key with show_keys = 0 — available, invisible. Reversible: the import is one transaction into a fresh workspace.db and never writes to the source, so reverting is deleting one file, and every lifecycle rule runs dry for 14 days afterwards.

Open questions, and what the slice delivers

Section titled “Open questions, and what the slice delivers”
# Question
1 Attachments. The locked model has no attachment table but RunRec needs task↔file links (runrec-study §5); proposed here as an attachments JSON column of Drive-relative paths. Confirm, or admit a table
2 Cycles. Nobody has them, yet the stale-close exclusion names “an active project with a target date”. Is project.target_on enough, or is the exclusion cut in v1?
3 Leaving review — must it be a different principal than the one who entered it, or is that workspace policy rather than an invariant?
4 Comment vs chat. When comment.chat_ref is set and both exist, which is canonical?
5 Trust on a child item created by delegate: the parent’s, the delegate’s, or min(parent, delegate) (teamwork open question 4)?
6 Priority is kept as 0–4, a field 51% of current rows never received an opinion on (audit §1). Keep it, or derive urgency from due_on?
7 Duplicates at import. 1,503 near-duplicate title pairs exist, mostly false positives (audit §4). Does the Gardener propose merges, or only flag them for the first weekly review?
8 Status names are configurable but the seeded set is English. Where does localisation live?

The slice delivers every table above, the thirteen CLI entries, board and triage and item detail and the approval queue, all six lifecycle timers running dry for two weeks, CAS on owner and status, and the Gardener import. Deliberately later: the edge replica and offline queue; mobile; cycles, milestones, estimates, SLAs and custom fields; embedding-based duplicate detection; cross-workspace roll-ups (that is Home — a computed lens, never a container); rich-text co-editing; and any automatic creation path that skips triage or an approval.