app.messages as a [Space, Message] tuple. The space is already bound to the originating conversation — you don’t need to resolve it yourself to reply.
Receiving messages
The Message shape
Every message conforms to .| Field | Description |
|---|---|
id | Platform-assigned message identifier. |
content | Discriminated union on type — see Narrowing content for the full set of variants. |
sender | The who sent the message. |
space | The the message was sent into. |
platform | Name of the provider that delivered the message (e.g. “iMessage”, “terminal”). |
timestamp | Date of when the message was sent. |
react(reaction) | React to this message. Returns the reaction Message — keep it as the handle to unsend() later. No-op on platforms that don’t support reactions. |
reply(…content) | Reply threaded to this message. Falls back silently on platforms without thread support. |
edit(newContent) | Rewrite the content of this outbound message. Fire-and-forget. |
unsend() | Retract this outbound message. Fire-and-forget. |
Narrowing content
Content is a discriminated union. Narrow on message.content.type before accessing fields:
Content
The incoming content variants that every Message carries. Most platforms only emit a subset — narrow defensively.
Content
The incoming content variants that every Message carries. Most platforms only emit a subset — narrow defensively.
| Type | Fields |
|---|---|
"text" | text: string |
"markdown" | markdown: string — outbound-only styled text |
"attachment" | id: string, name: string, mimeType: string, size?: number, read(), stream() |
"voice" | name?: string, mimeType: string, duration?: number, size?: number, read(), stream() |
"contact" | name?, phones?, emails?, addresses?, org?, urls?, birthday?, note?, photo?, user? |
"richlink" | url: string, title(), summary(), cover() |
"reaction" | emoji: string, target: Message |
"poll" | title: string, options: { title: string }[] |
"poll_option" | option: { title }, poll: Poll, selected: boolean, title: string — sent as a vote |
"group" | items: Message[] — bundled multi-message unit |
"reply" | content: Content, target: Message — threaded reply wrapping inner content |
"edit" | content: Content, target: Message — rewrite of a previously-sent message |
"unsend" | target: Message — retraction of a previously-sent message |
"typing" | state: "start" | "stop" — typing indicator signal |
"streamText" | stream: () => AsyncIterable<string> — streaming text content |
"custom" | raw: unknown — platform-specific structured data |
"effect" (an iMessage screen effect wrapping inner content) appear on messages you sent and are echoed by the platform; see iMessage for the builder.
Filtering out your own messages
On platforms where your account also receives its own sends, guard with a platform-specific check — for example, iMessage carries anisFromMe flag on the raw message extra you can expose through a provider message.schema.
For unified logic, compare the sender to a known identity:
Acting on a message
Every message is its own context. You can reply, react, or send new content into the space:react and reply.