Skip to content

[CHA-5933] feat: support scheduled messages - #278

Merged
nijeesh-stream merged 4 commits into
mainfrom
feature/cha-5933-scheduled-messages
Oct 19, 2026
Merged

nijeesh-stream merged 4 commits into
mainfrom
feature/cha-5933-scheduled-messages

Conversation

@nijeesh-stream

Copy link
Copy Markdown
Contributor

TLDR

Backends can now schedule a chat message to be sent later, on behalf of any user, and then list, edit, cancel or send early those scheduled messages. Message.send(...).scheduledAt(date) stores the message instead of sending it; the new ScheduledMessage class covers the rest. Risk is low: the change is additive (one new optional request field, two new optional response fields, one new model and service). The backend endpoints are behind a per-app flag and not yet released, so merge after backend CHA-5924 ships.

Ticket

CHA-5933

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

Backend endpoints are behind a per-app flag and not yet released; merge after backend CHA-5924 ships.

API surface

Call Endpoint
Message.send(type, id).scheduledAt(date) POST /channels/{type}/{id}/message with scheduled_at; response gains scheduled_message
ScheduledMessage.query().userId(..).filter(..).sort(..).limit(..).next(..) POST /scheduled_messages/query (user_id is required server-side; filter cid and status; only scheduled_at ascending; limit 1-100, default 20; cursor next)
ScheduledMessage.update(id).message(..).scheduledAt(..) PATCH /scheduled_messages/{id} (either or both)
ScheduledMessage.delete(id) DELETE /scheduled_messages/{id} (cancel, or remove a failed one)
ScheduledMessage.sendNow(id) POST /scheduled_messages/{id}/send_now
  • ScheduledMessage mirrors the backend ScheduledMessageResponse: id, cid, user, message (content without an ID; mentioned_users holds user IDs), scheduled_at, status, failure_reason, created_at, updated_at, and channel (query results only). Status values are exposed as STATUS_* constants.
  • Event gains scheduledMessage and messageId (the sent message's ID on scheduled_message.sent).
  • New doc page docs/messages/scheduled_messages.md.

Not added, on purpose:

  • include_scheduled_messages on GetOrCreateChannel: the backend marks it ignore_if_server_side, so it does nothing for a server SDK.
  • App settings (chat_scheduled_messages_enabled, max lead time): not exposed through the app settings API.
  • CHANGELOG.md: generated at release time.

How to verify

  1. ./gradlew test --tests '*ScheduledMessageTest*' passes (8 serialization tests, no API credentials needed).
  2. ./gradlew spotlessCheck javadoc passes.
  3. ScheduledMessageIntegrationTest (schedule, query, update, delete, send now against a real app) runs only with STREAM_SCHEDULED_MESSAGES_ENABLED=true, because the feature is behind a per-app flag that is off on the CI app. It has not been run yet: no app with the feature is available until the backend ships.

🤖 Generated with Claude Code

nijeesh-stream and others added 4 commits October 15, 2026 00:17
Message.send gains scheduledAt, and its response carries the stored
scheduled message. A new ScheduledMessage model lists (query), edits
(update), cancels (delete) and sends early (sendNow) the scheduled
messages of any user. Event reads scheduled_message and message_id for
the scheduled_message.* events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v1 API returns a scheduled message's custom fields as top-level keys
of its message object, as for regular messages, not under "custom".
Drop the custom field so they land in getAdditionalFields(), as on Message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

/** On scheduled_message.sent, the ID of the message that was sent. */
@Nullable
@JsonProperty("message_id")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Should Fix] This typed field also takes message_id from events other than scheduled_message.sent. The backend puts a top-level message_id on every message webhook (message.new, message.updated, ...) and on reminder.* and poll.* events. Today Jackson puts that key in getAdditionalFields(); after this change it goes to getMessageId() and is no longer in the map.

Why it matters: a backend that parses webhooks into Event and reads getAdditionalFields().get("message_id") gets null after upgrading, with no compile error.

Fix: keep the typed field, but make the Javadoc say it is the ID of the message the event is about (on scheduled_message.sent, the sent message), and mention the move from getAdditionalFields() to getMessageId() in the commit message so the changelog carries it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — #278 merged before this was addressed, so the fix is in a follow-up: #279 (commit 8b59fc5).

Rather than only documenting the move, it keeps full backward compatibility: messageId is no longer a Jackson-mapped field; getMessageId()/setMessageId() read and write additionalFields["message_id"]. So getAdditionalFields().get("message_id") keeps working on message.*/reminder.*/poll.* events, JSON round-trips exactly as in 1.50.0, and getMessageId() works on every event including scheduled_message.sent. Tests cover a message.new event with message_id and a scheduled_message.sent event.

@nijeesh-stream
nijeesh-stream merged commit b3dad6d into main Oct 19, 2026
5 checks passed
@nijeesh-stream
nijeesh-stream deleted the feature/cha-5933-scheduled-messages branch October 19, 2026 13:15
nijeesh-stream added a commit that referenced this pull request Oct 21, 2026
Event.messageId from #278 was a typed Jackson field, so a top-level
message_id on message.*, reminder.* and poll.* webhooks moved out of
getAdditionalFields() and getAdditionalFields().get("message_id")
returned null after upgrading.

getMessageId()/setMessageId() now read and write the additional-fields
map instead, so parsing and serialization match 1.50.0 and both
accessors return the ID on every event.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants