Tasks
The task module provides lightweight task management for tracking follow-ups, callbacks, and action items tied to contacts. Tasks can be created manually by users or automatically extracted from call transcriptions by AI. AI execution of complex workflows is handled by Plans.
How It Works
Section titled “How It Works”Manual path: User creates task → status "open", source "manual" → Linked to contact, optionally assigned to team member → Optional due date with time for reminders
AI path (post-call): Call ends → recording processed → transcription ready → create_tasks_from_call(call_id, org_id) → LLM extracts action items as structured JSON → Tasks created with source "ai", linked to call → Auto-assigned to contact owner (if exactly one)
User works the task: → Mark done → status "done", completed_at set → Dismiss → status "dismissed" → Reopen → status "open", completed_at cleared
Background (every 60s): → SendTaskRemindersJob polls for open tasks past due_date → Sends notification to assigned member (or contact's members) → Sets reminder_sent_at to prevent duplicate notificationsTasks appear in the contact activity timeline, dashboard KPIs, and notification feed.
Data Model
Section titled “Data Model”Task Table
Section titled “Task Table”| Column | Type | Required | Default | Notes |
|---|---|---|---|---|
id | UUID | yes | auto | Primary key |
organization_id | UUID | yes | — | FK → organization |
contact_id | UUID | yes | — | FK → contact |
title | String | yes | — | Short description |
description | Text | no | null | Optional details |
status | String | yes | "open" | open / done / dismissed |
category | String | yes | — | See categories below |
source | String | yes | "manual" | manual / ai (immutable) |
priority | String | no | null | urgent / high / medium / low |
due_date | TIMESTAMPTZ | no | null | Date or datetime; stored as UTC |
reminder_sent_at | TIMESTAMPTZ | no | null | Set when reminder notification fires |
assigned_to | UUID | no | null | FK → member |
created_at | Timestamp | yes | now() | Auto-set |
completed_at | Timestamp | no | null | Set when status → done |
call_id | UUID | no | null | Optional call context |
Categories
Section titled “Categories”Five fixed categories — each task must have exactly one:
| Value | Label |
|---|---|
follow-up | Follow Up |
callback | Callback |
send-info | Send Info |
meeting | Meeting |
general | General |
All enums use kebab-case serialization and implement label() and badge_variant() for UI rendering.
pub enum TaskStatus { Open, Done, Dismissed }pub enum TaskCategory { FollowUp, Callback, SendInfo, Meeting, General }pub enum TaskPriority { Urgent, High, Medium, Low }pub enum TaskSource { Manual, Ai }API Endpoints
Section titled “API Endpoints”All routes are org-scoped and permission-gated.
Collection
Section titled “Collection”| Method | Route | Description |
|---|---|---|
GET | /api/tasks | List tasks with filters |
POST | /api/tasks | Create a task (source=manual, status=open) |
Instance
Section titled “Instance”| Method | Route | Description |
|---|---|---|
GET | /api/tasks/:id | Get task details |
PUT | /api/tasks/:id | Update task fields (not status/source) |
POST | /api/tasks/:id/done | Mark done |
POST | /api/tasks/:id/dismiss | Dismiss |
POST | /api/tasks/:id/reopen | Reopen |
DELETE | /api/tasks/:id | Delete |
Contact-scoped
Section titled “Contact-scoped”| Method | Route | Description |
|---|---|---|
GET | /api/contacts/:id/tasks | All tasks for a contact |
The contact-scoped endpoint enforces both Task:Collection:List and contact-level access control via check_contact_access. You must have Contact:Instance:View (or ViewAssigned if the contact is assigned to you) to retrieve tasks for a given contact. See Contact Access Control for details.
List Filters
Section titled “List Filters”Pass as query parameters to GET /api/tasks:
| Param | Type | Description |
|---|---|---|
status | string | Filter by status |
category | string | Filter by category |
contact_id | UUID | Filter by contact |
assigned_to | UUID | Filter by assignee |
sort | string | Omit for newest-first (default), "due_date" for due-date ascending |
limit | number | Max 200, default 50 |
offset | number | Pagination offset |
Due-Date Reminders
Section titled “Due-Date Reminders”How Reminders Work
Section titled “How Reminders Work”A background job (SendTaskRemindersJob) runs every 60 seconds and checks for open tasks that are past their due_date but haven’t been notified yet (reminder_sent_at IS NULL).
// Query: status = "open" AND due_date <= now AND reminder_sent_at IS NULLFor each matching task, the job resolves a notification target using a strict three-tier strategy:
- Assigned member — if
assigned_tois set, resolve that member’suser_id(scoped byorganization_idfor multi-tenancy safety) and target them - Contact’s assigned members — if no assignee, query the
contact_userjoin table for the contact’s assigned members, then resolve theiruser_ids in a batch query - Skip — if neither tier produces a target, the job logs a debug message, marks
reminder_sent_atto prevent reprocessing, and moves on. It never falls back to notifying all organization members
When a target is found, the job sends a notification via notify() with category TaskReminder and sets reminder_sent_at.
Updating a task’s due_date resets reminder_sent_at to None, so the reminder fires again for the new time.
Timezone Handling
Section titled “Timezone Handling”Dates are stored as UTC (TIMESTAMPTZ) and displayed in the organization’s timezone:
- Input →
parse_local_datetime(s, &tz)converts org-local"YYYY-MM-DDTHH:MM"or"YYYY-MM-DD"toDateTime<FixedOffset>in UTC - Output →
format_local_datetime(&dt, &tz)converts back to org-local"YYYY-MM-DDTHH:MM"for form pre-population - Display →
format_datetime_shortandformat_datetime_longrender human-friendly strings
Smart display: midnight times render as date-only ("Mar 28"), times render with AM/PM ("Mar 28, 3:00 PM").
Date Utilities
Section titled “Date Utilities”Located in src/shared/utils/date/:
| Function | Purpose |
|---|---|
parse_local_datetime(s, tz) | Parse "YYYY-MM-DDTHH:MM" or "YYYY-MM-DD" in org timezone → DateTime<FixedOffset> |
format_local_datetime(dt, tz) | DateTime<FixedOffset> → "YYYY-MM-DDTHH:MM" in org timezone |
format_datetime_short(s) | Short display: "Mar 28, 3:00 PM" or "Mar 28" |
format_datetime_long(s) | Long display: "Mar 28, 2026 3:00 PM" or "Mar 28, 2026" |
format_relative_time(s) | Human-friendly relative time: "3d ago", "2w ago", "just now" |
parse_ymd(s) | Parse "YYYY-MM-DD" → NaiveDate |
format_relative_time (in src/shared/utils/format_relative_time.rs) returns time buckets: "just now" (< 1 min), "45m ago", "8h ago", "3d ago", "2w ago", "6mo ago", ">1y ago", or "Never" for None. Used by task cards, contact tasks tab, and task details (which shows both relative and absolute: "2w ago (April 14, 2026 at 3:42 PM)").
AI Task Extraction from Calls
Section titled “AI Task Extraction from Calls”After a call recording is processed and transcribed, the post-call pipeline calls create_tasks_from_call to extract actionable follow-ups using an LLM.
Pipeline Integration
Section titled “Pipeline Integration”The function is called in process_twilio_recording (in src/mods/twilio/utils/process_twilio_recording_util.rs), after call analysis and plan creation:
if let Err(e) = crate::mods::task::create_tasks_from_call(call_id, organization_id).await { tracing::error!(error = %e, call_id = %call_id, "Failed to create tasks from call");}Failures are logged but don’t block the rest of the post-call pipeline (report emails, etc.).
How Extraction Works
Section titled “How Extraction Works”Located in src/mods/task/services/create_tasks_from_call_service.rs:
- Fetch call and transcription — skips silently if no transcription or no linked contact
- Idempotency check — queries for existing AI tasks on this call (scoped to organization). Skips if any exist
- LLM structured output — sends the transcription to the
ExtractTasksAI area model (default:google/gemini-3.1-pro-preview) with a system prompt that extracts concrete commitments - Task creation — inserts up to 5 tasks in a database transaction
- AI usage logging — logs the LLM call via
spawn_log_ai_usagewith featureExtractTasks
Extracted Task Schema
Section titled “Extracted Task Schema”The LLM returns structured JSON matching this schema:
| Field | Type | Description |
|---|---|---|
title | String | Concise action description (max 255 chars) |
category | String | follow-up, callback, send-info, meeting, or general |
description | String? | Context from the conversation (max 2000 chars) |
priority | String? | urgent, high, medium, or low (default: medium) |
due_date_offset_days | i64? | Days from now (clamped 0–365). Due time set to 17:00 in org timezone |
Task Properties Set by AI Extraction
Section titled “Task Properties Set by AI Extraction”| Field | Value |
|---|---|
source | "ai" |
status | "open" |
call_id | The originating call’s ID |
assigned_to | Contact owner (if exactly one), otherwise null |
due_date | Calculated from due_date_offset_days at 17:00 org-local time, handling DST |
AI Area Configuration
Section titled “AI Area Configuration”The ExtractTasks variant is registered in AiArea and AiUsageFeature:
pub const EXTRACT_TASKS: &str = "google/gemini-3.1-pro-preview";
// AiArea::ExtractTasks — key: "extract_tasks", complexity: "Medium"You can override the model per-organization in the AI settings page under “Extract Tasks from Call”.
UI Indicators
Section titled “UI Indicators”AI-generated tasks display visual indicators in the UI:
- Task card — violet sparkle icon (✨) before the title and an “AI” badge (
BadgeVariant::Secondarywith violet styling) - Task details — sparkle icon and “AI” badge next to the source field, plus a “View Call” link when
call_idis present that navigates to the source call
Services
Section titled “Services”The TaskService in src/mods/task/services/task_service.rs provides shared query logic:
get_tasks(db, org_id, filters)— Fetches tasks withINNER JOIN contactandLEFT JOIN member + userto populatecontact_nameandassigned_to_name. Uses aliased column selection for performance.get_task_by_id(db, org_id, task_id)— Single fetch with same joins, org-scoped.set_task_status(db, model, new_status, completed_at)— Updates status and refetches with joins.
UI Components
Section titled “UI Components”Task Components
Section titled “Task Components”Located in src/mods/task/components/:
| Component | Purpose |
|---|---|
TaskCard | Card using shared EntityCard shell — title, status/priority/AI badges, category, linked contact name, relative due date, assignee |
TaskListComponent | Status-tabbed list with EntityGrid for responsive card layout |
TaskDetailsComponent | Sectioned detail view (Details, Schedule, Description) with color-coded action buttons and icons |
TaskFilterBar | Category and sort dropdowns for the task list page header |
EditTaskComponent | Full edit form with DateTimePicker |
CreateTaskComponent | Create form with DateTimePicker |
TaskInlineForm | Compact form for contact right panel (uses align_right on picker) |
EntityCard Migration
Section titled “EntityCard Migration”TaskCard uses the shared EntityCard shell. The contact name is a clickable link to ContactDetailsView (with stop_propagation to avoid triggering card navigation). Due dates display as relative time ("2w ago", "3d ago") via format_relative_time.
TaskListComponent renders status tabs (Open / Done / Dismissed) with count badges, then displays filtered tasks in a vertical card list with a dashed empty state when no tasks match the active tab.
TaskFilterBar
Section titled “TaskFilterBar”TaskFilterBar renders category and sort dropdowns in the task list header, next to the “New Task” button. It accepts Signal<Option<TaskCategory>> and Signal<TaskListSort> and fires change callbacks.
When filters change, TaskListView resets pagination state (accumulated_tasks, current_page, total_pages) and refetches. Infinite scroll passes the same filter/sort signals so paginated loads stay consistent.
TaskListSort
Section titled “TaskListSort”A client-side enum in src/mods/task/types/task_list_sort_type.rs:
pub enum TaskListSort { CreatedAtDesc, // label: "Newest First", api_value: None (server default) DueDateAsc, // label: "Due Date", api_value: Some("due_date")}TaskDetailsComponent
Section titled “TaskDetailsComponent”The detail view organizes fields into three sections with uppercase headers:
- Details — category, contact (linked), priority, source, source call
- Schedule — due date (relative + absolute), assigned to, created, completed
- Description — full text
Action buttons are color-coded with icons: “Mark Done” (green, CircleCheck), “Dismiss” (muted, CircleX), “Reopen” (outline, RotateCcw). Buttons are permission-gated — hidden entirely if the user lacks TaskInstancePermission::Update.
Contact Tasks Tab
Section titled “Contact Tasks Tab”ContactTasksTab displays tasks within the contact detail view’s right panel. The tab trigger shows an open task count badge (e.g., “Tasks ⓵”) counting tasks where status == Open.
Each task renders as a collapsible card with inline expansion:
- Collapsed — title, status badge, priority, relative due date. Click anywhere to expand
- Expanded — metadata grid (category, priority, due date, assigned to, source, created), description, action buttons (Mark Done / Dismiss / Reopen), and a “View Full Details” link
- Edit mode — hover-visible pencil icon opens inline editing
Expansion and edit mode are mutually exclusive — opening one closes the other. Empty state shows an “Add your first task” CTA.
The tab includes an “Add Task” button that toggles a TaskInlineForm for creating tasks directly in context.
DateTimePicker
Section titled “DateTimePicker”A presets-first popover component in src/ui/date_time_picker_ui.rs that replaces native <input type="datetime-local"> inputs.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
value | Option<String> | None | "YYYY-MM-DDTHH:MM" or "YYYY-MM-DD" |
on_change | EventHandler<Option<String>> | — | Fires on selection or clear |
mode | DateTimeMode | DateTime | DateTime or DateOnly |
placeholder | &'static str | "Pick a date..." | Trigger text when empty |
class | String | "" | Extra CSS classes on trigger |
align_right | bool | false | Aligns popover to right edge (for narrow containers) |
The picker shows quick presets first (Today, Tomorrow, Next Monday, Next Week) with a calendar expansion. In DateTime mode, you get a 15-minute time grid and a “No time” option for date-only values.
Calendar
Section titled “Calendar”A month-view calendar component in src/ui/calendar_ui.rs used inside DateTimePicker. Supports month navigation with left/right arrows and highlights the selected date and today.
| View | Route | Description |
|---|---|---|
TaskListView | /tasks | Status tabs, category/sort filter bar, infinite-scroll paginated cards |
TaskDetailsView | /tasks/:id | Sectioned detail view with color-coded actions |
CreateTaskView | /tasks/create | Full create form |
Authorization
Section titled “Authorization”Task { Collection { List, Create } Instance { View, Update, Delete }}Status transitions (done, dismiss, reopen) require the Update permission.
Cross-Module Integration
Section titled “Cross-Module Integration”- Twilio (post-call pipeline) —
create_tasks_from_callruns after transcription inprocess_twilio_recording, extracting AI tasks from call content - AI module —
AiArea::ExtractTasksandAiUsageFeature::ExtractTaskstrack model configuration and usage for task extraction - Dashboard — Open task counts replace todo counts in KPI cards (
TaskStatusCount) - Notifications —
TaskCompletedvariant with backward-compatible#[serde(alias = "todo")]deserialization;SendTaskRemindersJobsendsTaskcategory notifications for overdue tasks - Contact timeline — Tasks appear as activity entries via
timeline_task_entry_component - Assistant — AI create/update task tools accept
"YYYY-MM-DDTHH:MM"or"YYYY-MM-DD"fordue_dateand resetreminder_sent_aton changes - Background jobs —
get_task_jobs()returnsSendTaskRemindersJob, registered insrc/app/jobs/app_jobs.rs
Migration from Todos
Section titled “Migration from Todos”This module replaces the entire src/mods/todo/ module. The migration:
- Creates the
tasktable with indexes - Drops three tables:
todo,todo_type_tool,todo_type - No data migration — existing todos are not carried over
Run just migrate to apply, then just generate to regenerate SeaORM schemas.