Skip to content

AI Check-In Prompts — Feature & Testing Guide

Overview

AI check-in prompts are personalized daily notifications generated by an LLM that nudge users to engage with their goals. A background job runs daily at 10:00 (initially gated by an internal allowlist during ramp — see Scheduling), builds context from each user's active goals (streaks, milestones, days since last update), and generates a short (~120 character) motivational message referencing a specific goal by name. The prompt is delivered as a real-time notification and push notification, and appears as a prominent card on the Dashboard.

This surface is cataloged alongside every other Coach touchpoint in coach-surfaces.md — see its check-in prompt entries for fallback rung and owner-agent details.


How It Works

End-to-End Flow

  1. GenerateAiCheckInPromptsJob runs daily at 10:00 (background job, allowlist-gated during ramp)
  2. For each eligible user, the job builds context from their top 5 active goals
  3. Ai::CoachService.generate_check_in_prompt(context) calls the configured LLM
  4. A UserNotification is created with kind: ai_check_in
  5. Real-time WebSocket subscription broadcasts the notification
  6. Push notification is sent via SendPushNotification
  7. Frontend receives the event and shows a toast
  8. CheckInPromptCard on the Dashboard displays the latest unacknowledged prompt
  9. User taps "View Goal" (navigates to the referenced goal) or dismisses
  10. AcknowledgeNotification mutation marks it as read; card disappears

Backend: GenerateAiCheckInPromptsJob

Eligibility

A user receives a daily check-in prompt if:

  • They have at least one active goal (non-completed, non-deleted)
  • They do NOT have an unacknowledged ai_check_in notification from today

Context Building

The job fetches the user's top 5 most-recently-updated goals, excluding habits already checked in today, and builds a multiline context string:

Goal StateContext Included
Habit with streak"{name}" — {N}-day streak
Non-habit with progress"{name}" — {X}% complete
Goal with milestonesAppends ({X}/{Y} milestones done)
Any goalAppends last updated {N} days ago

Notification Created

FieldValue
kindai_check_in (enum value 7)
contentAI-generated message (≤120 chars)
detailsJSON: { goal_public_id: "...", generated_at: "..." }
acknowledgedfalse

Delivery

  • Real-time: RailsServerSchema.subscriptions.trigger('notificationUpdate', ...) via ActionCable
  • Push: SendPushNotification.call(user_id:, title:, body:) for mobile devices

Error Handling

Failures for individual users are rescued and logged — one user's LLM error doesn't prevent other users from getting prompts.

Scheduling

The job is scheduled daily at 10:00 in rails_api/config/cronotab.rb:

ruby
Crono.perform(AiCoaching::GenerateAiCheckInPromptsJob).every 1.day, at: '10:00'

Generation is currently gated by an internal allowlist during ramp: perform reads ENV['AI_CHECK_IN_INTERNAL_ALLOWLIST'] (comma-separated emails) and only generates prompts for users on that list — if the allowlist is empty, the job returns immediately and no prompts are generated for anyone. Removing the gate (widening the rollout to all eligible users) means removing the allowlist check in rails_api/app/jobs/ai_coaching/generate_ai_check_in_prompts_job.rb.


Backend: CoachService.generate_check_in_prompt

LLM Prompt

The service sends a system prompt instructing the LLM to:

  • Reference a specific goal or habit by name
  • Keep the message under 120 characters (push notification length)
  • Be warm and encouraging, not pushy
  • Mention streaks to motivate continuation
  • Mention progress toward milestones when close
  • Vary tone: celebratory, curious, or motivating
  • Avoid generic phrases like "Keep it up!" alone

Post-Processing

  • Extracts the text content from the LLM response
  • Truncates to 120 characters (hard limit for push notifications)
  • Returns a plain string

LLM Provider Support

Provider routing is handled by LiteLLM — see ai-coach.md#llm-backend-service for the full LiteLLM configuration and env vars (LITELLM_URL, LITELLM_MASTER_KEY). Direct provider env vars (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are LiteLLM's concern, not Rails's.

When no prompt is generated, generate_check_in_prompt returns nil (no MockLlm fallback class exists).


Frontend: CheckInPromptCard

Location

Rendered on the Dashboard between the ActionHub and Today's Habits section. Only visible in the active user state (3+ goals).

How It Finds the Prompt

The component reads from the shared notification state (via useNotifications) and finds the first unacknowledged notification where kind === 'ai_check_in'. If none exists, the card is not rendered.

Display

  • Container: Rounded card with gradient background (primary/10 to primary/20)
  • Badge: Sparkles icon with "Coach" label
  • Content: The Coach-generated message text (truncated to 120 chars)
  • CTA: "View Goal" button — only visible if goal_public_id exists in detailsJson
  • Dismiss: X button in the top-right corner

Actions

ActionWhat Happens
View GoalAcknowledges the notification + navigates to /goal/{goalPublicId}
Dismiss (X)Acknowledges the notification — card disappears

Data Flow

  1. detailsJson is parsed to extract goal_public_id
  2. acknowledgeNotification(id) calls ACKNOWLEDGE_NOTIFICATION_MUTATION
  3. Notification is removed from reactive state
  4. Card's v-if condition fails → card disappears

Frontend: /check-in route

/check-in is a plain redirect to /coach (ionic_frontend/src/router/index.ts) — there is no dedicated CheckIn.vue page. The route was migrated in PR #915 and the standalone component was removed; see docs/product/site-map.md for the route-alias table.

All check-in prompt interaction happens on the Dashboard via CheckInPromptCard (see below) — "View Goal" and dismiss are handled there, not on a separate check-in page.


UserNotification Model

Kind Enum

ValueIntegerDescription
encouragement0Someone encouraged your goal
badge1Badge earned
general2Generic notification
comment3Comment on your update
reminder4General reminder
past_due5Goal needs attention
follow6Someone followed your goal
ai_check_in7AI-generated check-in prompt

Details JSON Schema (for ai_check_in)

json
{
  "goal_public_id": "abc123def456",
  "generated_at": "2026-03-22T08:00:00Z"
}

UI Components

CheckInPromptCard

  • No props (reads from notification store)
  • Gradient card with Sparkles icon badge
  • Truncated AI message text
  • "View Goal" CTA (conditional on goal_public_id)
  • Dismiss X button
  • Integrates with useNotifications composable

NotificationsPanel (integration)

  • AI check-in prompts also appear in the notification bell panel
  • Shown alongside other notification types
  • Same acknowledge/dismiss behavior

Testing the Feature

Manual Testing Checklist

1. Job Generates Prompt

  • [ ] Run the job manually: GenerateAiCheckInPromptsJob.perform_now in Rails console
  • [ ] Verify a UserNotification with kind: 'ai_check_in' is created
  • [ ] Verify notification content is ≤120 characters
  • [ ] Verify details JSON contains goal_public_id and generated_at
  • [ ] Verify the message references a specific goal by name

2. Eligibility — Active Goals Required

  • [ ] Create a user with no goals
  • [ ] Run the job → verify no notification is created for that user
  • [ ] Add an active goal → run again → verify notification is created

3. Eligibility — No Duplicate Today

  • [ ] Run the job → notification created
  • [ ] Run the job again on the same day → verify no second notification
  • [ ] Acknowledge the first notification → run again → verify still no duplicate (already sent today)

4. Dashboard Card Appears

  • [ ] Sign in as a user with an unacknowledged ai_check_in notification
  • [ ] Navigate to Dashboard
  • [ ] Verify CheckInPromptCard appears with the Coach message
  • [ ] Verify "Coach" badge and Sparkles icon display
  • [ ] Verify "View Goal" button is visible

5. View Goal Action

  • [ ] Tap "View Goal" on the CheckInPromptCard
  • [ ] Verify navigation to the correct goal detail page
  • [ ] Return to Dashboard → verify card has disappeared (notification acknowledged)

6. Dismiss Action

  • [ ] With an unacknowledged AI check-in prompt visible
  • [ ] Tap the X (dismiss) button
  • [ ] Verify card disappears immediately
  • [ ] Verify notification is marked as acknowledged in the backend
  • [ ] Reload Dashboard → verify card does not reappear

7. Real-Time Delivery

  • [ ] Have the Dashboard open in a browser
  • [ ] Trigger the job in a separate console
  • [ ] Verify toast notification appears in real-time (without page reload)
  • [ ] Verify CheckInPromptCard appears on Dashboard

8. Push Notification

  • [ ] Register a device token (mobile or web push)
  • [ ] Run the job
  • [ ] Verify push notification arrives on the device
  • [ ] Verify push notification text matches the AI-generated content

9. Context Accuracy

  • [ ] Create a habit with a 10-day streak
  • [ ] Run the job
  • [ ] Verify the generated message mentions the streak or habit name
  • [ ] Create a goal at 80% progress with milestones
  • [ ] Run the job → verify message references progress or milestones

10. No Card When No Prompts

  • [ ] Acknowledge all AI check-in notifications
  • [ ] Navigate to Dashboard
  • [ ] Verify CheckInPromptCard does NOT render
  • [ ] Verify no empty card or placeholder shows

11. Multiple Users

  • [ ] Create 3 users, each with different goal profiles
  • [ ] Run the job
  • [ ] Verify each user gets a unique, personalized message
  • [ ] Verify messages reference each user's specific goals

12. No LLM Response

  • [ ] Configure the environment so generate_check_in_prompt returns nil (e.g., LiteLLM gateway unreachable)
  • [ ] Run the job
  • [ ] Verify no notification is created for that user (no fallback text — see Known Behaviors #5)
  • [ ] Verify the job continues to the next user without raising

Backend Unit Tests

bash
cd rails_api

# Job specs — eligibility, context building, notification creation
bundle exec rspec spec/jobs/ai_coaching/generate_ai_check_in_prompts_job_spec.rb

# Service specs — message generation, truncation
bundle exec rspec spec/services/ai/coach_service_spec.rb

# Interaction specs — acknowledge notification
bundle exec rspec spec/interactions/gamification/acknowledge_notification_spec.rb

# Model specs
bundle exec rspec spec/models/user_notification_spec.rb

# GraphQL specs
bundle exec rspec spec/graphql/mutations/acknowledge_notification_spec.rb

Frontend Unit Tests

bash
cd ionic_frontend

# Composable tests — notification state, acknowledge, subscribe
npm run test:unit -- --run -t "useNotifications"

# Component tests (if added)
npm run test:unit -- --run -t "CheckInPromptCard"

GraphQL API Reference

Mutations

graphql
mutation AcknowledgeNotification($notificationId: ID!) {
  acknowledgeNotification(id: $notificationId) {
    result { success }
  }
}

Subscriptions

graphql
subscription NotificationUpdate($userId: ID!) {
  notificationUpdate(userId: $userId) {
    id
    kind              # "ai_check_in" for this feature
    content           # AI-generated message (≤120 chars)
    details_json      # JSON string: { goal_public_id, generated_at }
    acknowledged      # false when first delivered
    created_at_time   # ISO timestamp
  }
}

Queries

AI check-in notifications are returned as part of the user's notifications list:

graphql
query UserQuery($id: ID!) {
  user(id: $id) {
    notifications {
      id
      kind
      content
      detailsJson
      acknowledged
      createdAtTime
    }
  }
}

Known Behaviors & Edge Cases

  1. 120-character limit: Messages are hard-truncated to 120 characters to fit push notification constraints. The LLM is prompted to stay under this limit, and the service truncates as a safety net.

  2. One prompt per day per user: The job checks for existing unacknowledged ai_check_in notifications from today before generating. This prevents notification spam even if the job runs multiple times.

  3. Allowlist-gated during ramp: GenerateAiCheckInPromptsJob is scheduled daily at 10:00 in cronotab.rb, but only generates prompts for users whose email is in ENV['AI_CHECK_IN_INTERNAL_ALLOWLIST'] (comma-separated). Users not on the allowlist receive nothing until the allowlist is widened or removed.

  4. Top goal selection: The goal_public_id in the notification details always references the user's most-recently-updated goal (first in the top-5 context list). This is the goal the "View Goal" button navigates to.

  5. No mock fallback: There is no MockLlm class. When the LLM produces no output, generate_check_in_prompt returns nil and no notification is created for that user that day (see Error Handling above).

  6. Notification store is module-level: The useNotifications composable uses a shared module-level ref. All components on the same page see the same notification state. Acknowledging from the CheckInPromptCard also removes it from the NotificationsPanel.

  7. Subscription field naming: The WebSocket subscription returns details_json and created_at_time in snake_case. The composable normalizes these to camelCase (detailsJson, createdAtTime) before storing.

  8. Card visibility: The CheckInPromptCard only renders in the Dashboard's active user state (3+ goals) and only when there's an unacknowledged ai_check_in notification. Users with fewer than 3 goals won't see it even if a notification exists.

  9. Push notification requires device token: SendPushNotification only sends if the user has registered a device token via SEND_DEVICE_TOKEN_MUTATION. Web-only users without push registration won't receive push notifications (but will still see the in-app card and toast).

  10. Error isolation: If the LLM fails for one user, the job catches the error and continues to the next user. This prevents one failure from blocking all other users' prompts.

  11. Already-checked-in habits excluded from context: build_user_context filters out any habit goal where checked_in_today? is true. Coach is only prompted about habits that still need attention, preventing stale "due today" messages after a check-in has been logged.

Last updated: 2026-07-06 (doc audit: /check-in route corrected to a plain /coach redirect — no CheckIn.vue exists; removed remaining MockLlm references to match the "no mock fallback" behavior documented elsewhere in this page)

Loading…