Sub-skills
Time zones in depth
Run `cubek skill`, then `cubek skill time`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/time.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
Time zones: scheduling, per-day reports, local times in pages and emails.
Instants stay UTC ISO strings (t.timestamp(), toISOString()); a user's
wall clock is an IANA zone name ("Europe/Madrid") stored as a string.
time converts between them with the tz database built into cubek (every
DST rule). cubek-ts has no Intl and no local-time Date: use time.
Browser code in client/ has Intl; the visitor's zone is
Intl.DateTimeFormat().resolvedOptions().timeZone (send it in a form field).
time.toLocal(instant, zone)→{ year, month, day, hour, minute, second, weekday, offset }(weekday 0 = Sunday; offset: minutes east of UTC).time.fromLocal({ year, month, day, hour?, minute?, second? }, zone)→ the UTC ISO instant. Fields roll over likeDate.UTC:day: d + 1is the next day,minute: 540is 09:00. A time the clocks skip (spring forward) moves forward (02:30 → 03:30); a repeated one (fall back) is the first,{ dst: "later" }the second,{ dst: "reject" }throws on both.time.format(instant, zone, "YYYY-MM-DD HH:mm"): tokensYYYY MM DD HH mm ss SSS Z; default2026-10-25T02:30:00+02:00. Human text:fmt.date(instant, { format: "datetime", locale: "es", timeZone: zone }).time.zones()(a zone picker),time.isZone(name)(check input first: an unknown zone throws).- Pure: they run in
cubek testand in live queries.
Weekly availability → UTC slots
// schema.ts
import { table, t } from "cubek"
export const hosts = table("hosts", { id: t.id(), name: t.string(), zone: t.string() })
// Weekly hours in the host's zone: weekday 0 = Sunday, minutes from midnight.
export const windows = table("windows", {
id: t.id(),
hostId: t.ref(hosts),
weekday: t.number(),
startMinute: t.number(),
endMinute: t.number(),
}).index("hostId")
export const bookings = table("bookings", { id: t.id(), hostId: t.ref(hosts), startAt: t.timestamp() }).index("hostId", "startAt")// src/lib/slots.ts
import { time } from "cubek"
export interface Window {
weekday: number
startMinute: number
endMinute: number
}
/** Slot starts (UTC ISO, sorted) for `days` local days of the host from `nowIso`. */
export function slots(zone: string, weekly: Window[], nowIso: string, days: number, minutes: number): string[] {
const today = time.toLocal(nowIso, zone)
const out: string[] = []
for (let i = 0; i < days; i++) {
// Noon is never skipped by DST: a safe probe for the local date.
const day = time.toLocal(time.fromLocal({ year: today.year, month: today.month, day: today.day + i, hour: 12 }, zone), zone)
for (const w of weekly) {
if (w.weekday !== day.weekday) continue
for (let m = w.startMinute; m + minutes <= w.endMinute; m += minutes) {
const at = time.fromLocal({ year: day.year, month: day.month, day: day.day, minute: m }, zone)
if (at > nowIso) out.push(at)
}
}
}
return out.sort()
}// src/app.tsx
import { app, db, fmt, time } from "cubek"
import { slots } from "./lib/slots"
// /book/<hostId>?tz=America/New_York: the host's free slots in the visitor's zone.
app.get("/book/:hostId", async (c) => {
const host = await db.hosts.get(c.param("hostId"))
if (host === null) return c.notFound()
const tz = c.query("tz") ?? host.zone
if (!time.isZone(tz)) return c.json({ error: "unknown time zone" }, 400)
const now = new Date().toISOString()
const taken = new Set((await db.bookings.find({ where: { hostId: host.id, startAt: { gte: now } }, limit: 1000 })).map((b) => b.startAt))
const free = slots(host.zone, await db.windows.find({ where: { hostId: host.id }, limit: 50 }), now, 14, 30).filter((at) => !taken.has(at))
return c.json(free.map((at) => ({ at, day: time.format(at, tz, "YYYY-MM-DD"), label: fmt.date(at, { format: "datetime", timeZone: tz }) })))
})Per-day reports in the user's zone
A local day is [fromLocal(day), fromLocal(day + 1)): 23 or 25 hours on DST
days. Group rows by their local date with time.format(at, zone, "YYYY-MM-DD").
// src/lib/report.ts
import { db, time } from "cubek"
/** Bookings per local day of `zone` in the month `month` ("2026-10"). */
export async function perDay(hostId: string, month: string, zone: string): Promise<Map<string, number>> {
const [y, m] = month.split("-").map((x) => Number(x))
const start = time.fromLocal({ year: y, month: m, day: 1 }, zone)
const end = time.fromLocal({ year: y, month: m + 1, day: 1 }, zone)
const rows = await db.bookings.find({ where: { hostId, startAt: { gte: start, lt: end } }, limit: 1000 })
const days = new Map<string, number>()
for (const b of rows) {
const day = time.format(b.startAt, zone, "YYYY-MM-DD")
days.set(day, (days.get(day) ?? 0) + 1)
}
return days
}Cron runs in UTC: for "08:00 in each user's zone", schedule hourly
(task.schedule("digest", "0 * * * *")) and act on users whose
time.toLocal(new Date(), user.zone).hour === 8.
Business hours: SLA due times
Add N business minutes in the company's zone, skipping weekends and
holidays. Walk local days (noon probe) and build each instant with
fromLocal from wall-clock minutes: never add elapsed milliseconds, which
drift by an hour across DST changes and weekends.
// src/lib/business.ts
import { time } from "cubek"
export interface Hours {
startMinute: number // 540 = 09:00 local
endMinute: number // 1020 = 17:00
}
/** `fromIso` + `minutes` of business time in `zone`: Mon-Fri within `hours`, not on `holidays` (local "YYYY-MM-DD"). */
export function addBusinessMinutes(fromIso: string, zone: string, minutes: number, hours: Hours, holidays: string[] = []): string {
const off = new Set(holidays)
const now = time.toLocal(fromIso, zone)
let at = Math.max(now.hour * 60 + now.minute, hours.startMinute) // minute of the local day
let left = minutes
for (let i = 0; i < 366; i++) {
const noon = time.fromLocal({ year: now.year, month: now.month, day: now.day + i, hour: 12 }, zone)
const d = time.toLocal(noon, zone)
if (d.weekday !== 0 && d.weekday !== 6 && !off.has(time.format(noon, zone, "YYYY-MM-DD")) && at < hours.endMinute) {
if (at + left <= hours.endMinute) return time.fromLocal({ year: d.year, month: d.month, day: d.day, minute: at + left }, zone)
left -= hours.endMinute - at
}
at = hours.startMinute
}
throw new Error("no business time within a year")
}
// Hours: addBusinessMinutes(createdAt, "Europe/Madrid", 4 * 60, { startMinute: 540, endMinute: 1020 }, ["2026-12-25"])
export function main() {
// Fri 16:00 Madrid + 2h → Mon 10:00 (09:00Z: CET after the Oct 25 change).
return addBusinessMinutes("2026-10-23T14:00:00Z", "Europe/Madrid", 120, { startMinute: 540, endMinute: 1020 })
}Check it: cubek run src/lib/business.ts prints "2026-10-26T09:00:00.000Z".