RocketChat/Rocket.Chat · error · Error
error-business-hour-finish-time-before-start-time
error-business-hour-finish-time-before-start-time
Error message
error-business-hour-finish-time-before-start-time
What it means
When saving a business hour, AbstractBusinessHour.convertWorkHours converts each work day's start and finish from the business hour's timezone to UTC and rejects an open day whose finish instant is before its start instant, throwing Error('error-business-hour-finish-time-before-start-time'). This forbids intervals that are invalid or inverted after timezone conversion, which also rules out overnight spans modeled as a single entry.
Source
Thrown at apps/meteor/server/lib/omnichannel/business-hour/AbstractBusinessHour.ts:99
businessHourData.active = Boolean(businessHourData.active);
businessHourData = this.convertWorkHours(businessHourData);
if (businessHourData._id) {
await this.BusinessHourRepository.updateOne({ _id: businessHourData._id }, {
$set: businessHourData,
} as UpdateFilter<ILivechatBusinessHour>); // TODO: Remove this cast when TypeScript is updated
return businessHourData._id;
}
const { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);
return insertedId;
}
private convertWorkHours(businessHourData: ILivechatBusinessHour): ILivechatBusinessHour {
businessHourData.workHours.forEach((hour: any) => {
const startUtc = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
const finishUtc = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
if (hour.open && finishUtc.isBefore(startUtc)) {
throw new Error('error-business-hour-finish-time-before-start-time');
}
if (hour.open && startUtc.isSame(finishUtc)) {
throw new Error('error-business-hour-finish-time-equals-start-time');
}
hour.start = {
time: hour.start,
utc: {
dayOfWeek: startUtc.clone().format('dddd'),
time: startUtc.clone().format('HH:mm'),
},
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'HH:mm'),
},
};
hour.finish = {View on GitHub (pinned to b2c16d5842)
Solutions
- Fix the entry so finish is after start in the business hour's own timezone
- Split overnight coverage into two entries (e.g. open until 23:59, reopen at 00:00) since inverted ranges are rejected
- Double-check the timezone selected on the business hour matches the timezone the times were entered in
- For 24/7 coverage use the 24-hour open business-hour type instead of work-hour entries
Example fix
// before - overnight span in one entry, throws after UTC conversion
workHours: [{ day: 'Monday', open: true, start: '22:00', finish: '02:00' }]
// after - split into two non-inverted entries
workHours: [
{ day: 'Monday', open: true, start: '00:00', finish: '02:00' },
{ day: 'Monday', open: true, start: '22:00', finish: '23:59' },
] Defensive patterns
Strategy: validation
Validate before calling
import moment from 'moment-timezone';
const validateWorkHours = (workHours: any[], tz: string) =>
workHours.every((h) => {
if (!h.open) return true;
const start = moment.tz(`${h.day}:${h.start}`, 'dddd:HH:mm', tz).utc();
const finish = moment.tz(`${h.day}:${h.finish}`, 'dddd:HH:mm', tz).utc();
return finish.isAfter(start);
});
// before saving a business hour:
if (!validateWorkHours(businessHour.workHours, businessHour.timezone.name)) {
// fix intervals or split overnight ranges - the server will reject the save
} Type guard
const isValidWorkHour = (hour: { day: string; open: boolean; start: string; finish: string }, tz: string): boolean => {
if (!hour.open) return true;
const s = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', tz).utc();
const f = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', tz).utc();
return f.isAfter(s);
}; Prevention
- Never model overnight coverage as a single entry with finish before start - split it across two entries
- Always set the business hour's timezone explicitly and enter times in that timezone
- Mirror the server's UTC-conversion check in the admin UI before submit
When it happens
Trigger: Saving a Livechat business hour whose workHours entry has open=true and finish earlier than start once converted to UTC - e.g. start 22:00 finish 02:00 in the business hour's timezone, or a locally-valid range that flips because the configured timezone differs from the one the operator entered times in.
Common situations: Overnight shifts entered as one entry (Mon 22:00 - Tue 06:00); business-hour timezone set to UTC while times were given in local time; DST transitions shifting an interval across midnight.
Related errors
- error-business-hour-finish-time-equals-start-time
- Invalid type
- error-duplicated-sla
- error-business-hour-name-already-in-use
- error-invalid-room
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/1a9291877086f4e7.
Report an issue: GitHub.