Goal Templates — Curated Library
A curated library of 28 hand-built goal templates across fitness, learning, financial, habit/wellness, career, productivity, and relationships domains. Users can select a template when creating a goal to prefill the form with realistic milestones, timelines, and example structure — eliminating the blank-slate problem for new goal creators.
Overview
Goal Templates ship as a read-only, curator-authored feature that reduces friction at goal creation. Users tap "Start from a template" in GoalCreate.vue, browse 28 curated templates grouped by theme, select one, and see their goal form prefilled with template name, category, estimated duration, and milestone suggestions. All fields remain editable — templates are starting points, not constraints. The goal + milestones create atomically in a single round-trip via the extended addGoal mutation.
For users: Templates make it easier to get started when motivation is high but direction is uncertain. "Run Your First 5K" is more actionable than a blank form.
For the product: PostHog telemetry captures template adoption. The has_created_from_template user property enables cohort analysis: users who start with templates show higher engagement metrics than legacy blank-form creators.
GraphQL Surface
Queries
goalTemplates
query GetGoalTemplates {
goalTemplates {
publicId
name
description
theme # One of: fitness, learning, financial, habit_wellness, career, productivity, relationships
category { name }
estimatedDurationDays
imageUrl # Nullable; bespoke template hero or null for category-default stock photo
displayOrder
milestones {
name
order
daysOffsetFromStart
}
}
}Unauthenticated-accessible query. Returns all 28 non-deleted templates ordered by (theme, display_order) (alphabetical by theme, then curator-ordered).
Mutations
addGoal (extended)
The standard addGoal mutation accepts two new optional arguments:
mutation CreateGoalFromTemplate(
$name: String!
$description: String
$targetDate: String
$fromTemplateId: ID
$milestones: [MilestoneInput!]
) {
addGoal(
name: $name
description: $description
targetDate: $targetDate
fromTemplateId: $fromTemplateId
milestones: $milestones
) {
goal {
publicId
name
fromTemplate { name } # Nullable; used for badge rendering
}
errors
}
}fromTemplateId(ID, optional): The template'spublic_idif creating from a template. Stored ingoals.from_template_id(string column).milestones(array ofMilestoneInput, optional): Template milestone list (or user-customized). Array of{ name: String!, order: Int!, daysOffsetFromStart: Int! }.
Pre-v1.21 calls (no fromTemplateId, no milestones array) work unchanged — full backward compatibility.
Goal Type fields
type GoalType {
fromTemplate: GoalTemplateType # Nullable
# ... existing fields
}
type GoalTemplateType {
publicId: ID!
name: String!
description: String!
theme: String! # One of: fitness, learning, financial, habit_wellness, career, productivity, relationships
category: GoalCategoryType
estimatedDurationDays: Int!
imageUrl: String # Nullable
displayOrder: Int!
milestones: [GoalTemplateMilestoneType!]!
}
type GoalTemplateMilestoneType {
name: String!
order: Int!
daysOffsetFromStart: Int!
}Telemetry Events
All events fire from documented call sites. Verified by Phase 85 Vitest assertions on posthog.capture mocks.
| Event | Fire-site | Payload | Notes |
|---|---|---|---|
template_browsed | ionic_frontend/src/components/TemplatePickerModal.vue:357 (watch isOpen with { immediate: true }) | { source: 'goal_create' } | Fires each time isOpen transitions from false → true. { immediate: true } fires the watcher on mount; if modal is closed at mount (typical), only the reset branch runs. |
template_selected | ionic_frontend/src/components/TemplatePickerModal.vue:388 (confirmTemplate() method; posthog.capture call at line 390) | { templateId: string, templateName: string, theme: string } | Fires on "Use this template" confirmation, before emitting the select event up to parent. |
goal_created_from_template | ionic_frontend/src/composables/useGoalForm.ts:237 (on addGoal mutation success when fromTemplateId is present) | { templateId: string, templateName: string, goalPublicId: string } | Fires only when fromTemplateId is present in the mutation input; legacy addGoal calls (no fromTemplateId) do not fire this event. |
has_created_from_template (user property) | ionic_frontend/src/composables/useClerkSync.ts (setPostHogUser 4th param) + rails_api/app/graphql/types/user_type.rb (resolver) | true if user has ≥1 goal with from_template_id set | Set at app boot via the hasCreatedFromTemplate field (computed from goals.where.not(from_template_id: nil).exists?); also set immediately at addGoal success via posthog.people.set({ has_created_from_template: true }). Mirrors the v1.18 has_enneagram_assessment pattern. |
Feature Scope — v1.21
In scope:
- 28 curated templates (4 each across fitness, learning, financial, habit/wellness, career, productivity, relationships)
- Template picker modal from
GoalCreate.vue - Form prefill on selection (name, category, target date computed from estimated duration, milestone list)
- Atomic goal + milestone creation via extended
addGoalmutation - "From template: [name]" badge on
Goal.vuedetail page - Three PostHog capture events +
has_created_from_templateuser property
Out of scope (deferred to future milestones):
- User-generated templates or template marketplace
- Template ratings, usage analytics, or popularity sort
- Template editing by users (templates are read-only curator-owned content)
- Separate
goal_template_milestonestable (JSONB array on template model) - Separate
CreateGoalFromTemplatemutation (extendaddGoalinstead) - Top-level "Templates" discovery surface (picker accessible only from goal creation)
Brand Voice & PBC Alignment
Template copy is action-forward and coaches-as-friend tone. No paywall, no Supporter-only templates, no FOMO. Templates are free-tier core feature.
Example templates (all brand-voice approved):
- Fitness: "Run Your First 5K", "Build a 90-Day Strength Foundation", "Train for a Half Marathon"
- Learning: "Learn to Code in 90 Days", "Reach Conversational Fluency in a New Language", "Finish an Online Course in 30 Days"
- Financial: "Build a 6-Month Emergency Fund", "Pay Off Credit Card Debt", "Start Investing with $1,000"
- Habit/Wellness: "Build a Daily Meditation Practice", "Complete a 30-Day Alcohol-Free Reset", "Establish a Consistent Morning Routine"
Milestone names lead with action verbs. No clinical language, no generic motivation ("You can do it!"). Specific, encouraging, achievable.
Technical Notes
- Model:
GoalTemplateinherits fromPublicRecord,acts_as_paranoid. Columns:public_id,name,description,theme(indexed),category_id,estimated_duration_days,image_url(nullable),milestones(JSONB, array of{name, order, days_offset_from_start}),display_order,deleted_at, timestamps. - Data layer: Phase 82 (PR #671) delivered model, migration, GraphQL type, and placeholder seeds.
- Picker UI: Phase 84 (PR #682) delivered modal, card component, and form integration.
- Telemetry + content: Phase 85 (PR #698) delivered all three PostHog events,
has_created_from_templateuser property, and the canonical template set (now 28 templates across 7 themes).
Related
- Roadmap: v1.21-goal-template-library-ROADMAP.md
- PRD: Phase 4 Engagement & Retention — Feature 5
- Milestone narrative: v1.21 — Goal Template Library
Last updated: 2026-07-06