Skip to content

Commit 34d0dbe

Browse files
fix: timezone handling scheduler workflows
1 parent b8447a3 commit 34d0dbe

File tree

9 files changed

+100
-2109
lines changed

9 files changed

+100
-2109
lines changed

api/app/clients/tools/structured/Workflow.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,49 @@ class WorkflowTool extends Tool {
477477
);
478478
}
479479

480+
// Get user's timezone for schedule conversion
481+
let userTimezone = 'UTC'; // Default to UTC
482+
try {
483+
const { User } = require('~/db/models');
484+
const user = await User.findById(userId).select('timezone').lean();
485+
if (user && user.timezone) {
486+
userTimezone = user.timezone;
487+
logger.debug(`[WorkflowTool] Using user timezone: ${userTimezone} for user ${userId}`);
488+
}
489+
} catch (error) {
490+
logger.warn(`[WorkflowTool] Could not get user timezone for ${userId}, using UTC:`, error);
491+
}
492+
493+
// Convert schedule from user timezone to UTC if needed
494+
if (triggerType === 'schedule' && scheduleConfig && userTimezone !== 'UTC') {
495+
try {
496+
const { convertTimeToUTC } = require('~/server/services/Scheduler/utils/cronUtils');
497+
498+
// Parse the cron expression (assumes format: minute hour day month weekday)
499+
const parts = scheduleConfig.trim().split(/\s+/);
500+
if (parts.length === 5) {
501+
const [minute, hour, day, month, weekday] = parts;
502+
503+
// Only convert if hour and minute are specific values (not wildcards)
504+
if (hour !== '*' && minute !== '*' && !hour.includes('/') && !minute.includes('/')) {
505+
const hourNum = parseInt(hour);
506+
const minuteNum = parseInt(minute);
507+
508+
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
509+
// Convert to UTC using the same function as frontend
510+
const { hour: utcHour, minute: utcMinute } = convertTimeToUTC(hourNum, minuteNum, userTimezone);
511+
scheduleConfig = `${utcMinute} ${utcHour} ${day} ${month} ${weekday}`;
512+
513+
logger.info(`[WorkflowTool] Converted schedule from ${userTimezone} to UTC: ${hour}:${minute} -> ${utcHour}:${utcMinute}`);
514+
}
515+
}
516+
}
517+
} catch (error) {
518+
logger.error(`[WorkflowTool] Error converting schedule to UTC:`, error);
519+
// Continue with original schedule if conversion fails
520+
}
521+
}
522+
480523
// Track app connection status for messaging
481524
let isAppConnected = true;
482525

api/server/services/Scheduler/utils/cronUtils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ function getTimeUntilExecution(nextRun) {
274274

275275
module.exports = {
276276
calculateNextRun,
277+
convertTimeToUTC,
277278
validateCronExpression,
278279
getOverdueTime,
279280
getTimeUntilExecution,

client/src/components/SidePanel/WorkflowBuilder/Trigger/ScheduleTrigger.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import type { ScheduleType } from '../types';
3+
import { getTimezoneAbbreviation } from '~/utils/timezone';
34

45
interface ScheduleTriggerProps {
56
scheduleType: ScheduleType;
@@ -13,6 +14,7 @@ interface ScheduleTriggerProps {
1314
scheduleConfig: string;
1415
setScheduleConfig: (config: string) => void;
1516
isTesting: boolean;
17+
userTimezone?: string;
1618
}
1719

1820
const ScheduleTrigger: React.FC<ScheduleTriggerProps> = ({
@@ -27,7 +29,9 @@ const ScheduleTrigger: React.FC<ScheduleTriggerProps> = ({
2729
scheduleConfig,
2830
setScheduleConfig,
2931
isTesting,
32+
userTimezone,
3033
}) => {
34+
const timezoneAbbr = userTimezone ? getTimezoneAbbreviation(userTimezone) : '';
3135
return (
3236
<div className="space-y-3">
3337
{/* Schedule Type Selection */}
@@ -58,7 +62,7 @@ const ScheduleTrigger: React.FC<ScheduleTriggerProps> = ({
5862
{scheduleType !== 'custom' && (
5963
<div>
6064
<label className="mb-2 block text-sm font-medium text-text-primary">
61-
What time?
65+
What time? {timezoneAbbr && <span className="text-xs text-text-secondary">({timezoneAbbr})</span>}
6266
</label>
6367
<input
6468
type="time"
@@ -69,6 +73,11 @@ const ScheduleTrigger: React.FC<ScheduleTriggerProps> = ({
6973
isTesting ? 'cursor-not-allowed opacity-50' : ''
7074
}`}
7175
/>
76+
{userTimezone && (
77+
<p className="mt-1 text-xs text-text-secondary">
78+
This workflow will run at {scheduleTime} in your timezone ({userTimezone})
79+
</p>
80+
)}
7281
</div>
7382
)}
7483

@@ -141,7 +150,7 @@ const ScheduleTrigger: React.FC<ScheduleTriggerProps> = ({
141150
{scheduleType === 'custom' && (
142151
<div>
143152
<label className="mb-2 block text-sm font-medium text-text-primary">
144-
Cron expression
153+
Cron expression (UTC)
145154
</label>
146155
<input
147156
type="text"

client/src/components/SidePanel/WorkflowBuilder/Trigger/TriggerPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface TriggerPanelProps {
4040
isLoadingTriggers: boolean;
4141
filteredAppTriggers: AppTriggerType[];
4242
isIntegrationConnected: (appSlug: string) => boolean;
43+
userTimezone?: string;
4344
}
4445

4546
const TriggerPanel: React.FC<TriggerPanelProps> = ({
@@ -73,6 +74,7 @@ const TriggerPanel: React.FC<TriggerPanelProps> = ({
7374
isLoadingTriggers,
7475
filteredAppTriggers,
7576
isIntegrationConnected,
77+
userTimezone,
7678
}) => {
7779
return (
7880
<div className="space-y-3">
@@ -112,6 +114,7 @@ const TriggerPanel: React.FC<TriggerPanelProps> = ({
112114
scheduleConfig={scheduleConfig}
113115
setScheduleConfig={setScheduleConfig}
114116
isTesting={isTesting}
117+
userTimezone={userTimezone}
115118
/>
116119
)}
117120

0 commit comments

Comments
 (0)