Skip to content

Scheduling Rules

Event Scheduler separates the source of a scheduling rule from the way that rule is enforced. Availability and calendars describe when a resource can be used. Conflict settings decide whether a violation is shown as a warning, ignored, confirmed, or rejected.

This matters because closed time is not a single setting. A lunch break, a public holiday, an operating calendar, and a one-off shift extension all participate in the same conflict model, but they do not have the same precedence.

Most applications choose one of these setups:

| Goal | Configuration | | :-- | :-- | | Show conflicts but allow the user to save | conflicts: { enabled: true, policy: 'mark' } | | Block every detected conflict | conflicts: { enabled: true, policy: 'prevent' } | | Block only selected conflict types | conflicts: { enabled: true, policy: 'mark', rules: { ... } } | | Turn conflict detection off | conflicts: { enabled: false } |

Use rules when different mistakes need different behavior:

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
conflicts: {
enabled: true,
policy: 'mark',
rules: {
overlap: 'error',
'outside-availability': 'error',
'blocked-time': 'error',
},
},
};

In this example:

  • overlap: 'error' rejects an event when it overlaps another event in the same resource row.
  • 'outside-availability': 'error' rejects an event when it is outside the resource's working time.
  • 'blocked-time': 'error' rejects an event when it touches a blocked, holiday, or break window.

error means the local scheduler mutation is not committed for the changed event. Create, move, resize, edit, paste, template, recurrence, and bulk changes are checked against the projected next schedule before the event list is updated. Existing unrelated conflicts can remain on screen; they do not block an unrelated edit unless the edited event is part of a blocking conflict.

If you only want to display a conflict, use warning. If you want to hide a conflict from scheduler results, use ignore. If you want to require a custom confirmation flow, use confirm.

The three most common rule names answer different user questions:

| Rule | User-facing meaning | Triggered by | Typical setting | | :-- | :-- | :-- | :-- | | overlap | "This booking collides with another booking." | Two visible event segments overlap on the same date. By default this is checked within the same resource. | Use warning when overlaps are allowed but should be visible. Use error for rooms, equipment, or people that cannot be double-booked. | | outside-availability | "This booking is outside allowed working time." | The event is not fully covered by a dated kind: 'working' availability entry, or by the resolved calendar when no dated working availability applies. | Use error when users must schedule only inside shifts, operating hours, or resource calendars. | | blocked-time | "This booking touches unavailable time." | The event overlaps a matching kind: 'blocked', kind: 'holiday', or kind: 'break' availability entry. | Use error for PTO, maintenance, lunch breaks, holidays, freezes, and other hard closures. |

The policy value attached to each rule controls enforcement:

| Rule policy | What the user sees | Does it block the local change? | | :-- | :-- | :-- | | warning | Conflict styling and conflict data with warning severity. | No. | | error | Conflict styling and conflict data with error severity. | Yes, when the changed event is part of that conflict. | | confirm | Conflict data with confirm severity for custom confirmation handling. | No automatic blocking by the conflict engine. Use mutation events or validateMutation if your app should stop the change until the user confirms. | | allow | Conflict data stays visible as a warning. | No. | | ignore | Conflict is removed from visible conflict results. | No. |

The scheduler uses these inputs when it decides whether an event is valid for a resource and time range:

| Input | Purpose | | :-- | :-- | | eventSchedulerAvailability | Dated working, blocked, holiday, or break intervals. Use it for operational data that happens on specific dates. | | eventScheduler.calendars | Recurring working days, holidays, working hours, and per-resource calendar resolution. Use it for the normal schedule baseline. | | eventScheduler.conflicts | Converts detected conflicts into warnings, blocking errors, confirmation states, or ignored results. | | eventScheduler.nonWorkingTime | Visual closed-cell styling for simple schedules. It does not create blocking conflicts by itself. |

Use availability when the answer depends on a date, such as PTO, maintenance, a temporary room closure, or an extra working shift. Use calendars when the answer is recurring, such as weekday business hours or a weekend crew calendar.

eventSchedulerAvailability entries use EventSchedulerAvailabilityKind:

| Kind | Meaning | Conflict behavior | | :-- | :-- | :-- | | working | A dated interval where matching resources are available. | If any matching working availability exists for the resource/date, the event must be fully covered by matching working intervals or it receives outside-availability. | | blocked | A dated unavailable interval, often for maintenance, holds, or PTO. | Events that overlap it receive blocked-time. | | holiday | A dated unavailable interval for holiday closure or non-operating time. | Events that overlap it receive blocked-time. | | break | A dated unavailable interval inside an otherwise working day. | Events that overlap it receive blocked-time. |

Unavailable entries are checked by overlap. If an event touches a matching blocked, holiday, or break interval, the scheduler raises a blocked-time conflict for the overlapping part.

