Skip to main content

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';
tip

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 (default true) drops COMPLETED and BYE matchUps that carry no scheduledDate. These represent finished play that was never anchored to a specific day; counting them would block legitimate scheduling for the day in question.
  • excludePriorDates (default true) drops matchUps whose scheduledDate is 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,
});
note

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 whose courtOrder changed: { matchUpId, courtId, from, to }
  • unresolvable — matchUps that cannot be deconflicted by spacing, each with a reason:
    • 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:

bucketattributesbehaviour
writtenscheduledDate, scheduledTime, startTime, stopTime, resumeTime, endTime, calledAt, courtId, courtIds, allocatedCourts, venueId, courtOrder, courtAnnotation, timeModifiers, homeParticipantIdapplied
derivedisoDateString, milliseconds, time, venueName, venueAbbreviation, courtName, averageMinutes, recoveryMinutes, timeAfterRecovery, typeChangeRecoveryMinutes, typeChangeTimeAfterRecovery, endDateignored silently
anything elselock, official, scorekeeper, timekeeper, scoredTime, misspellingsreturned 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/or courtId/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:

valuebehaviour
truekeep the placement
falserelease 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
undefinedpreserve — 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), scoredTime is 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_TIME time-item was recorded — useful for analytics on tournament-director behaviour (how promptly results are entered). An actual endTime, when present, supersedes it.
  • No legacy mirror. Like calledAt, scoredTime is a CODES-native schedule attribute with no legacy time-item equivalent — it is always read from and written to matchUp.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 courtschedule.calledAt is 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 setMatchUpCalledAt already emit notices, and subscribers receive matchUps through hydration, so no new notice type and no new mutation are involved.
  • Publishing inherits it. Embargo and scheduleVisibilityFilters are applied to the hydrated schedule, downstream of this.
  • The record still carries the value. A TODS export will contain a timeModifiers entry that no longer displays anywhere. That is the price of not destroying an operator's stated intent.