Repository navigation
feat: add scheduled messages API - #1951
Open
NikolaGoranNaumovski wants to merge 4 commits into
Open
NikolaGoranNaumovski wants to merge 4 commits into
NikolaGoranNaumovski wants to merge 4 commits into
Conversation
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.
NikolaGoranNaumovski
requested review from
MartinCupela,
isekovanic and
oliverlaz
and removed request for
isekovanic
October 28, 2026 08:49
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.
NikolaGoranNaumovski
force-pushed
the
feat/cha-5934-scheduled-messages-api
branch
from
November 3, 2026 09:07
1679755 to
b4a398e
Compare
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CLA
Description of the changes, What, Why and How?
Note
The backend (CHA-5924) shipped in chat
v239.72.0, behind the per-app flagchat_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
scheduled_atonsendMessage, four client methods for the/scheduled_messagesendpoints, the fivescheduled_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.channel.sendMessagewithscheduled_atskips the offline queue, because replaying a queued scheduled send would add it to the channel as if it had been sent. Sends withoutscheduled_atare unchanged.Ticket
Sending
SendMessageOptions.scheduled_at?: string | Dateschedules the message. The response then carriesscheduled_message, andmessageis not added to the channel: it arrives later throughmessage.new.scheduled_atthat is past or now is sent immediately as a normal message: the response hasmessageand noscheduled_message.scheduled_atgoes straight to the API, never through the offline queue.Client methods
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 }filtertakescidandstatus(scheduled,sending,failed);sortis{ scheduled_at: 1 }.Channel queries
ChannelQueryOptions.include_scheduled_messagesandChannelOptions.include_scheduled_messages(client-side only) ask the server for the user's scheduled messages per channel.ChannelAPIResponse.scheduled_messagesis typed. Storing it is part of feat: support scheduled messages State Layer #1915.Events
scheduled_message.created,.updated,.deleted,.sentand.failedare added toEVENT_MAP, andEvent.scheduled_messageis 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.exception_fields.reasonscheduled_messages_disabledscheduled_at_too_soonscheduled_at_too_farscheduled_limit_channelscheduled_limit_userscheduled_message_id_takenscheduled_message_not_editablescheduled_message_sendingscheduled_message_conflictscheduled_message_already_sentscheduled_message_canceledgetScheduledMessageErrorReason(error)returns the reason of a failed request: it readsexception_fields.reason(now typed onAPIErrorResponse) and falls back to the error code. It returnsundefinedfor 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 fourscheduled_messages_*channel config fields. Any field the config does not have falls back toDEFAULT_SCHEDULED_MESSAGE_LIMITS:minLeadTimeSecondsmaxLeadTimeSecondsmaxPerChannelmaxPerUserTypes and constants
ScheduledMessageStatus,ScheduledMessageFailureReason,ScheduledMessageErrorReason,ScheduledMessageEventType.ScheduledMessageResponse,ScheduledMessageContent,ScheduledMessageChannelResponse,ScheduledMessageLocation,ScheduledMessageAPIResponse,QueryScheduledMessagesOptions,QueryScheduledMessagesResponse,UpdateScheduledMessageOptions,ScheduledMessageFilters,ScheduledMessageSort,ActiveScheduledMessageStatus,ScheduledMessagesChannelConfig.ScheduledMessageLimits.Backend contract notes
scheduled_atis rounded up to the next whole minute; show the response'sscheduled_at, not the picked time.message.id: sending the same id again returns the stored row. Scheduling again needs a newmessage.id.scheduled_message.message.mentioned_usersholds user IDs; the SendMessage response'smessage.mentioned_usersholds user objects.pending: truecannot be combined withscheduled_at.Tests
isScheduledMessageNotFoundError: true for a 404, false for other API errors and plain errors.include_scheduled_messagessent byqueryChannelsandchannel.query, and not sent unless asked.sendMessagewithscheduled_at: payload and response, nothing added to the channel, offline queue skipped; withoutscheduled_atthe offline queue is still used (checked withMockOfflineDB).Changelog
scheduled_atonchannel.sendMessage,client.queryScheduledMessages/updateScheduledMessage/deleteScheduledMessage/sendScheduledMessageNow,include_scheduled_messageson channel queries, thescheduled_message.*events, and error codes 114–124.getScheduledMessageErrorReason,isScheduledMessageNotFoundErrorandgetScheduledMessageLimits.