Skip to content

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.

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 notifications

Tasks appear in the contact activity timeline, dashboard KPIs, and notification feed.

ColumnTypeRequiredDefaultNotes
idUUIDyesautoPrimary key
organization_idUUIDyesFK → organization
contact_idUUIDyesFK → contact
titleStringyesShort description
descriptionTextnonullOptional details
statusStringyes"open"open / done / dismissed
categoryStringyesSee categories below
sourceStringyes"manual"manual / ai (immutable)
priorityStringnonullurgent / high / medium / low
due_dateTIMESTAMPTZnonullDate or datetime; stored as UTC
reminder_sent_atTIMESTAMPTZnonullSet when reminder notification fires
assigned_toUUIDnonullFK → member
created_atTimestampyesnow()Auto-set
completed_atTimestampnonullSet when status → done
call_idUUIDnonullOptional call context

Five fixed categories — each task must have exactly one:

ValueLabel
follow-upFollow Up
callbackCallback
send-infoSend Info
meetingMeeting
generalGeneral

All enums use kebab-case serialization and implement label() and badge_variant() for UI rendering.

src/mods/task/types/
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 }

All routes are org-scoped and permission-gated.

MethodRouteDescription
GET/api/tasksList tasks with filters
POST/api/tasksCreate a task (source=manual, status=open)
MethodRouteDescription
GET/api/tasks/:idGet task details
PUT/api/tasks/:idUpdate task fields (not status/source)
POST/api/tasks/:id/doneMark done
POST/api/tasks/:id/dismissDismiss
POST/api/tasks/:id/reopenReopen
DELETE/api/tasks/:idDelete
MethodRouteDescription
GET/api/contacts/:id/tasksAll 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.

Pass as query parameters to GET /api/tasks:

ParamTypeDescription
statusstringFilter by status
categorystringFilter by category
contact_idUUIDFilter by contact
assigned_toUUIDFilter by assignee
sortstringOmit for newest-first (default), "due_date" for due-date ascending
limitnumberMax 200, default 50
offsetnumberPagination offset

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).

src/mods/task/jobs/send_task_reminders_job.rs
// Query: status = "open" AND due_date <= now AND reminder_sent_at IS NULL

For each matching task, the job resolves a notification target using a strict three-tier strategy:

  1. Assigned member — if assigned_to is set, resolve that member’s user_id (scoped by organization_id for multi-tenancy safety) and target them
  2. Contact’s assigned members — if no assignee, query the contact_user join table for the contact’s assigned members, then resolve their user_ids in a batch query
  3. Skip — if neither tier produces a target, the job logs a debug message, marks reminder_sent_at to 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.

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" to DateTime<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_short and format_datetime_long render human-friendly strings

Smart display: midnight times render as date-only ("Mar 28"), times render with AM/PM ("Mar 28, 3:00 PM").

Located in src/shared/utils/date/:

FunctionPurpose
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)").

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.

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.).

Located in src/mods/task/services/create_tasks_from_call_service.rs:

  1. Fetch call and transcription — skips silently if no transcription or no linked contact
  2. Idempotency check — queries for existing AI tasks on this call (scoped to organization). Skips if any exist
  3. LLM structured output — sends the transcription to the ExtractTasks AI area model (default: google/gemini-3.1-pro-preview) with a system prompt that extracts concrete commitments
  4. Task creation — inserts up to 5 tasks in a database transaction
  5. AI usage logging — logs the LLM call via spawn_log_ai_usage with feature ExtractTasks

The LLM returns structured JSON matching this schema:

FieldTypeDescription
titleStringConcise action description (max 255 chars)
categoryStringfollow-up, callback, send-info, meeting, or general
descriptionString?Context from the conversation (max 2000 chars)
priorityString?urgent, high, medium, or low (default: medium)
due_date_offset_daysi64?Days from now (clamped 0–365). Due time set to 17:00 in org timezone
FieldValue
source"ai"
status"open"
call_idThe originating call’s ID
assigned_toContact owner (if exactly one), otherwise null
due_dateCalculated from due_date_offset_days at 17:00 org-local time, handling DST

The ExtractTasks variant is registered in AiArea and AiUsageFeature:

src/mods/ai/types/ai_models_type.rs
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”.

AI-generated tasks display visual indicators in the UI:

  • Task card — violet sparkle icon (✨) before the title and an “AI” badge (BadgeVariant::Secondary with violet styling)
  • Task details — sparkle icon and “AI” badge next to the source field, plus a “View Call” link when call_id is present that navigates to the source call

The TaskService in src/mods/task/services/task_service.rs provides shared query logic:

  • get_tasks(db, org_id, filters) — Fetches tasks with INNER JOIN contact and LEFT JOIN member + user to populate contact_name and assigned_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.

Located in src/mods/task/components/:

ComponentPurpose
TaskCardCard using shared EntityCard shell — title, status/priority/AI badges, category, linked contact name, relative due date, assignee
TaskListComponentStatus-tabbed list with EntityGrid for responsive card layout
TaskDetailsComponentSectioned detail view (Details, Schedule, Description) with color-coded action buttons and icons
TaskFilterBarCategory and sort dropdowns for the task list page header
EditTaskComponentFull edit form with DateTimePicker
CreateTaskComponentCreate form with DateTimePicker
TaskInlineFormCompact form for contact right panel (uses align_right on picker)

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 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.

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")
}

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.

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.

A presets-first popover component in src/ui/date_time_picker_ui.rs that replaces native <input type="datetime-local"> inputs.

Props:

PropTypeDefaultDescription
valueOption<String>None"YYYY-MM-DDTHH:MM" or "YYYY-MM-DD"
on_changeEventHandler<Option<String>>Fires on selection or clear
modeDateTimeModeDateTimeDateTime or DateOnly
placeholder&'static str"Pick a date..."Trigger text when empty
classString""Extra CSS classes on trigger
align_rightboolfalseAligns 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.

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.

ViewRouteDescription
TaskListView/tasksStatus tabs, category/sort filter bar, infinite-scroll paginated cards
TaskDetailsView/tasks/:idSectioned detail view with color-coded actions
CreateTaskView/tasks/createFull create form
src/bases/auth/types/resources.rs
Task {
Collection { List, Create }
Instance { View, Update, Delete }
}

Status transitions (done, dismiss, reopen) require the Update permission.

  • Twilio (post-call pipeline)create_tasks_from_call runs after transcription in process_twilio_recording, extracting AI tasks from call content
  • AI moduleAiArea::ExtractTasks and AiUsageFeature::ExtractTasks track model configuration and usage for task extraction
  • Dashboard — Open task counts replace todo counts in KPI cards (TaskStatusCount)
  • NotificationsTaskCompleted variant with backward-compatible #[serde(alias = "todo")] deserialization; SendTaskRemindersJob sends Task category 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" for due_date and reset reminder_sent_at on changes
  • Background jobsget_task_jobs() returns SendTaskRemindersJob, registered in src/app/jobs/app_jobs.rs

This module replaces the entire src/mods/todo/ module. The migration:

  1. Creates the task table with indexes
  2. Drops three tables: todo, todo_type_tool, todo_type
  3. No data migration — existing todos are not carried over

Run just migrate to apply, then just generate to regenerate SeaORM schemas.