How Reminders Are Scheduled, Stored, and Triggered in LifeTrace: A Complete Technical Guide

LifeTrace implements reminders as a three-layer system where user-defined time offsets are persisted as JSON on the Todo model, scheduled as individual APScheduler date jobs, and triggered through a dedicated execution handler that builds in-memory notifications.

In the freeu-group/lifetrace repository, the reminder system bridges persistent storage, background job scheduling, and real-time notification delivery. This article breaks down exactly how the application stores reminder offsets in the database, schedules them using APScheduler with SQLite persistence, and triggers the final notification while handling deduplication and dismissal states.

How Reminders Are Stored in the Database

The Todo Model and reminder_offsets Field

Reminders are stored directly on the Todo entity as a JSON-encoded list of integers. In lifetrace/storage/models.py, the Todo class defines the reminder_offsets column as a string field that accepts raw JSON:


# lifetrace/storage/models.py

class Todo(Base):
    __tablename__ = "todos"
    # ... other fields ...

    reminder_offsets = Column(String, default="[]")  # Stores JSON like "[5, 15, 30]"

This design allows each Todo to carry its own reminder configuration—specifically, the number of minutes before the Todo’s scheduled time (due date, deadline, or start time) that a notification should fire.

Normalization and JSON Serialization

Before offsets reach the database, they pass through validation and serialization utilities in lifetrace/storage/todo_manager_utils.py. The _normalize_reminder_offsets function ensures the input is a list of non-negative integers, while _serialize_reminder_offsets converts the clean list to a JSON string:


# lifetrace/storage/todo_manager_utils.py

def _normalize_reminder_offsets(offsets):
    """Ensure offsets is a list of non-negative integers."""
    if not offsets:
        return []
    return [int(o) for o in offsets if int(o) >= 0]

def _serialize_reminder_offsets(offsets):
    """Convert list to JSON string for storage."""
    return json.dumps(_normalize_reminder_offsets(offsets))

This two-step process guarantees data integrity—negative values are stripped, and the database always receives valid JSON.

How Reminders Are Scheduled

APScheduler Integration and Job Creation

The actual scheduling logic resides in lifetrace/jobs/deadline_reminder.py. When a Todo is created or modified, the system invokes schedule_todo_reminders, which calculates absolute trigger times and registers individual date jobs with APScheduler.

For each offset in reminder_offsets, the function computes:

reminder_at = schedule_time - timedelta(minutes=offset)

Where schedule_time is resolved from the Todo’s due, deadline, or dtstart field. The function then adds a job with a deterministic ID format:

job_id = f"todo_reminder_{todo_id}_{int(reminder_at.timestamp())}"

This ID format ensures that each reminder instance is unique and traceable. Jobs are stored in APScheduler’s SQLite job store (configured in lifetrace/jobs/scheduler.py via SchedulerManager), meaning they persist across application restarts.

Handling Todo Updates and Rescheduling

When a user edits a Todo—changing its due date or reminder offsets—the system must invalidate old reminders and create new ones. This is handled by refresh_todo_reminders in lifetrace/services/todo_service.py:


# lifetrace/services/todo_service.py

def refresh_todo_reminders(todo: Todo):
    # 1. Remove existing jobs for this todo

    remove_todo_reminder_jobs(todo.id)
    
    # 2. Re-schedule based on current offsets and schedule time

    schedule_todo_reminders(todo)

The remove_todo_reminder_jobs function queries APScheduler for jobs with IDs matching the prefix todo_reminder_{todo_id}_ and removes them before the new schedule is applied. This prevents duplicate or stale notifications.

How Reminders Are Triggered and Delivered

Job Execution and Notification Building

When a scheduled time arrives, APScheduler invokes execute_todo_reminder_job from lifetrace/jobs/deadline_reminder.py. This function performs several validation steps before generating a notification:

  1. Re-loads the Todo from the database to ensure it still exists and is active.
  2. Validates the schedule—if the Todo’s due date changed and the reminder is no longer relevant, the job exits silently.
  3. Checks dismissal state via notification_storage.is_notification_dismissed to prevent re-notifying for acknowledged reminders.

