Effective Workout API
The Effective Workout API provides intelligent workout calculation services that determine the precise exercises, sets, reps, and weights for any workout session. This system combines program templates, user progression data, coach overrides, and added exercises to generate personalized, effective workouts.
Overview
The Effective Workout system is a core component of OpenLift that transforms static workout templates into dynamic, personalized training sessions. It calculates optimal training parameters based on:
- Program Snapshots: Base workout template structure
- Progression Data: User's training history and performance
- Coach Overrides: Customized modifications by trainers
- Added Exercises: User or coach-added supplementary exercises
- Current Context: User's program week, recovery status, and goals
Core Concepts
Effective Exercise
An Effective Exercise represents a single exercise in a workout with all parameters calculated and ready for execution:
interface EffectiveExercise {
exerciseId: string; // Exercise to perform
order: number; // Execution order
notesForUser: string | null; // Instructions/notes
// Calculated targets
targetSets: number; // Sets to perform
targetRepsMin: number; // Minimum reps per set
targetRepsMax: number; // Maximum reps per set
targetRestSeconds: number; // Rest between sets
targetWeight?: number; // Weight to use (if applicable)
// Metadata
isAdded: boolean; // Coach/user added exercise
originalPweSnapshotId: string | null; // Source template ID
}
Effective Workout Data
An Effective Workout contains all exercises with their calculated parameters for a complete training session:
interface EffectiveWorkoutData {
pwtSnapshotId: string; // Workout template ID
effectiveExercises: EffectiveExercise[]; // All exercises with targets
}
GraphQL Schema
Queries
Get Effective Workout
Calculate an effective workout for a specific user, week, and template:
query GetEffectiveWorkout($input: GetEffectiveWorkoutInput!) {
effectiveWorkout(input: $input) {
pwtSnapshotId
effectiveExercises {
exerciseId
order
notesForUser
targetSets
targetRepsMin
targetRepsMax
targetRestSeconds
targetWeight
isAdded
originalPweSnapshotId
}
}
}
Check Running Workout
Check if there's an active (unfinished) workout for a program user instance:
query CheckRunningWorkout($input: CheckRunningWorkoutInput!) {
runningWorkout(input: $input) {
id
startedAt
endedAt
userId
programUserInstanceId
pwtSnapshotId
status
}
}
Get All Running Workouts
Get all active workouts for a program user instance:
query GetRunningWorkouts($input: CheckRunningWorkoutInput!) {
runningWorkouts(input: $input) {
id
startedAt
endedAt
userId
programUserInstanceId
pwtSnapshotId
status
}
}
Input Types
GetEffectiveWorkoutInput
input GetEffectiveWorkoutInput {
programUserInstanceId: ID! # User's program instance
planWeekNumber: Int! # Target week number
pwtSnapshotMasterId: ID! # Workout template master ID
}
CheckRunningWorkoutInput
input CheckRunningWorkoutInput {
programUserInstanceId: ID! # Program instance to check
}
Object Types
EffectiveWorkout
type EffectiveWorkout {
pwtSnapshotId: ID! # Workout template snapshot ID
effectiveExercises: [EffectiveExercise!]! # Calculated exercises
}
EffectiveExercise
type EffectiveExercise {
exerciseId: ID! # Exercise to perform
order: Int! # Execution order
notesForUser: String # Instructions for user
targetSets: Int! # Target number of sets
targetRepsMin: Int! # Minimum reps per set
targetRepsMax: Int! # Maximum reps per set
targetRestSeconds: Int! # Rest period in seconds
targetWeight: Float # Target weight (if applicable)
isAdded: Boolean! # Whether exercise was added by coach/user
originalPweSnapshotId: String # Source PWE snapshot ID
}
Authentication & Authorization
All Effective Workout operations require user authentication:
// Add authentication headers
final headers = {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
};
Flutter Integration
Setting Up GraphQL Operations
1. Define GraphQL Documents
// lib/graphql/effective_workout_queries.dart
const String GET_EFFECTIVE_WORKOUT = '''
query GetEffectiveWorkout(\$input: GetEffectiveWorkoutInput!) {
effectiveWorkout(input: \$input) {
pwtSnapshotId
effectiveExercises {
exerciseId
order
notesForUser
targetSets
targetRepsMin
targetRepsMax
targetRestSeconds
targetWeight
isAdded
originalPweSnapshotId
}
}
}
''';
const String CHECK_RUNNING_WORKOUT = '''
query CheckRunningWorkout(\$input: CheckRunningWorkoutInput!) {
runningWorkout(input: \$input) {
id
startedAt
endedAt
userId
programUserInstanceId
pwtSnapshotId
status
}
}
''';
const String GET_RUNNING_WORKOUTS = '''
query GetRunningWorkouts(\$input: CheckRunningWorkoutInput!) {
runningWorkouts(input: \$input) {
id
startedAt
endedAt
userId
programUserInstanceId
pwtSnapshotId
status
}
}
''';
2. Create Data Models
// lib/models/effective_workout.dart
class EffectiveExercise {
final String exerciseId;
final int order;
final String? notesForUser;
final int targetSets;
final int targetRepsMin;
final int targetRepsMax;
final int targetRestSeconds;
final double? targetWeight;
final bool isAdded;
final String? originalPweSnapshotId;
EffectiveExercise({
required this.exerciseId,
required this.order,
this.notesForUser,
required this.targetSets,
required this.targetRepsMin,
required this.targetRepsMax,
required this.targetRestSeconds,
this.targetWeight,
required this.isAdded,
this.originalPweSnapshotId,
});
factory EffectiveExercise.fromJson(Map<String, dynamic> json) {
return EffectiveExercise(
exerciseId: json['exerciseId'],
order: json['order'],
notesForUser: json['notesForUser'],
targetSets: json['targetSets'],
targetRepsMin: json['targetRepsMin'],
targetRepsMax: json['targetRepsMax'],
targetRestSeconds: json['targetRestSeconds'],
targetWeight: json['targetWeight']?.toDouble(),
isAdded: json['isAdded'],
originalPweSnapshotId: json['originalPweSnapshotId'],
);
}
}
class EffectiveWorkout {
final String pwtSnapshotId;
final List<EffectiveExercise> effectiveExercises;
EffectiveWorkout({
required this.pwtSnapshotId,
required this.effectiveExercises,
});
factory EffectiveWorkout.fromJson(Map<String, dynamic> json) {
return EffectiveWorkout(
pwtSnapshotId: json['pwtSnapshotId'],
effectiveExercises: (json['effectiveExercises'] as List)
.map((e) => EffectiveExercise.fromJson(e))
.toList(),
);
}
}
class RunningWorkout {
final String id;
final DateTime startedAt;
final DateTime? endedAt;
final String userId;
final String programUserInstanceId;
final String pwtSnapshotId;
final String status;
RunningWorkout({
required this.id,
required this.startedAt,
this.endedAt,
required this.userId,
required this.programUserInstanceId,
required this.pwtSnapshotId,
required this.status,
});
factory RunningWorkout.fromJson(Map<String, dynamic> json) {
return RunningWorkout(
id: json['id'],
startedAt: DateTime.parse(json['startedAt']),
endedAt: json['endedAt'] != null ? DateTime.parse(json['endedAt']) : null,
userId: json['userId'],
programUserInstanceId: json['programUserInstanceId'],
pwtSnapshotId: json['pwtSnapshotId'],
status: json['status'],
);
}
}
3. Create Service Class
// lib/services/effective_workout_service.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import '../graphql/effective_workout_queries.dart';
import '../models/effective_workout.dart';
class EffectiveWorkoutService {
final GraphQLClient _client;
EffectiveWorkoutService(this._client);
/// Get calculated effective workout for a specific program instance and week
Future<EffectiveWorkout> getEffectiveWorkout({
required String programUserInstanceId,
required int planWeekNumber,
required String pwtSnapshotMasterId,
}) async {
final options = QueryOptions(
document: gql(GET_EFFECTIVE_WORKOUT),
variables: {
'input': {
'programUserInstanceId': programUserInstanceId,
'planWeekNumber': planWeekNumber,
'pwtSnapshotMasterId': pwtSnapshotMasterId,
},
},
errorPolicy: ErrorPolicy.all,
);
final result = await _client.query(options);
if (result.hasException) {
throw result.exception!;
}
final data = result.data?['effectiveWorkout'];
if (data == null) {
throw Exception('No effective workout data received');
}
return EffectiveWorkout.fromJson(data);
}
/// Check if there's a running workout for a program user instance
Future<RunningWorkout?> getRunningWorkout({
required String programUserInstanceId,
}) async {
final options = QueryOptions(
document: gql(CHECK_RUNNING_WORKOUT),
variables: {
'input': {
'programUserInstanceId': programUserInstanceId,
},
},
errorPolicy: ErrorPolicy.all,
);
final result = await _client.query(options);
if (result.hasException) {
throw result.exception!;
}
final data = result.data?['runningWorkout'];
if (data == null) {
return null;
}
return RunningWorkout.fromJson(data);
}
/// Get all running workouts for a program user instance
Future<List<RunningWorkout>> getRunningWorkouts({
required String programUserInstanceId,
}) async {
final options = QueryOptions(
document: gql(GET_RUNNING_WORKOUTS),
variables: {
'input': {
'programUserInstanceId': programUserInstanceId,
},
},
errorPolicy: ErrorPolicy.all,
);
final result = await _client.query(options);
if (result.hasException) {
throw result.exception!;
}
final data = result.data?['runningWorkouts'] as List? ?? [];
return data.map((e) => RunningWorkout.fromJson(e)).toList();
}
}
4. State Management with Provider
// lib/providers/effective_workout_provider.dart
import 'package:flutter/foundation.dart';
import '../services/effective_workout_service.dart';
import '../models/effective_workout.dart';
class EffectiveWorkoutProvider extends ChangeNotifier {
final EffectiveWorkoutService _service;
EffectiveWorkout? _currentWorkout;
RunningWorkout? _runningWorkout;
List<RunningWorkout> _runningWorkouts = [];
bool _isLoading = false;
String? _errorMessage;
EffectiveWorkoutProvider(this._service);
// Getters
EffectiveWorkout? get currentWorkout => _currentWorkout;
RunningWorkout? get runningWorkout => _runningWorkout;
List<RunningWorkout> get runningWorkouts => _runningWorkouts;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
/// Load effective workout for a specific program instance and week
Future<void> loadEffectiveWorkout({
required String programUserInstanceId,
required int planWeekNumber,
required String pwtSnapshotMasterId,
}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
_currentWorkout = await _service.getEffectiveWorkout(
programUserInstanceId: programUserInstanceId,
planWeekNumber: planWeekNumber,
pwtSnapshotMasterId: pwtSnapshotMasterId,
);
} catch (e) {
_errorMessage = e.toString();
_currentWorkout = null;
} finally {
_isLoading = false;
notifyListeners();
}
}
/// Check for running workout
Future<void> checkRunningWorkout({
required String programUserInstanceId,
}) async {
try {
_runningWorkout = await _service.getRunningWorkout(
programUserInstanceId: programUserInstanceId,
);
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
}
}
/// Load all running workouts
Future<void> loadRunningWorkouts({
required String programUserInstanceId,
}) async {
try {
_runningWorkouts = await _service.getRunningWorkouts(
programUserInstanceId: programUserInstanceId,
);
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
}
}
/// Clear current workout data
void clearCurrentWorkout() {
_currentWorkout = null;
_errorMessage = null;
notifyListeners();
}
}
5. UI Implementation
// lib/screens/effective_workout_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/effective_workout_provider.dart';
import '../models/effective_workout.dart';
class EffectiveWorkoutScreen extends StatefulWidget {
final String programUserInstanceId;
final int planWeekNumber;
final String pwtSnapshotMasterId;
const EffectiveWorkoutScreen({
Key? key,
required this.programUserInstanceId,
required this.planWeekNumber,
required this.pwtSnapshotMasterId,
}) : super(key: key);
@override
_EffectiveWorkoutScreenState createState() => _EffectiveWorkoutScreenState();
}
class _EffectiveWorkoutScreenState extends State<EffectiveWorkoutScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadWorkoutData();
});
}
Future<void> _loadWorkoutData() async {
final provider = context.read<EffectiveWorkoutProvider>();
// Check for running workouts first
await provider.checkRunningWorkout(
programUserInstanceId: widget.programUserInstanceId,
);
// Load effective workout
await provider.loadEffectiveWorkout(
programUserInstanceId: widget.programUserInstanceId,
planWeekNumber: widget.planWeekNumber,
pwtSnapshotMasterId: widget.pwtSnapshotMasterId,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Week ${widget.planWeekNumber} Workout'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadWorkoutData,
),
],
),
body: Consumer<EffectiveWorkoutProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text(
'Error loading workout',
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 8),
Text(provider.errorMessage!),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadWorkoutData,
child: Text('Retry'),
),
],
),
);
}
// Show running workout warning if exists
if (provider.runningWorkout != null) {
return _buildRunningWorkoutWarning(provider.runningWorkout!);
}
if (provider.currentWorkout == null) {
return Center(
child: Text('No workout data available'),
);
}
return _buildWorkoutContent(provider.currentWorkout!);
},
),
);
}
Widget _buildRunningWorkoutWarning(RunningWorkout runningWorkout) {
return Center(
child: Card(
margin: EdgeInsets.all(16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.warning, size: 48, color: Colors.orange),
SizedBox(height: 16),
Text(
'Active Workout Found',
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 8),
Text(
'You have an unfinished workout started on ${runningWorkout.startedAt.toString().split('.')[0]}',
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
// Navigate to resume workout
Navigator.pushReplacementNamed(
context,
'/resume-workout',
arguments: runningWorkout.id,
);
},
child: Text('Resume Workout'),
),
OutlinedButton(
onPressed: () {
// Show confirmation to cancel workout
_showCancelWorkoutDialog(runningWorkout);
},
child: Text('Cancel Workout'),
),
],
),
],
),
),
),
);
}
Widget _buildWorkoutContent(EffectiveWorkout workout) {
return Column(
children: [
// Workout header
Container(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${workout.effectiveExercises.length} Exercises',
style: Theme.of(context).textTheme.titleLarge,
),
ElevatedButton(
onPressed: () => _startWorkout(workout),
child: Text('Start Workout'),
),
],
),
),
// Exercise list
Expanded(
child: ListView.separated(
itemCount: workout.effectiveExercises.length,
separatorBuilder: (context, index) => Divider(),
itemBuilder: (context, index) {
final exercise = workout.effectiveExercises[index];
return EffectiveExerciseCard(exercise: exercise);
},
),
),
],
);
}
void _startWorkout(EffectiveWorkout workout) {
Navigator.pushNamed(
context,
'/start-workout',
arguments: {
'workout': workout,
'programUserInstanceId': widget.programUserInstanceId,
},
);
}
void _showCancelWorkoutDialog(RunningWorkout runningWorkout) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Cancel Active Workout?'),
content: Text(
'This will permanently cancel your current workout session. This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Keep Workout'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// TODO: Implement cancel workout functionality
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: Text('Cancel Workout'),
),
],
),
);
}
}
class EffectiveExerciseCard extends StatelessWidget {
final EffectiveExercise exercise;
const EffectiveExerciseCard({
Key? key,
required this.exercise,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 16,
child: Text('${exercise.order}'),
),
SizedBox(width: 12),
Expanded(
child: Text(
'Exercise ${exercise.exerciseId}', // You'll want to resolve this to exercise name
style: Theme.of(context).textTheme.titleMedium,
),
),
if (exercise.isAdded)
Chip(
label: Text('Added'),
backgroundColor: Colors.blue.shade100,
),
],
),
SizedBox(height: 12),
_buildTargetRow('Sets', '${exercise.targetSets}'),
_buildTargetRow(
'Reps',
exercise.targetRepsMin == exercise.targetRepsMax
? '${exercise.targetRepsMin}'
: '${exercise.targetRepsMin}-${exercise.targetRepsMax}',
),
if (exercise.targetWeight != null)
_buildTargetRow('Weight', '${exercise.targetWeight} lbs'),
_buildTargetRow(
'Rest',
'${Duration(seconds: exercise.targetRestSeconds).inMinutes}:${(exercise.targetRestSeconds % 60).toString().padLeft(2, '0')}',
),
if (exercise.notesForUser != null) ...[
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text(
exercise.notesForUser!,
style: TextStyle(color: Colors.blue.shade700),
),
),
],
),
),
],
],
),
),
);
}
Widget _buildTargetRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontWeight: FontWeight.w500)),
Text(value),
],
),
);
}
}
Common Use Cases
1. Load Today's Workout
// Get effective workout for current week
await effectiveWorkoutProvider.loadEffectiveWorkout(
programUserInstanceId: userProgramInstance.id,
planWeekNumber: userProgramInstance.currentWeekNumber,
pwtSnapshotMasterId: todaysWorkoutTemplate.id,
);
2. Check for Incomplete Workouts
// Check if user has running workouts before starting new one
await effectiveWorkoutProvider.checkRunningWorkout(
programUserInstanceId: userProgramInstance.id,
);
if (effectiveWorkoutProvider.runningWorkout != null) {
// Show resume workout dialog
showRunningWorkoutDialog();
}
3. Preview Next Week's Workout
// Load future week workout for preview
await effectiveWorkoutProvider.loadEffectiveWorkout(
programUserInstanceId: userProgramInstance.id,
planWeekNumber: userProgramInstance.currentWeekNumber + 1,
pwtSnapshotMasterId: nextWeekTemplate.id,
);
Error Handling
Common Error Types
Authentication Errors
// Handle authentication issues
if (result.exception?.graphqlErrors.any(
(error) => error.extensions?['code'] == 'UNAUTHENTICATED'
) == true) {
// Redirect to login
await authService.logout();
Navigator.pushReplacementNamed(context, '/login');
}
Program Instance Errors
// Handle invalid program instance
if (result.exception?.graphqlErrors.any(
(error) => error.message.contains('Program user instance not found')
) == true) {
showSnackBar('Program not found. Please contact support.');
return;
}
Workout Template Errors
// Handle missing workout template
if (result.exception?.graphqlErrors.any(
(error) => error.message.contains('Workout template not found')
) == true) {
showSnackBar('Workout template unavailable for this week.');
return;
}
Error Recovery Strategies
Network Errors
Future<void> _handleNetworkError() async {
await Future.delayed(Duration(seconds: 2));
return _loadWorkoutData(); // Retry operation
}
Server Errors
void _handleServerError(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Server Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
_contactSupport();
},
child: Text('Contact Support'),
),
],
),
);
}
Best Practices
1. Workout Calculation Timing
- Pre-calculate workouts: Load effective workouts before user needs them
- Cache calculations: Store effective workout data locally for offline access
- Background updates: Refresh calculations when progression data changes
2. Running Workout Management
- Always check first: Verify no running workouts before starting new ones
- Clear warnings: Provide obvious options to resume or cancel incomplete workouts
- State consistency: Ensure workout state remains consistent across app restarts
3. Exercise Target Display
- Progressive disclosure: Show essential targets first, detailed info on tap
- Visual hierarchy: Use clear typography and spacing for exercise parameters
- Rest timing: Convert seconds to human-readable minutes:seconds format
4. Performance Optimization
- Lazy loading: Load exercise details only when needed
- Image caching: Cache exercise demonstration images/videos
- State persistence: Maintain workout state during app backgrounding
5. User Experience
- Loading states: Show clear loading indicators during calculations
- Error recovery: Provide retry mechanisms for failed operations
- Offline support: Cache effective workouts for offline workout execution
The Effective Workout API provides the foundation for intelligent, personalized workout experiences. By combining program templates with user progression data, it ensures every workout session is optimally calibrated for the user's current fitness level and training goals.