Task ↔ Email Links

Design proposal · taskservice, notificationservicev2, task frontend, email frontend · 2026-10-15

Recommendation in one line. Link tasks to email threads, store the link in taskservice as a generic task_links row, and let both the email detail view and the task view read from taskservice. Notificationservicev2 only gains two aggregate fields on the thread.

1. Where things stand

Tasks

One single-valued entity link (entityType, entityCode, entityNumber) on the task row, validated at write time by calling the owning service. List is filterable by entity type and code.

Email threads

A thread groups an outbound email with its replies and bounces. It also carries one entity link. The thread detail page in the email frontend is the "email details view".

Between them

Nothing. Taskservice has no gRPC client for notificationservice, and notificationservice has none for taskservice.

2. Decisions

Granularity: thread, not message. A task relates to a conversation, not to one reply. Message-level links multiply rows without adding meaning, and a message always resolves to its thread. A nullable message_code can be added later if a specific message ever needs pinning.
Owner: taskservice. The link is an attribute of the task, exactly like the entity link. One write path, one permission model, and the task list can be filtered by thread code with a plain join. Storing it in notificationservicev2 would force that service to validate task codes and would split "what is this task about" across two databases.
Shape: generic link table, scoped to EmailThread for now. Mirrors the existing EntityRef {type, code}. Documents or comments can be linked later without a new table or new endpoints. The alternative, a dedicated task_email_threads table, costs the same today and one more table next time.

3. Data model (taskservice)

ColumnTypeNotes
idserial
task_idintFK → tasks.id, ON DELETE CASCADE
link_typevarcharenum, only EmailThread initially
link_codevarcharthe thread code, e.g. thr_a1b2c3d4e5
owner, tenant_hierarchyvarcharsame defaults as tasks (RLS)
created_by, created_atwho linked it and when
Unique (task_id, link_type, link_code) · Index (link_type, link_code) for the "tasks for this thread" lookup

Taskservice stores only the code. Subject, status and activity of the thread are fetched live, so nothing goes stale.

4. API changes

Protorepo · taskservice/v1

message TaskLink {
  string type = 1;   // "EmailThread"
  string code = 2;
}

message Task {
  ...
  repeated TaskLink links = 22;   // always returned, like category_slugs
}

message CreateTaskRequest {
  ...
  repeated TaskLink links = 12;   // "create task from this email" in one call
}

message AddTaskLinkRequest    { string code = 1; TaskLink link = 2; }
message AddTaskLinkResponse   { Task task = 1; }
message RemoveTaskLinkRequest { string code = 1; string link_type = 2; string link_code = 3; }
message RemoveTaskLinkResponse{ Task task = 1; }

service TaskService {
  ...
  rpc AddTaskLink(AddTaskLinkRequest) returns (AddTaskLinkResponse) {}
  rpc RemoveTaskLink(RemoveTaskLinkRequest) returns (RemoveTaskLinkResponse) {}
}

Protorepo · notificationservice/v1

message EmailThread {
  ...
  uint32 message_count = 10;
  optional google.protobuf.Timestamp last_message_at = 11;
}

Cheap aggregates on the thread query. They let the task view show activity without loading messages and are useful on the thread list as well.

REST · taskservice

MethodPathPurpose
POST/tasks/:code/linksBody {type, code}. Validates the thread exists via notificationservice gRPC GetEmailThread. Returns the task.
DELETE/tasks/:code/links/:type/:linkCodeUnlink. Returns the task.
GET/tasks?filters=links.code==thr_xTasks for a thread. New filterable columns links.type, links.code in the paginate config.
POST/tasksAccepts links[] on create.

REST · notificationservicev2

No new endpoints. The email frontend calls taskservice directly, as the entity task widget already does for claims and policies. Avoids a notificationservice → taskservice dependency.

Validation and audit

5. Request flow

Email frontendthread detail Task frontendtask detail / edit taskservicetask_links notificationserviceemail_threads GET /tasks?filters=links.code==thr_x POST /tasks/:code/links GetEmailThread (validate) GET /email-threads/:code, messages GET /email-threads?filters=code$in:… (subject, status, activity)

6. UI

Email thread detail → "Tasks" panel

Reuses the existing entity task list widget, pinned to the thread code instead of an entity. The create button prefills the thread link and copies the thread's entity ref into the task's entity link, so a task created from a claim email also lands on that claim.

Tasks (3)+ Create task from this email
SubjectStatusPriorityAssigneeDue
Request missing documentsIn progressHighA. Müller2026-10-20
Confirm coverage questionTo doMedium——
Follow up on bounceDoneLowJ. Smith2026-10-10

Task detail / edit → "Emails" section

Lists linked threads with subject, entity number, thread status, last message date and message count. Each row links to the thread page. "Link email" opens a thread search backed by the notificationservice list endpoint. Unlink calls the delete endpoint.

Emails (2)+ Link email
SubjectEntityStatusLast messageMessages
Re: Claim CLM-2026-0042 documentsCLM-2026-0042open2 h ago5
Your policy POL-2024-000123POL-2024-000123closed3 d ago2

Threads are fetched in one call with a code $in filter. A code that does not come back is rendered as "unavailable" rather than failing the task load.

7. Edge cases

CaseBehaviour
Thread deleted or not visibleTask view shows the code marked unavailable. Link stays until someone removes it.
Task deletedLinks cascade.
Thread reassigned to another entityThe task's own entity link is independent and stays as is. Deliberate: the email link is additional to the entity link.
Same thread linked twiceUnique constraint → 409 or idempotent no-op. Recommend idempotent.
Thread from another tenantTaskservice validates with the caller's headers, so the lookup is tenant-scoped and returns 404.

8. Rollout order

  1. Protorepo: TaskLink, new rpcs, thread aggregate fields. Merge first, PR pipelines compile against master.
  2. Taskservice: migration, entity, notificationservice client + resolver, endpoints, list filter, changelog, e2e specs.
  3. Notificationservicev2: message_count and last_message_at on the thread.
  4. Frontends: tasks panel on the thread page, emails section on the task page.

9. Open question for the team

Generic task_links versus a dedicated task_email_threads table. The recommendation is generic: identical cost now, and it spares a second table and a second set of endpoints the next time something needs to hang off a task.