# Scheduling Rules
URL: https://pro.rv-grid.com/guides/event-scheduler/scheduling-rules/
Source: src/content/docs/guides/event-scheduler/scheduling-rules/index.mdx
Description: Understand how Event Scheduler combines availability, calendars, and conflict policies into 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.

## Start With The Outcome

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:

```ts
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`.

## Rule Names

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. |

## Rule Inputs

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.

## Availability Kinds

`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.

## Precedence

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.

## Resource Matching

Availability can be global or resource-specific:

```ts
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.

## Conflict Outcomes

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:

```ts
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`:

```ts
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.',
    },
  },
};
```

## Custom Event Prevention

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:

```ts
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:

```ts
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](../event-editing/).

## Calendar Fallback

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

```ts
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`.

## Dated Working Overrides Calendar Hours

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

```ts
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.

## Lunch Breaks and Blocked Time

Breaks are unavailable windows, not holes in working coverage:

```ts
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`.

## Holidays and Maintenance

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

```ts
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`.

## Split Working Coverage

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

```ts
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.

## Visual Non-Working Time

`nonWorkingTime` is useful for simple visual backgrounds:

```ts
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.
