Back to deblo
deblo

Tasks, Goals, and Recurring Reminders

The AI creates tasks from conversation context. Recurrence engine, due-date notifications, org-scoped visibility, and threaded comments on homework assignments.

Thales & Claude | March 25, 2026 13 min deblo
deblotasksgoalsrecurrencenotificationsproductivity

An AI tutor that only answers questions is a search engine with a personality. A real tutor does more. A real tutor says, "You have a math exam on Friday -- have you reviewed fractions yet?" A real tutor assigns homework, tracks whether it was completed, and follows up if it was not. A real tutor sets goals with the student and measures progress toward them.

That is why we built task management directly into the chat. Not as a separate product. Not as a tab the user has to discover. As a natural extension of the conversation. When a student says "Aide-moi a preparer mon examen de maths vendredi," the AI does not just explain concepts -- it creates a task with a due date, sets a reminder, and checks in the next day.

This article covers the data models, the AI tool integration, the recurrence engine, and the notification system that ties it all together.

Why Tasks in an AI Tutor

The insight came from observing real usage in Abidjan. Students would have productive conversations with the AI, learn a concept, close the app, and then forget to practice. Professionals would discuss a project plan with the AI advisor, get detailed recommendations, and then lose track of the action items.

The AI was generating valuable output. But output without follow-through is wasted. Students need homework tracking. Professionals need project management. Organizations (schools, families, companies) need team coordination. The AI was already the center of the user's workflow. Adding task management was the natural next step.

We considered integrating with external task managers (Todoist, Google Tasks, Notion). We rejected this for three reasons:

1. Our users are in Africa. Many have never used a dedicated task management app. Adding a third-party integration adds friction and confusion. 2. Context loss. When a task lives in Todoist, the AI has no visibility into it. It cannot check whether the student completed their homework. It cannot adjust its teaching based on what tasks are overdue. 3. Organization scope. Our task system is org-scoped. A teacher can assign tasks to students. A manager can assign tasks to team members. External task apps do not understand our organization model.

So we built it ourselves.

The Data Models

The task system has four models: Goal, Task, TaskComment, and TaskNotification.

Task

The Task model is the core entity:

class Task(Base):
    __tablename__ = "tasks"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) org_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=True, index=True) creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) assignee_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True) title = Column(String(300), nullable=False) description = Column(Text, nullable=True) status = Column(String(20), nullable=False, default="todo") # Statuses: todo | in_progress | done | cancelled priority = Column(String(10), nullable=False, default="medium") # Priorities: low | medium | high | urgent due_date = Column(Date, nullable=True, index=True) due_time = Column(String(5), nullable=True) # "HH:MM" recurrence = Column(String(10), nullable=True) # Recurrence: daily | weekly | monthly | yearly goal_id = Column(UUID(as_uuid=True), ForeignKey("goals.id"), nullable=True) tags = Column(JSONB, nullable=True) completed_at = Column(DateTime(timezone=True), nullable=True) is_archived = Column(Boolean, default=False) position = Column(Integer, default=0) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

__table_args__ = ( Index("ix_tasks_org_status_due", "org_id", "status", "due_date"), ) ```

Several design decisions are worth highlighting:

org_id is nullable. Personal tasks (created by a user for themselves) have no organization. Organization tasks (assigned by a teacher or manager) have an org_id. This dual purpose -- personal productivity and team coordination -- is handled by a single model with optional scoping.

assignee_id is nullable. A task can be unassigned (e.g., a personal reminder) or assigned to a specific user. When the AI creates a task during conversation, the creator and assignee are both the current user. When a teacher assigns homework through the organization dashboard, the creator is the teacher and the assignee is the student.

due_time is a string, not a Time column. We store "HH:MM" as a plain string because time zone handling for due times is complex and unnecessary at our current scale. The display layer formats it according to the user's locale.

tags is JSONB. Tags like ["mathematiques", "fractions", "examen"] are stored as a PostgreSQL JSONB array. This allows GIN indexing for fast tag-based queries without a separate join table.

The composite index on (org_id, status, due_date) optimizes the most common query: "Show me all open tasks for this organization, ordered by due date." This query powers the task dashboard.

Goal

Goals are higher-level objectives that tasks can be grouped under:

class Goal(Base):
    __tablename__ = "goals"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) org_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) title = Column(String(300), nullable=False) description = Column(Text, nullable=True) scope = Column(String(10), nullable=False, default="personal") # Scope: personal | team period = Column(String(15), nullable=False, default="monthly") # Period: daily | weekly | monthly | quarterly | yearly target_count = Column(Integer, nullable=True) start_date = Column(Date, nullable=True) end_date = Column(Date, nullable=True) is_completed = Column(Boolean, default=False) completed_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) ```

A goal might be "Complete all fraction exercises this month" with a target_count of 20 and a period of "monthly." Tasks linked to this goal via task.goal_id contribute to the goal's progress. The scope field distinguishes personal goals from team goals visible to the entire organization.

TaskComment and TaskNotification

