# Resources, events and intervals

> The data model: resource and event ids, half-open intervals, ISO date-times with seconds, civil times without time zones, and typed custom fields with EventData<T>.

Source: https://superscheduler.org/en/docs/resources-events-intervals/
Reviewed: 2026-10-07

Rows are ResourceData objects with an id and a name; bars are EventData objects with id, text, start, end and the resource id they belong to. Ids are strings or numbers compared strictly, so 1 and '1' differ. Intervals are half-open, [start, end), and dates are civil wall-clock values written as ISO strings with seconds; the library never converts time zones. Add your own fields with EventData<YourFields> and narrow them when they come back from the control.

SuperScheduler draws two arrays: **resources**, the rows, and **events**, the bars on those rows. Both are plain objects you create from your own data. Getting four rules right avoids nearly every "my event does not show up" problem: ids are strictly typed, intervals exclude their end, date strings include seconds, and times are wall-clock values without a time zone.

The rules on this page apply to both editions unless a section says otherwise. Lite accepts a subset of the fields; see [Quick start with Lite](https://superscheduler.org/en/docs/quick-start-lite/#event-fields).

## Resources
A resource needs an `id` and a `name`. In Pro, `ResourceData` is open: you can keep your own fields (`floor`, `kind`, `capacity`) on the same object and read them back in callbacks and rendering hooks.

Fields you will use most in Pro:

| Field | Purpose |
|---|---|
| `id` | String or number; unique among resources |
| `name` | Row header text |
| `backColor`, `cssClass`, `html`, `toolTip` | Row header appearance (`html` is trusted markup) |
| `minHeight`, `eventHeight` | Row geometry for this row only |
| `cellsDisabled` | Every cell of the row rejects drops and selections |
| `columns` | Cells for extra row header columns (with `rowHeaderColumns`) |
| `children`, `expanded` | A resource tree (with `treeEnabled`) |
| `frozen` | `'top'` or `'bottom'`: the row stays visible while scrolling |

### Resource trees
To group rows, nest resources in `children` and set `treeEnabled` on the scheduler. Without `treeEnabled`, children are ignored and the list stays flat. A parent starts collapsed unless its `expanded` field is `true`. Parents can hold events like any row; set `treePreventParentUsage` to keep them as pure group headers. Trees, row columns and row selection are covered in [Trees, columns and selection](https://superscheduler.org/en/docs/trees-columns-selection/). Lite accepts flat lists only.

## Events
An event needs `id`, `text`, `start`, `end` and, to appear on a row, `resource`. Optional fields change its appearance and behavior:

| Field | Purpose |
|---|---|
| `backColor`, `fontColor`, `borderColor`, `barColor` | Colors of the bar, its text, border and duration bar |
| `cssClass` | Classes for your own CSS |
| `html` | Content as trusted HTML (escape user data with `SuperScheduler.Util.escapeHtml`) |
| `toolTip`, `bubbleHtml` | Native tooltip, or hover bubble content |
| `moveDisabled`, `resizeDisabled` | Lock this event against moving or resizing |
| `moveHDisabled`, `moveVDisabled` | Allow moving only between rows, or only in time |
| `clickDisabled`, `deleteDisabled` | Opt this event out of clicks or deletion |
| `tags` | Any value for your own use |

Every field in this table is Pro only except `backColor`, `fontColor`, `cssClass`, `toolTip` and `tags`, which Lite accepts too.

## Ids are strings or numbers, compared strictly
`ResourceId` and `EventId` are `string | number`, and comparisons use the value and its type. The number `101` and the string `'101'` are different ids. An event with `resource: '101'` is not drawn on a row whose id is `101`, and `control.events.find('7')` does not find the event with id `7`.

This matters most when data comes from several sources: a database driver may return numeric room ids while a form or a URL gives strings. Normalize ids at the boundary where data enters your application, and keep one type per kind of id.

> **Behavior:**
> In Pro, `control.events.add()` throws `SuperScheduler.Exception` when the id already exists, and `control.events.update()` with an unknown id does nothing (it does not add). Use `add` for new events and `update` for existing ones.

## Intervals are half-open
An event occupies `[start, end)`: the start instant belongs to it, the end instant does not. Three consequences:

- **Back-to-back events do not overlap.** A stay ending at 11:00 and the next one starting at 11:00 on the same room are compatible, also when overlaps are refused.
- **A date-only end is the first free day.** `start: '2026-10-02'`, `end: '2026-10-05'` covers 2, 3 and 4 October. To show the 5th as well, the end is `'2026-10-06'`.
- **Durations are plain differences.** `end - start` is the duration, with no "plus one day" adjustments.

Your backend should use the same rule. Two intervals overlap when `a.start < b.end && b.start < a.end`; a date range query for what is visible between `from` and `to` is `start < to AND end > from`.

In Pro, `eventEndSpec: 'Date'` switches to inclusive date-only ends for day-based plannings: an event ending on `'2026-10-05'` then covers the 5th. The library converts the value internally and gives it back in the same convention. Use one convention per scheduler.

## Date strings
`start`, `end` and every date option accept a `SuperScheduler.Date` or an ISO 8601 string:

| Input | Accepted | Read as |
|---|---|---|
| `'2026-10-02'` | yes | Midnight at the start of 2 October |
| `'2026-10-02T14:00:00'` | yes | 14:00 |
| `'2026-10-02 14:00:00'` | yes | 14:00 (a space instead of `T`) |
| `'2026-10-02T14:00:00.250'` | yes | With milliseconds |
| `'2026-10-02T14:00'` | **no, throws** | Seconds are required |
| `'2026-10-02T14:00:00+02:00'` | yes, with care | 12:00: an offset converts the value to UTC wall-clock time |
| `new Date()` | no | A native `Date` does not type-check as `start` or `end` |

Two rules prevent most surprises: always include seconds, and do not send offsets or `Z` unless you want the UTC wall clock. Format values as `yyyy-MM-ddTHH:mm:ss` in the time zone of the place being planned.

## Civil time, no time zones
SuperScheduler works with civil (wall-clock) date-times: `2026-10-25T02:30:00` is "half past two on the 25th" exactly as written, with no time zone and no daylight-saving shifts. The scheduler displays what you give it and gives back values in the same form.

That is what a planning board needs: a hotel in Madrid shows check-in at 14:00 local time for every user, wherever their browser is. It also means the conversions are yours:

- If your backend stores instants (UTC timestamps), convert them to the wall clock of the resource's location before passing them, and back to instants when saving.
- If different resources sit in different time zones, decide which wall clock the view shows; the scheduler has a single time axis.
- Recurring events (every Monday at 9:00) must reach the scheduler as concrete occurrences that your application expanded.

> **Limitation:**
> The library does not convert time zones, apply daylight-saving rules or expand recurrence rules. See [Locales, dates and time zones](https://superscheduler.org/en/docs/locales-dates-timezones/) for strategies.

## Custom fields with EventData&lt;T&gt;
Your events usually carry more than a label: a guest code, a status, a price. In Pro, `EventData` has no index signature, so an object literal with extra properties fails TypeScript's excess property check. Declare your fields once and use the generic, `SuperScheduler.EventData<YourFields>`:

```ts
// src/bookings.ts
import type { SuperScheduler } from 'super-scheduler'

/** Fields your application adds to every booking. */
export interface BookingFields {
  guestCode: string
  status: 'tentative' | 'confirmed' | 'checkedIn'
  adults: number
}

export type BookingEvent = SuperScheduler.EventData<BookingFields>

// ResourceData accepts extra properties: keep your own row fields next to id and name.
export const rooms: SuperScheduler.ResourceData[] = [
  { id: 101, name: 'Room 101', floor: 1, kind: 'double' },
  { id: 102, name: 'Room 102', floor: 1, kind: 'suite' },
]

// EventData has no index signature: type the array so literals may carry custom fields.
export const bookings: BookingEvent[] = [
  {
    id: 'bk-1042',
    resource: 101, // the same type as the room id: 101 and '101' are different ids
    start: '2026-10-02T14:00:00',
    end: '2026-10-05T11:00:00',
    text: 'Booking 1042',
    guestCode: 'G-1042',
    status: 'confirmed',
    adults: 2,
  },
]

const STATUSES: ReadonlySet<string> = new Set(['tentative', 'confirmed', 'checkedIn'])

/**
 * Objects that come back from the control (handler arguments, onEventsChange) are typed as plain
 * EventData. Narrow them instead of casting, so a malformed object is caught where it appears.
 */
export function isBooking(data: SuperScheduler.EventData): data is BookingEvent {
  return (
    'guestCode' in data &&
    typeof data.guestCode === 'string' &&
    'status' in data &&
    typeof data.status === 'string' &&
    STATUSES.has(data.status) &&
    'adults' in data &&
    typeof data.adults === 'number'
  )
}
```
Data coming back from the control is typed as plain `EventData`: handler arguments, `onEventsChange`, `control.events.list`. The library keeps your fields, but TypeScript cannot know they are there. Narrow with a guard like `isBooking` instead of casting; a guard also catches objects your own code built incorrectly. Rendering hooks receive an open copy (`args.data` in `onBeforeEventRender`), so you can read fields directly there, but a guard keeps the types exact:

```tsx
// src/TypedPlanning.tsx
import { useMemo } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { bookings, isBooking, rooms } from './typed-data'

export function TypedPlanning({ onOpen }: { onOpen: (guestCode: string) => void }) {
  const owned = useMemo(() => bookings.slice(), [])

  const config = useMemo<SchedulerProps>(
    () => ({
      startDate: '2026-10-01',
      days: 31,
      scale: 'Day',
      onBeforeEventRender: (args) => {
        // args.data is a per-render copy: start and end are always SuperScheduler.Date here.
        if (!isBooking(args.data)) return
        const nights = Math.round(
          (args.data.end.getTime() - args.data.start.getTime()) / 86_400_000,
        )
        // html is trusted markup: escape every value that came from users.
        const code = SuperScheduler.Util.escapeHtml(args.data.guestCode)
        args.data.html = `${code} · ${nights} night${nights === 1 ? '' : 's'}`
        args.data.cssClass = `booking booking--${args.data.status}`
      },
      onEventClick: (args) => {
        // args.e is a wrapper: id(), start(), text() are methods; data is the raw object.
        const data = args.e.data
        if (isBooking(data)) onOpen(data.guestCode)
      },
    }),
    [onOpen],
  )

  return <SuperSchedulerComponent {...config} resources={rooms} events={owned} />
}
```
You should see each booking labeled with its guest code and number of nights, and `onOpen` called with the guest code when you click it.

Lite's `EventData` is not generic and accepts no extra fields in TypeScript: keep application data in `tags`, which `onEventClick` hands back in `e.data.tags`.

## Values after a drag or a resize
When a user moves or resizes an event in Pro, the library does not edit your object. It replaces it with a new object, `{ ...old, start, end, resource }`, in which `start` and `end` are `SuperScheduler.Date` instances. Events nobody touched keep the strings you passed. Code that reads events must therefore accept both forms:

```ts
// src/event-dates.ts
import { SuperScheduler } from 'super-scheduler'

/**
 * After a drag or a resize, the committed event holds SuperScheduler.Date values in start and end;
 * events nobody touched keep the strings you passed. Normalize both to one canonical string.
 */
export function toIso(value: SuperScheduler.DateInput): string {
  // `value` is `yyyy-MM-ddTHH:mm:ss` (plus `.fff` when milliseconds are not zero).
  return typeof value === 'string' ? new SuperScheduler.Date(value).value : value.value
}

/** The part of an event your backend stores. */
export function toSavePayload(event: SuperScheduler.EventData) {
  if (event.resource === undefined) throw new Error(`Event ${String(event.id)} has no resource`)
  return {
    id: String(event.id),
    resource: String(event.resource),
    start: toIso(event.start),
    end: toIso(event.end),
  }
}

/** Half-open overlap, the rule the scheduler applies: touching intervals do not overlap. */
export function overlaps(a: SuperScheduler.EventData, b: SuperScheduler.EventData): boolean {
  const date = (value: SuperScheduler.DateInput) => new SuperScheduler.Date(value)
  return SuperScheduler.Util.overlaps(date(a.start), date(a.end), date(b.start), date(b.end))
}
```
A few details help when you persist or compare these values:

- `JSON.stringify` writes a `SuperScheduler.Date` as `yyyy-MM-ddTHH:mm:ss`, so a whole event object serializes to valid ISO strings.
- `String(date)` and `date.value` give the same text; `date.toString('d MMM HH:mm', 'en-us')` formats with a pattern and a locale.
- Compare dates with `a.equals(b)` or by `getTime()`. `===` compares object identity.
- `getDay()` is the day of the month (1 to 31), unlike the native `Date`. The weekday is `getDayOfWeek()` (0 is Sunday) or `dayOfWeekISO()` (1 is Monday).
- Test the type with `value instanceof SuperScheduler.Date`, never with the constructor name. In Lite, use Lite's own `SuperScheduler.Date`; when both editions are installed, exchange ISO strings between them.

→ https://superscheduler.org/en/examples/fleet-rentals/
→ https://superscheduler.org/en/examples/lab-instruments/
## Next steps
- Keep events in React state and persist changes: [Controlled events and callbacks](https://superscheduler.org/en/docs/controlled-state/).
- Show hours and minutes instead of days: [Hours, minutes, days and zoom](https://superscheduler.org/en/docs/time-scales-zoom/).
- Customize the bars with your fields: [React render slots](https://superscheduler.org/en/docs/react-render-slots/) and [Theming](https://superscheduler.org/en/docs/theming/).
