Coach Features — Feature & Testing Guide
Overview
Objectuve integrates Coach features throughout the app via three capabilities: Insight Cards (contextual cards on every major page), Goal Coach (interactive chat for personalized advice), and Milestone Generation (auto-suggest sub-goals). The backend uses an LLM service (CoachService) that routes all calls through Ai::ServiceClient, which proxies to LiteLLM.
This page covers what Coach does at runtime. For how users configure Coach (persona, tone, depth, focus, frequency), see Meet Coach. For a comprehensive architectural reference mapping all Coach surfaces (frontend, backend services, prompt templates) and fallback rungs, see Coach Surfaces Map.
Related docs:
- Dedicated AI Service PRD — original product rationale and design exploration
- AI Insight Intelligence — mood-goal correlation insight and pace-suggestion accept, on the same
useAiInsightssurface - AI Coaching Expansion — the goal-discovery quiz and the admin feedback weekly summary, both new
Ai::CoachServicemethods
Four AI surfaces now touch goal creation — worth disambiguating up front, since they were built in four separate phases and are easy to conflate:
| Surface | Method | What it does | Docs |
|---|---|---|---|
| Coach-Drafted Goals | generate_goal_draft | Idea sentence → a full drafted goal (title, category, kind, target date, milestones) | This page, below |
| Goal-discovery quiz | suggest_goals | Helps a user who isn't sure what to set pick a goal | AI Coaching Expansion |
| Refine Description | refine_description | Rewrites the goal's description prose (≤240 chars) | This page, GraphQL API Reference |
| Goal Quality Check | refine_goal | Critiques a goal the user has already formed for specificity, realism, and right-sizing, before they save it | This page, below |
Insight Cards
What They Are
Insight cards are contextual tips, actions, insights, or celebrations that appear on key pages. Each card has a type, title, message, and optional CTA button.
Insight Types
| Type | Icon | When Used |
|---|---|---|
| tip | Sparkles (purple) | General advice or best practices |
| action | Zap (yellow) | Something the user should do now |
| insight | Lightbulb | Pattern or trend observation |
| celebration | Trophy | Recognizing an achievement or milestone |
Where Insights Appear
| Page | Backend-Supported | Context Provided | Example Insight |
|---|---|---|---|
| Dashboard | ✅ Yes | Active/completed counts, streak, urgent goal, weekly check-ins | "Time to Check In" if urgent goal inactive >3 days |
| Goals List | ✅ Yes | Average progress, needs-attention count, categories | "Goals Need Attention" if any are behind |
| Goal Detail | ✅ Yes | Goal name, progress %, days left, milestones, events, status | "Almost There!" if progress ≥75% |
| Achievements | ✅ Yes | Unlocked/total badge count, level, nearest badge | "Badges Within Reach" if partially progressed |
| Communities | ✅ Yes | Joined count, posts this week | "Stronger Together" if no communities joined |
| Admin | ❌ No | User/goal counts, growth rates | Client template only (not part of daily batch) |
Note on backend support: The 5 backend-supported pages (dashboard, goals, goal-detail, achievements, communities) receive insights from the daily batch job. The admin page uses the client-side generateMockInsight template only and is not wired to the backend batch.
Two more insights ride this same composable but sit outside the daily batch pack: a mood-goal correlation insight (Dashboard) and a pace-suggestion card with one-tap accept (Goals), both flag-gated and owner-scoped. See AI Insight Intelligence for details.
Context Building
The useAiInsights(page) composable builds page-specific context for each insight request:
Dashboard context:
activeGoalCount,completedGoalCount,streak,urgentGoalName,urgentGoalDaysSince,weeklyCheckIns
Goals context:
averageProgress,needsAttentionCount,goalCategories[]
Goal Detail context:
goalName,goalProgress,goalDaysLeft,hasMilestones,eventCount,goalStatus
Achievements context:
unlockedBadgeCount,totalBadgeCount,userLevel,nearestBadgeKey
How Insights Are Sourced
Insights follow a three-rung fallback ladder:
Rung 1: Backend daily batch — Backend job (
AiCoaching::GenerateDailyInsightPackJob) runs once per day and populatesRails.cachewith LLM-generated insights for each user per page. Frontend retrieves via theme.currentInsightsGraphQL query. Cached for 24 hours per(user, page)key.Rung 2: Persona-aware library — If the backend pack is unavailable (AI disabled, rate-limited, or job backlog), fallback copy from
Ai::Fallbacks::Insights(6 personas × 4 surface types) is returned instead. Persona-aware, brand-voice clean, never returns nil.Rung 3: Client-side template — If the backend never returns a pack (e.g., network error), frontend
generateMockInsight()rule template runs locally based on page context. Provides fallback copy without LLM context. Kept through v1.15 soak window, then deleted in v1.15.
Eligibility: Backend daily batch only generates insights for users with active (non-completed) goals. Inactive users receive no batch pack, falling through to Rung 2 (persona-aware library) or Rung 3 (client template).
Caching & Session Behavior
- Apollo cache: Frontend
me.currentInsightsresult cached by Apollo for the session. - In-session re-fetch: Frontend
cacheTimestampsMap tracks when each page's insight was last fetched. If ≥15 minutes have elapsed since the last fetch, the query re-runs on next navigation to that page. - Dismiss state: Dismissed insights stored in
localStorageunder'ai-insights-dismissed'key with stable IDs and timestamps. Dismissed insights stay hidden for 24 hours. After expiry, the insight can re-appear on the page.
Card UI
- Indigo-themed card with left gradient accent bar
- Icon in gradient circle (indigo-to-purple)
- Title (bold) + message text
- Optional CTA button with ChevronRight icon
- Dismiss button (X) when
dismissibleprop is true - Compact variant available via
compactprop - Loading skeleton with pulsing animation
Goal Coach
What It Is
An interactive chat modal where users can ask for personalized goal advice. The Coach responds with motivational, strategic, or tactical guidance.
Opening the Coach
- Available from the Goal Detail page
- Click the Coach button to open the modal
Chat Flow
- Modal opens with a welcome message contextually generated from the current goal
- Suggested prompts appear as chips (e.g., "How can I stay motivated?", "What milestones should I set?")
- User types a question or taps a prompt
- AI generates a response, structured into short paragraphs (with a bullet list only when it's genuinely enumerating steps)
- Chat history persists within the modal session
Chat UI
- Mobile: 80% height bottom sheet
- Desktop: 600x500px centered modal
- Welcome message with Coach avatar (gradient sparkles icon)
- Suggested prompt chips (indigo borders)
- User messages: blue background, right-aligned, plain text
- Coach messages: indigo background, left-aligned, rendered as sanitized structured HTML — see Structured reply rendering below
- Loading: three animated bouncing dots
- Input field with send button (disabled when empty or loading)
Structured reply rendering
Coach replies render as short paragraphs separated by whitespace, with a real bullet list when the reply enumerates steps — not as one dense block of text. Implemented in CoachChatPanel.vue's formatAssistantContent():
- The reply text is split on blank lines into paragraphs and
-/*line groups into list items - Paragraphs become
<p>tags, list groups become<ul><li>tags;**bold**becomes<strong>and*italic*/_italic_becomes<em> - The result is sanitized via DOMPurify with an allow-list of exactly
['p', 'br', 'ul', 'ol', 'li', 'strong', 'em']andALLOWED_ATTR: []— no links, attributes, or scripts can reach the DOM - The assistant bubble's
v-htmlhost is a<div>(not a<p>, which can't contain block children like<ul>); block/list spacing (12px between blocks, 4px between list items,coach/70-tinted disc markers) is applied via Tailwind classes on that wrapper only, never on the generated inner HTML - A single-paragraph reply renders with no extra margin — visually identical to the old unstructured output
The user's own messages are unaffected — they still render as plain text via a <p> in the sibling branch.
Backend
The CoachService.get_advice(user_context) method:
- Builds a prompt with the user's context (goal details, progress, history)
- Calls the configured LLM provider
- Returns a text response instructed to be structured into short paragraphs, with a bullet list reserved for genuine step enumerations — via a core rule in
Ai::Prompts::Base.build_personality_preamble(Ai::Prompts::Coaching::VERSION = 6) - Routes the request through
Ai::ServiceClientto LiteLLM
Milestone Generation
What It Does
Automatically suggests 3–5 milestones based on a goal's name and description. Available during goal creation when the goal name has 3+ characters.
How to Use
- On the GoalCreate page, enter a goal name (minimum 3 characters)
- Tap Auto-Generate button (appears next to the milestones section)
- Button shows "Thinking..." spinner during generation
- 3–5 milestone suggestions appear in the milestone list
- User can keep, edit, or remove any suggestion
Backend
The CoachService.generate_milestones(goal_name, goal_context) method:
- Builds a prompt asking for 3–5 specific, actionable milestones
- Calls the configured LLM provider
- Parses the response as a JSON array of strings
- Returns the milestone text suggestions
Coach-Drafted Goals
What It Does
Turns a single freeform sentence into a complete, form-ready goal: title, category, kind, suggested target date, a short "why", and 3–5 first milestones. Available on the GoalCreate screen when the page opens normally (not via deeplink or template pre-fill).
How to Use
- Open the New Goal screen — the Coach hero appears by default
- Type what you want to achieve (e.g. "I want to run a 5K by spring")
- Tap Build with Coach or press Enter
- The hero shows a loading skeleton while the draft generates
- Form fields hydrate automatically: title, category, kind, target date, and milestones are pre-filled
- Edit any field before saving
Backend
The CoachService.generate_goal_draft(idea, categories:, kinds:, user_id:) method:
- Builds a prompt from the user's idea, passing the full list of available GoalCategories and GoalTypes as allowed values
- Calls the
coaching/milestonesLLM model viaAi::ServiceClient - Parses the JSON response and resolves category/kind names to reference-table integer ids
- Returns a hash:
{ title, category_id, category_name, kind_id, kind_name, target_date, why, milestones } - Results cached for 1 hour — same idea returns the same draft until the cache expires
Fallback
If the service is unavailable (AI disabled, rate-limited, or nil response), the mutation returns { draft: null }. GoalCreate stays in form phase and the user proceeds with manual entry — no error toast. A blank idea raises INVALID_INPUT before the AI call.
Goal Quality Check
What It Does
A pre-commit, opt-in critique of a goal the user has already formed — not a draft generator like Coach-Drafted Goals above. Reads the goal-as-typed (title, description, target date, category, kind, milestones) and returns up to 3 one-tap-acceptable suggestions to make it more specific, realistically dated, or right-sized (e.g. add a milestone, tighten a vague title, set a target date). Non-blocking: Save is always available whether or not the user checks. Ships dark behind the goal_refinement_enabled flag (0% rollout).
How to Use
- On the GoalCreate screen, fill in a goal (title required)
- Tap Check my goal — a card appears: "Give it a gut check"
- Coach returns 0–3 suggestions, each with a one-sentence issue, a coaching suggestion, and an Accept chip showing the concrete replacement value, plus a Dismiss (×)
- Tap Accept on a suggestion to apply its
suggested_valueto the matching field (title, description, target date, or a new milestone) — the field flashes a brief coach-ring pulse and focus moves to the next remaining suggestion's Accept button (or to Create goal if none remain) - Tap Dismiss to discard a suggestion without applying it
- If the goal is already solid, the card shows "Looks solid — nothing to tighten."
- If Coach is unavailable (flag off, AI disabled, or rate-limited), the card shows "Coach is resting — save whenever you're ready." — Save is never blocked
Backend
The CoachService.refine_goal(name, description:, target_date:, category_name:, kind_name:, milestones:, user_id:) method:
- Builds a prompt (
Ai::Prompts::GoalQualityCheck,VERSION = 1) asking the model to gut-check the goal-as-formed for a vague title, missing/unrealistic target date, or no milestones — instructed not to invent issues with an already-solid goal - Calls the
coaching/milestonesLLM model viaAi::ServiceClient, on thegoal_quality_checkrate-limit category (5/min per user) and gated byAi::SettingsGuard.enabled?('coaching_enabled', ...)(same backend kill-switch as Coach-Drafted Goals and Refine Description) - Parses the JSON response into an array of up to 3
{ field, issue, suggestion, suggested_value }hashes (fieldis one oftitle,description,target_date,milestone), or an empty array if the goal is already solid - Results cached for 1 hour by prompt hash — resubmitting the same goal-as-formed returns the same critique until the cache expires
- Returns
nilon any failure (AI disabled, rate-limited, parse error) — fail-open; therefineGoalmutation catches this and returns{ suggestions: nil }rather than raising, so the frontend hides the surface instead of erroring
Operates entirely on unsaved form data — no persisted model, no migration, no new table.
Frontend
useGoalRefinement.ts composable (models on useGoalDraft.ts, never throws) drives a GoalQualityCheck.vue card mounted in GoalCreate.vue's form phase. State machine: idle → loading → suggestions (≤3, client-clamped) | clear (all-solid) | unavailable (fail-open or rate-limited). Gated by the goal_refinement_enabled PostHog flag (ionic_frontend/src/lib/featureFlags.ts, fail-closed — surface never renders when the flag is off); a dev override key (objectuve_goal_refinement_enabled_override) exists for local/preview testing.
Rate limiting & flags
Distinct from the frontend PostHog flag: the refineGoal mutation also checks the backend coaching_enabled Settings kill-switch and its own goal_quality_check rate-limit category (5 requests/min/user, Ai::RateLimiter). Hitting the rate limit surfaces as the throttle copy above, not an error toast.
LLM Backend Service
CoachService (rails_api/app/services/ai/coach_service.rb)
Routes all LLM calls through Ai::ServiceClient (rails_api/app/services/ai/service_client.rb), which proxies to a LiteLLM gateway. Two environment variables are required:
| Variable | Purpose |
|---|---|
LITELLM_URL | LiteLLM gateway URL (default: http://localhost:4000) |
LITELLM_MASTER_KEY | Authentication key for the LiteLLM gateway |
Provider selection (OpenAI, Anthropic, Ollama, etc.) is configured inside LiteLLM, not via Rails environment variables. No direct provider API keys are read by Rails.
Methods
| Method | Input | Output |
|---|---|---|
generate_milestones(goal_name, goal_context) | Goal name + optional description | Array of 3–5 milestone strings |
generate_goal_draft(idea, categories:, kinds:) | Freeform idea sentence + allowed category/kind lists | { title, categoryId, categoryName, kindId, kindName, targetDate, why, milestones } or null |
refine_goal(name, description:, target_date:, category_name:, kind_name:, milestones:) | Goal-as-formed (unsaved) | Array of ≤3 { field, issue, suggestion, suggested_value }, [] if solid, or null |
get_advice(user_context) | User context string (question + goal data) | Structured motivational text (short paragraphs, optional bullet list) |
generate_insight(page_context) | Page name + context object | { type, title, message, cta_label } |
UI Components
AiInsightCard
- Props:
type,title,message,ctaLabel,dismissible,loading,compact - Emits:
dismiss,cta-click - Indigo gradient accent, responsive icon sizing
AiCoachModal
- Props: none (uses
useAiCoach()composable internally) - Responsive sizing (bottom sheet on mobile, centered on desktop)
- Smooth enter/leave animations
- Auto-scrolls to latest message
GoalQualityCheck
- Emits:
check,accept,dismiss,dismiss-all,all-resolved - States: idle/prompt · loading · suggestions (≤3) · accepted (field flash + focus move) · all-clear · unavailable/coach-resting
- 44×44 tap targets, coach focus rings, reduced-motion → opacity-only; light + dark
Testing the Feature
Manual Testing Checklist
1. Dashboard Insight
- [ ] Navigate to Dashboard with active goals
- [ ] Verify an Insight card appears
- [ ] Verify the insight is contextually relevant (e.g., mentions streak or urgent goal)
- [ ] Tap dismiss (X) → verify card disappears
- [ ] Reload page within 24 hours → verify dismissed insight stays hidden
- [ ] Wait 24 hours (or clear localStorage) → verify insight returns
2. Goals Page Insight
- [ ] Navigate to Goals page
- [ ] Verify insight appears with goals-specific context
- [ ] If goals need attention: verify "action" type insight
- [ ] Tap CTA button → verify navigation or action triggers
3. Goal Detail Insight
- [ ] Navigate to a goal detail page
- [ ] Verify insight reflects goal status (at-risk → "Get Back on Track", almost done → "Almost There!")
- [ ] Navigate to a different goal → verify insight updates for new context
4. Achievements Insight
- [ ] Navigate to Achievements page
- [ ] Verify insight mentions badge progress
- [ ] Tap CTA → verify it applies the relevant filter
5. Goal Coach
- [ ] Navigate to a goal detail page
- [ ] Open Coach modal
- [ ] Verify welcome message appears with goal context
- [ ] Verify suggested prompts display as chips
- [ ] Tap a suggested prompt → verify Coach response appears
- [ ] Type a custom question → verify Coach responds
- [ ] Ask something that prompts a multi-part answer → verify the reply renders as separated paragraphs and/or a bullet list, not one dense block
- [ ] Verify chat history persists within the session
- [ ] Close and reopen → verify fresh session starts
6. Coach-Drafted Goals
- [ ] Open the New Goal screen (no deeplink, no template pre-fill)
- [ ] Verify the Coach hero appears (not the form)
- [ ] Type "I want to run a 5K" → tap Build with Coach
- [ ] Verify loading skeleton shows during generation
- [ ] Verify form fields hydrate: title, category, kind, target date, and milestones pre-filled
- [ ] Verify milestones are relevant to the idea
- [ ] Edit one field → verify edit persists after submission
- [ ] Clear the idea field and tap Build → verify the button is disabled or an error appears
- [ ] Deeplink to create (e.g. with
goalKindIdparam) → verify hero is bypassed and form shows directly - [ ] Template select → verify hero is bypassed
7. Goal Quality Check
- [ ] On GoalCreate, enter a vague goal title (no description, no target date, no milestones)
- [ ] Tap Check my goal → verify a loading skeleton, then ≤3 suggestions appear
- [ ] Tap Accept on a suggestion → verify the matching field updates and flashes, and focus moves to the next Accept button (or Create goal, if none remain)
- [ ] Tap Dismiss on a suggestion → verify it's removed without changing the field
- [ ] Enter a specific, dated, milestone-having goal → tap Check → verify "Looks solid — nothing to tighten."
- [ ] With
goal_refinement_enabledoff (or via dev override), verify the card never renders and Save is unaffected - [ ] Verify Save is always available, before and after tapping Check
8. AI Milestone Generation
- [ ] Start creating a new goal
- [ ] Enter name "Learn to play guitar" (3+ chars)
- [ ] Tap Auto-Generate
- [ ] Verify loading spinner appears
- [ ] Verify 3–5 milestones are suggested
- [ ] Verify milestones are relevant to "Learn to play guitar"
- [ ] Edit one milestone → verify edit persists
- [ ] Remove one milestone → verify it's deleted
- [ ] Try with < 3 character name → verify button is disabled/hidden
9. Insight Caching
- [ ] Load Dashboard → note the insight content
- [ ] Navigate away and return within 15 minutes
- [ ] Verify same insight displays (cached)
- [ ] Wait 15+ minutes or clear cache → verify new insight generates
10. Loading States
- [ ] On slow connection, verify insight card shows skeleton animation
- [ ] Verify Coach modal shows bouncing dots while waiting for response
- [ ] Verify Generate Milestones shows "Thinking..." text
Backend Unit Tests
cd rails_api
# Service specs
bundle exec rspec spec/services/ai/coach_service_spec.rb
# GraphQL specs
bundle exec rspec spec/requests/mutations/ai/generate_milestones_spec.rb
bundle exec rspec spec/requests/mutations/ai/generate_goal_draft_spec.rb
bundle exec rspec spec/requests/mutations/ai/refine_goal_spec.rb
bundle exec rspec spec/requests/mutations/ai/get_insight_spec.rb
bundle exec rspec spec/requests/mutations/ai/get_advice_spec.rbFrontend Unit Tests
cd ionic_frontend
npm run test:unit -- --run -t "AiInsightCard"
npm run test:unit -- --run -t "AiCoachModal"
npm run test:unit -- --run -t "useAiInsights"
npm run test:unit -- --run -t "useAiCoach"
npm run test:unit -- --run -t "CoachGoalHero"
npm run test:unit -- --run -t "useGoalDraft"
npm run test:unit -- --run -t "GoalQualityCheck"
npm run test:unit -- --run -t "useGoalRefinement"GraphQL API Reference
Queries
me.currentInsights
Fetches the daily insight pack for the logged-in user:
query GetCurrentInsights {
me {
currentInsights {
page # "dashboard" | "goals" | "goal-detail" | "achievements" | "communities"
id # Stable insight ID (e.g., "dashboard-tip-consistency")
type # "tip" | "action" | "insight" | "celebration"
title
message
ctaLabel # Optional CTA button text
}
}
}Behavior:
- Returns
[](empty array) whenSettings.ai.batch_enabled = false(intentional asymmetry — see below) - Returns one insight per page in the pack (max 5, one per supported page)
- Uses fallback logic if backend pack unavailable: Rung 2 (persona-aware) or Rung 3 (client template)
- Does NOT raise
GraphQL::ExecutionError— returns[]silently
Intentional asymmetry with mutations: The getInsight and generateMilestones mutations raise AI_DISABLED error when the feature is disabled. The currentInsights query returns [] instead. Why? The query is called on every page mount across the app. A hard error would surface as a toast on every navigation, creating a poor UX. Returning [] allows the frontend to render the fallback rung silently.
Mutations
mutation GetInsight($pageContext: JSON!) {
getInsight(pageContext: $pageContext) {
insightId
insightType # "tip" | "action" | "insight" | "celebration"
title
message
ctaLabel # Optional CTA button text
}
}
mutation GetAdvice($goalId: String, $page: String!, $userMessage: String!) {
getAdvice(goalId: $goalId, page: $page, userMessage: $userMessage) {
advice # structured motivational text (short paragraphs, optional bullet list)
}
}
mutation GenerateMilestones($goalName: String!, $goalContext: String) {
generateMilestones(goalName: $goalName, goalContext: $goalContext) {
milestones # Array of milestone text strings
}
}
mutation GenerateGoalDraft($idea: String!) {
generateGoalDraft(idea: $idea) {
draft {
title
why
categoryId # Integer reference-table id (GoalCategory)
categoryName
kindId # Integer reference-table id (GoalType)
kindName
targetDate # ISO8601 date or null for habits/ongoing goals
milestones # Array of milestone text strings
}
}
}
mutation RefineGoal(
$name: String!
$description: String
$targetDate: String
$categoryName: String
$kindName: String
$milestones: [String!]
) {
refineGoal(
name: $name
description: $description
targetDate: $targetDate
categoryName: $categoryName
kindName: $kindName
milestones: $milestones
) {
suggestions {
field # "title" | "description" | "target_date" | "milestone"
issue
suggestion
suggestedValue
}
}
}refineGoal behavior: raises INVALID_INPUT if name is blank (before the AI call); raises AI_DISABLED if the backend coaching_enabled Settings flag is off; raises AI_RATE_LIMITED at 5 requests/min/user; returns { suggestions: null } (not an error) on any other AI failure, so the frontend fails open and hides the surface rather than showing a toast.
Known Behaviors & Edge Cases
LiteLLM gateway: All LLM calls route through
Ai::ServiceClient, which readsLITELLM_URLandLITELLM_MASTER_KEY. Provider selection and model routing are configured inside LiteLLM, not in Rails. If the gateway is unreachable, the service raises an error that surfaces as a toast notification.Insight caching prevents flicker: Insights are cached for 15 minutes to prevent regeneration on every page navigation. Dismiss state is separate (24-hour localStorage persistence).
Coach context assembly: Context (goal details, page, user message, personality profile, motivation profile) is assembled server-side by
Ai::BuildCoachContext. The system prompt also includes## Personality profile(enneagram framing, when available) and## Goal motivation profile(motivation framing, when available to personalize tone and recommendations). This means the AI has context even for short questions like "help".Milestone generation minimum: The goal name must be at least 3 characters. With shorter names, the Auto-Generate button is hidden/disabled.
Rate limiting: There is no client-side rate limiting on AI requests. Excessive use would hit the LLM provider's rate limits, returning errors that display as toast notifications.
Insight types are hints: The LLM is asked to return an insight type (tip/action/insight/celebration), but it may not always match the expected pattern. The frontend handles any type gracefully.
Coach history is session-scoped: Chat history is stored in the composable's reactive state. Closing the modal and reopening starts a fresh session — there is no persistence across sessions.
Admin insights: The admin page uses the same insight infrastructure with admin-specific context (user counts, growth rates). Only users with
admin: truesee the admin page and its insights.Goal Quality Check is dark by default: gated by the
goal_refinement_enabledPostHog flag at 0% rollout. It also depends on the backendcoaching_enabledSettings flag, same as Coach-Drafted Goals and Refine Description — either flag being off hides/disables the surface, fail-open, with no error toast.Goal Quality Check field scope: the prompt and
GoalRefinementSuggestionTypeonly ever returnfield: title | description | target_date | milestone— category and recurrence are not covered by this slice.
Last updated: 2026-07-27 (OBJ-1815: Coach chat replies now render as structured short paragraphs + an optional bullet list instead of one dense block, PR #1826)