diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx
index 843778837e..647b021a43 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx
@@ -27,7 +27,7 @@ import { ANONYMOUS_USER_ID } from '@/lib/auth/constants'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { isHosted } from '@/lib/core/config/feature-flags'
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
-import { getBrowserTimezone, getSupportedTimezones } from '@/lib/core/utils/timezone'
+import { getBrowserTimezone, getTimezoneOptions } from '@/lib/core/utils/timezone'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload'
@@ -42,11 +42,8 @@ import { clearUserData } from '@/stores'
const logger = createLogger('General')
-/** IANA zones for the timezone picker; labels drop underscores so search reads naturally. */
-const TIMEZONE_OPTIONS = getSupportedTimezones().map((tz) => ({
- label: tz.replace(/_/g, ' '),
- value: tz,
-}))
+/** Human-friendly timezone options for the picker, common zones first. */
+const TIMEZONE_OPTIONS = getTimezoneOptions()
/**
* Extracts initials from a user's name.
@@ -419,28 +416,19 @@ export function General() {
-
-
-
-
-
-
-
- The timezone scheduled tasks run in. Defaults to this device's zone.
-
-
+
+
+
-
diff --git a/apps/sim/lib/core/utils/timezone.test.ts b/apps/sim/lib/core/utils/timezone.test.ts
index f0966144f3..935a9061ce 100644
--- a/apps/sim/lib/core/utils/timezone.test.ts
+++ b/apps/sim/lib/core/utils/timezone.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
getSupportedTimezones,
+ getTimezoneOptions,
wallClockNow,
zonedClockDate,
zonedWallClockToUtc,
@@ -68,6 +69,37 @@ describe('getSupportedTimezones', () => {
})
})
+describe('getTimezoneOptions', () => {
+ it('renders every zone as "City (GMT±HH:MM)"', () => {
+ const options = getTimezoneOptions()
+ expect(options.length).toBeGreaterThan(0)
+ for (const option of options) {
+ expect(option.label).toMatch(/^.+ \(GMT[+-]\d{2}:\d{2}\)$/)
+ }
+ })
+
+ it('orders zones alphabetically by city', () => {
+ const cities = getTimezoneOptions().map((option) =>
+ option.label.replace(/ \(GMT[+-]\d{2}:\d{2}\)$/, '')
+ )
+ expect(cities).toEqual([...cities].sort((a, b) => a.localeCompare(b)))
+ })
+
+ it('uses a live DST-aware offset and a friendly city', () => {
+ const options = getTimezoneOptions()
+ expect(options.find((o) => o.value === 'UTC')?.label).toBe('UTC (GMT+00:00)')
+ // India has no DST, so this offset is stable regardless of when the test runs.
+ expect(
+ options.find((o) => o.value === 'Asia/Kolkata' || o.value === 'Asia/Calcutta')?.label
+ ).toMatch(/^(Kolkata|Calcutta) \(GMT\+05:30\)$/)
+ })
+
+ it('has no duplicate values', () => {
+ const values = getTimezoneOptions().map((o) => o.value)
+ expect(new Set(values).size).toBe(values.length)
+ })
+})
+
describe('zonedClockDate', () => {
const instant = new Date('2026-06-15T13:00:00.000Z')
diff --git a/apps/sim/lib/core/utils/timezone.ts b/apps/sim/lib/core/utils/timezone.ts
index a0869af08f..5118061bb4 100644
--- a/apps/sim/lib/core/utils/timezone.ts
+++ b/apps/sim/lib/core/utils/timezone.ts
@@ -38,6 +38,50 @@ export function getSupportedTimezones(): string[] {
return zones.includes('UTC') ? zones : ['UTC', ...zones]
}
+/** A timezone choice for a picker: the canonical IANA value plus a display label. */
+export interface TimezoneOption {
+ value: string
+ label: string
+}
+
+/** The city/locale portion of an IANA id, formatted for display (e.g. `Los Angeles`). */
+function timezoneCity(timeZone: string): string {
+ return (timeZone.split('/').pop() ?? timeZone).replace(/_/g, ' ')
+}
+
+/** `GMT±HH:MM` for an offset expressed in minutes east of UTC (e.g. `GMT-08:00`). */
+function formatGmtOffset(offsetMinutes: number): string {
+ const sign = offsetMinutes >= 0 ? '+' : '-'
+ const absMinutes = Math.abs(offsetMinutes)
+ const hours = String(Math.floor(absMinutes / 60)).padStart(2, '0')
+ const minutes = String(absMinutes % 60).padStart(2, '0')
+ return `GMT${sign}${hours}:${minutes}`
+}
+
+/**
+ * Timezone options for a picker. Each zone reads as `City (GMT±HH:MM)` — city
+ * first, offset for reference — and the list is sorted alphabetically by city,
+ * the order usability research (NN/g, Smart Interface Design Patterns) found
+ * users expect; offset-sorting confuses people who don't know their offset. The
+ * offset is computed live, so it tracks DST automatically. Pair this with the
+ * picker's search and a browser-detected default. Values stay canonical IANA
+ * ids — what we persist.
+ */
+export function getTimezoneOptions(): TimezoneOption[] {
+ const now = new Date()
+ return getSupportedTimezones()
+ .map((value) => ({
+ value,
+ city: timezoneCity(value),
+ offsetMinutes: Math.round(timezoneOffsetMs(now, value) / 60_000),
+ }))
+ .sort((a, b) => a.city.localeCompare(b.city))
+ .map(({ value, city, offsetMinutes }) => ({
+ value,
+ label: `${city} (${formatGmtOffset(offsetMinutes)})`,
+ }))
+}
+
/**
* An instant's wall-clock time in `timeZone` as a naive `yyyy-MM-ddTHH:mm`
* string. Lets callers reason about a user's local date/time without UTC — e.g.