Development instance — data may be reset at any time

Notifications

Reach the player when something happens in your game — "your farm needs water", "the negotiation took a turn". Call ctx.io.notify({ title, body, image?, tag? }) from onEvent (or a tool's execute) to push a notification into the player's cross-game inbox (the nav bell) and, when they're on the site, a toast. Detect a condition in onTick and bridge to onEvent with emitInputEvent. Notifications are replay-safe, free (only the content you generate spends), and coalesce by tag.

A background or time-driven game is only useful if it can reach the player when something happens — a farm about to dry out, a turn timer about to expire, a negotiation that took a turn. ctx.io.notify(...) is how a game does that.

A notification lands in the player's cross-game inbox (the bell in the site nav, with an unread badge) and, when they're currently on the site, as a toast. The inbox is per-player, so a farm notification shows up even while they're playing a different game. Clicking it deep-links back to the session that sent it.

Sending a notification

notify lives on ctx.io, alongside the other outward capabilities. It's available in onEvent and inside a tool's execute (both receive the event context). It is not available in onTick — a tick has no io. Detect the condition in onTick and bridge into onEvent with emitInputEvent:

ts
import { defineBackend } from '@curtain/sdk'

defineBackend<InputEvent, OutputEvent, State>({
  initialState: { water: 100, warned: false },
  tickInterval: 60_000, // check once a (game) minute

  onTick: ({ state, emitInputEvent }) => {
    const s = state.get()
    state.set(d => { d.water -= 1 })
    // Cross the threshold once, then route to onEvent to compose + send.
    if (!s.warned && s.water <= 20) {
      state.set(d => { d.warned = true })
      emitInputEvent({ type: 'low-water' })
    }
  },

  onEvent: async (event, { state, io }) => {
    if (event.type === 'low-water') {
      io.notify({
        title: 'Your farm needs water',
        body: 'The crops will wilt soon — come tend them.',
      })
    }
  },
})

Rich notifications

Compose the content first — generate copy with an agent, a thumbnail with generateImage — then pass the results in. The thumbnail's image must be a platform image URL from a prior io.activities.generateImage(...) (or a game asset), the same path normal generated game images take:

ts
onEvent: async (event, { io }) => {
  if (event.type === 'low-water') {
    const art = await io.activities.generateImage('low-water-thumb', {
      prompt: 'a wilting tomato plant in dry soil, soft morning light',
    })
    io.notify({
      title: 'Your farm needs water',
      body: 'The tomatoes are wilting.',
      image: art.url,             // a platform URL from generateImage
      tag: 'farm-needs-water',    // see coalescing, below
    })
  }
}

Coalescing with tag

For a recurring condition, pass a tag. A new notification with the same tag replaces the prior unread one for that player+game, so "your farm needs water" doesn't pile up ten entries deep while the player is away. Omit tag for one-off events (each is a new inbox entry).

Good to know

  • Replay-safe. A notify is recorded on its turn and reproduced on a rewind/replay without re-sending — editing or retrying a turn never re-notifies the player.
  • notify is free. Only the work you do to build the content (an agent call, a generateImage) costs credits — the same as any turn. The notify call itself isn't billed.
  • Throttled. Notifications are rate-limited per session, so a runaway loop can't notification-storm; over-limit calls still land in the inbox but skip the live toast.
  • Use it for events, not chatter. A notification is a "come back, something happened" pointer — not a running commentary. The turn-by-turn detail still lives in the session; the notification is the summary that pulls the player back to it.

See also time-and-ticks for the onTickemitInputEvent pattern and background play (backgroundExecution), which is what makes a notification worth sending while the player is away.