LIVE DEMO
Preview:
https://resource-scheduler.bubbleapps.io/version-testEditor:
https://bubble.io/page?id=resource-scheduler&test_plugin=1788169839014x195626234316062720_current&tab=Design&name=indexSETUP
Step 1 — Place the element
Drop "ResourceScheduler" on your page and give it a height. The calendar fills
the height you set and scrolls inside it. 600px is a good start. Width can be
anything — resources scroll sideways with the time column pinned.
Step 2 — Set the date and the timezone
date the day to show, as YYYY-MM-DD
timezone an IANA name such as Europe/Kyiv, Asia/Dubai, America/New_York
Every time in the calendar is printed in that zone, not the visitor's browser
zone. Leave it empty and you get UTC.
Step 3 — Feed it your resources
Two ways to do this — use whichever fits your data.
A) Straight from your database (recommended)
columns pick your resource data type (e.g. Room)
columns_data Search for [your type] — the list of resources to show
column_title which field is the resource's name
column_subtitle optional, e.g. the staff member's name
column_sort optional, controls the order
column_key optional — see the note below
B) A JSON array, if your data doesn't come from a Bubble list
columns_json:
[{"id":"r1","title":"Room 1","subtitle":"Dr. Haddad","sort_key":"1"},
{"id":"r2","title":"Room 2","subtitle":"Dr. Ivanova","sort_key":"2"}]
If both are filled in, the database list wins.
About column_key: bookings need to say which resource they belong to.
By default the plugin matches on the resource's unique id — fine if your
booking's resource field is a proper link to the resource. If you instead
store a short code on your resource (e.g. "R1") and reference that code
from your bookings, set column_key to the field holding that code, and use
the same code in event_column below. This is the easiest option when your
resource field is text rather than a link.
Step 4 — Feed it your bookings
A) Straight from your database (recommended)
events pick your booking data type (e.g. Appointment)
events_data Search for [your type]
event_column which field points at the resource (a link, or a
text field matching column_key if you set one)
event_start which field holds the start date/time
event_duration optional, minutes — defaults to 60
event_title which field is the bold line on the card
event_subtitle optional, shown after the time
event_status optional, see step 6
event_color optional, overrides the status colour for this booking
B) A JSON array
events_json:
[{"id":"a1","column_id":"r1","start":"2026-09-01T05:00:00Z",
"duration_min":60,"title":"Marsh, Elena","subtitle":"Checkup",
"status":"confirmed"}]
A booking whose resource doesn't match any column appears under
"Unassigned" at the end, rather than disappearing.
Step 5 — Add blocked time (optional)
Same two options as bookings, with a label instead of a title.
A) blocks / blocks_data / block_column / block_start / block_duration /
block_title
B) blocks_json:
[{"id":"b1","column_id":"r1","start":"2026-09-01T09:00:00Z",
"duration_min":60,"label":"Lunch"}]
These draw as hatched bands and are not clickable.
Step 6 — Define your statuses (optional)
statuses_json drives the card colours AND the legend from one list.
[{"key":"scheduled","label":"Scheduled","color":"#5cc8d6"},
{"key":"confirmed","label":"Confirmed","color":"#4ec98a"},
{"key":"cancelled","label":"Cancelled","color":"#e5646d","muted":true}]
key what you put in a booking's "status"
label what the legend shows
color the card's left edge
muted dims the card and strikes through the title
Leave the field empty to use five sensible defaults.
Step 7 — Handle clicks
Three workflow events:
When ResourceScheduler A's event_clicked a booking was clicked
When ResourceScheduler A's event_edit_clicked its pencil icon was clicked
(separate from event_clicked, so you can open a read-only detail view on a
card click and an edit form on the pencil — or route both to the same
popup if you don't need the distinction)
When ResourceScheduler A's slot_clicked empty space was clicked
States available in workflows:
clicked_event_id the booking's id — use it to look the record up
clicked_event_title its title
clicked_event_json the whole booking as JSON, for your own fields
clicked_column_id which resource was clicked
clicked_time "9:15 am"
clicked_minutes 555, minutes from midnight
clicked_date the day shown
event_count bookings currently drawn
column_count resources currently drawn
A typical booking flow: on slot_clicked, show a popup, and create your record
using clicked_column_id and clicked_minutes.
Step 8 — Actions
Refresh redraws the calendar, e.g. after saving a record
Scroll to hour scrolls the grid to a given hour, e.g. 9
BUILDING THE JSON (only if you're using option B above)
Bubble hands lists over as text, so use :format as text on a repeating group's
data source, with a comma as the delimiter, then wrap the result in square
brackets. Quotes inside your values will break the JSON — run names through
:find & replace on the " character first.
If you're feeding the calendar straight from your database (option A in
steps 3–5), you can skip this section entirely.
APPEARANCE
bg_color grid background
card_color booking cards and the header
line_color grid lines and borders
ink_color main text
dim_color secondary text, hour labels
accent_color fallback card colour and hover
now_color the current-time line
block_color the edge of blocked bands
day_start first hour to show on an empty day (default 7)
day_end last hour on an empty day (default 20)
px_per_min vertical zoom; raise it for longer cards (default 1.1)
slot_step rounding for empty-slot clicks, in minutes (default 15)
show_now_line the red current-time line
show_edit the pencil icon on cards
show_legend / legend_at_bottom / legend_show_all
block_label what an unlabelled blocked band is called
TROUBLESHOOTING
Times are off by several hours
Check timezone. An empty field means UTC.
The calendar is empty but you have data
If using database lists (option A): open the browser console — the plugin
warns if a list is still loading, and prints a mismatch if a booking's
resource field doesn't match any column's id (or its column_key, if set).
If using JSON (option B): the console names the field whose JSON failed to
parse. Unescaped quotes inside names are the usual cause.
Bookings sit side by side instead of on top of each other
That is deliberate. Overlapping bookings split the column between them.
Nothing fits on a phone
A day with four resources needs width. The grid scrolls sideways and the time
column stays pinned.