Sub-skills

Time zones in depth

For your agentCopy this and give it to the AI you're building with.
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 like Date.UTC: day: d + 1 is the next day, minute: 540 is 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"): tokens YYYY MM DD HH mm ss SSS Z; default 2026-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 test and 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".