Skip to content

feat: add scheduled messages API - #1951

Open
NikolaGoranNaumovski wants to merge 4 commits into
masterfrom
feat/cha-5934-scheduled-messages-api
Open

NikolaGoranNaumovski wants to merge 4 commits into
masterfrom
feat/cha-5934-scheduled-messages-api

Conversation

@NikolaGoranNaumovski

@NikolaGoranNaumovski NikolaGoranNaumovski commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

CLA

  • I have signed the Stream CLA (required).
  • Code changes are tested

Description of the changes, What, Why and How?

Note

The backend (CHA-5924) shipped in chat v239.72.0, behind the per-app flag chat_scheduled_messages_enabled (off by default). The state layer that keeps scheduled messages in memory (client.scheduledMessages) follows in #1915, stacked on this PR.

TLDR

  • Problem: The backend supports scheduled messages: send a message later, then edit, cancel or send it now. The JS SDK has no types, methods or events for them, so neither the React nor the React Native SDK can build on it.
  • What/how: This PR adds the API contract only: types and const objects, scheduled_at on sendMessage, four client methods for the /scheduled_messages endpoints, the five scheduled_message.* events, error codes 114–124, and three helpers for UIs (error reason, not-found check, scheduling limits). No state is kept; that is feat: support scheduled messages State Layer #1915.
  • Risks: Low. Everything is additive. One behaviour change: channel.sendMessage with scheduled_at skips the offline queue, because replaying a queued scheduled send would add it to the channel as if it had been sent. Sends without scheduled_at are unchanged.

Ticket

Sending

  • SendMessageOptions.scheduled_at?: string | Date schedules the message. The response then carries scheduled_message, and message is not added to the channel: it arrives later through message.new.
  • A scheduled_at that is past or now is sent immediately as a normal message: the response has message and no scheduled_message.
  • A send with scheduled_at goes straight to the API, never through the offline queue.

Client methods

Method Endpoint Returns
queryScheduledMessages({ filter?, sort?, limit?, next?, user_id? }) POST /scheduled_messages/query { scheduled_messages, next? }
updateScheduledMessage(id, { message?, scheduled_at? }) PATCH /scheduled_messages/{id} { scheduled_message }
deleteScheduledMessage(id) DELETE /scheduled_messages/{id} { scheduled_message }
sendScheduledMessageNow(id) POST /scheduled_messages/{id}/send_now { scheduled_message }

filter takes cid and status (scheduled, sending, failed); sort is { scheduled_at: 1 }.

Channel queries

  • ChannelQueryOptions.include_scheduled_messages and ChannelOptions.include_scheduled_messages (client-side only) ask the server for the user's scheduled messages per channel.
  • ChannelAPIResponse.scheduled_messages is typed. Storing it is part of feat: support scheduled messages State Layer #1915.

Events

scheduled_message.created, .updated, .deleted, .sent and .failed are added to EVENT_MAP, and Event.scheduled_message is typed. All five are sent to the scheduling user only.

Errors

Error codes 114–124 are added to APIErrorCodes. None is retryable: a conflict (122) should be reloaded, not resent.

Code exception_fields.reason
114 scheduled_messages_disabled
115 scheduled_at_too_soon
116 scheduled_at_too_far
117 scheduled_limit_channel
118 scheduled_limit_user
119 scheduled_message_id_taken
120 scheduled_message_not_editable
121 scheduled_message_sending
122 scheduled_message_conflict
123 scheduled_message_already_sent
124 scheduled_message_canceled

getScheduledMessageErrorReason(error) returns the reason of a failed request: it reads exception_fields.reason (now typed on APIErrorResponse) and falls back to the error code. It returns undefined for any other error.

isScheduledMessageNotFoundError(error) is true for a 404. Update, delete and send-now on a scheduled message that was already sent or deleted return a plain 404 (code 16) with no reason (codes 123 and 124 did not appear in a live run), so UIs and the state layer use this helper to recognise it, for example to show "Already sent" and drop the item.

Limits

getScheduledMessageLimits(channel) reads the four scheduled_messages_* channel config fields. Any field the config does not have falls back to DEFAULT_SCHEDULED_MESSAGE_LIMITS:

Limit Default
minLeadTimeSeconds 60
maxLeadTimeSeconds 120 days
maxPerChannel 10
maxPerUser 100

Types and constants

  • Const objects with a same-named type: ScheduledMessageStatus, ScheduledMessageFailureReason, ScheduledMessageErrorReason, ScheduledMessageEventType.
  • Response and request types: ScheduledMessageResponse, ScheduledMessageContent, ScheduledMessageChannelResponse, ScheduledMessageLocation, ScheduledMessageAPIResponse, QueryScheduledMessagesOptions, QueryScheduledMessagesResponse, UpdateScheduledMessageOptions, ScheduledMessageFilters, ScheduledMessageSort, ActiveScheduledMessageStatus, ScheduledMessagesChannelConfig.
  • Helper type: ScheduledMessageLimits.

Backend contract notes

  • A future scheduled_at is rounded up to the next whole minute; show the response's scheduled_at, not the picked time.
  • Retries are keyed on message.id: sending the same id again returns the stored row. Scheduling again needs a new message.id.
  • scheduled_message.message.mentioned_users holds user IDs; the SendMessage response's message.mentioned_users holds user objects.
  • pending: true cannot be combined with scheduled_at.

Tests

  • Each client method: endpoint, method, payload, id encoding, query sort normalization, no options.
  • Error codes, and the reason helper for every code 114–124, reason over code, unknown reason, unrelated errors.
  • isScheduledMessageNotFoundError: true for a 404, false for other API errors and plain errors.
  • Limits: no config, full config, partial config.
  • include_scheduled_messages sent by queryChannels and channel.query, and not sent unless asked.
  • sendMessage with scheduled_at: payload and response, nothing added to the channel, offline queue skipped; without scheduled_at the offline queue is still used (checked with MockOfflineDB).

Changelog

  • Add scheduled messages: scheduled_at on channel.sendMessage, client.queryScheduledMessages / updateScheduledMessage / deleteScheduledMessage / sendScheduledMessageNow, include_scheduled_messages on channel queries, the scheduled_message.* events, and error codes 114–124.
  • Add getScheduledMessageErrorReason, isScheduledMessageNotFoundError and getScheduledMessageLimits.

Add the scheduled message types and const objects
(ScheduledMessageStatus, ScheduledMessageFailureReason,
ScheduledMessageErrorReason, ScheduledMessageEventType), the
scheduled_at and include_scheduled_messages options, the
scheduled_messages_* channel config fields and
APIErrorResponse.exception_fields.

Add the queryScheduledMessages, updateScheduledMessage,
deleteScheduledMessage and sendScheduledMessageNow client methods, the
scheduled_message.* events, and error codes 114-124. Only 122 is
not marked retryable either: a conflict should be reloaded, not resent.

Add getScheduledMessageErrorReason(), which reads
exception_fields.reason and falls back to the error code, and
getScheduledMessageLimits() with the backend defaults.

channel.sendMessage with scheduled_at skips the offline queue, so a
replayed scheduled send can't land in the channel as a sent message.
Update, delete and send-now on a scheduled message that was already sent
or deleted return a plain 404 (code 16) rather than an error reason, so
getScheduledMessageErrorReason cannot classify it. Expose a helper that
detects it, so the state layer can drop the stale item and UIs can say
"Already sent".
ScheduledMessageAPIError is only used by getScheduledMessageErrorReason, so
it no longer needs to be part of the public types.
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.

1 participant