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
GenerateAiCheckInPromptsJobruns daily at 10:00 (background job, allowlist-gated during ramp)- For each eligible user, the job builds context from their top 5 active goals
Ai::CoachService.generate_check_in_prompt(context)calls the configured LLM- A
UserNotificationis created withkind: ai_check_in - Real-time WebSocket subscription broadcasts the notification
- Push notification is sent via
SendPushNotification - Frontend receives the event and shows a toast
CheckInPromptCardon the Dashboard displays the latest unacknowledged prompt- User taps "View Goal" (navigates to the referenced goal) or dismisses
AcknowledgeNotificationmutation 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_innotification 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 State | Context Included |
|---|---|
| Habit with streak | "{name}" — {N}-day streak |
| Non-habit with progress | "{name}" — {X}% complete |
| Goal with milestones | Appends ({X}/{Y} milestones done) |
| Any goal | Appends last updated {N} days ago |
Notification Created
| Field | Value |
|---|---|
kind | ai_check_in (enum value 7) |
content | AI-generated message (≤120 chars) |
details | JSON: { goal_public_id: "...", generated_at: "..." } |
acknowledged | false |
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:
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/10toprimary/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_idexists indetailsJson - Dismiss: X button in the top-right corner
Actions
| Action | What Happens |
|---|---|
| View Goal | Acknowledges the notification + navigates to /goal/{goalPublicId} |
| Dismiss (X) | Acknowledges the notification — card disappears |
Data Flow
detailsJsonis parsed to extractgoal_public_idacknowledgeNotification(id)callsACKNOWLEDGE_NOTIFICATION_MUTATION- Notification is removed from reactive state
- Card's
v-ifcondition 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
| Value | Integer | Description |
|---|---|---|
encouragement | 0 | Someone encouraged your goal |
badge | 1 | Badge earned |
general | 2 | Generic notification |
comment | 3 | Comment on your update |
reminder | 4 | General reminder |
past_due | 5 | Goal needs attention |
follow | 6 | Someone followed your goal |
ai_check_in | 7 | AI-generated check-in prompt |
Details JSON Schema (for ai_check_in)
{
"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
useNotificationscomposable
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_nowin Rails console - [ ] Verify a
UserNotificationwithkind: 'ai_check_in'is created - [ ] Verify notification
contentis ≤120 characters - [ ] Verify
detailsJSON containsgoal_public_idandgenerated_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_innotification - [ ] 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_promptreturnsnil(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
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.rbFrontend Unit Tests
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
mutation AcknowledgeNotification($notificationId: ID!) {
acknowledgeNotification(id: $notificationId) {
result { success }
}
}Subscriptions
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:
query UserQuery($id: ID!) {
user(id: $id) {
notifications {
id
kind
content
detailsJson
acknowledged
createdAtTime
}
}
}Known Behaviors & Edge Cases
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.
One prompt per day per user: The job checks for existing unacknowledged
ai_check_innotifications from today before generating. This prevents notification spam even if the job runs multiple times.Allowlist-gated during ramp:
GenerateAiCheckInPromptsJobis scheduled daily at 10:00 incronotab.rb, but only generates prompts for users whose email is inENV['AI_CHECK_IN_INTERNAL_ALLOWLIST'](comma-separated). Users not on the allowlist receive nothing until the allowlist is widened or removed.Top goal selection: The
goal_public_idin 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.No mock fallback: There is no
MockLlmclass. When the LLM produces no output,generate_check_in_promptreturnsniland no notification is created for that user that day (see Error Handling above).Notification store is module-level: The
useNotificationscomposable 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.Subscription field naming: The WebSocket subscription returns
details_jsonandcreated_at_timein snake_case. The composable normalizes these to camelCase (detailsJson,createdAtTime) before storing.Card visibility: The CheckInPromptCard only renders in the Dashboard's active user state (3+ goals) and only when there's an unacknowledged
ai_check_innotification. Users with fewer than 3 goals won't see it even if a notification exists.Push notification requires device token:
SendPushNotificationonly sends if the user has registered a device token viaSEND_DEVICE_TOKEN_MUTATION. Web-only users without push registration won't receive push notifications (but will still see the in-app card and toast).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.
Already-checked-in habits excluded from context:
build_user_contextfilters out any habit goal wherechecked_in_today?istrue. 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)