Content Windows
A content window is the calendar-driven half of Almanac: a daily shop, a
weekly playlist, a featured slot, a seasonal event. The window itself never
ticks. Its instance is a pure function of the world's virtual clock -
floor(virtual time / cycle length) - so every client computes the same open
instance at the same moment, and a window never reads as half-flipped.
Creating and previewing windows requires a platform admin token; listing them and reading positions and transitions does not.
Define a window
cat > daily-shop.json <<'JSON'
{
"name": "daily-shop",
"kind": "rotation",
"schedule": {
"recurrence": "every_virtual_day",
"anchor": "world_midnight"
},
"rotation": { "source": "lottery_table:shop_stock_t2", "slots": 3 },
"hooks": { "pubsub_channel": "shop.flips" },
"preview_horizon_windows": 7
}
JSON
snug almanac windows create --world-id GgMcwrwWABuCsQMFUQct --file ./daily-shop.json
snug almanac windows list --world-id GgMcwrwWABuCsQMFUQct --page-size 25The list endpoint also takes sort_by (created_at or name) and
sort_order over HTTP - an unrecognised field is 400 invalid_request -
though the CLI exposes only --page and --page-size.
kind is rotation, playlist, featured or seasonal_event - a label
carried through to transition events, not a behaviour switch. recurrence
drives the cycle length:
| Recurrence | Cycle |
|---|---|
every_virtual_day | hours_per_day |
every_virtual_week | hours_per_day * 7 |
season_bound | the named season's months, in virtual days |
season_bound requires season_name; omitting it is
400 invalid_window. Naming a season the calendar does not define is not
rejected - verified on a world whose LongWinter spans two months, the
matching window got a 60-virtual-day cycle while a window naming
NoSuchSeason was accepted with 201 and silently fell back to a
one-month, 30-virtual-day cycle. Spell season names carefully.
Where every window is right now
windows positions resolves the world clock once and reports each window
against it, which is the query to build a shop UI on:
snug --output json almanac windows positions --world-id GgMcwrwWABuCsQMFUQct{
"world_id": "GgMcwrwWABuCsQMFUQct",
"now": {
"real_instant": "2026-08-29T12:47:19.934897Z",
"virtual_ms": 771240,
"dilation": 24.0,
"paused": false,
"virtual_time": { "year": 0, "month": "Frostwane", "day": 1, "hour": 0, "phase": "night" }
},
"positions": [
{
"window_id": "yBdmQddLxZrHhdGmsSXM",
"instance": 0,
"cycle_virtual_ms": 86400000,
"position_virtual_ms": 771240,
"remaining_virtual_ms": 85628760,
"remaining_real_ms": 3567865,
"next_transition_real": "2026-08-29T13:46:47.799897Z"
}
]
}now.virtual_ms and every remaining_* come from a single clock read, so
they never disagree with each other. The lighter
almanac now snapshot carries the same information in active_windows and
next_transitions alongside weather.
Because the instance is derived from world time rather than from creation time, a window added to a world that has already been running for 30 virtual days opens at instance 30, not 0.
Preview ahead
windows preview resolves future instances with their contents, so designers
can see what next week's shop holds before it opens. It is admin-only.
snug --output json almanac windows preview \
--world-id GgMcwrwWABuCsQMFUQct --window-id yBdmQddLxZrHhdGmsSXM --count 3{
"instances": [
{
"instance": 1,
"starts_at_real": "2026-08-29T13:46:47.800Z",
"ends_at_real": "2026-08-29T14:46:47.800Z",
"contents": [
"lottery_table:shop_stock_t2#662606",
"lottery_table:shop_stock_t2#686087",
"lottery_table:shop_stock_t2#972350"
]
}
]
}The preview is honest: the three slot tokens above came from a preview taken an hour before instance 1 opened, and the real transition event
for instance 1 carried exactly the same three. count clamps silently at
100: asking for 500 returns 100 instances rather than an error.
One caveat on contents. A slot is a deterministic hash of
(rotation.source, instance, slot index), rendered as {source}#{n}. The
source string is an opaque label: nothing resolves it against a real
Lottery table today, so treat the tokens
as a stable per-slot seed you map to your own catalogue, not as item IDs.
Transition events
A background worker polls worlds roughly once a second and, for each window whose instance has advanced, appends a transition event to the world's capped stream. Read them newest-first:
snug --output json almanac transitions --world-id GgMcwrwWABuCsQMFUQct --limit 50{
"transitions": [
{
"world_id": "GgMcwrwWABuCsQMFUQct",
"window_id": "yBdmQddLxZrHhdGmsSXM",
"window_name": "daily-shop",
"kind": "rotation",
"previous_instance": 0,
"instance": 1,
"virtual_ms": 86493168,
"season": "Winter",
"phase": "night",
"contents": ["lottery_table:shop_stock_t2#662606", "..."],
"at_real": "2026-08-29T12:54:21.105191Z",
"next_transition_real": "2026-08-29T13:00:20.717191Z",
"stream_id": "1788008061108-0"
}
]
}Verified on a world dilated to 36000 (one virtual day every 2.4 real seconds): instances 30 through 34 each produced exactly one event, in order, with no gaps and no duplicates. Pausing the clock stops the fanout dead - the event count held steady across eight real seconds that would otherwise have produced three flips - and resuming picks it back up.
--limit defaults to 50 and clamps at 200; the stream itself keeps only the
most recent 1000 events. Treat it as a recent-history tail, not an audit log.
GET /transitions is currently the only way an application learns about a
flip. subscribe returns the names a realtime subscription would use:
snug --output json almanac subscribe --world-id GgMcwrwWABuCsQMFUQct --region northreach{
"world_id": "GgMcwrwWABuCsQMFUQct",
"region": "northreach",
"pubsub_channel": "world:GgMcwrwWABuCsQMFUQct:northreach",
"websocket_topic": "world:GgMcwrwWABuCsQMFUQct:northreach"
}Two things to know before you build on that. The returned name contains a
colon, which PubSub channel validation
rejects, so subscribing to it fails outright with 400 invalid_channel - it
is also not the name the server publishes on, which is
almanac.transitions.{world_id}. And transition events do not reach PubSub
subscribers at all: with listeners attached to both that canonical channel and
a window's hooks.pubsub_channel, a manual publish to each arrived
immediately while five real transitions fired in the same span and none was
delivered. Poll GET /transitions until that seam is fixed.
The other two hook fields, webhook_event and timeline_stream, round-trip
through create and list but nothing reads them - no webhook is delivered and
no timeline entry is written.
Reference
- Almanac API - every endpoint, callable
- Almanac overview - worlds, clocks, calendars and weather
- Related: Lottery for the weighted tables a rotation source names, PubSub for channel naming rules, Timeline for durable activity feeds