TaskComment enables threaded discussion on tasks -- a teacher can comment on a student's task to provide guidance. TaskNotification tracks notification state per user per task:

class TaskNotification(Base):
    __tablename__ = "task_notifications"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) task_id = Column(UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False) notif_type = Column(String(20), nullable=False) # Types: assigned | due_soon | overdue | completed | comment message = Column(String(500), nullable=False) is_read = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now())

__table_args__ = ( Index("ix_task_notif_user_read", "user_id", "is_read"), ) ```

The ondelete="CASCADE" on task_id ensures that when a task is deleted, all its notifications and comments are cleaned up automatically. The composite index on (user_id, is_read) optimizes the unread notification count query that runs on every page load.

The create_task AI Tool

The most powerful feature of the task system is not the UI -- it is the AI tool. The AI can create tasks during conversation, contextualizing them with information from the discussion:

# In tool_executor.py
if func_name == "create_task" and user:
    from datetime import date as date_type
    from app.models.task import Task as TaskModel
    from app.models.org_membership import OrgMembership
    from app.services.task_service import create_notification as create_task_notif

# Find the user's active organization (if any) mem_result = await db.execute( select(OrgMembership.org_id).where( OrgMembership.user_id == user.id, OrgMembership.status == "active", ).limit(1) ) task_org_id = mem_result.scalar_one_or_none()

title = func_args.get("title", "").strip() if not title: return {"success": False, "error": "Le titre est requis"}

priority = func_args.get("priority", "medium") if priority not in ("low", "medium", "high", "urgent"): priority = "medium"

due_date = None if func_args.get("due_date"): try: due_date = date_type.fromisoformat(func_args["due_date"]) except ValueError: pass

new_task = TaskModel( id=uuid4(), org_id=task_org_id, creator_id=user.id, title=title, description=func_args.get("description"), priority=priority, due_date=due_date, due_time=func_args.get("due_time"), tags=func_args.get("tags"), ) db.add(new_task) await db.flush()

await create_task_notif( db, user.id, new_task.id, "created", f'Tache creee par Deblo : "{title}"', )

return { "success": True, "task_id": str(new_task.id), "title": title, "priority": priority, "due_date": func_args.get("due_date"), } ```

The tool definition in the LLM tools array describes the parameters the AI can pass:

  • title (required): a short description of the task
  • description (optional): detailed notes
  • priority (optional): low, medium, high, or urgent
  • due_date (optional): ISO format date string
  • due_time (optional): HH:MM format
  • tags (optional): array of strings

In practice, the AI uses this tool naturally. A conversation like this:

Student: "J'ai un controle de maths sur les fractions vendredi." > AI: "D'accord ! Je cree un rappel pour toi." [calls create_task with title "Reviser les fractions pour le controle", due_date "2026-03-28", priority "high", tags ["mathematiques", "fractions", "controle"]] > AI: "C'est fait ! J'ai cree une tache 'Reviser les fractions pour le controle' avec echeance vendredi. Tu veux qu'on commence la revision maintenant ?"

The AI creates the task, the student sees it in their task dashboard, and on Thursday they receive a "due_soon" notification reminding them to study.

The Recurrence Engine

Some tasks repeat. A student might have weekly homework. A professional might have a monthly SYSCOHADA filing deadline. The recurrence engine handles this through a background scheduler:

# backend/app/services/task_scheduler.py

async def _check_due_soon(db) -> int: """Create 'due_soon' notifications for tasks due today (once per task).""" today = date.today() count = 0

result = await db.execute( select(Task).where( Task.due_date == today, Task.status.in_(["todo", "in_progress"]), Task.is_archived == False, ) ) tasks = result.scalars().all()

for task in tasks: if await _already_notified(db, task.id, "due_soon"): continue

target_user = task.assignee_id or task.creator_id await create_notification( db, target_user, task.id, "due_soon", f'La tache "{task.title}" est prevue pour aujourd\'hui', ) count += 1

return count

async def _check_overdue(db) -> int: """Create 'overdue' notifications for past-due tasks (once, never repeated).""" today = date.today() count = 0

result = await db.execute( select(Task).where( Task.due_date < today, Task.status.in_(["todo", "in_progress"]), Task.is_archived == False, ) ) tasks = result.scalars().all()

for task in tasks: if await _already_notified(db, task.id, "overdue"): continue

target_user = task.assignee_id or task.creator_id await create_notification( db, target_user, task.id, "overdue", f'La tache "{task.title}" est en retard', ) count += 1