If all checks pass, the function builds a notification payload:

notification = {
    "id": f"todo_{todo.id}_reminder_{int(reminder_at.timestamp())}",
    "title": todo.name,
    "content": f"Due in {_format_remaining(remaining_seconds)}",
    "type": "todo_reminder",
    "todo_id": todo.id,
    "created_at": datetime.now(timezone.utc).isoformat()
}

This payload is then passed to notification_storage.add_notification, which places it in an in-memory store accessible to the frontend.

Deduplication and Dismissal Tracking

The notification storage layer in lifetrace/storage/notification_storage.py handles deduplication and user dismissal states. The add_notification method checks for existing entries with the same ID before insertion, while clear_notification marks specific reminders as dismissed:


# lifetrace/storage/notification_storage.py

_dismissed_notifications = set()  # In-memory tracking of dismissed IDs

def clear_notification(notification_id: str):
    """Mark a notification as dismissed to prevent re-triggering."""
    _dismissed_notifications.add(notification_id)
    # Also remove from active notifications if present

    _active_notifications.pop(notification_id, None)

When execute_todo_reminder_job runs, it queries is_notification_dismissed against this set. If the user has previously dismissed the reminder, the job exits without generating a new notification, ensuring a clean user experience.

Summary

  • Storage: Reminder offsets are stored as JSON strings in the reminder_offsets column of the todos table, validated by _normalize_reminder_offsets in lifetrace/storage/todo_manager_utils.py.
  • Scheduling: APScheduler creates persistent date jobs via schedule_todo_reminders in lifetrace/jobs/deadline_reminder.py, using deterministic IDs like todo_reminder_{id}_{timestamp} and storing jobs in SQLite.
  • Rescheduling: Updates to Todos trigger refresh_todo_reminders in lifetrace/services/todo_service.py, which removes old jobs by prefix and recreates new ones.
  • Triggering: At execution time, execute_todo_reminder_job validates the Todo state, checks dismissal status against notification_storage, and builds the notification payload.
  • Delivery: Notifications are held in an in-memory store (lifetrace/storage/notification_storage.py) with deduplication and dismissal tracking to prevent spam.

Frequently Asked Questions

What happens to reminders when a Todo is updated?

When you modify a Todo’s due date or reminder offsets, the refresh_todo_reminders function in lifetrace/services/todo_service.py automatically removes all existing APScheduler jobs for that Todo (matching the prefix todo_reminder_{todo_id}_) and recreates new jobs based on the current offsets and schedule time. This ensures no stale or duplicate reminders fire.

How does LifeTrace handle missed or late reminders?

The system uses APScheduler’s misfire grace time (configured in lifetrace/config/default_config.yaml). If a reminder job fails to execute at the exact scheduled moment (e.g., due to server restart), but the delay is within the grace window, APScheduler immediately triggers execute_todo_reminder_job. If the delay exceeds the grace period, the job is skipped to avoid confusing late notifications.

Where are scheduled reminder jobs persisted?

Jobs are stored in a SQLite database managed by APScheduler’s SQLAlchemyJobStore, configured in lifetrace/jobs/scheduler.py within the SchedulerManager class. This persistence layer ensures that scheduled reminders survive application restarts and process crashes, unlike the in-memory notification store which resets on restart.

How can I manually check scheduled reminders for debugging?

You can inspect active jobs by retrieving the scheduler instance from lifetrace/jobs/scheduler.py and iterating over jobs:

from lifetrace.jobs.scheduler import get_scheduler_manager

scheduler = get_scheduler_manager()
for job in scheduler.get_all_jobs():
    if job.id.startswith("todo_reminder"):
        print(f"{job.id} fires at {job.next_run_time}")

This lists all pending reminder jobs with their exact trigger times, helping verify that offsets were calculated correctly relative to the Todo’s due date.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →