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 state —
requestNotifications: 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:
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:
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
notifyis recorded on its turn and reproduced on a rewind/replay without re-sending — editing or retrying a turn never re-notifies the player. notifyis free. Only the work you do to build the content (an agent call, agenerateImage) 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 onTick → emitInputEvent
pattern and background play (backgroundExecution), which is what makes a notification
worth sending while the player is away.