Getting started with ToolUp.Scheduling
Getting started with ToolUp.Scheduling
End-to-end walkthrough: define a bookable resource, book a slot, render a calendar.
Prerequisites
- A working ToolUp Platform app.
IEntityStoreenabled —ServerConfig.EntityStore = EnabledEntityStore. Resources and bookings persist as entities.
1. Add the packages
<ItemGroup>
<PackageReference Include="ToolUp.Scheduling.Server" />
</ItemGroup>
2. Define a resource
A Resource represents one bookable thing — a chair, a room, a person's calendar, a piece of equipment.
open ToolUp.Scheduling
let stylistOne : Resource = {
ResourceId = ResourceId "stylist-1"
Name = "Jane (Senior Stylist)"
Description = Some "Cuts, colours, consultations"
AvailabilityWindows = [
// Mon-Fri 09:00-17:00 UK time
{ Days = [ DayOfWeek.Monday; Tuesday; Wednesday; Thursday; Friday ]
StartTime = TimeSpan(9, 0, 0)
EndTime = TimeSpan(17, 0, 0)
Timezone = "Europe/London" }
// Saturday 10:00-14:00
{ Days = [ DayOfWeek.Saturday ]
StartTime = TimeSpan(10, 0, 0)
EndTime = TimeSpan(14, 0, 0)
Timezone = "Europe/London" }
]
BlockedTimes = [] // ad-hoc blackouts (holidays, sick days)
SlotDurationMinutes = 60
BufferBetweenSlotsMinutes = 15 // 15-min gap between back-to-back bookings
}
AvailabilityWindows are recurring weekly. BlockedTimes overlay one-off blackouts. Slot duration + buffer determine the candidate-slot grid.
3. Wire SchedulingServerApp
ServerApp.empty
|> ServerApp.withConfig {
ServerConfig.defaults with
EntityStore = EnabledEntityStore
}
|> ServerApp.withAuth authProvider
|> ServerApp.addModules modules
|> SchedulingServerApp.fromServerApp
|> SchedulingServerApp.withResource stylistOne
|> SchedulingServerApp.run
That's it. The scheduling API + persistence is now in place.
4. List slots for a date range
Client-side:
let! slots =
SchedulingClient.proxy.ListSlots {
ResourceId = ResourceId "stylist-1"
Start = DateTime(2026, 5, 12)
End = DateTime(2026, 5, 19) // one week
}
// slots : Slot list (each Free | Booked | Blocked)
Slot:
type Slot = {
ResourceId: ResourceId
Start: DateTime
End: DateTime
Status: SlotStatus
}
and SlotStatus =
| Free
| Booked of BookingId
| Blocked of reason: string
The server derives slots from the resource's AvailabilityWindows + buffer + existing bookings. Free slots are bookable; Booked / Blocked aren't.
5. Book a slot
let! result =
SchedulingClient.proxy.Book {
ResourceId = ResourceId "stylist-1"
Start = DateTime(2026, 5, 12, 14, 0, 0)
End = DateTime(2026, 5, 12, 15, 0, 0)
Notes = Some "Customer: Jane Smith — colour + cut"
}
match result with
| Ok booking ->
// Booking confirmed — booking.BookingId is the reference
...
| Error OutsideAvailability ->
// Slot is outside the stylist's availability windows
...
| Error SlotOccupied ->
// Another booking claimed overlapping time (concurrent caller won the race)
...
| Error ResourceNotFound -> ...
| Error Forbidden -> ...
The server's per-resource SemaphoreSlim ensures two concurrent callers booking the same slot get one success + one SlotOccupied. No double-bookings.
6. Cancel a booking
let! result = SchedulingClient.proxy.Cancel bookingId
// result : Result<unit, BookingError>
Cancelled bookings free the slot for re-booking. Audit log records BookingCancelled.
7. Render a calendar grid (UI)
The SDK ships no built-in calendar UI — the data primitives let you render whatever grid your module needs:
open Feliz
let calendarView slots =
Html.div [
prop.className "calendar-grid"
prop.children [
for slot in slots do
Html.div [
prop.className (
match slot.Status with
| Free -> "slot-free"
| Booked _ -> "slot-booked"
| Blocked _ -> "slot-blocked")
prop.text (slot.Start.ToString("HH:mm"))
prop.onClick (fun _ ->
if slot.Status = Free then bookSlot slot)
]
]
]
For complex calendar shapes (week view, month view, drag-to-extend slot), use a Feliz wrapper around a calendar component library (FullCalendar, React Big Calendar). The SDK doesn't bundle one; consumers pick what fits.
8. Recurring bookings
For recurring appointments ("weekly therapy session for 12 weeks"):
let weeklyRule = {
Frequency = Weekly
Interval = 1
ByDayOfWeek = [ DayOfWeek.Tuesday ]
ByDayOfMonth = []
Count = Some 12
Until = None
}
let dates =
RecurrenceExpander.expand
weeklyRule
(startDate = DateTime(2026, 5, 12))
// Book each date
for date in dates do
let! _ = SchedulingClient.proxy.Book {
ResourceId = stylistId
Start = date.AddHours(14.)
End = date.AddHours(15.)
Notes = Some "Recurring — Jane Smith"
}
If any individual booking fails (slot occupied, outside availability), the loop continues; the caller decides whether to roll back the already-booked dates or partially proceed.
For atomic series booking (all-or-nothing), use BookSeries:
let! result = SchedulingClient.proxy.BookSeries {
ResourceId = stylistId
DurationMinutes = 60
Recurrence = weeklyRule
StartDate = DateTime(2026, 5, 12)
StartTime = TimeSpan(14, 0, 0)
Notes = Some "Recurring"
}
// result : Result<Booking list, BookingError * DateTime list>
// Ok bookings = all succeeded
// Error (err, conflictDates) = none booked; conflicts are the dates that would fail
BookSeries either books every occurrence or none. Useful for "all 12 weeks must work, or I'll pick a different time".
9. Export to iCalendar
let! ics = SchedulingClient.proxy.ExportICalendar resourceId
// ics : string (.ics content)
Serve as a download:
let icalRoute : HttpHandler =
fun next ctx -> task {
let resourceId = ResourceId (ctx.Request.Query.["resource"].ToString())
let! ics = serviceProvider.GetRequiredService<IBookingScheduler>().ExportICalendar resourceId
ctx.Response.Headers.ContentType <- "text/calendar"
ctx.Response.Headers.ContentDisposition <- "attachment; filename=bookings.ics"
return! ctx.Response.WriteAsync(ics) |> Async.AwaitTask
}
Or expose it directly in your module's API.
Next steps
- concepts.md — data model, concurrency model, recurrence semantics, iCalendar wire format.
- api-reference.md — full public surface.
- extending.md — custom
IBookingSchedulerimpls, multi-resource patterns, calendar UI.