Working entries are checked by coverage. If an event runs from 09:00 to 13:00, the matching working availability must cover that whole range. A 09:00 to 12:00 working interval is not enough; the uncovered 12:00 to 13:00 portion creates an outside-availability conflict.

For each visible event segment, Event Scheduler applies rules in this order:

  1. Check matching unavailable availability entries.
  2. Check matching dated kind: 'working' availability for that resource/date.
  3. If no dated working availability matches, check the resolved calendar.
  4. Apply conflict policy and per-type rules to decide whether the detected conflict is shown or blocks the mutation.

Unavailable availability is always considered. Dated working availability is more specific than calendars for outside-availability checks. Calendars are the fallback only when no matching dated kind: 'working' availability exists for that resource/date.

This means a dated working entry can narrow or extend the calendar for that date. If a room normally follows a 08:00 to 18:00 calendar but has a dated working entry from 10:00 to 14:00, events for that room/date must be covered by 10:00 to 14:00. The calendar does not fill the rest of the day for outside-availability checks.

Availability can be global or resource-specific:

grid.eventSchedulerAvailability = [
{
id: 'all-hands-break',
kind: 'break',
title: 'Company break',
startDateTime: '2026-06-08T12:00:00.000Z',
endDateTime: '2026-06-08T13:00:00.000Z',
},
{
id: 'room-a-maintenance',
resourceId: 'room-a',
kind: 'blocked',
title: 'Projector maintenance',
startDateTime: '2026-06-08T15:00:00.000Z',
endDateTime: '2026-06-08T16:00:00.000Z',
},
{
id: 'training-team-shift',
resourceIds: ['trainer-a', 'trainer-b'],
kind: 'working',
title: 'Training shift',
startDateTime: '2026-06-08T10:00:00.000Z',
endDateTime: '2026-06-08T18:00:00.000Z',
},
];

An availability entry without resourceId or resourceIds applies globally. An entry with resourceId applies to that one resource. An entry with resourceIds applies to any listed resource.

Calendars resolve per resource in this order:

  1. resource.calendarId
  2. eventScheduler.calendars.resourceCalendarId(resource)
  3. eventScheduler.calendars.primaryCalendarId

If calendar resolution finds no calendar, calendar-based outside-availability checks are skipped for that resource/date. Availability rules can still create conflicts.

Rule detection and mutation blocking are separate steps.

The scheduler first creates conflict records. Each conflict has a type, policy, severity, message, related event ids, related segment ids, and date/time metadata. Then the configured policy decides whether that conflict is just reported or whether it prevents a local mutation.

blocked-time is raised when an event overlaps matching kind: 'blocked', kind: 'holiday', or kind: 'break' availability. The conflict includes the event id, segment id, resource id when available, date, overlapping time range, and the availability id.

outside-availability is raised when an event is outside allowed working time. This can come from dated kind: 'working' availability or from the resolved calendar fallback.

overlap is raised when two events overlap on the same date. With the default scope: 'same-resource', the events must also share the same resource. With scope: 'global', overlaps are detected across resources.

By default, conflict behavior depends on eventScheduler.conflicts.policy:

| Policy | Default result | | :-- | :-- | | mark | Conflicts are warnings. Mutations are allowed unless a rule overrides the type. | | prevent | Conflicts are blocking errors by default. | | allow | Conflicts are ignored unless a rule overrides the type. |

Use rules when some conflict types should be stricter than others:

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
conflicts: {
enabled: true,
policy: 'mark',
rules: {
'outside-availability': 'error',
'blocked-time': 'error',
overlap: 'warning',
},
},
};

With this configuration, outside availability and blocked time reject create, move, resize, and edit mutations. Overlaps remain visible warnings.

You can customize conflict text with messages:

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
conflicts: {
enabled: true,
policy: 'mark',
rules: {
'blocked-time': 'error',
},
messages: {
'blocked-time': 'This slot is closed. Choose another time.',
},
},
};

Conflict rules are the declarative way to stop known scheduler violations. For product-specific checks that do not fit calendars, availability, or built-in conflict types, use the scheduler mutation lifecycle.

The main event-based prevention point is event-scheduler-before-event-change. It fires before the local event array is updated. Call preventDefault() to cancel the pending create, move, resize, edit, delete, paste, template, recurrence, or bulk mutation:

grid.addEventListener('event-scheduler-before-event-change', (event) => {
if (event.detail.action === 'move' && event.detail.event?.status === 'completed') {
event.preventDefault();
}
});

Use validateMutation when you prefer a config-level hook instead of a DOM event. Return false or a string message to reject the same pending mutation:

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
validateMutation: ({ changes }) => {
if (changes?.resourceId === 'locked-room') {
return 'That room is locked.';
}
},
};

