Working Hours
Audience: Sales · Support · QA · Engineering · Management · Where in app: Settings → Working Hours (organization) and per-user availability · Plan availability: All plans
Working hours decide when the widget is online — humans available to take chats and calls — versus offline, where chat falls back to an email-capture flow and calls fall back to a missed-call notification or the AI agent. The launcher's heading/subheading copy and which channels actually connect all depend on this online/offline state.
What it does
- Computes whether the team is "online" right now, per the configured schedule and timezone.
- Swaps the launcher copy between online and offline text.
- Routes chat/calls differently when offline (email fallback, callback, or AI).
- Drives missed-call notifications when a visitor tries to call outside hours.
How it works
Where hours live
| Level | Where | Controls |
|---|---|---|
| Organization | companies.working_hours[] + companies.timezone | Default availability for the whole company |
| User (team member) | users.working_hours + users.timezone | Personal availability — an agent appears online only when both org and personal hours match |
Agency-managed accounts may have the agency override defaults; the working-hours editor shows whose hours are active.
Schema
companies.working_hours = [
{ "day": "Monday", "start_time": "09:00", "end_time": "17:00" },
{ "day": "Tuesday", "start_time": "09:00", "end_time": "17:00" }
]
companies.timezone = "Pacific/Auckland"
companies.timezone_offset = 720 // minutes — denormalized convenience field
day is the full weekday name ("Monday", "Tuesday", …). A day with no entry means unavailable that day.
How online status is computed
The widget computes isOnline client-side (checkIsWithinWorkingHours):
- Compute the current date/time in
companies.timezone(defaultUTCif unset). - Find today's row in
working_hours[]by full weekday name. - If
start_time ≤ now ≤ end_time, online; otherwise offline.
Important defaults from the code:
- No working hours configured at all → always online. An empty schedule does not mean "closed"; it means "no restriction."
- No row for today's weekday → offline for that day.
- On any error computing hours → defaults to online (fail-open).
The backend re-runs equivalent logic server-side for any decision that matters (routing, AI takeover, etc.), so the client value is a display/UX hint, not the security boundary.
Configuration & options
| Setting | Field | Notes |
|---|---|---|
| Org schedule | companies.working_hours[] | Array of {day, start_time, end_time} |
| Org timezone | companies.timezone | IANA name (e.g. Pacific/Auckland) — source of truth |
| Offset (denormalized) | companies.timezone_offset | Minutes; convenience only |
| User schedule | users.working_hours | Per-agent |
| User timezone | users.timezone | Per-agent |
| Online copy | online_title_text, online_title_text_sub_heading | Launcher when online |
| Offline copy | offline_title_text, offline_title_text_sub_heading | Launcher when offline |
| Offline chat | chatbot_offline_message, chatbot_offline_questions[] | Email + question capture |
| Inbound AI call | ai_inbound_call.enable | Route offline calls to AI |
| Missed-call email | notifications.missed_call | Triggers when a visitor calls outside hours |
Online mode behavior
- Launcher shows
online_title_text+ subheading. - Chat connects directly to a human if chat routing is set to human (otherwise per AI config).
- Calls ring active agents.
- AI auto-trigger and call auto-trigger fire per their URL rules.
Offline mode behavior
- Launcher shows
offline_title_text+ subheading. chatbot_offline_messageandchatbot_offline_questions[]capture an email + question.- Call buttons can fall back to a callback request, or route to inbound AI if
ai_inbound_call.enable. notifications.missed_callemail fires when a visitor tries to call outside hours.
Behaviors & edge cases
- Empty schedule = always online (fail-open). This surprises admins who clear all rows expecting the widget to go offline — it does the opposite.
- Error fallback = online. A malformed timezone or schedule defaults to online rather than hiding availability.
- DST is handled correctly via the IANA timezone name;
timezone_offsetis not the source of truth and can drift across DST. - No holiday calendar. There is no built-in holiday feature. Admins either clear
working_hoursrows for the affected day(s), or use ahide_on_pagesrule during a closure window. - Visitor timezone is also detected (for friendly "we're online" messaging), but online/offline is computed against the company timezone, not the visitor's.
- Offline still installed. "Offline" is a normal state, not a broken install — the widget is present and capturing leads.
Plan & limits
- Working hours are available on all plans.
- Per-user (team-member) availability requires the company to have team members; effective availability is the intersection of org and personal hours.
- AI inbound-call fallback requires the AI calling feature to be enabled (verify per-tier gating).
Technical implementation
- Online computation:
checkIsWithinWorkingHours()inwidget/src/stores/widget.ts; consumed infetchCompany()to setisOnline. - Timezone helpers:
widget/src/utils/timezone.ts. - Types:
widget/src/types/company.ts—WorkingHour,companies.timezone. - Admin UI: organization working-hours editor under Settings → Working Hours (frontend settings).
- Nox: can answer "is the team online right now?", "what hours is the team scheduled?", and "when was someone last on-call?" using
working_hoursandusers.last_active_at. Nox cannot edit hours — an admin must do that in Settings. - See the engineering service page: /services/widget.