Skip to content

Commit bc2850e

Browse files
committed
fix: automation schedules run in the rule's resolved timezone, not UTC
The automation create/update help text and the automation skill card claimed schedules are always stored and evaluated in UTC, and told callers to convert local wall-clock times to UTC before passing them to --at or --cron-expr. The API resolves a rule's timezone from the caller's member timezone, then the account timezone, falling back to UTC only when neither is set. Since the curated create/update commands never send a timezone, following the old advice and pre-converting to UTC shifts the schedule by the account's UTC offset instead of producing the intended wall-clock time. Rewrite the help text and skill card prose to say: pass the local time directly, and use the safari automation-rule-create --timezone flag to pin a specific timezone when the caller's default isn't wanted (create only; update cannot change a rule's timezone).
1 parent 39bfc14 commit bc2850e

3 files changed

Lines changed: 28 additions & 24 deletions

File tree

internal/cli/automation.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
const automationHTTPPostOnlyCron = "0 0 * * *"
16-
const automationUTCNote = "Convert local wall-clock requests to UTC before passing --at or --cron-expr."
16+
const automationTimezoneNote = "The rule's timezone defaults to the caller's member timezone, then the account timezone; pass the user's local wall-clock time here, do not convert it to UTC."
1717

1818
func newAutomationCmd() *cobra.Command {
1919
cmd := newGroupCmd("automation", "Manage AI SRE Automations")
@@ -60,9 +60,11 @@ By default the rule is enabled. Use --disabled only when the user explicitly
6060
asks to create it disabled. team_id=0 means personal scope; --team-id >0 creates
6161
the rule under that team. The scope is immutable after creation.
6262
63-
Schedule helpers build a 5-field UTC cron expression. --at and --cron-expr are
64-
interpreted in UTC, not the caller's local timezone. Convert local wall-clock
65-
requests to UTC before passing --at or --cron-expr.
63+
Schedule helpers build a 5-field cron expression evaluated in the rule's
64+
timezone, which defaults to the caller's member timezone, then the account
65+
timezone. Pass the user's local wall-clock time to --at or --cron-expr;
66+
do not convert it to UTC first. This command has no --timezone flag; use
67+
safari automation-rule-create --timezone to pin a different one.
6668
6769
For HTTP POST-only rules, pass --http-post-trigger without a schedule; the CLI
6870
sends a valid placeholder cron and disables the schedule trigger.`, "Automations", "RuleWriteCreate"),
@@ -111,10 +113,10 @@ the rule under that team. The scope is immutable after creation.
111113

112114
cmd.Flags().StringVar(&name, "name", "", "Automation name")
113115
cmd.Flags().Int64Var(&teamID, "team-id", 0, "Scope team ID; 0 means personal scope")
114-
cmd.Flags().StringVar(&schedule, "schedule", "", "UTC schedule helper: hourly, daily, weekly, or cron")
115-
cmd.Flags().StringVar(&at, "at", "", "UTC time in HH:MM; for hourly schedules, only the minute is used. "+automationUTCNote)
116+
cmd.Flags().StringVar(&schedule, "schedule", "", "Schedule helper: hourly, daily, weekly, or cron")
117+
cmd.Flags().StringVar(&at, "at", "", "Local time in HH:MM (rule's timezone); for hourly schedules, only the minute is used. "+automationTimezoneNote)
116118
cmd.Flags().StringVar(&weekday, "weekday", "", "Weekday for weekly schedules: sun, mon, tue, wed, thu, fri, sat, or 0-7")
117-
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Exact 5-field UTC cron expression; overrides --schedule helpers. "+automationUTCNote)
119+
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Exact 5-field cron expression in the rule's timezone; overrides --schedule helpers. "+automationTimezoneNote)
118120
cmd.Flags().BoolVar(&disabled, "disabled", false, "Create the Automation disabled")
119121
cmd.Flags().BoolVar(&scheduleEnabled, "schedule-enabled", true, "Whether the schedule trigger is enabled")
120122
cmd.Flags().BoolVar(&httpPostTrigger, "http-post-trigger", false, "Create and enable an HTTP POST trigger")
@@ -221,9 +223,11 @@ func newAutomationUpdateCmd() *cobra.Command {
221223
The personal/team scope is intentionally not exposed here. Scope is immutable
222224
after creation; create a new Automation if the target person/team scope needs to change.
223225
224-
Schedule helpers build a 5-field UTC cron expression. --at and --cron-expr are
225-
interpreted in UTC, not the caller's local timezone. Convert local wall-clock
226-
requests to UTC before passing --at or --cron-expr.`, "Automations", "RuleWriteUpdate"),
226+
Schedule helpers build a 5-field cron expression evaluated in the rule's
227+
timezone, set at creation from the caller's member timezone, then the account
228+
timezone. Pass the user's local wall-clock time to --at or --cron-expr;
229+
do not convert it to UTC first. This command has no --timezone flag; the
230+
rule's timezone cannot be changed after creation.`, "Automations", "RuleWriteUpdate"),
227231
Example: ` flashduty automation update auto_123 --name "Daily brief v2" --cron-expr "15 1 * * *"
228232
flashduty automation update auto_123 --disable
229233
flashduty automation update auto_123 --enable-http-post-trigger --rotate-http-post-token`,
@@ -315,10 +319,10 @@ func newAutomationUpdateCmd() *cobra.Command {
315319
}
316320

