Scheduler
A use-case-agnostic resource/timeline planning board: nested resources down the side, day/week/month scale views across the top, author-defined categories and configurable bars. Bind resources and events to named queries for a live schedule.


Reach for it when you need:
- Production scheduling across lines
- Planned maintenance windows
- Shift and crew rosters
Component name: Scheduler · Category: Scheduler
In the palette: A use-case-agnostic resource/timeline planning board — nested resources, Day/Week/Month scale views, author-defined categories and configurable bars. Bind resources/events to Named Queries for live schedules.
Properties
Any property can be bound to a tag, another component's property, or a query.
| Property | Type | Default | Description |
|---|---|---|---|
resources | array | [{"id":"team-eng","label":"Engineering","parentId":"","group… (truncated) | Sidebar rows (nest via parentId). Edit inline, or bind to a Named Query returning { id, label, parentId }. |
events | array | [{"id":"e1","resourceId":"alice","label":"Sprint planning","… (truncated) | Timeline blocks. Edit inline, or bind to a Named Query returning { id, resourceId, label, start, end, category }. |
categories | array | [{"name":"design","color":"#7c3aed","label":"Design"},{"name… (truncated) | Category colours + legend labels. Any category name works; blank colour = auto-palette. |
bands | array | [{"resourceId":"*","start":"2026-01-05T00:00:00","end":"2026… (truncated) | Background stripes (shift windows, sprints, availability). resourceId '*' spans every row. |
dependencies | array | [{"from":"e2","to":"e3","type":"FS"},{"from":"e6","to":"e8",… (truncated) | Links between events, drawn as arrows. Each = { from: eventId, to: eventId, type: FS|SS|FF|SF }. |
dependencyStyle | object | {"endMarker":"arrow","line":"orthogonal","color":"","width":… (truncated) | Dependency link look — end marker (arrow/circle/diamond/none), line (orthogonal/curved/straight), colour, width. |
rangeStart | string | 2026-01-05T00:00:00 | Visible window start — ISO string or epoch-ms. |
rangeEnd | string | 2026-01-10T00:00:00 | Visible window end — ISO string or epoch-ms. |
snapMinutes | number | 15 | Drag/resize snap grid in minutes (0 = free). |
appearance | object | {"barStyle":"gradient","barRadius":6,"barShowTime":false,"ro… (truncated) | Bar style + row/sidebar sizing. |
axis | object | {"timeFormat":"24h","dateFormat":"MMM d","showGrid":true,"sh… (truncated) | Time axis — clock format, grid, now-line. |
toolbar | object | {"show":true,"scales":"day,week,month,quarter,year","showLeg… (truncated) | Toolbar — visibility, scale presets, legend. |
popup | object | {"enabled":true,"view":"","params":{}} | Event click popover — built-in details, plus an optional embedded View. |
slotPopup | object | {"enabled":false,"view":"","params":{},"durationMinutes":60,… (truncated) | Empty-slot click popover — opens on blank timeline and embeds a View (your form) with the clicked slot as a param. |
selectedEvent | object | {} | Output: the event last clicked in LiveView ({ id, resourceId, label, start, end, category }). |
selectedSlot | object | {} | Output: the empty slot last clicked in LiveView ({ resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes }). |
style | style | {} | Custom CSS properties |
Events
Attach event actions to these in the Event Manager.
| Event | Label | Group | Payload |
|---|---|---|---|
click | Click | Mouse | The DOM event. |
dblclick | Double click | Mouse | The DOM event. |
contextmenu | Right click | Mouse | The DOM event. |
mouseenter | Mouse enter | Mouse | The DOM event. |
mouseleave | Mouse leave | Mouse | The DOM event. |
eventClick | Event click | Scheduler | The clicked block, under event. |
eventChange | Event change (drag/resize) | Scheduler | The block plus its new { start, end } after a drag/resize commit. |
eventDoubleClick | Event double click | Scheduler | The clicked block, under event. |
slotSelect | Empty slot select | Scheduler | { resourceId, time } for a click on empty timeline. |
contextmenu fires your configured actions at runtime only — in LiveView and in preview. On the Designer canvas the right-click is captured to open the Event Manager, so testing it there will not run your actions. Test right-click in preview.
Notes
Its events carry enough context to write back: eventChange fires after a drag or resize commit with the block and its new { start, end }, and slotSelect gives you { resourceId, time } for a click on empty timeline — the hook for scheduling something new.
Driving the board from a database
Everything above is editable inline, which is how you prototype. A real schedule comes out of tables. Two are enough:
CREATE TABLE schedule_resource (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
parent_id TEXT NULL REFERENCES schedule_resource(id)
);
CREATE TABLE schedule_event (
id TEXT PRIMARY KEY,
resource_id TEXT NOT NULL REFERENCES schedule_resource(id),
label TEXT NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
category TEXT NOT NULL
);
CREATE INDEX schedule_event_window ON schedule_event (starts_at, ends_at);
Named query SchedulerResources feeds the sidebar. The column names have to match the
property contract — id, label, parentId — so alias them, and quote the aliases on
PostgreSQL or they arrive folded to lower case:
SELECT id, label, parent_id AS "parentId"
FROM schedule_resource
ORDER BY parent_id NULLS FIRST, label
Named query SchedulerEvents feeds the bars. Take the visible window as parameters so the
board never pulls the whole history:
SELECT id,
resource_id AS "resourceId",
label,
starts_at AS "start",
ends_at AS "end",
category
FROM schedule_event
WHERE starts_at < :rangeEnd
AND ends_at > :rangeStart
ORDER BY starts_at
That WHERE is an overlap test, not containment. A maintenance window that began yesterday
and ends tomorrow must still be drawn on today's board; starts_at BETWEEN :rangeStart AND :rangeEnd would silently drop it.
Then bind:
| Property | Binding |
|---|---|
resources | Query → named query SchedulerResources |
events | Query → named query SchedulerEvents, with :rangeStart / :rangeEnd supplied from the board's own range or a date picker |
categories | Left inline — these are your colour scheme, not data |
Writing a drag back
eventChange hands you the moved block. Send it to a gateway script:
def on_event_change(event):
system.db.runNamedQuery("SchedulerMoveEvent", {
"id": event["id"],
"resourceId": event["resourceId"],
"start": event["start"],
"end": event["end"],
})
-- SchedulerMoveEvent
UPDATE schedule_event
SET resource_id = :resourceId,
starts_at = :start,
ends_at = :end
WHERE id = :id
Re-run the events query afterwards so the board reflects what was actually stored — if the write is rejected, the bar should snap back rather than lie.
Things that bite
categoryis a join key, not a colour. A row whosecategorymatches no entry incategoriesstill draws, but with an auto-assigned colour that changes as the data changes. Constrain the column, or seedcategoriesfrom the same table.- Timestamps may be ISO strings or epoch milliseconds. Mixing the two in one result set is the usual cause of bars landing in 1970.
parentIdmust reference a resource in the same result set. A child whose parent is filtered out by the query does not render.