Skip to main content

Scheduling Profile

Overview​

A Scheduling Profile is a declarative configuration that defines which rounds from which events should be scheduled on specific dates and at specific venues. It serves as the blueprint for automated multi-day tournament scheduling, allowing the Garman scheduling algorithm to process rounds across multiple events in a coordinated manner.

Key Benefits:

  • Multi-Event Coordination: Schedule rounds from singles, doubles, and team events together
  • Venue-Specific Planning: Assign different events/rounds to different venues
  • Day-by-Day Control: Define exactly what gets scheduled each day
  • Automated Execution: Use scheduleProfileRounds() to automatically schedule all configured rounds
  • Persistent Configuration: Stored as tournament extension, survives save/load cycles

Data Structure​

A scheduling profile is an array of date-specific configurations, each containing venue-specific round assignments.

Profile Structure​

type SchedulingProfile = ScheduleDateProfile[];

type ScheduleDateProfile = {
scheduleDate: string; // ISO date string 'YYYY-MM-DD'
venues: VenueProfile[]; // Venue assignments for this date
};

type VenueProfile = {
venueId: string; // UUID of venue
rounds: RoundProfile[]; // Rounds to schedule at this venue
};

type RoundProfile = {
// Required identifiers
tournamentId: string; // Tournament UUID
eventId: string; // Event UUID
drawId: string; // Draw UUID
structureId: string; // Structure UUID
roundNumber: number; // Round number (1-based)

// Optional scheduling controls
notBeforeTime?: string; // 'HH:MM' - earliest start time for this round
roundSegment?: {
segmentNumber: number; // Which segment (1-based)
segmentsCount: number; // Total segments in round
};

// Optional display attributes (not used by scheduling engine)
eventName?: string; // Display name
roundName?: string; // Display name (e.g., 'R16', 'QF', 'SF')
sortOrder?: number; // Display order
};

Creating a Scheduling Profile​

Manual Creation​

Build the profile structure manually for complete control:

import { tournamentEngine } from 'tods-competition-factory';

// Define profile for 3-day tournament
const schedulingProfile = [
// Day 1: First rounds of multiple events
{
scheduleDate: '2024-06-15',
venues: [
{
venueId: 'venue-main-courts',
rounds: [
{
tournamentId: 'tournament-123',
eventId: 'event-singles-u18',
drawId: 'draw-singles-u18',
structureId: 'structure-main',
roundNumber: 1,
notBeforeTime: '09:00',
},
{
tournamentId: 'tournament-123',
eventId: 'event-doubles-u18',
drawId: 'draw-doubles-u18',
structureId: 'structure-main',
roundNumber: 1,
notBeforeTime: '11:00',
},
],
},
],
},
// Day 2: Later rounds
{
scheduleDate: '2024-06-16',
venues: [
{
venueId: 'venue-main-courts',
rounds: [
{
tournamentId: 'tournament-123',
eventId: 'event-singles-u18',
drawId: 'draw-singles-u18',
structureId: 'structure-main',
roundNumber: 2, // R16
},
{
tournamentId: 'tournament-123',
eventId: 'event-singles-u18',
drawId: 'draw-singles-u18',
structureId: 'structure-main',
roundNumber: 3, // QF
},
],
},
],
},
// Day 3: Finals
{
scheduleDate: '2024-06-17',
venues: [
{
venueId: 'venue-center-court',
rounds: [
{
tournamentId: 'tournament-123',
eventId: 'event-singles-u18',
drawId: 'draw-singles-u18',
structureId: 'structure-main',
roundNumber: 4, // SF
notBeforeTime: '10:00',
},
{
tournamentId: 'tournament-123',
eventId: 'event-singles-u18',
drawId: 'draw-singles-u18',
structureId: 'structure-main',
roundNumber: 5, // Final
notBeforeTime: '14:00',
},
],
},
],
},
];

// Attach to tournament
tournamentEngine.setState(tournamentRecord);
const result = tournamentEngine.setSchedulingProfile({ schedulingProfile });

if (result.success) {
console.log('Scheduling profile configured');
}

Programmatic Generation​

Build profiles dynamically based on tournament structure:

// Get all events and their draw structures
const { events } = tournamentEngine.getEvents();

const schedulingProfile = [];
const scheduleDates = ['2024-06-15', '2024-06-16', '2024-06-17'];
const venueId = 'venue-main';

// Assign rounds to dates
scheduleDates.forEach((scheduleDate, dayIndex) => {
const rounds = [];

events.forEach((event) => {
event.drawDefinitions?.forEach((draw) => {
draw.structures?.forEach((structure) => {
const maxRound = structure.matchUps?.reduce((max, mu) => Math.max(max, mu.roundNumber), 0);

// Schedule specific rounds on specific days
if (dayIndex === 0 && [1].includes(structure.roundNumber)) {
// Day 1: Round 1
rounds.push({
tournamentId: event.tournamentId,
eventId: event.eventId,
drawId: draw.drawId,
structureId: structure.structureId,
roundNumber: 1,
eventName: event.eventName,
});
} else if (dayIndex === 1 && maxRound >= 2) {
// Day 2: Round 2
rounds.push({
tournamentId: event.tournamentId,
eventId: event.eventId,
drawId: draw.drawId,
structureId: structure.structureId,
roundNumber: 2,
eventName: event.eventName,
});
}
// ... more logic
});
});
});

if (rounds.length > 0) {
schedulingProfile.push({
scheduleDate,
venues: [{ venueId, rounds }],
});
}
});

tournamentEngine.setSchedulingProfile({ schedulingProfile });

Using Scheduling Profiles​

Automated Scheduling​

Once a profile is configured, use scheduleProfileRounds() to automatically schedule all defined rounds:

// Schedule all rounds defined in the profile
const result = tournamentEngine.scheduleProfileRounds({
periodLength: 30, // Scheduling block size in minutes (see note below)
checkPotentialRequestConflicts: true, // Consider potential matchUp conflicts
});

// Handle results
const {
scheduledDates, // Array of dates where matches were scheduled
scheduledMatchUpIds, // All successfully scheduled matchUpIds
noTimeMatchUpIds, // MatchUps that couldn't be fit into available time
noFormatMatchUpIds, // MatchUps of profiled rounds with no matchUpFormat: not placed
overLimitMatchUpIds, // MatchUps blocked by daily limits
requestConflicts, // by date: { [date]: [{ request, scheduleTimes, matchUpIds }] }, the person requests placements broke
} = result;

console.log(`Scheduled ${scheduledMatchUpIds.length} matches`);
console.log(`${noTimeMatchUpIds.length} matches need more court time`);
console.log(`${overLimitMatchUpIds.length} matches blocked by daily limits`);
What is periodLength?

periodLength defines the scheduling block size in minutes (default: 30). It controls:

  • Time slot granularity: Creates time blocks at regular intervals (e.g., 30 minutes = 9:00, 9:30, 10:00, ...)
  • Match grouping: Groups matches by duration relative to block size for efficient scheduling
  • Court utilization: Smaller blocks (15 min) = finer control; larger blocks (60 min) = simpler but may waste time

Recommended values:

  • 15 minutes: Short format tournaments (Fast4, no-ad)
  • 30 minutes: Standard tournaments (best of 3 sets) ← Most common
  • 60 minutes: Long format events (best of 5 sets)

See Period Length documentation for detailed explanation.

Partial Scheduling​

Schedule specific dates from the profile:

// Schedule only specific dates
const result = tournamentEngine.scheduleProfileRounds({
scheduleDates: ['2024-06-15', '2024-06-16'], // Only these dates
periodLength: 30,
});

Clear and Reschedule​

Clear existing schedules before applying profile:

// Clear specific dates: only matchUps scheduled on these dates are cleared
const result = tournamentEngine.scheduleProfileRounds({
clearScheduleDates: ['2024-06-15'], // Clear these dates first
scheduleDates: ['2024-06-15'], // Then reschedule
periodLength: 30,
});

// Clear EVERYTHING: every scheduled matchUp in the tournament records
const result = tournamentEngine.scheduleProfileRounds({
clearScheduleDates: true,
periodLength: 30,
});

clearScheduleDates: true is not limited to the profile: it clears matchUps on every date and in every event, including ones the profile never mentions and ones scheduled by hand. Completed and locked matchUps keep their schedules either way. A date cleared but not included in scheduleDates is left unscheduled.

scheduleProfileGrid accepts the same option. Because the grid usually runs after scheduleProfileRounds has set dates and times, note that clearing there removes those dates and times as well as grid positions.

Dry Run​

Preview scheduling without making changes:

const result = tournamentEngine.scheduleProfileRounds({
dryRun: true, // Don't actually schedule, just report what would happen
periodLength: 30,
});

console.log('Would schedule:', result.scheduledMatchUpIds.length);
console.log('Would have conflicts:', Object.values(result.requestConflicts).flat().length);

Validation​

Validate a profile before applying it:

const { valid, error } = tournamentEngine.validateSchedulingProfile({
schedulingProfile,
});

if (!valid) {
console.error('Invalid profile:', error);
} else {
console.log('Profile is valid');
tournamentEngine.setSchedulingProfile({ schedulingProfile });
}

Validation Checks:

  • All required fields present (scheduleDate, venueId, tournamentId, eventId, drawId, structureId, roundNumber)
  • Valid date formats
  • Referenced IDs exist in tournament
  • Round numbers valid for their structures
  • No duplicate round assignments on same date

Round Segmentation​

For large rounds, split scheduling across multiple sessions:

{
scheduleDate: '2024-06-15',
venues: [{
venueId: 'venue-main',
rounds: [
// Morning session: First half of R32
{
eventId: 'event-singles',
drawId: 'draw-main',
structureId: 'structure-main',
roundNumber: 1,
notBeforeTime: '09:00',
roundSegment: {
segmentNumber: 1, // First segment
segmentsCount: 2 // Split into 2 segments
}
},
// Afternoon session: Second half of R32
{
eventId: 'event-singles',
drawId: 'draw-main',
structureId: 'structure-main',
roundNumber: 1,
notBeforeTime: '14:00',
roundSegment: {
segmentNumber: 2, // Second segment
segmentsCount: 2
}
}
]
}]
}

Multi-Venue Scheduling​

Assign different events to different venues on the same day:

{
scheduleDate: '2024-06-15',
venues: [
{
venueId: 'venue-center-court',
rounds: [
// Feature matches on center court
{
eventId: 'event-singles-championship',
drawId: 'draw-main',
structureId: 'structure-main',
roundNumber: 4, // Semifinals
notBeforeTime: '13:00'
}
]
},
{
venueId: 'venue-outside-courts',
rounds: [
// Earlier rounds on outside courts
{
eventId: 'event-singles-u16',
drawId: 'draw-main',
structureId: 'structure-main',
roundNumber: 1
},
{
eventId: 'event-doubles-u16',
drawId: 'draw-main',
structureId: 'structure-main',
roundNumber: 1
}
]
}
]
}

Best Practices​

1. Logical Progression​

Order rounds logically within each day:

// Good: Logical progression
rounds: [
{ roundNumber: 1, eventName: 'U18 Singles' },
{ roundNumber: 2, eventName: 'U18 Singles' },
{ roundNumber: 1, eventName: 'U18 Doubles' },
];

// Avoid: Random ordering
rounds: [
{ roundNumber: 2, eventName: 'U18 Singles' },
{ roundNumber: 1, eventName: 'U18 Doubles' },
{ roundNumber: 1, eventName: 'U18 Singles' },
];

2. Use notBeforeTime​

Prevent early rounds from delaying featured matches:

{
scheduleDate: '2024-06-17',
venues: [{
venueId: 'venue-center-court',
rounds: [
{
roundNumber: 4, // Semifinals
notBeforeTime: '11:00' // Morning session
},
{
roundNumber: 5, // Final
notBeforeTime: '15:00' // Afternoon feature
}
]
}]
}

3. Balance Court Load​

Distribute matches across available venues:

// Check court availability before assigning
const { venues } = tournamentEngine.getVenuesAndCourts();

venues.forEach((venue) => {
const courtCount = venue.courts?.length || 0;
const availableHours = 8; // Court availability hours
const matchesPerCourt = 6; // Estimate
const capacity = courtCount * matchesPerCourt;

console.log(`${venue.venueName}: ${capacity} matches/day capacity`);
});

4. Consider Dependencies​

Schedule rounds in order that respects match dependencies. The factory's getMatchUpDependencies method builds a full dependency graph that crosses structure boundaries (e.g., consolation draws, Round Robin → Playoff links) and can be used to validate profile ordering programmatically:

// Day 1: Round 1 must complete before Round 2 can start
{
scheduleDate: '2024-06-15',
venues: [{
venueId: 'venue-main',
rounds: [
{ roundNumber: 1 } // R32
]
}]
},
// Day 2: Round 2 only after Round 1 winners determined
{
scheduleDate: '2024-06-16',
venues: [{
venueId: 'venue-main',
rounds: [
{ roundNumber: 2 } // R16 - depends on R32 completion
]
}]
}

Common Scenarios​

Weekend Tournament​

Two-day local tournament:

const schedulingProfile = [
{
scheduleDate: '2024-06-15', // Saturday
venues: [
{
venueId: 'local-club',
rounds: [
{ eventId: 'singles-open', roundNumber: 1 },
{ eventId: 'singles-open', roundNumber: 2 },
{ eventId: 'doubles-open', roundNumber: 1 },
],
},
],
},
{
scheduleDate: '2024-06-16', // Sunday
venues: [
{
venueId: 'local-club',
rounds: [
{ eventId: 'singles-open', roundNumber: 3 }, // SF
{ eventId: 'singles-open', roundNumber: 4, notBeforeTime: '13:00' }, // Final
{ eventId: 'doubles-open', roundNumber: 2 }, // SF
{ eventId: 'doubles-open', roundNumber: 3, notBeforeTime: '15:00' }, // Final
],
},
],
},
];

Championship Week​

Multi-venue, multi-event championship:

const schedulingProfile = [
// Days 1-3: Qualifying and early rounds
...['2024-07-01', '2024-07-02', '2024-07-03'].map((date) => ({
scheduleDate: date,
venues: [
{
venueId: 'qualifying-venue',
rounds: qualifyingRounds,
},
{
venueId: 'main-venue',
rounds: earlyMainDrawRounds,
},
],
})),
// Days 4-5: Later rounds at main venue only
...['2024-07-04', '2024-07-05'].map((date) => ({
scheduleDate: date,
venues: [
{
venueId: 'main-venue',
rounds: laterRounds,
},
],
})),
// Day 6: Finals on center court
{
scheduleDate: '2024-07-06',
venues: [
{
venueId: 'center-court',
rounds: finalsRounds,
},
],
},
];

Multi-Event Youth Tournament​

Age-group tournament with staggered schedules:

const ageGroups = ['U12', 'U14', 'U16', 'U18'];
const schedulingProfile = [];

// Spread age groups across 4 days
ageGroups.forEach((ageGroup, dayIndex) => {
const date = addDays('2024-08-01', dayIndex);

schedulingProfile.push({
scheduleDate: date,
venues: [
{
venueId: 'youth-center',
rounds: [
{
eventId: `singles-${ageGroup}`,
drawId: `draw-singles-${ageGroup}`,
structureId: 'main',
roundNumber: 1,
eventName: `${ageGroup} Singles Round 1`,
},
{
eventId: `doubles-${ageGroup}`,
drawId: `draw-doubles-${ageGroup}`,
structureId: 'main',
roundNumber: 1,
eventName: `${ageGroup} Doubles Round 1`,
},
],
},
],
});
});

Retrieving Scheduling Profile​

Get the current profile from tournament record:

// Get as extension
const { extension } = tournamentEngine.findExtension({
name: 'schedulingProfile',
});

const schedulingProfile = extension?.value;

// Or query specific information
schedulingProfile.forEach(({ scheduleDate, venues }) => {
console.log(`Date: ${scheduleDate}`);
venues.forEach(({ venueId, rounds }) => {
console.log(` Venue: ${venueId}`);
console.log(` Rounds: ${rounds.length}`);
});
});

Integration with Other Scheduling Features​

Combined with Scheduling Policy​

Profiles work alongside scheduling policies:

API Reference: findExtension

// Set policy for recovery times and daily limits
tournamentEngine.attachPolicies({
policyDefinitions: POLICY_SCHEDULING_DEFAULT,
});

tournamentEngine.setMatchUpDailyLimits({
dailyLimits: { SINGLES: 2, DOUBLES: 1, total: 3 },
});

// Then use profile for round-by-round scheduling
tournamentEngine.setSchedulingProfile({ schedulingProfile });
tournamentEngine.scheduleProfileRounds({ periodLength: 30 });
// Garman scheduling will respect policy limits

pro: true (removed)​

pro: true selected an older court-and-time scheduler, deprecated since 7.15.0 and removed on 2026-10-28. The flag is accepted and ignored until the next major: every call takes the profile scheduler. To place matchUps on court grid positions (courtOrder), use scheduleProfileGrid, which can run after scheduleProfileRounds has set dates and times.