317321
cmd.Flags().StringVar(&name, "name", "", "New Automation name")
318-
cmd.Flags().StringVar(&schedule, "schedule", "", "UTC schedule helper: hourly, daily, weekly, or cron")
319-
cmd.Flags().StringVar(&at, "at", "", "UTC time in HH:MM; for hourly schedules, only the minute is used. "+automationUTCNote)
322+
cmd.Flags().StringVar(&schedule, "schedule", "", "Schedule helper: hourly, daily, weekly, or cron")
323+
cmd.Flags().StringVar(&at, "at", "", "Local time in HH:MM (rule's timezone); for hourly schedules, only the minute is used. "+automationTimezoneNote)
320324
cmd.Flags().StringVar(&weekday, "weekday", "", "Weekday for weekly schedules: sun, mon, tue, wed, thu, fri, sat, or 0-7")
321-
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Exact 5-field UTC cron expression; overrides --schedule helpers. "+automationUTCNote)
325+
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Exact 5-field cron expression in the rule's timezone; overrides --schedule helpers. "+automationTimezoneNote)
322326
cmd.Flags().BoolVar(&enableRule, "enable", false, "Enable the Automation")
323327
cmd.Flags().BoolVar(&disableRule, "disable", false, "Disable the Automation")
324328
cmd.Flags().BoolVar(&enableSchedule, "enable-schedule", false, "Enable the schedule trigger")

internal/cli/automation_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestAutomationCreateDailyDefaultsEnabled(t *testing.T) {
3434
assertBody(t, stub.lastBody, "prompt", "Summarize yesterday's incidents")
3535
}
3636

37-
func TestAutomationScheduleHelpDocumentsUTC(t *testing.T) {
37+
func TestAutomationScheduleHelpDocumentsTimezone(t *testing.T) {
3838
saveAndResetGlobals(t)
3939

4040
for _, args := range [][]string{
@@ -46,8 +46,8 @@ func TestAutomationScheduleHelpDocumentsUTC(t *testing.T) {
4646
t.Fatalf("%v unexpected error: %v", args, err)
4747
}
4848
for _, want := range []string{
49-
"UTC",
50-
"Convert local wall-clock requests to UTC before passing --at or --cron-expr.",
49+
"do not convert it to UTC",
50+
automationTimezoneNote,
5151
} {
5252
if !strings.Contains(out, want) {
5353
t.Fatalf("%v help missing %q\n%s", args, want, out)

skills/flashduty/reference/automation.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ Prereq: `SKILL.md` read. Automations create AI SRE sessions on a schedule or thr
2929
## Scheduling
3030

3131
- Default create behavior: enabled immediately. Use `--disabled` only if the user asks for a disabled Automation.
32-
- No timezone flag is exposed by the current API. Automation schedules are stored and sent as UTC cron.
33-
- If the user asks for a local wall-clock schedule, first identify the intended timezone from the session context, runner `date`, or the user's wording. Convert that local time to UTC before calling the CLI. If the timezone is unclear, ask before creating or updating the schedule.
34-
- Helper schedules:
35-
- `--schedule hourly --at 00:15` -> minute 15 of every UTC hour.
36-
- `--schedule daily --at 01:30` -> every day at 01:30 UTC.
37-
- `--schedule weekly --weekday mon --at 02:00` -> every Monday at 02:00 UTC.
38-
- For exact minute-level control, use `--cron-expr '<minute> <hour> <day> <month> <weekday>'` in UTC.
39-
- Example: Asia/Shanghai 11:00 is UTC 03:00, so use `--schedule daily --at 03:00` or `--cron-expr "0 3 * * *"`.
32+
- `create`/`update` expose no `--timezone` flag. The cron expression runs in the rule's timezone, which the server resolves from the caller's member timezone, then the account timezone, falling back to UTC only when neither is set.
33+
- Pass the user's local wall-clock time directly to `--at` or `--cron-expr` — do not convert it to UTC first. The rule already runs in the caller's own resolved timezone, so a manual UTC conversion shifts the schedule by the account's UTC offset.
34+
- Helper schedules (times are in the rule's resolved timezone, not UTC):
35+
- `--schedule hourly --at 00:15` -> minute 15 of every hour.
36+
- `--schedule daily --at 01:30` -> every day at 01:30.
37+
- `--schedule weekly --weekday mon --at 02:00` -> every Monday at 02:00.
38+
- For exact minute-level control, use `--cron-expr '<minute> <hour> <day> <month> <weekday>'` in that same local time.
39+
- To pin a rule to a specific timezone (e.g. UTC) regardless of the caller's default, use `safari automation-rule-create --timezone <IANA tz>` instead — the curated `create`/`update` commands cannot set it, and `update` cannot change it after creation.
4040
- HTTP POST-only rule: pass `--http-post-trigger` without schedule flags. The CLI sends a placeholder cron and disables the schedule trigger.
4141

4242
## Hot flow - create from chat

0 commit comments

Comments
 (0)