Notifications

Reach the player when something happens in your game — "your farm needs water", "the negotiation took a turn". Declare requestNotifications true in defineBackend, then 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

The game must declare that it sends notifications: set requestNotifications: true in defineBackend. Calling notify without the declaration throws.

The declaration can also be a pure function of game staterequestNotifications: s => s.introDone — for games that grow into notifications: the platform re-resolves it after every settled turn (so the in-game permission ask appears when it first matters), and notify works only while it currently resolves true (called earlier, it throws).

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 '@aichatgames/sdk'

defineBackend<InputEvent, OutputEvent, State>({
  initialState: { water: 100, warned: false },
  requestNotifications: true, // required to call io.notify
  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 is a mediaId from a prior io.activities.generateImage(...) (or a game asset), the same handle normal generated game images use; the platform resolves it to an image when showing the notification, and a media that hasn't finished generating shows no image:

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.mediaId,         // a mediaId 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.