Concepts / Events
Concepts

Events

An event is something your app offers to wake agents with, such as ci.finished or chat.message. You define each event once, serve the events on your MCP server, and emit an occurrence whenever it happens. Every agent subscribed with matching arguments wakes up.

Events follow MCP Events, the Model Context Protocol's standard for this. Clients that speak it, such as ChatGPT, subscribe natively. Every other agent subscribes with a tool the SDK adds, and Wakeshift wakes it the way that agent can be woken. Your code is the same for all of them.

Define an event

TypeScript
const message = wake.defineEvent("chat.message", {
  description: "A new message in a channel",
  args: z.object({ channel: z.string(), from: z.string().optional() }),
  payload: z.object({ from: z.string(), text: z.string() }),
  authorize: (subscriber, { channel }) => canRead(subscriber.user, channel),
  text: (m, { channel }) => `${m.from} in #${channel}: ${m.text}`,
})
Option What it is
name What clients subscribe to: lowercase words joined by dots (chat.message, game.turn), at most 64 characters, not starting with wakeshift..
description What it is, for the agent and for the person who approves the subscription.
args What a subscriber picks, as a zod object. Its required string, number and boolean fields route occurrences: an occurrence reaches the subscribers who picked the same values. Their values are lowercase IDs (a-z 0-9 . _ -). An optional field filters: a subscriber who sets it only gets occurrences whose payload field of the same name has that value.
payload What each occurrence carries, as a zod object. emit checks it before sending.
text The line the agent reads first, from the payload and arguments.
authorize Who may subscribe with these arguments. Wakeshift refuses the subscription when it returns false. Default: everyone.
personal Each subscription belongs to its subscriber, and each occurrence goes to one subscriber. See personal events.
deleteOnDelivery Wakeshift deletes each occurrence's text and payload once it's delivered, keeping only that it happened (when, and where it went). An agent can't have it again after that: a replay skips it. An occurrence that's never delivered expires as any other does. Use it for messages people wouldn't expect a third party to keep, such as chat messages.

Define every event before serveEvents. Your events go into your app's manifest the first time you serve or emit one.

Serve them

TypeScript
const server = new McpServer({ name: "chat", version: "1.0.0" })
wake.serveEvents(server, { user: (extra) => extra.authInfo?.extra?.userId as string | undefined })

serveEvents answers MCP Events requests (events/list, events/subscribe, events/unsubscribe) for clients that speak it, and adds three tools:

  • subscribe_events, for every agent that doesn't subscribe natively. ChatGPT never sees it.
  • unsubscribe_events and list_subscriptions, for every agent, so a person can always see and stop what they're subscribed to by asking their agent.

The tools' wording is fixed, and the same in every SDK. Keep one McpServer per MCP session (a transport with sessions), and call serveEvents before connecting it.

user is your own ID for the person behind a request; subscriptions belong to that user. By default it's the guest that guest sign-in signed in. Native subscriptions and personal events need a user; other agents can subscribe to shared events without one.

Emit

TypeScript
const { matched, duplicate } = await message.emit(
  { channel: "eng" },
  { from: "ada", text: "shipped it" },
  { id: deliveryId },
)
  • The routing arguments say who gets it: here, everyone subscribed to eng. Optional (filter) arguments aren't passed to emit; they match against the payload.
  • id makes retries safe: the same id within a day emits once, and the retry returns duplicate: true.
  • matched counts the subscriptions the occurrence reached.
  • The text is at most 1000 bytes, and the whole request at most 64 KB.

Personal events

A personal event is for one subscriber at a time, so an occurrence can carry something only they may see: their cards in a game, their own messages.

TypeScript
const turn = wake.defineEvent("game.turn", {
  description: "It's your move at a table",
  args: z.object({ table: z.string() }),
  payload: z.object({ hole: z.array(z.string()), toCall: z.number() }),
  text: (t) => `Your move: ${t.toCall} to call`,
  personal: true,
})

await turn.emit({ table: "juniper" }, { hole: ["As", "Kd"], toCall: 2 }, { to: "guest:abc123" })
  • to is required for a personal event, and refused for any other. It names the subscriber: the user serveEvents gave for them.
  • Nothing in the arguments names the subscriber, so nobody can subscribe to someone else's: Wakeshift takes who's subscribing from their own sign-in.

Emit from a webhook

When a service that can only send webhooks should emit one of your events, give it an event's publish URL. Each POST to it emits the event with the arguments you made the URL for:

TypeScript
const { id, url } = await message.publishUrl({ channel: "eng" })
  • The body is the payload: a JSON object that matches the event's payload schema. Anything else gets 400 bad_payload, and nothing is emitted.
  • Your text function doesn't run, since your code isn't involved. The agent reads the event's name and the start of the payload: chat.message: {"from":"ci","text":"deployed"}.
  • For a personal event, pass { to }: the URL emits to that one subscriber.
  • wake.revokePublishUrl(id) turns it off; it answers 410 from then on, and also once your app stops offering the event.
  • Sizes, rate limits and retries work as for publish URLs: 64 KB, 60 posts a minute, and an Idempotency-Key header to emit once.

When the sender's body doesn't match your payload, receive the webhook in your own handler and call emit from there.

Subscribe from your own tools

A tool like "join a table" can set up the caller's wake-ups in the same step, instead of asking the agent to call subscribe_events afterwards:

TypeScript
server.registerTool("join", { inputSchema: { table: z.string(), ...wake.args } }, async ({ table, ...wakeArgs }, extra) => {
  const user = extra.authInfo?.extra?.userId as string
  const sub = await turn.subscribe(extra, { table }, { server, user, wakeArgs })
  return sub.result(`You're seated at ${table}.`)
})
  • event.subscribe(extra, args, { server, user, wakeArgs }) subscribes the conversation that called your tool. It returns the subscription's id and result(text): your text, with what that agent needs in order to be woken. Your tool decides who may; authorize isn't consulted.
  • Spread ...wake.args into the tool's input and pass them on as wakeArgs: they tell Wakeshift which conversation is calling.
  • Clients that speak MCP Events subscribe themselves; tell them which event and arguments to subscribe to.
  • event.unsubscribe(extra, args, …) ends the caller's subscription with those arguments, for a tool like "leave". event.end(user, id) ends one of a user's subscriptions by its ID.

When a subscription ends

Ended by What happens
The agent calls unsubscribe_events, or a native client unsubscribes It's the agent's own request.
Your code calls event.unsubscribe or event.end The conversation is told the subscription ended.
No delivery or fresh subscribe for 30 days It expires, and the conversation is told. A native client renews its own subscriptions.
You forget the user Their subscriptions end and their streams close.
A person revokes your app in their Wakeshift console (apps on Wakeshift accounts) Its subscriptions end at once.

Occurrences emitted before a subscription ended still reach the conversation, ahead of the notice.

How occurrences reach the agent

An occurrence is marked as information from outside, never as an instruction from the agent's user, and stamped with your app as the sender. If the conversation isn't connected, it waits. Wakeshift paces wake-ups for each agent. How quickly each agent wakes, and what its user approves, is on the agents page.

Topics

Events are built on topics, which you can also use directly: a topic's publish URL, which takes any body, and waking one conversation by its subscription ID, are topic features.