TASK_ATTRIBUTES_DESIGN.md8.4 KBView on GitHub # Task Attributes
Structured semantic signals on a task, generated by the agent alongside the task itself.
**v1 scope:** two attributes — `promised` and `deadline`. Behavior: **capture + display + ranking**
(reminders / recurring reschedule are explicitly out of scope for v1).
## Current state
A task (`user_tasks`, [aop-schema.ts:904](../../../server/src/db/aop-schema.ts#L904)) carries only two
structured qualifiers: `taskType` (enum) and `dueDate` (a scheduling timestamp). The one jsonb column,
`taskActionData`, is the **action payload** (which thread / slack channel / calendar event), not metadata.
There is no home for *why a task matters*:
- A **commitment** ("I told them I'd send the deck Friday") is invisible — indistinguishable from any other follow-up.
- A **hard external deadline** is actively pushed into freetext. The due-date instruction says verbatim:
*"Deadlines are NOT blockers — use 'NOW' and put the deadline in notes."*
([createTaskTool.ts:58](../../../server/src/mastra/tools/tasks/createTaskTool.ts#L58)). So a real deadline
lands in `notes` where nothing can rank, badge, or warn on it.
Ranking today is **LLM-in-prompt**, not code. Code-level ordering is `dueDate`-only everywhere
([listTasksTool.ts:315](../../../server/src/mastra/tools/tasks/listTasksTool.ts#L315),
[user-tasks.ts:124](../../../server/src/trpc/routes/user-tasks.ts#L124)). The actual scoring formula lives in
the daily-agenda skill prompt [SKILL.md](../../../server/.claude/skills/tasks/SKILL.md) (Step 2 — Rank):
`priority_weight × log(dealValue + 1) × recency_bonus`. Neither `priority` nor `dealValue` is a task column —
both are joined from the conversation. So a new task-sourced ranking signal means editing that prompt formula.
## Proposed changes
Add one jsonb column, `attributes`, holding a typed discriminated-union **array** — orthogonal to
`taskType`/`dueDate`. A task can be a `follow-up`, due Monday, `promised`, AND tied to a `deadline` at once.
```ts
// apps/server/src/db/aop-schema.ts
export type TaskAttribute =
| { kind: 'promised'; to?: string; quote?: string; madeOn?: string }
| { kind: 'deadline'; date: string; hard: boolean; label?: string };
// on user_tasks, alongside taskActionData:
attributes: jsonb('attributes').$type<TaskAttribute[]>().notNull().default([]),
```
- **`promised`** — an explicit commitment the user made. `to` = who it was promised to, `quote` = the verbatim
promise, `madeOn` = ISO date it was made. Boosts ranking; never auto-cancelled; badge on the card.
- **`deadline`** — a real external date, distinct from `dueDate` (which is *when we act*). `date` = the deadline,
`hard` = true for a fixed date (contract expiry, RFP close) vs a soft target, `label` = what's due
("RFP submission"). Boosts ranking as the date nears; badge on the card. Replaces the notes-stuffing rule.
No DB CHECK constraint on the jsonb (structure is validated at the zod layer, matching `taskActionData`).
### Generation (write path)
The agent produces attributes through the same tool it already uses. `AttributesSchema` (zod discriminated-union
array, one rich `.describe()` per kind — the descriptions *are* the agent's instructions) is added to
[schemas.ts](../../../server/src/mastra/tools/tasks/schemas.ts), wired into `createTaskTool` and `updateTaskTool`
inputs, and persisted in both write paths (agent tool + the `createTask` tRPC route). The skill instructions
([conversation-action-management.ts](../../../server/src/mastra/skills/event-execution/conversation-action-management.ts))
are updated so the deadline guidance attaches a `deadline` attribute instead of writing to `notes`, and a short
"when to attach promised / deadline" section is added. `listTasksTool`'s `<existing_tasks>` block surfaces
attributes so the agent can see and update them (mark a promise kept, drop a passed deadline).
### Display + ranking (read path)
Both tRPC reads return the full row, so the new column flows through automatically. Frontend mirror types
(`HydratedUserTask`, local `TaskCardTask`) gain `attributes`, and `serializeTaskForStream` includes it. The card
([TaskKanbanCard.tsx](components/TaskKanbanCard.tsx)) renders a `promised` / `deadline` chip in the row-1 badge
cluster next to the due-date badge. Ranking: the [SKILL.md](../../../server/.claude/skills/tasks/SKILL.md)
Step 2 formula gets an attribute multiplier (e.g. `promised` and near/overdue `deadline` bump the score) so
committed and deadline-bound work rises in the daily agenda.
## Critical files
| File | Change |
|---|---|
| [aop-schema.ts](../../../server/src/db/aop-schema.ts) | `TaskAttribute` union + `attributes` column |
| `apps/server/src/db/migrations/0051_add_user_task_attributes.sql` | new migration |
| [schemas.ts](../../../server/src/mastra/tools/tasks/schemas.ts) | `AttributesSchema` + field description; add to `serializeTaskForStream` |
| [createTaskTool.ts](../../../server/src/mastra/tools/tasks/createTaskTool.ts) | accept + persist `attributes` |
| [updateTaskTool.ts](../../../server/src/mastra/tools/tasks/updateTaskTool.ts) | accept + persist `attributes` |
| [listTasksTool.ts](../../../server/src/mastra/tools/tasks/listTasksTool.ts) | surface attributes in `<existing_tasks>` |
| [conversation-action-management.ts](../../../server/src/mastra/skills/event-execution/conversation-action-management.ts) | replace notes-deadline rule; teach attribute usage |
| [user-tasks.ts](../../../server/src/trpc/routes/user-tasks.ts) | persist `attributes` in `createTask` route |
| [userTasksSlice.ts](slice/userTasksSlice.ts) | `TaskAttribute` type + field on `HydratedUserTask` |
| [TaskKanbanCard.tsx](components/TaskKanbanCard.tsx) | badge + `attributes` on `TaskCardTask` |
| [SKILL.md](../../../server/.claude/skills/tasks/SKILL.md) | Step 2 ranking formula |
## Implementation plan
### Phase 1 — Schema + column
- [ ] Add `TaskAttribute` union to `aop-schema.ts` and the `attributes` jsonb column (`notNull().default([])`) in the NEW COLUMNS block.
- [ ] Add migration `0051_add_user_task_attributes.sql` (`ADD COLUMN attributes jsonb NOT NULL DEFAULT '[]'::jsonb`).
- [ ] Verify: `pnpm types` in `apps/server`; apply the migration against the dev DB and confirm the column exists.
### Phase 2 — Generation
- [ ] Add `AttributesSchema` + a `TaskFieldDescriptions.attributes` entry to `schemas.ts`; include `attributes` in `serializeTaskForStream`.
- [ ] Wire `attributes` into `createTaskTool` / `updateTaskTool` inputs and their insert/update.
- [ ] Persist `attributes` in the `createTask` tRPC route.
- [ ] Rewrite the deadline rule in `conversation-action-management.ts` (deadline → attribute, not notes) and add a "when to attach promised / deadline" note. Surface attributes in `listTasksTool`.
- [ ] Verify: unit test that a create call with a `deadline`/`promised` attribute round-trips to the row; `pnpm types`.
### Phase 3 — Display
- [ ] Add `TaskAttribute` + `attributes` to `HydratedUserTask` and local `TaskCardTask`.
- [ ] Render a promised / deadline chip in the `TaskKanbanCard` row-1 badge cluster (deadline chip shows relative urgency; both use `cursor-pointer` if interactive; standard Tailwind text sizes only).
- [ ] Verify: card renders both badges for a seeded task; `pnpm types` in `apps/mail`.
### Phase 4 — Ranking
- [ ] Add an attribute multiplier to the Step 2 formula in `SKILL.md` (promised bump; deadline bump scaled by proximity/overdue).
- [ ] Verify: run the daily-agenda skill against a fixture where a promised/deadline task and a plain follow-up tie on dueDate — the attributed task ranks higher.
## Verification (end to end)
1. Trigger `update-next-steps-and-tasks` on a conversation whose latest email contains an explicit promise and a hard date.
2. Confirm the created row's `attributes` holds `promised` + `deadline` (not buried in `notes`).
3. Confirm the task card shows both badges and the daily agenda ranks it above an equivalent plain follow-up.
## Gotchas
- Frontend re-declares its own copies of the task types ([userTasksSlice.ts](slice/userTasksSlice.ts)) — keep `TaskAttribute` in sync with the server union by hand.
- The `user_tasks` status CHECK constraint already drifted (`planned_followup` vs `recommended`, [aop-schema.ts:1034](../../../server/src/db/aop-schema.ts#L1034)) — do not add a CHECK on the new jsonb column; validate at the zod layer only.
- `deadline.date` is deliberately *not* `dueDate`. Don't collapse them: `dueDate` is when we act (may be "NOW"); `deadline.date` is the external constraint.