6/8 - Public Beta (Discord)
|See Changelog
Skip to main content

Program Plans API

The Program Plans API provides a comprehensive fitness program catalog system that manages structured workout programs, enables program discovery through advanced filtering, provides AI-powered recommendations, and handles program creation and management. This system serves as the foundation for organized training progression in OpenLift.

⚠️ CRITICAL: THIN CLIENT IMPLEMENTATION ONLY

What Flutter SHOULD do:

  • ✅ Consume GraphQL endpoints for program data
  • ✅ Display program catalogs and details
  • ✅ Handle filtering UI and search interfaces
  • ✅ Cache program data for offline browsing

What Flutter MUST NOT do:

  • ❌ Implement program recommendation algorithms
  • ❌ Calculate program progression or weekly modifications
  • ❌ Duplicate program filtering business logic
  • ❌ Make decisions about program suitability

🏗️ Program Plans Architecture

Core Program Structure

Program Plans use a hierarchical structure designed for progressive training:

Program Plan Architecture
interface ProgramPlan {
// Basic Information
id: string;
name: string;
description: string;
author?: string;
version?: string;

// Program Configuration
trainingFocus: TrainingFocus;
level: TrainingLevel; // BEGINNER | INTERMEDIATE | ADVANCED
goal: TrainingGoal; // MUSCLE_GAIN | STRENGTH_GAIN | WEIGHT_LOSS | ENDURANCE
durationWeeks: number; // Program length
daysPerWeek: number; // Training frequency

// Program Structure
workoutTemplateLinks: TemplateLink[]; // Ordered workout templates
weeklyModifiers: WeeklyModifier[]; // Progressive modifications
loopWeekly: boolean; // Whether program repeats

// Requirements & Categorization
equipment: string[]; // Required equipment
tags: string[]; // Program tags/keywords
workoutSplit?: WorkoutSplit; // Categorization split

// Publishing & Access
visibility: Visibility; // PUBLIC | PRIVATE | SHARED
programType: ProgramType; // STOCK | CUSTOM | COMMUNITY
isPublished: boolean; // Available in catalog
stock: boolean; // OpenLift official program

// Media & Presentation
coverImageFileKey?: string;
coverImageUrl?: string; // Signed URL
}

Program Structure Components

  1. Template Links - Ordered list of workout templates that make up the program
  2. Weekly Modifiers - Progressive changes to intensity/volume by week
  3. Training Focus - Primary muscle group or training emphasis
  4. Equipment Requirements - List of required gym equipment
  5. Workout Split - Training split categorization (Push/Pull/Legs, Upper/Lower, etc.)

🔍 GraphQL Schema

Core Types

ProgramPlan Object Type
type ProgramPlan {
id: ID!
name: String!
description: String!
author: String
version: String

# Program Configuration
trainingFocus: ProgramTrainingFocus!
level: TrainingLevel!
goal: TrainingGoal!
durationWeeks: Int!
daysPerWeek: Int!
loopWeekly: Boolean!

# Structure & Content
workoutTemplateLinks: [ProgramPlanTemplateLink!]!
weeklyModifiers: [PlanWeeklyModifier!]!

# Requirements
equipment: [String!]!
tags: [String!]!
workoutSplit: WorkoutSplit

# Publishing
visibility: Visibility!
programType: ProgramType!
isPublished: Boolean!
stock: Boolean!
deleted: Boolean!

# Media
coverImageFileKey: String
coverImageUrl: String

# Split Analysis
splitConfidence: Float
userConfirmedSplit: Boolean!

# Relations
createdBy: User!
createdAt: DateTime!
}

🔧 GraphQL Operations

Queries

