Whim • Shipped 2026

iMessage group-chat agent

Role

iMessage Extension & Game Runtime

Timeline

Sep 2026 (Hack the North)

Team

Me

Chang

Jack

Karan

Skills

TypeScript

Cloudflare Workers

Durable Objects

Swift

iMessage Extensions

LLM Sandboxing

Overview

A friend in the group chat who makes the plan and the game

Whim joins an iMessage group chat through Linq's iMessage API. Ask it for dinner ideas and it posts a ballot the group votes on with tapbacks. Ask it to "make us a card game" and a playable game lands in the thread a few seconds later. Every chat gets its own stateful agent on a Cloudflare Durable Object, so Whim remembers the group's plans, votes, and scores.

Four of us built it at Hack the North 2026, where it won both the Best Use of Cloudflare and Best Use of Linq prize tracks. Chang, Jack, and Karan built the agent loop, research, and booking. I owned the games: the native Messages extension people play in, the engine that turns a prompt into a game, and the sandbox that runs model-written games.

How It Works

One message, from a phone to a Durable Object and back

Whim is one Cloudflare Worker. At its center is a Durable Object for each group chat, built on Cloudflare's Agents SDK, which keeps that chat's messages, plans, votes, and games in its own SQLite database. The diagram follows a single @mention from someone's phone to a card back in the thread.

CLOUDFLARE@Whimwebhookchat idtool loopreplyGroup chatLinqCard imagesWorkerChat agentWorkflowsModelBrowser
01 / 05
Someone @mentions Whim
Whim has its own iMessage number through Linq. Every message, tapback, and location share in the group reaches the Worker as a signed webhook.

Giving every chat its own Durable Object means every event for that chat lands in one place, in order. Tapback votes, game moves, and agent turns for the same chat can't race each other on different machines, and turns run one at a time, so the chat's state needs no locks and no outside database.

Whim only speaks by calling a send_message tool, at most three times a turn. It wakes up when someone @mentions it, replies to one of its messages, or answers within three minutes of a question it asked. Everything else in the chat still gets stored, so when it wakes it has the context.

A few Durable Objects are shared across chats. One keeps each person's profile and anything they asked Whim to remember. People who opt into introductions get a short blurb embedded with Workers AI and stored in Vectorize, and when someone asks to meet people, Whim searches that pool and checks with the match privately before introducing anyone.

Plans and Votes

Tapbacks as a ballot

Most of what a group chat decides is where to go. Ask Whim for dinner or a weekend plan and it starts a research Workflow that plans search queries, searches, scores the sources, reads the best pages, and writes up candidates. Flights, hotels, and events come from Google Flights, Google Hotels, Ticketmaster, and Luma.

PLAN CARDpost♥♥👍😂last votelink to finish♥ Pho👍 Tacos😂 Ramen‼️ PizzaLJSChat agentBookingVenue’s form
01 / 05
Whim posts a plan
After a research run, the agent posts a plan card with two to four options.

Voting uses the reactions iMessage already has. The plan card lists up to four options, and a ♥, 👍, 😂, or ‼️ on the card is a vote for option one through four. Each person gets one vote, and a new tapback replaces the old one.

Booking runs as a second Workflow. A browser agent opens the venue's real reservation page and fills it in for the group, with two limits written into the code: it stops at any payment form, and in the build we demoed it stops one click before confirming and texts the group a link to finish. Shopping follows the same pattern through Shopify's Universal Commerce Protocol. Whim searches a store's catalog and builds a cart through the store's own MCP endpoint, and checkout runs in demo mode, so nothing gets charged.

The Messages Extension

Native game screens inside the thread

Whim's games arrive as iMessage app cards. Tapping one opens my Swift Messages extension, which renders the game natively: a lobby showing who's in, round screens that flip to a reveal once everyone has answered, blackjack, and a finish screen with the result. Each phone polls the chat's agent every two seconds, so the whole group sees the same game.

The extension also ships Camera Runner, a dino-style runner you play by pinching your fingers at the front camera. Apple's Vision framework tracks the hand on-device, and when the thumb and index fingertips meet, the runner jumps. A finished run can go back into the chat as a challenge poster for everyone else to beat.

When a game ends, Whim posts the result back into the thread: a result ticket for group verdicts and sandbox games, and a winner line for scored rounds. Game cards are pre-rendered into the chat's Durable Object as soon as they exist, which cut a card's load from a 2.2s cold render to under 0.1s.

The Game Runtime

From one sentence to a playable game

When someone asks for a game, a classifier model (Jev) reads the request and picks a surface: choice rounds (trivia, "who's most likely to", group verdicts) or tap-to-dodge. It also picks how the game scores: one right answer per round, points for matching the majority, or no points with the group's pick as the verdict. Below a 0.75 confidence floor, Whim goes with the likelier surface instead of asking the chat to choose.

A generator model then fills in a definition for that one surface. Zod validates it against the schema, the server trims it to the round cap and enforces the scoring rule the router chose, and a simulator plays it to completion before anyone sees it. The model writes data and the server runs the rules, so every game Whim posts is known to finish.

“who’s most likely to…”RouterRoundsDodgeWeb gameGeneratorZod schemaServer rulesSimulatorWiringSandboxGame card
01 / 05
Someone asks for a game
The chat model calls make_game with a topic. The asker’s own message rides along, so details like player count survive the handoff.

Scores come from the server too. In tap-to-dodge, each phone sends only the times it tapped, and the server replays that trace against seeded obstacles in 50 ms ticks, so a phone can't report a score it didn't earn.

Sandboxing Generated Games

Running model-written HTML without trusting it

Dice and board games go to a third tier, where the model writes the whole game as one HTML page. Before posting, the server checks that the page is wired to the runtime (it has to call WHIM.ready, updateGame, and onGame) and gives the model one rewrite if it isn't.

The worker serves that page with a CSP sandbox that allows scripts but leaves out allow-same-origin, so the page runs with a null origin even though Whim serves it. default-src 'none' blocks outside scripts, images, and forms, connect-src keeps requests on Whim's own host, and the game's state endpoint is the only route that sends CORS headers, so it's the only response the page can read.

Multiplayer comes from a runtime I inject into every page at serve time. Game state lives in the chat's Durable Object as a revisioned blob, and every write is a compare-and-swap on the revision. When two phones move at once, one gets a 409 and retries on top of the new state. The runtime also draws a turn strip ("Your turn", "Waiting on Jeremy") from the game's fields, so every generated game shows whose move it is.

SANDBOXED PAGES · NULL ORIGINexpects 7expects 7Phone APhone BState endpointOther routesGame state · rev 7
01 / 05
Sandboxed on arrival
Each phone loads the generated page in a web view. A CSP sandbox without allow-same-origin gives it a null origin, and the runtime is injected at serve time.

Learn More

Whim is open source: github.com/chang-07/htn-26