return count ```

The scheduler runs as a background asyncio.Task started during the FastAPI lifespan, following the same pattern as our payment poller (described in article 6). It loops on a configurable interval (currently every 30 minutes) and performs two checks:

1. Due soon -- tasks with due_date == today that have not yet been notified. The _already_notified helper queries the TaskNotification table to prevent duplicate notifications. A student will receive exactly one "due soon" notification per task, even if the scheduler runs 48 times that day.

2. Overdue -- tasks with due_date < today that are still open (not done, not cancelled, not archived) and have not been flagged as overdue. Again, exactly one notification per task.

Anti-spam is critical. Early in development, we had a bug where the scheduler would create a new "overdue" notification every 30 minutes for every overdue task. A student with 5 overdue tasks would receive 240 notifications per day. The _already_notified guard -- a simple existence check in the notification table -- solved this permanently.

For recurring tasks, the system generates new task instances when a recurring task is marked as done. If a task has recurrence = "weekly", completing it creates a new task with the same title, description, and tags, but with due_date set to the next occurrence (7 days later). The original task stays in "done" status for the historical record.

The Task API

The REST API for tasks follows standard CRUD patterns with org-scoped filtering:

  • POST /api/tasks -- create a task (creator_id from JWT, org_id from active membership)
  • GET /api/tasks -- list tasks with filters: status, due_date_from, due_date_to, assignee_id, priority, tags
  • PATCH /api/tasks/{id} -- update status, priority, due date, description, assignee
  • DELETE /api/tasks/{id} -- soft delete (sets is_archived = True)
  • POST /api/tasks/{id}/comments -- add a comment
  • GET /api/tasks/{id}/comments -- list comments for a task

The listing endpoint is the most interesting because of its filtering capabilities. A teacher viewing their organization's task dashboard can filter by:

  • Status: "Show me all in-progress tasks"
  • Due date range: "Show me tasks due this week"
  • Assignee: "Show me tasks assigned to student X"
  • Priority: "Show me all urgent tasks"

All filters are combined with AND logic and applied through SQLAlchemy's where clauses. The composite index on (org_id, status, due_date) ensures that the most common query pattern -- "all open tasks for my org, sorted by due date" -- hits the index directly.

The Task Dashboard

On the frontend, the task dashboard at /dashboard/tasks displays tasks in a Kanban-style layout with four columns: To Do, In Progress, Done, and Cancelled. Each task card shows:

  • Title (truncated to 2 lines)
  • Priority badge (color-coded: gray for low, blue for medium, orange for high, red for urgent)
  • Due date (with relative formatting: "Aujourd'hui," "Demain," "Vendredi," or the full date)
  • Assignee avatar (for org tasks)
  • Tag chips

Tasks can be moved between columns by clicking status buttons on the card. On mobile, we considered swipe-to-complete gestures but opted for explicit tap targets instead -- swipe gestures conflict with the chat interface's horizontal swipe navigation.

How It All Connects

The task system does not exist in isolation. It is woven into the fabric of the platform:

The AI creates tasks from conversation context. The create_task tool gives the AI agency to turn discussion into action. The student does not need to manually create a task -- the AI does it proactively.

The AI reads tasks to provide context. When a student starts a new conversation, the system prompt includes their upcoming tasks. The AI can say, "Tu as un controle de physique demain -- tu veux qu'on revise ?" without the student having to mention it.

Notifications bridge conversations. A "due_soon" notification brings the student back to the app. When they tap the notification, they land on the task detail and can start a new conversation about the task's topic.

Organizations enable assignment. A teacher can create tasks and assign them to students. The student receives the task in their personal dashboard. The teacher sees completion status in the organization dashboard. This replaces paper homework lists and WhatsApp group messages.

Goals provide structure. A goal like "Preparer le BEPC" can have 30 tasks under it, each tracking a specific revision topic. The goal's progress bar shows how many tasks are complete, giving the student a visual sense of preparation status.

The task system turned our AI tutor from a conversation tool into a productivity platform. Students do not just learn -- they plan, track, and complete. Professionals do not just discuss -- they organize, delegate, and follow through. The AI is not just answering questions. It is managing workflows.

---

This is article 18 of 20 in the "How We Built Deblo.ai" series.

1. AI Tutoring for 250 Million African Students 2. 100 Sessions Later: The Architecture of an AI Education Platform 3. The Agentic Loop: 24 AI Tools in a Single Chat 4. System Prompts That Teach: Anti-Cheating, Socratic Method, and Grade-Level Adaptation 5. WhatsApp OTP and the African Authentication Problem 6. Credits, FCFA, and 6 African Payment Gateways 7. SSE Streaming: Real-Time AI Responses in SvelteKit 8. Voice Calls With AI: Ultravox, LiveKit, and WebRTC 9. Building a React Native K12 App in 7 Days 10. 101 AI Advisors: Professional Intelligence for Africa 11. Background Jobs: When AI Takes 30 Minutes to Think 12. From Abidjan to 250 Million: The Deblo.ai Story 13. Generating PDFs, Spreadsheets, and Slide Decks From a Chat Message 14. Organizations: Families, Schools, and Companies on One Platform 15. Interactive Quizzes With LaTeX: Testing Students Inside a Chat 16. RAG Pipeline: Document Search With pgvector and Semantic Chunking 17. Six Languages, One Platform: i18n for Africa 18. Tasks, Goals, and Recurring Reminders (you are here) 19. AI Memory and Context Compression 20. Observability: Tracking Every LLM Call in Production

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles