Schedule Governor
The Schedule Governor provides methods for assigning dates, times, venues, and courts to tournament matchUps. It supports manual assignment, automated scheduling, and hybrid approaches for tournaments ranging from simple club events to complex multi-day professional circuits.
import { scheduleGovernor } from 'tods-competition-factory';
See Scheduling Overview, Automated Scheduling, Pro Scheduling, and Scheduling Policy for comprehensive scheduling concepts.
Manual Scheduling Methods
addCourtGridBooking
Adds a booking to the court grid for pro scheduling.
engine.addCourtGridBooking({
courtId, // required
startTime, // required - time string (HH:mm)
endTime, // required - time string (HH:mm)
bookingType, // required - type of booking
date, // required - date string (YYYY-MM-DD)
});
addMatchUpScheduledDate
Assigns a scheduled date to a matchUp.
engine.addMatchUpScheduledDate({
matchUpId, // required
drawId, // required
scheduledDate, // required - ISO date string (YYYY-MM-DD)
disableNotice, // optional boolean - suppress notifications
});
Example:
engine.addMatchUpScheduledDate({
matchUpId: 'match-123',
drawId: 'draw-456',
scheduledDate: '2024-06-15',
});
addMatchUpScheduledTime
Assigns a scheduled time to a matchUp.
engine.addMatchUpScheduledTime({
matchUpId, // required
drawId, // required
scheduledTime, // required - time string (HH:mm)
disableNotice, // optional boolean - suppress notifications
});
Example:
engine.addMatchUpScheduledTime({
matchUpId: 'match-123',
drawId: 'draw-456',
scheduledTime: '14:30',
});
addSchedulingProfileRound
Adds a round to an existing scheduling profile.
engine.addSchedulingProfileRound({
scheduleDate, // required - date string
venueId, // required
drawId, // required
structureId, // required
roundNumber, // required
});
assignMatchUpVenue
Assigns a venue to a matchUp.
engine.assignMatchUpVenue({
matchUpId, // required
drawId, // required
venueId, // required
disableNotice, // optional boolean - suppress notifications
});
Example:
engine.assignMatchUpVenue({
matchUpId: 'match-123',
drawId: 'draw-456',
venueId: 'venue-789',
});
assignMatchUpCourt
Assigns a specific court to a matchUp.
engine.assignMatchUpCourt({
matchUpId, // required
drawId, // required
courtId, // required
courtDayDate, // optional - date string for multi-day scheduling
disableNotice, // optional boolean - suppress notifications
});
Example:
engine.assignMatchUpCourt({
matchUpId: 'match-123',
drawId: 'draw-456',
courtId: 'court-1',
courtDayDate: '2024-06-15',
});
assignMatchUpScorekeeper
Nominates a tournament participant as the official scorekeeper of a matchUp.
The nominee must be an INDIVIDUAL participant in the tournament; unlike an
official there is no role restriction — a competitor may also be nominated to
keep score. Stored the same way as court/official assignments (a
SCHEDULE.ASSIGNMENT.SCOREKEEPER first-class value surfaced as
matchUp.schedule.scorekeeper). Not cleared by rescheduling.
engine.assignMatchUpScorekeeper({
matchUpId, // required
drawId, // required
participantId, // required - an INDIVIDUAL participant in the tournament
disableNotice, // optional boolean - suppress notifications
});
Clear a nomination with removeMatchUpScorekeeper:
engine.removeMatchUpScorekeeper({ matchUpId, drawId });
A participant may instead (or additionally) be approved to keep score for any
matchUp by carrying the SCOREKEEPER role in participantRoleResponsibilities
(set via modifyParticipant), which is filterable via getParticipants:
engine.modifyParticipant({
participant: { participantId, participantRoleResponsibilities: ['SCOREKEEPER'] },
});
assignMatchUpTimekeeper
Assigns a tournament participant as the timekeeper of a matchUp — relevant
for timed matchUpFormats (e.g. INTENNSE bolt/serve clocks). Same participant
rule and storage as the scorekeeper (a SCHEDULE.ASSIGNMENT.TIMEKEEPER
first-class value surfaced as matchUp.schedule.timekeeper). Not cleared by
rescheduling. The TIMEKEEPER role is also available as a
participantRoleResponsibility.
engine.assignMatchUpTimekeeper({
matchUpId, // required
drawId, // required
participantId, // required - an INDIVIDUAL participant in the tournament
disableNotice, // optional boolean - suppress notifications
});
engine.removeMatchUpTimekeeper({ matchUpId, drawId });
setMatchUpScheduleLock
Pins a matchUp's placement so bulk and automated scheduling cannot move it —
the marquee match that must keep centre court at 19:00 while the rest of the day
is rebuilt around it. Stored first-class as matchUp.schedule.lock. Presence of
the object is the lock; attributes narrows it to specific placement fields.
Guards placement only: startTime / stopTime / resumeTime / endTime stay
writable so a pinned matchUp can still be played. Every placement mutation
accepts overrideScheduleLock: true for callers that have confirmed the move
with the operator; an override moves the placement but never removes the lock.
The lock goes inert (it is not deleted) once the matchUp completes, or while it
has no placement to guard.
See Schedule Locks for the full model.
engine.setMatchUpScheduleLock({
matchUpId, // required
drawId, // required
lock, // required - a ScheduleLock object, or null to unlock
disableNotice, // optional boolean - suppress notifications
});
// pin only the clock time; the court stays free to move
engine.setMatchUpScheduleLock({ matchUpId, drawId, lock: { attributes: ['scheduledTime'] } });
// unlock
engine.setMatchUpScheduleLock({ matchUpId, drawId, lock: null });
Automated Scheduling Methods
scheduleMatchUps
Auto-schedules matchUps on a given date using the Garman formula for optimal court utilization. Intelligently assigns courts and times based on court availability, match duration estimates, recovery times, and participant daily limits.
engine.scheduleMatchUps({
scheduleDate, // required - ISO date string (YYYY-MM-DD)
matchUpIds, // required - array of matchUpIds to schedule
// Venue selection
venueIds, // optional - defaults to all venues
// Scheduling parameters
periodLength, // optional - scheduling block size in minutes (default: 30)
averageMatchUpMinutes, // optional - average match duration (default: 90)
recoveryMinutes, // optional - recovery time between matches (default: 0)
// Time constraints
startTime, // optional - schedule start time (HH:mm)
endTime, // optional - schedule end time (HH:mm)
// Limits and policies
matchUpDailyLimits, // optional - SINGLES, DOUBLES, total limits
checkPotentialRequestConflicts, // optional boolean (default: true)
// Execution control
dryRun, // optional boolean - preview without changes
});
Period Length: Scheduling Block Size
The periodLength parameter controls the granularity of scheduling blocks:
- 15 minutes: Fine-grained scheduling for short-format matches
- 30 minutes (default): Standard scheduling for most tournaments
- 60 minutes: Coarse scheduling for long-format matches
Smaller period lengths provide more precise start times but may reduce court utilization. Larger periods improve grouping but reduce precision.
See: Automated Scheduling - Period Length for detailed explanation.
scheduleMatchUps Examples
Basic Scheduling:
engine.scheduleMatchUps({
scheduleDate: '2024-06-15',
matchUpIds: ['match-1', 'match-2', 'match-3'],
});
Custom Parameters:
engine.scheduleMatchUps({
scheduleDate: '2024-06-15',
matchUpIds,
venueIds: ['venue-1'],
periodLength: 30,
averageMatchUpMinutes: 90,
recoveryMinutes: 60,
startTime: '08:00',
endTime: '18:00',
matchUpDailyLimits: {
SINGLES: 2,
DOUBLES: 1,
total: 2,
},
});
Dry Run Preview:
const result = engine.scheduleMatchUps({
scheduleDate: '2024-06-15',
matchUpIds,
dryRun: true, // Preview without making changes
});
console.log('Would schedule:', result.scheduledMatchUpIds);
console.log('Would not fit:', result.noTimeMatchUpIds);
See: Automated Scheduling Concepts for algorithm details.
scheduleProfileRounds
Auto-schedules all rounds specified in a scheduling profile across multiple days and venues. Uses a pre-defined profile that maps rounds to specific dates and venues.
engine.scheduleProfileRounds({
// Scheduling parameters
periodLength, // optional - scheduling block size (default: 30)
// Date selection
scheduleDates, // optional - specific dates to schedule
clearScheduleDates, // optional - boolean or array of dates to clear first
// Court selection
courtIds, // optional array - restrict scheduling to only these courts
// Execution control
dryRun, // optional boolean - preview without changes
pro, // optional boolean - use grid scheduling instead of Garman
checkPotentialRequestConflicts, // optional boolean (default: true)
// Daily-limit accounting (apply to already-scheduled matchUps that contribute
// to per-participant per-day counters)
excludeNoDateCompleted, // optional boolean (default: true) - skip COMPLETED/BYE
// matchUps that carry no scheduledDate
excludePriorDates, // optional boolean (default: true) - skip matchUps whose
// scheduledDate is strictly before the date being scheduled
});
Daily-limit accounting with excludeNoDateCompleted / excludePriorDates:
The Day Plan defines what is being scheduled today. When the scheduler reconciles already-scheduled matchUps against the per-participant daily limits in the scheduling policy, historical or orphan matchUps should not consume today's budget:
excludeNoDateCompleted(defaulttrue) dropsCOMPLETEDandBYEmatchUps that carry noscheduledDate. These represent finished play that was never anchored to a specific day; counting them would block legitimate scheduling for the day in question.excludePriorDates(defaulttrue) drops matchUps whosescheduledDateis strictly before the date being scheduled. This is a defensive filter: in normal flow same-day filtering already excludes them, but the flag guarantees historical records never leak into a future day's counters.
Set either flag to false to restore the pre-defaults legacy semantics where every matchUp in the already-scheduled set consumes its participants' daily-limit budget regardless of status or date.
Court filtering with courtIds:
When courtIds is provided, the scheduler only considers those courts as available capacity. Useful when an operator wants auto-scheduling to operate against a subset of courts (e.g. when some courts are reserved for other purposes, or when scheduling is being applied per-court). Passing an empty array means "no courts are available" — the scheduler will run but place no matchUps. Omitting the parameter (the default) considers all enabled courts at the profile's venues.
In pro: true mode, matchUps will receive courtId assignments only from the filtered set. In default (Garman) mode, the filter constrains capacity but final court assignment may still be deferred to play time.
Returns:
{
(scheduledDates, // array - dates where matchUps were scheduled
scheduledMatchUpIds, // array - matchUpIds that were scheduled
noTimeMatchUpIds, // array - matchUps that couldn't fit
overLimitMatchUpIds, // array - matchUps exceeding participant limits
requestConflicts); // array - participant request conflicts
}
scheduleProfileRounds Examples
Schedule All Profile Dates:
const result = engine.scheduleProfileRounds({
periodLength: 30,
});
console.log('Scheduled dates:', result.scheduledDates);
console.log('No time for:', result.noTimeMatchUpIds.length, 'matchUps');
Schedule Specific Dates:
engine.scheduleProfileRounds({
scheduleDates: ['2024-06-15', '2024-06-16'],
periodLength: 30,
});
Clear and Reschedule:
engine.scheduleProfileRounds({
clearScheduleDates: true, // Clear all dates
periodLength: 30,
});
// Or clear specific dates
engine.scheduleProfileRounds({
clearScheduleDates: ['2024-06-15', '2024-06-16'],
scheduleDates: ['2024-06-15', '2024-06-16'],
periodLength: 30,
});
Dry Run Preview:
const result = engine.scheduleProfileRounds({
dryRun: true,
});
console.log('Would schedule:', result.scheduledMatchUpIds.length, 'matchUps');
console.log('Request conflicts:', result.requestConflicts);
Professional Grid Scheduling:
engine.scheduleProfileRounds({
pro: true, // Use grid scheduling
periodLength: 30,
});
SINGLES and DOUBLES matchUps are scheduled automatically. TEAM matchUps require manual court allocation using allocateTeamMatchUpCourts().
See: Scheduling Profile and Pro Scheduling for details.
scheduleProfileGrid
Profile-driven grid scheduling. Uses the scheduling profile to determine which rounds go on which dates at which venues, then places matchUps onto court grid positions (courtOrder) without assigning times. Used when an operator wants to manually assign times after the grid placement step.
engine.scheduleProfileGrid({
scheduleDates, // optional - specific dates to schedule
clearScheduleDates, // optional - boolean or array of dates to clear first
scheduleCompletedMatchUps, // optional boolean (default: false) - include
// already-COMPLETED matchUps in the placement pool
matchUpDailyLimits, // optional - { SINGLES, DOUBLES, total } per-participant
// daily limits; when omitted, no enforcement
minCourtGridRows, // optional number - rows per court (default: 10)
courtIds, // optional array - restrict placement to only these courts
});
When courtIds is provided, only courts in the set are considered as placement targets at each venue. Passing an empty array places nothing. Omitting the parameter places onto all courts at the profile's venues (the default).
Completed matchUps: By default the pro scheduler excludes matchUps that already carry a terminal status (COMPLETED, RETIRED, WALKOVER, DEFAULTED, etc.) from the placement pool — these don't need a court slot and would otherwise occupy grid rows, pushing newly-placed matchUps down. Set scheduleCompletedMatchUps: true only for callers (such as mocksEngine seeding) that intentionally want completed matchUps to receive grid coordinates.
Daily-limit enforcement (opt-in via matchUpDailyLimits): When provided, the pro scheduler refuses to place a matchUp whose entered participants or winner-advancement potentials would push any single participant past the per-day cap for the matchUp type or for the daily total. Counters are seeded from matchUps already on the grid for the date so subsequent runs see total daily load. Refused matchUps are reported in overLimitMatchUpIds, NOT in notScheduledMatchUpIds. Existing direct callers that omit matchUpDailyLimits see no behavior change.
Returns:
{
scheduledDates, // array - dates where matchUps were placed
scheduledMatchUpIds, // record<date, matchUpIds[]> - matchUps placed per date
notScheduledMatchUpIds, // record<date, matchUpIds[]> - matchUps that didn't fit per date
overLimitMatchUpIds, // record<date, matchUpIds[]> - matchUps rejected by daily limits
// (populated only when matchUpDailyLimits was provided)
}
Bulk Operations
bulkScheduleMatchUps
Schedules multiple matchUps at once with provided scheduling details.
engine.bulkScheduleMatchUps({
schedule, // required - array of schedule objects
});
bulkScheduleTournamentMatchUps
Efficiently schedules multiple matchUps with identical or varying schedule details.
engine.bulkScheduleTournamentMatchUps({
// When all matchUps have same schedule
matchUpIds, // array of matchUpIds
schedule, // schedule object { scheduledDate, scheduledTime, venueId, courtId }
// When matchUps have different schedules
matchUpDetails, // array of { matchUpId, schedule }
// Validation and control
checkChronology, // optional boolean - warn on scheduling errors
errorOnAnachronism, // optional boolean - throw error on chronological errors
removePriorValues, // optional boolean - clear existing scheduling timeItems
});
bulkScheduleTournamentMatchUps Examples
Same Schedule for All:
const schedule = {
scheduledDate: '2024-06-15',
scheduledTime: '08:00',
venueId: 'venue-1',
};
engine.bulkScheduleTournamentMatchUps({
matchUpIds: ['match-1', 'match-2', 'match-3'],
schedule,
});
Different Schedules:
const matchUpDetails = [
{
matchUpId: 'match-1',
schedule: {
scheduledDate: '2024-06-15',
scheduledTime: '08:00',
venueId: 'venue-1',
courtId: 'court-1',
},
},
{
matchUpId: 'match-2',
schedule: {
scheduledDate: '2024-06-15',
scheduledTime: '09:30',
venueId: 'venue-1',
courtId: 'court-2',
},
},
];
engine.bulkScheduleTournamentMatchUps({
matchUpDetails,
checkChronology: true,
errorOnAnachronism: true,
});
Replace All Schedules:
engine.bulkScheduleTournamentMatchUps({
matchUpIds,
schedule,
removePriorValues: true, // Clear previous scheduling
});
bulkRescheduleMatchUps
Shifts scheduled matchUps by a specified number of days and/or minutes. Useful for weather delays or venue changes.
const {
rescheduled, // array of inContext matchUps that were rescheduled
notRescheduled, // array of inContext matchUps that were NOT rescheduled
allRescheduled, // boolean - true if all matchUps rescheduled
dryRun, // boolean - indicates if this was a dry run
} = engine.bulkRescheduleMatchUps({
matchUpIds, // required - array of matchUpIds to reschedule
scheduleChange: {
daysChange: number, // number of days +/- to shift
minutesChange: number, // number of minutes +/- to shift
},
dryRun, // optional boolean - preview without changes
});
bulkRescheduleMatchUps Examples
Delay by One Day:
const result = engine.bulkRescheduleMatchUps({
matchUpIds: ['match-1', 'match-2'],
scheduleChange: {
daysChange: 1, // Move forward one day
},
});
console.log('Rescheduled:', result.rescheduled.length);
console.log('Failed:', result.notRescheduled.length);
Shift Start Times:
engine.bulkRescheduleMatchUps({
matchUpIds,
scheduleChange: {
minutesChange: 30, // Start 30 minutes later
},
});
Weather Delay:
// Rain delay - move all to next day, 2 hours earlier start
engine.bulkRescheduleMatchUps({
matchUpIds,
scheduleChange: {
daysChange: 1,
minutesChange: -120, // 2 hours earlier
},
});
Dry Run Preview:
const result = engine.bulkRescheduleMatchUps({
matchUpIds,
scheduleChange: { daysChange: 1 },
dryRun: true,
});
console.log('Would reschedule:', result.rescheduled.length);
console.log('Would fail:', result.notRescheduled.length);
bulkUpdatePublishedEventIds
Returns filtered array of publishedEventIds from all eventIds included in a bulk matchUp status update. Useful for determining which events need re-publishing after bulk scoring.
const { publishedEventIds } = engine.bulkUpdatePublishedEventIds({
outcomes, // array of matchUp outcomes
});
// Re-publish affected events
publishedEventIds.forEach((eventId) => {
engine.publishEvent({ eventId });
});
Use Case: After bulk scoring at end of day, identify and republish only the affected published events rather than all events.
bulkUpdateCourtAssignments
Updates court assignments for multiple matchUps at once.
engine.bulkUpdateCourtAssignments({
courtAssignments, // required - array of {matchUpId, courtId}
});
Clearing and Removing Schedules
clearMatchUpSchedule
Clears schedule information from a specific matchUp.
engine.clearMatchUpSchedule({
matchUpId, // required
drawId, // optional - optimizes lookup, triggers draw modification notice
scheduleAttributes, // optional - array of specific attributes to clear
});
clearMatchUpSchedule Examples
Clear All Schedule Attributes:
engine.clearMatchUpSchedule({
matchUpId: 'match-123',
drawId: 'draw-456',
});
Clear Specific Attributes:
import { scheduleConstants } from 'tods-competition-factory';
engine.clearMatchUpSchedule({
matchUpId: 'match-123',
scheduleAttributes: [scheduleConstants.SCHEDULED_TIME, scheduleConstants.SCHEDULED_DATE],
});
clearScheduledMatchUps
Clears schedules from multiple matchUps based on criteria.
engine.clearScheduledMatchUps({
scheduledDates, // optional - array of dates to clear
venueIds, // optional - array of venueIds to clear
scheduleAttributes, // optional - which attributes to clear
ignoreMatchUpStatuses, // optional - matchUp statuses to skip
});
clearScheduledMatchUps Examples
Clear Specific Date:
engine.clearScheduledMatchUps({
scheduledDates: ['2024-06-15'],
});
Clear Specific Venue:
engine.clearScheduledMatchUps({
venueIds: ['venue-1'],
});
Clear Only Times (Keep Dates):
import { scheduleConstants } from 'tods-competition-factory';
engine.clearScheduledMatchUps({
scheduledDates: ['2024-06-15'],
scheduleAttributes: [scheduleConstants.SCHEDULED_TIME],
});
Skip Completed MatchUps:
import { matchUpStatusConstants } from 'tods-competition-factory';
engine.clearScheduledMatchUps({
scheduledDates: ['2024-06-15'],
ignoreMatchUpStatuses: [
matchUpStatusConstants.COMPLETED,
matchUpStatusConstants.RETIRED,
matchUpStatusConstants.DEFAULTED,
],
});
calculateScheduleTimes
Calculates scheduling times based on match duration and court availability.
const { times } = engine.calculateScheduleTimes({
scheduleDate, // required
matchUpIds, // required
venueIds, // optional
});
isScheduleLocked
Whether a matchUp's placement is pinned by a director. Accepts the matchUp
either way round — { matchUpId, drawId } when only ids are in hand, or
{ matchUp } when it isn't (the cheap form: a table rendering hundreds of rows
should not resolve each one by id).
Returns the lock alongside the verdict so a caller can show why a matchUp is
pinned without a second lookup. scheduleLocked is false — with lock still
returned — when a lock exists but is inert: the matchUp completed, or there is
no placement left to guard. See Schedule Locks.
engine.isScheduleLocked({ matchUpId, drawId });
// → { success: true, scheduleLocked: true, lock: { reason: 'featured' } }
engine.isScheduleLocked({ matchUp });
engine.isScheduleLocked({ matchUp, attributes: ['courtId'] }); // narrow to one placement field
scheduleGovernor.matchUpScheduleLocked({ matchUp }) is the bare predicate
behind it — a plain boolean with no result envelope, used by the factory's own
enforcement. Consumers should prefer isScheduleLocked.
courtGridRows
Returns court grid data for pro scheduling visualization.
const { rows } = engine.courtGridRows({
scheduleDate, // required
});
findMatchUpFormatTiming
Finds format timing for a specific matchUp format.
const { timing } = engine.findMatchUpFormatTiming({
matchUpFormat, // required
});
findVenue
Finds a venue by ID with full details.
const { venue } = engine.findVenue({
venueId, // required
});
generateBookings
Generates booking objects for scheduled matchUps.
const { bookings } = engine.generateBookings({
scheduleDate, // required
});
generateVirtualCourts
Creates virtual courts for scheduling simulation.
const { courts } = engine.generateVirtualCourts({
venueId, // required
courtsCount, // required
});
getMatchUpsToSchedule
Returns matchUps that are ready to be scheduled.
const { matchUps } = engine.getMatchUpsToSchedule({
eventId, // optional
drawId, // optional
});
getPersonRequests
Returns scheduling requests for persons (officials, participants).
const { requests } = engine.getPersonRequests({
personId, // optional - filter by person
});
getProfileRounds
Returns rounds from a scheduling profile.
const { rounds } = engine.getProfileRounds({
scheduleDate, // optional - filter by date
});
getScheduledRoundsDetails
Returns detailed information about scheduled rounds.
const { details } = engine.getScheduledRoundsDetails();
getSchedulingProfile
Returns the current scheduling profile.
const { schedulingProfile } = engine.getSchedulingProfile();
getSchedulingProfileIssues
Validates scheduling profile and returns any issues.
const { issues } = engine.getSchedulingProfileIssues({
schedulingProfile, // optional - validate specific profile
});
Schedule Modifications
matchUpScheduleChange
Swaps the schedule details of two scheduled matchUps. Useful for manual adjustments in schedule grid interfaces.
engine.matchUpScheduleChange({
courtDayDate, // required - date string
sourceMatchUpContextIds, // required - source matchUp context
targetMatchUpContextIds, // required - target matchUp context
sourceCourtId, // optional - source court
targetCourtId, // optional - target court
});
Example:
engine.matchUpScheduleChange({
courtDayDate: '2024-06-15',
sourceMatchUpContextIds: {
tournamentId: 'tournament-1',
drawId: 'draw-1',
matchUpId: 'match-1',
},
targetMatchUpContextIds: {
tournamentId: 'tournament-1',
drawId: 'draw-1',
matchUpId: 'match-2',
},
sourceCourtId: 'court-1',
targetCourtId: 'court-2',
});
Use Case: Drag-and-drop schedule interfaces where matchUps are swapped between time slots or courts.
modifyMatchUpFormatTiming
Updates format timing parameters for scheduling.
engine.modifyMatchUpFormatTiming({
matchUpFormat, // required
averageMinutes, // optional - average duration
recoveryMinutes, // optional - recovery time
});
proAutoSchedule
Professional auto-scheduling with grid-based court assignment.
engine.proAutoSchedule({
scheduleDate, // required
venueIds, // optional
});
proConflicts
Detects scheduling conflicts in pro grid scheduling.
const { conflicts } = engine.proConflicts({
scheduleDate, // required
});
proColumnResolve
Court-preserving, scheduledTime-preserving conflict resolver. Where proAutoSchedule may reassign courts, proColumnResolve re-lays the grid vertically only: a matchUp never changes its courtId or its scheduledTime — only its courtOrder (row) changes, and blank rows are inserted to space colliding matchUps onto different rows.
It removes cross-column participant collisions — the same participant, or the same potential participant (winner advancement), appearing in two cells of the same row — by folding both CONFLICT_PARTICIPANTS and CONFLICT_POTENTIAL_PARTICIPANTS into each matchUp's deep dependency participant set.
Per-column ordering: completed matchUps are anchored at the top (in play order), in-progress beneath them, and to-be-played below in scheduledTime order (which also repairs a director's time-inversions). Every dependency source is placed in a strictly earlier row than the match it feeds.
const { resolved, unresolvable } = engine.proColumnResolve({
scheduledDate, // required
matchUps, // required — { inContext: true, nextMatchUps: true }
courtIds, // optional — restrict to specific courts
});
Returns:
resolved— matchUps whosecourtOrderchanged:{ matchUpId, courtId, from, to }unresolvable— matchUps that cannot be deconflicted by spacing, each with areason:chronology— a director scheduled a feeder at a later clock time than the match it feeds (physically impossible; times are never changed)orderingDeadlock— a same-column ordering cycle that had to be broken to make progress
Confirm the result by re-running proConflicts — every normally-placed row is conflict-free by construction.
publicFindCourt
Finds a court with privacy policies applied for public APIs.
const { court } = engine.publicFindCourt({
courtId, // required
policyDefinitions, // optional
});
removeCourtGridBooking
Removes a booking from the court grid.
engine.removeCourtGridBooking({
bookingId, // required
});
removeEventMatchUpFormatTiming
Removes custom format timing for an event.
engine.removeEventMatchUpFormatTiming({
eventId, // required
});
reorderUpcomingMatchUps
Reorders upcoming matchUps on a court, affecting their order of play.
engine.reorderUpcomingMatchUps({
matchUpContextIds, // required - array of matchUp context objects
firstToLast, // optional boolean - direction of reorder
});
Example:
const matchUpContextIds = [
{ tournamentId: 't1', drawId: 'd1', matchUpId: 'm1' },
{ tournamentId: 't1', drawId: 'd1', matchUpId: 'm2' },
{ tournamentId: 't1', drawId: 'd1', matchUpId: 'm3' },
];
engine.reorderUpcomingMatchUps({
matchUpContextIds,
firstToLast: true, // Move first to last
});
removeMatchUpCourtAssignment
Removes court assignment from a matchUp while preserving other schedule details.
engine.removeMatchUpCourtAssignment({
tournamentId, // optional - for multi-tournament scenarios
courtDayDate, // required - date string
matchUpId, // required
drawId, // required
});
Example:
engine.removeMatchUpCourtAssignment({
courtDayDate: '2024-06-15',
matchUpId: 'match-123',
drawId: 'draw-456',
});
Use Case: Remove court assignment while keeping date/time (e.g., court becomes unavailable, need to reassign).
Team Match Scheduling
allocateTeamMatchUpCourts
Allocates courts to individual matchUps within a TEAM matchUp (tie). Used for team competitions where multiple singles/doubles matches occur simultaneously.
engine.allocateTeamMatchUpCourts({
matchUpId, // required - team matchUp ID
drawId, // required
courtIds, // required - array of courtIds to allocate
removePriorValues, // optional boolean - clear previous allocations
});
Example:
// Team match with 4 singles and 2 doubles
engine.allocateTeamMatchUpCourts({
matchUpId: 'team-match-1',
drawId: 'draw-456',
courtIds: ['court-1', 'court-2', 'court-3', 'court-4'],
});
Use Case: Davis Cup or Fed Cup style ties where multiple matches play simultaneously on different courts.
Scheduling Profile Management
setSchedulingProfile
Stores a scheduling profile that defines which rounds are scheduled on which dates and venues. Used by scheduleProfileRounds().
engine.setSchedulingProfile({
schedulingProfile, // required - profile object
});
Profile Structure:
const schedulingProfile = [
{
scheduleDate: '2024-06-15',
venues: [
{
venueId: 'venue-1',
rounds: [
{
drawId: 'draw-1',
structureId: 'structure-1',
roundNumber: 1,
},
{
drawId: 'draw-2',
structureId: 'structure-2',
roundNumber: 1,
},
],
},
],
},
{
scheduleDate: '2024-06-16',
venues: [
{
venueId: 'venue-1',
rounds: [
{
drawId: 'draw-1',
structureId: 'structure-1',
roundNumber: 2,
},
],
},
],
},
];
engine.setSchedulingProfile({ schedulingProfile });
See: Scheduling Profile Concepts for detailed profile structure and creation.
Schedule Scenario Methods
Named alternate ("contingency") scheduling plans stored first-class on tournamentRecord.scheduling.scenarios. See Schedule Scenarios for the full concept.
addScheduleScenario
Creates an alternate scheduling plan. scenarioName is required; scenarioId is generated when omitted. The scenario is validated before it is stored.
const { scenarioId } = engine.addScheduleScenario({
scenario: {
scenarioName, // required
scheduledDates, // optional - ISO 'YYYY-MM-DD' dates the plan re-plans
placements, // ScenarioPlacement[] — a bulkScheduleMatchUps matchUpDetails payload
},
});
getScheduleScenarios / getScheduleScenario
Reads stored scenarios.
engine.getScheduleScenarios(); // → { scenarios: ScheduleScenario[] }
engine.getScheduleScenario({ scenarioId }); // → { scenario } | { error }
updateScheduleScenario
Merges updates over an existing scenario (preserving scenarioId) and re-validates.
engine.updateScheduleScenario({
scenarioId, // required
updates, // Partial<ScheduleScenario>
});
removeScheduleScenario
engine.removeScheduleScenario({ scenarioId });
applyScheduleScenario
Commits a scenario's placements as the official schedule via bulkScheduleMatchUps. Skips completed matchUps by default; removePriorValues defaults to true. The scenario is left in place after commit.
const result = engine.applyScheduleScenario({
scenarioId, // required
removePriorValues, // optional - default true
scheduleCompletedMatchUps, // optional - default false
});
// → { success: true, applied: <matchUps scheduled>, ... }
validateScheduleScenario
Shape + light referential validation for a scenario object (used internally by add/update).
engine.validateScheduleScenario({ scenario }); // → { valid: boolean, error?, info? }
getScenarioScheduleProjection
The unofficial "Plan mode" overlay — the official schedule with a scenario's placements laid on top, without writing to any matchUp. Cells are tagged official / planned; conflicts come from mergeFacilitySchedule.
const { scheduleCells, grid, conflicts, plannedMatchUpIds, skippedCompletedMatchUpIds } =
engine.getScenarioScheduleProjection({
scenarioId, // required
venueIds, // optional - restrict to these venues
});
getScheduleScenarioStatus
Reconciles a scenario against current state so a client can alert when a plan is out of date and preview what a commit would do.
engine.getScheduleScenarioStatus({ scenarioId });
// → { outOfDate, currentHash, basedOnHash, completedMatchUpIds, missingMatchUpIds, applicableMatchUpIds }
getScenarioScheduleView
Grid-ready projection for a client "Plan mode" — returns the same shape as competitionScheduleMatchUps (dateMatchUps / rows / courtsData / courtPrefix) with the scenario's placements laid on top, so the client renders the plan through its existing grid path. The overlay is applied to a throwaway deep copy; the real records / engine state are never mutated.
const view = engine.getScenarioScheduleView({
scenarioId, // required
matchUpFilters, // e.g. { scheduledDate }
withCourtGridRows, // default true
minCourtGridRows,
});
// → { dateMatchUps, rows, courtsData, courtPrefix, plannedMatchUpIds, skippedCompletedMatchUpIds, ... }
rebaseScheduleScenario
Re-anchors a scenario's drift baseline (basedOnHash) to the official schedule as it stands now — the explicit "I've reconciled, this plan is current" action.
engine.rebaseScheduleScenario({ scenarioId });
setMatchUpDailyLimits
Sets daily limits for participant matchUp participation.
engine.setMatchUpDailyLimits({
dailyLimits, // required - limits object
});
Example:
engine.setMatchUpDailyLimits({
dailyLimits: {
SINGLES: 2,
DOUBLES: 1,
total: 2,
},
});
setMatchUpHomeParticipantId
Designates a home participant for a matchUp (for home/away displays).
engine.setMatchUpHomeParticipantId({
matchUpId, // required
drawId, // required
participantId, // required
});
toggleParticipantCheckInState
Toggles participant check-in status for scheduling.
engine.toggleParticipantCheckInState({
participantId, // required
});
validateSchedulingProfile
Validates a scheduling profile for errors or conflicts.
const { valid, errors } = engine.validateSchedulingProfile({
schedulingProfile, // required
});
addMatchUpScheduleItems
Adds multiple schedule attributes to a matchUp in a single call. This is the method used internally by setMatchUpStatus when a schedule object is provided, and is the most efficient way to set date, time, court, and venue together.
engine.addMatchUpScheduleItems({
matchUpId, // required — target matchUp
drawId, // required — resolved to drawDefinition by engine
schedule: {
// required — schedule attributes to set
scheduledDate, // optional — 'YYYY-MM-DD'
scheduledTime, // optional — 'HH:mm' or ISO datetime
startTime, // optional — actual start time
endTime, // optional — actual end time
resumeTime, // optional — resume after suspension
stopTime, // optional — suspension time
courtId, // optional — assigned court
venueId, // optional — assigned venue
courtOrder, // optional — order on court
homeParticipantId, // optional — home team participant
calledAt, // optional — ISO instant; call to court. `null` clears
courtIds, // optional — for TEAM matchUps, allocate courts
allocatedCourts, // optional — alias for courtIds; accepts the read-side shape
},
removePriorValues, // optional boolean — clear existing schedule timeItems first
checkChronology, // optional boolean — defaults to true; validate time ordering
errorOnAnachronism, // optional boolean — return error on chronological violations
errorOnUnknownAttributes, // optional boolean — error instead of warning on attributes that will not be written
proConflictDetection, // optional boolean — detect pro scheduling conflicts
disableNotice, // optional boolean — suppress modification notices
});
calledAt
calledAt is an actual-play attribute and sits with startTime / stopTime /
resumeTime / endTime — none of which a schedule lock
guards, so a pinned matchUp can still be called to court, started, suspended and
completed.
Its undefined handling differs from setMatchUpCalledAt
deliberately. Called directly, that method reads undefined as clear. Here an
omitted key destructures to undefined too, so honouring that reading would make
every partial schedule write silently wipe a call-to-court:
engine.addMatchUpScheduleItems({ matchUpId, drawId, schedule: { calledAt: isoInstant } });
// later, a re-time that says nothing about calledAt — the call survives
engine.addMatchUpScheduleItems({ matchUpId, drawId, schedule: { scheduledTime: '15:00' } });
// explicit clear
engine.addMatchUpScheduleItems({ matchUpId, drawId, schedule: { calledAt: null } });
Attributes that will not be written are reported
The method writes the attributes it recognises and, before 6.32.0, silently ignored
everything else while still returning { success: true }. Two real attributes were
lost that way — allocatedCourts (see below) and calledAt.
Incoming keys are now sorted three ways:
| bucket | attributes | behaviour |
|---|---|---|
| written | scheduledDate, scheduledTime, startTime, stopTime, resumeTime, endTime, calledAt, courtId, courtIds, allocatedCourts, venueId, courtOrder, courtAnnotation, timeModifiers, homeParticipantId | applied |
| derived | isoDateString, milliseconds, time, venueName, venueAbbreviation, courtName, averageMinutes, recoveryMinutes, timeAfterRecovery, typeChangeRecoveryMinutes, typeChangeTimeAfterRecovery, endDate | ignored silently |
| anything else | lock, official, scorekeeper, timekeeper, scoredTime, misspellings | returned in warnings |
The derived bucket exists so that read-modify-write keeps working: a hydrated schedule carries a dozen keys the caller cannot omit and cannot influence, and warning about those would make every round-trip noisy.
const { warnings } = engine.addMatchUpScheduleItems({
matchUpId,
drawId,
schedule: { scheduledDate, official: personId },
});
// warnings → [{ code: 'UNWRITABLE_SCHEDULE_ATTRIBUTES', attributes: ['official'] }]
lock is in the reported bucket on purpose — silently discarding a director's pin is
the worst of these to discover later. Assign officials through the
officiating governor and pin with
setMatchUpScheduleLock.
Warnings rather than errors, because callers round-trip locked and scored matchUps
today. Pass errorOnUnknownAttributes: true to make it a hard error instead — the
same escalation errorOnAnachronism provides for chronology:
const result = engine.addMatchUpScheduleItems({
matchUpId,
drawId,
errorOnUnknownAttributes: true,
schedule: { scheduledDate, nonsense: true },
});
// result.error → UNWRITABLE_SCHEDULE_ATTRIBUTES; nothing is written
Court allocation: courtIds in, allocatedCourts out
A TEAM matchUp's court allocation is written as bare courtIds and read back as
schedule.allocatedCourts — court objects ({ courtId, venueId }, hydrated with courtName /
venueName). Because the write path originally destructured only courtIds, a schedule object read
off one matchUp and applied to another silently carried no allocation: the unrecognised key was
ignored, with no error and no warning.
allocatedCourts is now accepted as an alias, in either shape:
engine.addMatchUpScheduleItems({ matchUpId, drawId, schedule: { allocatedCourts: [courtIdA, courtIdB] } });
engine.addMatchUpScheduleItems({ matchUpId, drawId, schedule: { ...anotherMatchUp.schedule } }); // round-trips
An explicit courtIds wins when a caller supplies both. A non-array allocatedCourts is ignored
rather than treated as an error, since it cannot have come from a read.
Grid position is cleared on a date change
courtOrder, courtId, and venueId describe a matchUp's position on one
specific day's schedule grid. When a call changes scheduledDate to a different
day and does not supply an explicit courtOrder/courtId/venueId in the
same schedule object, the engine clears those stale grid-position attributes so
the matchUp does not inherit the prior day's row on its new day.
- A date-only re-date (e.g.
schedule: { scheduledDate, scheduledTime }) returns the matchUp to the unplaced pool for the new day — it keeps its date and time but is no longer pinned to a court or row. - Re-applying the same date leaves the existing grid position untouched.
- Supplying an explicit
courtOrder(and/orcourtId/venueId) alongside the new date is honored verbatim — a deliberate "move to a new day and row" is respected.
This also governs bulkScheduleMatchUps, which delegates to
addMatchUpScheduleItems per matchUp. The lower-level addMatchUpScheduledDate
primitive does not clear grid position; use addMatchUpScheduleItems (the path
TMX and integrations use) for date changes.
Assigning a BYE preserves scheduling
A tournament director may schedule an entire event and then swap participants around, placing byes temporarily or permanently. The engine therefore never discards scheduling on its own: a matchUp that becomes a BYE keeps its court, courtOrder and times.
assignDrawPositionBye (and setMatchUpStatus with matchUpStatus: 'BYE') accepts
preserveScheduling:
| value | behaviour |
|---|---|
true | keep the placement |
false | release allocatedCourts, courtId, venueId, courtAnnotation, courtOrder, scheduledDate, scheduledTime, timeModifiers — on both matchUp.schedule and legacy timeItems. Actual-play timestamps (startTime / stopTime / resumeTime / endTime) are never touched |
undefined | preserve — except on an operator position-action against a matchUp that already holds scheduling, which returns MATCHUP_HAS_SCHEDULING |
engine.assignDrawPositionBye({ drawId, structureId, drawPosition, isPositionAction: true });
// → { error: MATCHUP_HAS_SCHEDULING } when that drawPosition's matchUp holds a court or a time
engine.assignDrawPositionBye({ drawId, structureId, drawPosition, preserveScheduling: false });
// → { success: true }, court released
The refusal is deliberately scoped to isPositionAction — the flag the engine's own
positionActions payload carries, i.e. an action an operator chose. Engine-internal callers
(directLoser while a score is being entered, doubleExitAdvancement, positionSwap, draw
generation, and this function's own BYE cascade) never receive it and take the preserving
default, so entering a score can never hard-fail because the draw happened to be scheduled.
An explicit preserveScheduling: false propagates through the cascade: one operator decision
covers every matchUp that BYE reaches.
A BYE that holds a court is shown, not hidden
BYE matchUps are excluded from competitionScheduleMatchUps by default. Pass
courtByeMatchUps: true to include the ones holding a courtId, so they occupy a cell on
the schedule grid. proConflicts annotates them CONFLICT_BYE_SCHEDULED at SCHEDULE_WARNING
severity — a real double-booking or participant conflict on the same matchUp still outranks it.
This is what keeps a preserved placement honest. Before it, a byed matchUp kept its court and
disappeared from every schedule surface at once: the slot read as free, the next matchUp was
dropped onto it, and proConflicts reported a courtDoubleBooking naming a partner that had no
cell to click through to (production, 2026-08-22). Making the occupant visible — rather than
deleting the director's placement — is the fix.
Byes holding only a date/time occupy no cell and are not included; the WARNING tracks court occupancy specifically.
Auto-captured scoredTime
matchUp.schedule.scoredTime is a first-class ISO-8601 timestamp that the engine
captures automatically the first time a matchUp becomes scored — that is, when
a score with value, a winningSide, or a completed matchUpStatus is applied via
setMatchUpStatus. No caller action is required.
Behavior:
- Captured once. The first scored mutation stamps the timestamp; later score corrections preserve the original value (it records when the result first entered the system, not the latest edit).
- Cleared on removal. If the score is removed (the matchUp is reset to
TO_BE_PLAYED),scoredTimeis deleted, so a subsequent re-score gets a fresh stamp. - Proxy for completion time. It is a lightweight stand-in for "when did this
match actually finish" when no explicit
END_TIMEtime-item was recorded — useful for analytics on tournament-director behaviour (how promptly results are entered). An actualendTime, when present, supersedes it. - No legacy mirror. Like
calledAt,scoredTimeis a CODES-native schedule attribute with no legacy time-item equivalent — it is always read from and written tomatchUp.schedule.scoredTime.
This is engine-generated state, not an input: it is treated as read-only by consumers and reflects the engine's own capture, not a value supplied by the caller.
timeModifiers are suppressed once a matchUp's start is settled
A FOLLOWED_BY or AFTER_REST / NOT_BEFORE annotation is a promise about when a
matchUp may begin. Once that question is settled the annotation is not merely
redundant — on a published order of play it is misinformation, telling a player and a
referee that a match is waiting on something that has already happened.
From 6.32.0, hydration omits schedule.timeModifiers once either of two local
signals says the start is settled:
- Called to court —
schedule.calledAtis present. A match that has been called is on court; "followed by" is moot for it whatever else on that court did or did not finish. - Any score at all — a single game is enough, and the partial case is the one that matters: that is the state a live match sits in for an hour while the annotation goes on claiming it has not begun. A completed status settles it too, walkovers included.
Suppressed, never cleared
The stored value is untouched. That is what makes the behaviour reversible without a rule of its own:
engine.addMatchUpScheduleItems({ matchUpId, drawId, schedule: { timeModifiers: ['FOLLOWED_BY'] } });
engine.setMatchUpStatus({ matchUpId, drawId, outcome }); // any score
engine.findMatchUp({ matchUpId, inContext: true }).matchUp.schedule.timeModifiers; // undefined
engine.setMatchUpStatus({ matchUpId, drawId, outcome: { score: undefined, winningSide: undefined } });
engine.findMatchUp({ matchUpId, inContext: true }).matchUp.schedule.timeModifiers; // ['FOLLOWED_BY']
Consequences worth knowing:
- Read-side only. The write path reads storage directly, so adding and removing modifiers still operates on the real value — a UI that renders annotation controls from a hydrated matchUp will show none on a settled matchUp, which is intended.
- Subscribers get it for free. Score entry and
setMatchUpCalledAtalready emit notices, and subscribers receive matchUps through hydration, so no new notice type and no new mutation are involved. - Publishing inherits it. Embargo and
scheduleVisibilityFiltersare applied to the hydrated schedule, downstream of this. - The record still carries the value. A TODS export will contain a
timeModifiersentry that no longer displays anywhere. That is the price of not destroying an operator's stated intent.
Related Documentation
- Scheduling Overview - Core scheduling concepts
- Automated Scheduling - Algorithm details and Garman formula
- Pro Scheduling - Grid scheduling for professional tournaments
- Scheduling Profile - Multi-day scheduling profiles
- Scheduling Conflicts - Conflict detection and resolution
- Scheduling Policy - Policy configuration
- Venues and Courts - Venue setup and court availability
- Time Items - How schedules are stored