Get Program by ID
query ProgramPlan($id: ID!) {
programPlan(id: $id) {
id
name
description
author
version
trainingFocus {
id
name
description
primaryMuscleGroups {
id
name
}
}
level
goal
durationWeeks
daysPerWeek
loopWeekly
workoutTemplateLinks {
programWorkoutTemplateId
order
}
weeklyModifiers {
weekNumber
name
description
rpeMultiplier
repsMultiplier
weightMultiplier
}
equipment
tags
workoutSplit {
id
name
description
daysPerWeek
}
visibility
programType
isPublished
stock
coverImageFileKey
coverImageUrl
splitConfidence
userConfirmedSplit
createdBy {
id
username
fullName
}
createdAt
}
}

Variables:

{
"id": "60d5ecb54f3d4d2e8c8e4b5a"
}

Response Example:

{
"data": {
"programPlan": {
"id": "60d5ecb54f3d4d2e8c8e4b5a",
"name": "Beginner Strength Builder",
"description": "A 12-week progressive strength program designed for beginners focusing on compound movements and foundational strength development.",
"author": "OpenLift Team",
"version": "v2.1",
"trainingFocus": {
"id": "full_body_strength",
"name": "Full Body Strength",
"description": "Comprehensive strength training targeting all major muscle groups",
"primaryMuscleGroups": [
{"id": "chest", "name": "Chest"},
{"id": "back", "name": "Back"},
{"id": "legs", "name": "Legs"}
]
},
"level": "BEGINNER",
"goal": "STRENGTH_GAIN",
"durationWeeks": 12,
"daysPerWeek": 3,
"loopWeekly": false,
"workoutTemplateLinks": [
{
"programWorkoutTemplateId": "template_1_full_body_a",
"order": 1
},
{
"programWorkoutTemplateId": "template_2_full_body_b",
"order": 2
}
],
"weeklyModifiers": [
{
"weekNumber": 1,
"name": "Foundation Week",
"description": "Focus on form and technique",
"rpeMultiplier": 0.6,
"repsMultiplier": 1.0,
"weightMultiplier": 0.7
},
{
"weekNumber": 4,
"name": "Progression Week",
"description": "Increase intensity",
"rpeMultiplier": 0.8,
"repsMultiplier": 1.0,
"weightMultiplier": 0.85
}
],
"equipment": ["BARBELL", "DUMBBELLS", "BENCH", "SQUAT_RACK"],
"tags": ["beginner", "strength", "compound", "progressive"],
"workoutSplit": {
"id": "full_body",
"name": "Full Body",
"description": "Complete body workout targeting all muscle groups",
"daysPerWeek": 3
},
"visibility": "PUBLIC",
"programType": "STOCK",
"isPublished": true,
"stock": true,
"coverImageFileKey": "program_covers/beginner_strength.jpg",
"coverImageUrl": "https://storage.example.com/program_covers/beginner_strength.jpg?expires=1640999999",
"splitConfidence": 0.95,
"userConfirmedSplit": true,
"createdBy": {
"id": "admin_user",
"username": "openlift_admin",
"fullName": "OpenLift Team"
},
"createdAt": "2023-01-01T10:00:00.000Z"
}
}
}

Authorization: Public programs accessible to all; private programs require ownership Use Cases: Program detail views, program preview, enrollment preparation

Mutations

Create New Program Plan
mutation CreateProgramPlan($input: CreateProgramPlanInput!) {
createProgramPlan(input: $input) {
id
name
description
trainingFocus {
id
name
}
level
goal
durationWeeks
daysPerWeek
workoutTemplateLinks {
programWorkoutTemplateId
order
}
weeklyModifiers {
weekNumber
name
rpeMultiplier
repsMultiplier
weightMultiplier
}
equipment
tags
visibility
programType
isPublished
createdAt
}
}

Input Type:

input CreateProgramPlanInput {
# Required fields
name: String!
description: String!
trainingFocusId: ID!
level: TrainingLevel!
goal: TrainingGoal!
durationWeeks: Int!
daysPerWeek: Int!

# Optional metadata
version: String
author: String
coverImageFileKey: String

# Configuration
visibility: Visibility # Default: PRIVATE
programType: ProgramType # Default: CUSTOM
loopWeekly: Boolean # Default: false
isPublished: Boolean # Default: false
stock: Boolean # Default: false

# Requirements & categorization
equipment: [String!]
tags: [String!]
workoutSplitId: ID
userConfirmedSplit: Boolean

# Program structure
workoutTemplateLinks: [ProgramPlanTemplateLinkInput!]
weeklyModifiers: [ProgramPlanWeeklyModifierInput!]
}