Do not use final events such as event-scheduler-event-created, event-scheduler-event-changed, or event-scheduler-event-deleted for prevention. Those fire after the scheduler has accepted the change and are meant for persistence and state sync.

For the full local mutation lifecycle, cancelable before events, permission hooks, and persistence events, see Editing and Interaction.

Calendars are the normal baseline when no dated working availability exists:

import { DEFAULT_CALENDAR, type CalendarEntity } from '@revolist/revogrid-enterprise';
const weekdayCalendar: CalendarEntity = {
...DEFAULT_CALENDAR,
id: 'weekday',
name: 'Weekday office hours',
workingDays: [1, 2, 3, 4, 5],
workingHours: { start: '08:00', end: '18:00' },
holidays: ['2026-06-19'],
};
grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
calendars: {
primaryCalendarId: 'weekday',
calendars: [weekdayCalendar],
},
conflicts: {
enabled: true,
rules: { 'outside-availability': 'error' },
},
};

With no matching kind: 'working' availability for a resource/date, the calendar decides whether the event is inside working time. An event on a holiday, a closed weekday, or outside workingHours receives outside-availability.

A dated working entry becomes the outside-availability rule for its matching resource/date:

grid.eventSchedulerAvailability = [
{
id: 'room-a-short-day',
resourceId: 'room-a',
kind: 'working',
title: 'Short operating day',
startDateTime: '2026-06-08T10:00:00.000Z',
endDateTime: '2026-06-08T14:00:00.000Z',
},
];

If room-a also has a calendar with 08:00 to 18:00 working hours, the dated working entry wins for June 8, 2026. A 09:00 to 10:00 event and a 14:00 to 15:00 event both receive outside-availability because the event is not fully covered by the dated working interval. The calendar is not used to fill those hours.

Breaks are unavailable windows, not holes in working coverage:

grid.eventSchedulerAvailability = [
{
id: 'nurse-a-shift',
resourceId: 'nurse-a',
kind: 'working',
title: 'Day shift',
startDateTime: '2026-06-08T08:00:00.000Z',
endDateTime: '2026-06-08T16:00:00.000Z',
},
{
id: 'nurse-a-lunch',
resourceId: 'nurse-a',
kind: 'break',
title: 'Lunch',
startDateTime: '2026-06-08T12:00:00.000Z',
endDateTime: '2026-06-08T12:30:00.000Z',
},
];

An event from 11:30 to 12:30 is covered by the working shift, but it overlaps the lunch break. The result is blocked-time, not outside-availability.

Use holiday and blocked for unavailable windows that should conflict even when the calendar or working availability would otherwise allow the event:

grid.eventSchedulerAvailability = [
{
id: 'clinic-holiday',
kind: 'holiday',
title: 'Clinic closed',
startDateTime: '2026-06-19T00:00:00.000Z',
endDateTime: '2026-06-20T00:00:00.000Z',
},
{
id: 'room-b-maintenance',
resourceId: 'room-b',
kind: 'blocked',
title: 'Equipment maintenance',
reason: 'Calibration',
startDateTime: '2026-06-08T13:00:00.000Z',
endDateTime: '2026-06-08T15:00:00.000Z',
},
];

The holiday applies globally because it has no resource id. The maintenance entry applies only to room-b. Events that overlap either entry receive blocked-time.

Multiple working entries can combine to cover one event, but there cannot be a gap:

grid.eventSchedulerAvailability = [
{
id: 'room-c-morning',
resourceId: 'room-c',
kind: 'working',
title: 'Morning setup',
startDateTime: '2026-06-08T08:00:00.000Z',
endDateTime: '2026-06-08T12:00:00.000Z',
},
{
id: 'room-c-afternoon',
resourceId: 'room-c',
kind: 'working',
title: 'Afternoon setup',
startDateTime: '2026-06-08T13:00:00.000Z',
endDateTime: '2026-06-08T17:00:00.000Z',
},
];

An event from 09:00 to 11:00 is covered. An event from 11:00 to 14:00 is not covered because 12:00 to 13:00 is a gap between working intervals. The scheduler raises outside-availability for that event.

If you want the gap to be visibly unavailable and report a more specific conflict, add a kind: 'break' entry for the gap. The event will then receive blocked-time for the break overlap.

nonWorkingTime is useful for simple visual backgrounds:

grid.eventScheduler = {
view: 'week',
weekStartDate: '2026-06-08',
nonWorkingTime: {
enabled: true,
workingDays: [1, 2, 3, 4, 5],
workingHours: { start: '08:00', end: '18:00' },
className: 'closed-cell',
},
};

This marks closed cells, but it is not the rule source for blocking conflicts. Use eventScheduler.calendars or eventSchedulerAvailability when outside-working-time edits must produce outside-availability, then configure eventScheduler.conflicts to decide whether that conflict blocks.