input ProgramPlanTemplateLinkInput {
programWorkoutTemplateId: ID!
order: Int!
}

input ProgramPlanWeeklyModifierInput {
weekNumber: Int!
defaultModifierId: ID # Reference to default modifier
name: String # Custom modifier name
description: String
rpeMultiplier: Float
repsMultiplier: Float
weightMultiplier: Float
}

Variables Example:

{
"input": {
"name": "My Custom Upper/Lower Split",
"description": "A personalized 4-day upper/lower split focusing on strength and hypertrophy",
"trainingFocusId": "strength_hypertrophy",
"level": "INTERMEDIATE",
"goal": "MUSCLE_GAIN",
"durationWeeks": 8,
"daysPerWeek": 4,
"author": "John Doe",
"visibility": "PRIVATE",
"programType": "CUSTOM",
"equipment": ["BARBELL", "DUMBBELLS", "BENCH", "CABLE_MACHINE"],
"tags": ["upper-lower", "hypertrophy", "strength"],
"workoutSplitId": "upper_lower_4day",
"workoutTemplateLinks": [
{
"programWorkoutTemplateId": "template_upper_1",
"order": 1
},
{
"programWorkoutTemplateId": "template_lower_1",
"order": 2
},
{
"programWorkoutTemplateId": "template_upper_2",
"order": 3
},
{
"programWorkoutTemplateId": "template_lower_2",
"order": 4
}
],
"weeklyModifiers": [
{
"weekNumber": 1,
"name": "Base Week",
"description": "Establish baseline",
"rpeMultiplier": 0.7,
"repsMultiplier": 1.0,
"weightMultiplier": 0.8
},
{
"weekNumber": 4,
"name": "Intensity Week",
"description": "Peak intensity",
"rpeMultiplier": 0.9,
"repsMultiplier": 1.0,
"weightMultiplier": 0.95
}
]
}
}

Business Rules:

  • Only authenticated users can create programs
  • trainingFocusId must exist in the system
  • workoutTemplateLinks must reference valid templates
  • Weekly modifiers must have unique week numbers
  • Equipment list validated against supported equipment types

Error Handling:

  • NOT_FOUND - Invalid training focus or template ID
  • BAD_USER_INPUT - Invalid equipment type or duplicate week numbers
  • UNAUTHORIZED - Authentication required

📱 Flutter Implementation

Complete Program Plans Integration

program_plan_models.dart
import 'package:json_annotation/json_annotation.dart';

part 'program_plan_models.g.dart';

@JsonSerializable()
class ProgramPlan {
final String id;
final String name;
final String description;
final String? author;
final String? version;
final ProgramTrainingFocus trainingFocus;
final TrainingLevel level;
final TrainingGoal goal;
final int durationWeeks;
final int daysPerWeek;
final bool loopWeekly;
final List<ProgramTemplateLink> workoutTemplateLinks;
final List<WeeklyModifier> weeklyModifiers;
final List<String> equipment;
final List<String> tags;
final WorkoutSplit? workoutSplit;
final Visibility visibility;
final ProgramType programType;
final bool isPublished;
final bool stock;
final bool deleted;
final String? coverImageFileKey;
final String? coverImageUrl;
final double? splitConfidence;
final bool userConfirmedSplit;
final User createdBy;
final DateTime createdAt;

ProgramPlan({
required this.id,
required this.name,
required this.description,
this.author,
this.version,
required this.trainingFocus,
required this.level,
required this.goal,
required this.durationWeeks,
required this.daysPerWeek,
required this.loopWeekly,
required this.workoutTemplateLinks,
required this.weeklyModifiers,
required this.equipment,
required this.tags,
this.workoutSplit,
required this.visibility,
required this.programType,
required this.isPublished,
required this.stock,
required this.deleted,
this.coverImageFileKey,
this.coverImageUrl,
this.splitConfidence,
required this.userConfirmedSplit,
required this.createdBy,
required this.createdAt,
});

factory ProgramPlan.fromJson(Map<String, dynamic> json) =>
_$ProgramPlanFromJson(json);
Map<String, dynamic> toJson() => _$ProgramPlanToJson(this);

// Helper methods
bool get isOfficial => stock;
bool get isCustom => programType == ProgramType.custom;
bool get isCommunity => programType == ProgramType.community;
String get durationDisplay => '$durationWeeks weeks';
String get frequencyDisplay => '$daysPerWeek days/week';
String get equipmentSummary => equipment.join(', ');
bool get hasProgressiveModifications => weeklyModifiers.isNotEmpty;
}

@JsonSerializable()
class ProgramTemplateLink {
final String programWorkoutTemplateId;
final int order;

ProgramTemplateLink({
required this.programWorkoutTemplateId,
required this.order,
});

factory ProgramTemplateLink.fromJson(Map<String, dynamic> json) =>
_$ProgramTemplateLinkFromJson(json);
Map<String, dynamic> toJson() => _$ProgramTemplateLinkToJson(this);
}

@JsonSerializable()
class WeeklyModifier {
final int weekNumber;
final String name;
final String? description;
final double rpeMultiplier;
final double repsMultiplier;
final double weightMultiplier;

WeeklyModifier({
required this.weekNumber,
required this.name,
this.description,
required this.rpeMultiplier,
required this.repsMultiplier,
required this.weightMultiplier,
});

factory WeeklyModifier.fromJson(Map<String, dynamic> json) =>
_$WeeklyModifierFromJson(json);
Map<String, dynamic> toJson() => _$WeeklyModifierToJson(this);

// Helper methods
String get intensityLevel {
if (rpeMultiplier >= 0.9) return 'High';
if (rpeMultiplier >= 0.7) return 'Moderate';
return 'Low';
}

bool get isDeload => rpeMultiplier < 0.7 || weightMultiplier < 0.8;
bool get isPeakWeek => rpeMultiplier >= 0.9;
}

@JsonSerializable()
class ProgramTrainingFocus {
final String id;
final String name;
final String? description;
final List<MuscleGroup> primaryMuscleGroups;

ProgramTrainingFocus({
required this.id,
required this.name,
this.description,
required this.primaryMuscleGroups,
});

factory ProgramTrainingFocus.fromJson(Map<String, dynamic> json) =>
_$ProgramTrainingFocusFromJson(json);
Map<String, dynamic> toJson() => _$ProgramTrainingFocusToJson(this);
}

@JsonSerializable()
class WorkoutSplit {
final String id;
final String name;
final String? description;
final int daysPerWeek;

WorkoutSplit({
required this.id,
required this.name,
this.description,
required this.daysPerWeek,
});

factory WorkoutSplit.fromJson(Map<String, dynamic> json) =>
_$WorkoutSplitFromJson(json);
Map<String, dynamic> toJson() => _$WorkoutSplitToJson(this);
}

// Filter model for searching programs
@JsonSerializable()
class ProgramPlanFilter {
final String? workoutSplitId;
final List<String>? workoutSplitIds;
final TrainingLevel? level;
final TrainingGoal? goal;
final int? daysPerWeek;
final int? daysPerWeekMin;
final int? daysPerWeekMax;
final int? durationWeeks;
final int? durationWeeksMin;
final int? durationWeeksMax;
final List<String>? equipment;
final List<String>? equipmentAny;
final String? search;
final List<String>? tags;
final List<String>? tagsAny;
final List<ProgramType>? programTypes;
final bool? includeStock;
final bool? includeCustom;
final bool? includeCommunity;

ProgramPlanFilter({
this.workoutSplitId,
this.workoutSplitIds,
this.level,
this.goal,
this.daysPerWeek,
this.daysPerWeekMin,
this.daysPerWeekMax,
this.durationWeeks,
this.durationWeeksMin,
this.durationWeeksMax,
this.equipment,
this.equipmentAny,
this.search,
this.tags,
this.tagsAny,
this.programTypes,
this.includeStock,
this.includeCustom,
this.includeCommunity,
});

factory ProgramPlanFilter.fromJson(Map<String, dynamic> json) =>
_$ProgramPlanFilterFromJson(json);
Map<String, dynamic> toJson() => _$ProgramPlanFilterToJson(this);
}

// Enums
enum TrainingLevel { beginner, intermediate, advanced }
enum TrainingGoal { muscleGain, strengthGain, weightLoss, endurance, generalFitness }
enum Visibility { public, private, shared }
enum ProgramType { stock, custom, community }

🛡️ Business Logic & Rules

Program Plan Validation Rules

The system enforces comprehensive validation rules for program integrity:

Program Plan Business Rules
interface ProgramPlanValidationRules {
// Required fields validation
name: {
minLength: 3,
maxLength: 100,
pattern: /^[a-zA-Z0-9\s\-_]+$/
},

description: {
minLength: 10,
maxLength: 1000
},

// Program structure validation
durationWeeks: {
min: 1,
max: 52 // Maximum 1 year programs
},

daysPerWeek: {
min: 1,
max: 7
},

// Weekly modifiers validation
weeklyModifiers: {
uniqueWeekNumbers: true,
weekNumberRange: [1, durationWeeks],
multiplierRanges: {
rpeMultiplier: [0.3, 1.2],
repsMultiplier: [0.5, 2.0],
weightMultiplier: [0.5, 1.5]
}
},

// Template links validation
workoutTemplateLinks: {
uniqueOrder: true,
validTemplateIds: true,
maxTemplates: 20
}
}

Program Recommendation Algorithm

The AI-powered recommendation system uses multiple factors:

  1. User Profile Matching (40% weight)

    • Training goal alignment
    • Training level appropriateness
    • Experience and preferences
  2. Equipment Availability (30% weight)

    • Required equipment vs available equipment
    • Equipment complexity matching user level
  3. Schedule Compatibility (20% weight)

    • Days per week matching user frequency preference
    • Program duration fitting user commitment
  4. Historical Performance (10% weight)

    • Similar program completion rates
    • User feedback on similar programs
    • Community ratings and reviews

Access Control & Visibility

Program access follows a hierarchical visibility model:

Program Access Control
enum ProgramVisibility {
PUBLIC = "PUBLIC", // Visible to all users in catalog
PRIVATE = "PRIVATE", // Only visible to creator
SHARED = "SHARED" // Accessible via direct link sharing
}

enum ProgramType {
STOCK = "STOCK", // OpenLift official programs
CUSTOM = "CUSTOM", // User-created programs
COMMUNITY = "COMMUNITY" // Community-contributed programs
}

interface AccessRules {
canView: (user: User, program: ProgramPlan) => boolean;
canEdit: (user: User, program: ProgramPlan) => boolean;
canDelete: (user: User, program: ProgramPlan) => boolean;
canPublish: (user: User, program: ProgramPlan) => boolean;
}

🔄 State Management

Program Plans Provider

program_plans_provider.dart
import 'package:flutter/material.dart';

class ProgramPlansProvider extends ChangeNotifier {
final ProgramPlansService _service;

List<ProgramPlan> _programs = [];
List<RecommendedProgram> _recommendations = [];
ProgramPlanFilter _currentFilter = ProgramPlanFilter();
bool _isLoading = false;
String? _error;

List<ProgramPlan> get programs => _programs;
List<RecommendedProgram> get recommendations => _recommendations;
ProgramPlanFilter get currentFilter => _currentFilter;
bool get isLoading => _isLoading;
String? get error => _error;

ProgramPlansProvider(this._service);

Future<void> loadPrograms({
ProgramPlanFilter? filter,
bool refresh = false,
}) async {
if (refresh) _programs.clear();

_setLoading(true);
_currentFilter = filter ?? _currentFilter;

try {
final newPrograms = await _service.getProgramPlans(
filter: _currentFilter,
limit: 20,
offset: refresh ? 0 : _programs.length,
);

if (refresh) {
_programs = newPrograms;
} else {
_programs.addAll(newPrograms);
}

_error = null;
} catch (e) {
_error = e.toString();
} finally {
_setLoading(false);
}
}

Future<void> loadRecommendations() async {
_setLoading(true);

try {
_recommendations = await _service.getRecommendedPrograms(limit: 10);
_error = null;
} catch (e) {
_error = e.toString();
} finally {
_setLoading(false);
}
}

Future<ProgramPlan> createProgram(CreateProgramPlanInput input) async {
_setLoading(true);

try {
final newProgram = await _service.createProgramPlan(input);
_programs.insert(0, newProgram);
_error = null;
return newProgram;
} catch (e) {
_error = e.toString();
rethrow;
} finally {
_setLoading(false);
}
}

Future<void> updateProgram(
String programId,
UpdateProgramPlanInput input,
) async {
try {
final updatedProgram = await _service.updateProgramPlan(programId, input);

final index = _programs.indexWhere((p) => p.id == programId);
if (index != -1) {
_programs[index] = updatedProgram;
notifyListeners();
}
} catch (e) {
_error = e.toString();
notifyListeners();
rethrow;
}
}

Future<void> deleteProgram(String programId) async {
try {
await _service.deleteProgramPlan(programId);
_programs.removeWhere((p) => p.id == programId);
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
rethrow;
}
}

void updateFilter(ProgramPlanFilter filter) {
_currentFilter = filter;
loadPrograms(filter: filter, refresh: true);
}

void clearError() {
_error = null;
notifyListeners();
}

void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
}

🚀 Testing

Unit Tests

program_plans_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

void main() {
group('ProgramPlansService', () {
late MockGraphQLClient mockClient;
late ProgramPlansService service;

setUp(() {
mockClient = MockGraphQLClient();
service = ProgramPlansService(mockClient);
});

test('should return program plans with filtering', () async {
// Arrange
final mockData = {
'programPlans': [
{
'id': 'program1',
'name': 'Beginner Strength',
'level': 'BEGINNER',
'goal': 'STRENGTH_GAIN',
// ... other fields
}
]
};

when(mockClient.query(any)).thenAnswer((_) async => QueryResult(
data: mockData,
source: QueryResultSource.network,
options: QueryOptions(document: gql('')),
));

// Act
final filter = ProgramPlanFilter(level: TrainingLevel.beginner);
final result = await service.getProgramPlans(filter: filter);

// Assert
expect(result.length, equals(1));
expect(result.first.name, equals('Beginner Strength'));
expect(result.first.level, equals(TrainingLevel.beginner));

// Verify GraphQL call
final capturedOptions = verify(mockClient.query(captureAny)).captured.single as QueryOptions;
final variables = capturedOptions.variables;
expect(variables['filter']['level'], equals('BEGINNER'));
});

test('should create program plan successfully', () async {
// Test implementation for program creation
});

test('should handle program recommendations', () async {
// Test implementation for recommendations
});
});
}

📚 Migration Notes

  • All program IDs are MongoDB ObjectId strings
  • Weekly modifiers use multiplier-based progression (not additive)
  • Equipment requirements stored as string arrays for flexibility
  • Cover images use signed URLs with expiration
  • Program structure supports both linear and block periodization
  • Recommendation scoring uses machine learning algorithms (server-side)
  • Program types determine access and visibility rules

🆘 Support

For program plans questions:

  1. Check program structure validation rules for creation issues
  2. Verify equipment filtering logic matches user's available equipment
  3. Ensure recommendation algorithm considers all user preferences
  4. Contact backend team for training focus and workout split questions

Ready to manage program catalogs? Check out the Program User Instance API for user program enrollment and scheduling.