Skip to content

Show when a routine runs next, and read custom schedules in plain words - #136

Merged
abhishekgahlot2 merged 3 commits into
useagenthq:mainfrom
littlestjames82-sys:routine-next-run-107
Oct 21, 2026
Merged

abhishekgahlot2 merged 3 commits into
useagenthq:mainfrom
littlestjames82-sys:routine-next-run-107

Conversation

@littlestjames82-sys

@littlestjames82-sys littlestjames82-sys commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #107.

Each routine on the Bots page now shows when it runs next, with a day
and a time, in the routine's own timezone, next to a schedule read in
plain words: "Tuesdays and Thursdays at 8:30 AM (America/New_York) ·
last run 1d · next run Tuesday at 8:30 AM". Custom schedules with
weekday lists and ranges now read as words instead of raw cron, in
the routine rows and on the Schedules page. While typing a custom
schedule in the routine form, a live preview under the field shows
the schedule in words and the next run, or says the expression is
invalid.

How it meets the issue, point by point:

  • Next run on each routine: the Routines section appends "next run
    {day} at {time}" for enabled routines, computed in the routine's
    timezone. Paused routines show no next run, since they do not fire
    while off.
  • Live preview in the form: the routine form renders the words and
    the next run as you type, and "That expression is invalid." when
    the expression does not parse.
  • Lists and ranges as words: cadenceLabel in
    frontend/app/agent/schedules/schedules-data.ts now reads 2,4 as
    "Tuesdays and Thursdays" and 1-5 as "Weekdays" (0,6 reads
    "Weekends"); anything it cannot phrase falls back to the raw
    expression, as before.
  • Tests: automation-data.test.ts gains cases for custom schedules
    (a weekday list, a weekday range, a sub-hourly step, and a
    day-of-month plus weekday expression), a timezone case
    (America/New_York, and Pacific/Auckland where the run lands on a
    different local day), and invalid expressions.

How it is built:

  • The next run comes from the scheduler's own rules: the matcher
    from backend/src/schedules/cron.ts is ported into
    schedules-data.ts (same field grammar, same day-of-month and
    day-of-week OR rule), and the search walks the wall clock in the
    routine's timezone with Intl, so DST changes land on the right
    local time. The backend is untouched. The port was checked against
    the backend matcher directly: a differential run over 13 schedules
    and instants, including the 2026 spring-forward and fall-back
    transitions, agreed on every case.
  • cadenceLabel keeps its existing phrases for the schedules it
    already knew, so nothing that read as words before changes.

Checks run:

  • bun run typecheck (root, every package): clean
  • cd frontend && bun run test: 1692 pass, 0 fail (includes the 6 new tests)
  • cd frontend && bun run lint: exit 0; the 4 changed files are clean
  • Route JS (bun run build && bun run perf:routes, this branch vs
    main, both measured locally): /bots and /bots/[id] 662K gzip vs
    660K on main (+2K from the schedule matcher this change adds);
    /agent/schedules unchanged at 183K. Main already measures at the
    660 KB ceiling noted in frontend/AGENTS.md, so the number is here
    in the open rather than trimmed around.

Proof from a real run: screenshots below are the real Routines
section and routine form running in a browser (local dev build with
the routines endpoints served fixture data in the endpoint's shape,
since a seeded hosted account was not available). Before, the custom
schedule shows as raw cron and there is no next run; after, the rows
read in words with the next run, the form previews the issue's own
example (30 8 * * 2,4) as it is typed, and an invalid expression
(0 9 * * 9) gets the invalid message.

Before:
Routine rows before: raw cron, no next run

After: the rows, the form preview, and the invalid message:
Routine rows after: plain words and next run
Routine form previewing a custom schedule as it is typed
Invalid expression message in the routine form

Disclosure, plainly: I build agent tooling at Ghost Developer Studio
(Ghost Hands, a governed browser-automation tool, and Ghost Kernel, an
event-recording kernel) and used them while working on this. Ghost
Hands drove the browser for the screenshots above under a standard
policy, with every action recorded in a trail; Ghost Kernel recorded
the plan and verification for the change (its pre-change run recorded
the new tests as failing, its post-change run verified them). The
code here is mine to explain, and I ran every check listed above
myself.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@abhishekgahlot2 abhishekgahlot2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this, it's a really useful change and the plain-words schedule reads well. Two things need fixing before we can merge, because the preview can disagree with when the routine actually runs:

  1. Daylight saving fall-back picks the wrong next run. In schedules-data.ts (around 396-408) the search walks wall-clock times and assumes a one-hour shift. Cases that come out wrong compared with the backend's cronMatches:
    • 30 1 * * * in New York from 2026-11-01 05:45 UTC: expected 06:30 UTC the same day, preview says Nov 2.
    • 0,45 2 * * * in Berlin from 2026-10-25 00:30 UTC: expected 00:45, preview says 01:00.
    • 45 1 * * * in Lord Howe (30-minute shift) from 2026-04-04 14:00 UTC: expected 14:45, preview says 15:15.
      Checking both occurrences of a repeated local time and taking the earliest matching UTC instant should fix all three. Please add these as tests.
  2. A routine with no timezone uses the server's zone, not the browser's. The preview (around 313-320) uses the viewer's zone, so for a UTC server and a viewer in Kolkata, 0 9 * * * shows Wednesday 9 AM while the scheduler fires Tuesday 09:00 UTC. Either show it in the server's zone (and say so) or ask the backend for the next run.

Smaller things, not blocking: link the live preview to the input with aria-describedby and a polite live region; validate the cron before turning it into words on the list rows (61 9 * * 2,4 currently reads as "9:61 AM"); and every-minute schedules recompute on each render, so caching it would help with many routines.

Happy to re-review as soon as you push. Thanks again!

@abhishekgahlot2

Copy link
Copy Markdown
Contributor

recheck

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@littlestjames82-sys

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Oct 17, 2026
@littlestjames82-sys

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review, and for spelling out the failing cases. Pushed a fix round:

Fall-back next run: the conversion now resolves every UTC instant a wall time maps to (both occurrences of a repeated time, none for a skipped one) by sampling the offsets in effect around it, instead of landing on a single occurrence with a one-hour correction. The day scan also takes the earliest instant of the day rather than the first in wall order, which is what the Berlin case needed. Added tests for all three of your cases (New York, Berlin, and Lord Howe), plus the first-occurrence side of the New York case and a spring-forward gap case. I also checked the results against a minute-by-minute walk of the backend cronMatches and they agree.

No-timezone routines: I looked for the server zone in what the API already returns and it is not exposed anywhere, and there is no next-run endpoint to ask. So rather than inventing API surface in this PR, I took the honest half of your first option that the frontend can do today: the preview no longer substitutes the viewer's zone. A routine with no timezone (or an unknown one, which the backend also treats as server local) now shows no next run at all, while the cadence words and the Server timezone label still show. If you would rather expose the server zone from the API, I am happy to wire the preview to it as a follow-up and compute the exact day and time.

Smaller items: the live preview in the routine form is now tied to the cron input with aria-describedby and is a polite live region. cadenceLabel validates the expression before turning it into words, so list rows fall back to the raw expression instead of reading 9:61 AM. Next-run results are cached per cron, zone, and start minute, so every-minute schedules no longer rescan on each render.

Frontend suite is green: 1696 tests passing (1692 before, plus the 4 new ones), typecheck clean. Happy to adjust any of this.

@abhishekgahlot2

Copy link
Copy Markdown
Contributor

Thanks for digging into this, you're right. The server never says when a routine runs next, or which timezone a routine without one actually uses. We're fixing that server-side now: each routine will get a nextRunAt and a real timezone. Once that lands you can switch the preview to it and drop the client-side cron math. I'll edit this comment with the link.

@littlestjames82-sys

Copy link
Copy Markdown
Contributor Author

Thanks, that sounds right. The client-side math was only ever filling a gap in what the API returns, so I would rather read nextRunAt off the routine and drop it. Ping me here when the server change lands, or link it in that edited comment, and I will switch this preview over to it and re-run the frontend suite. The no-timezone case in this PR already shows no guessed next run, so nothing here will disagree with the scheduler in the meantime.

@abhishekgahlot2

Copy link
Copy Markdown
Contributor

The server side is in: #363 and #365 add next_due_at (UTC) on every automation and nextDueAt on bot routines in the API, a timezone that's always set, and a preview endpoint for the form (GET /api/automations/preview?cron=...&timezone=... returns next_runs). Could you switch the preview to those and drop the client-side cron math? Then we'll get this merged. Thanks again for flagging it.

@littlestjames82-sys

Copy link
Copy Markdown
Contributor Author

Server side received, thank you. Switched the preview over to it:

Routine rows: the next run now reads off the routine itself, the new nextDueAt from #363, rendered in the routine's timezone. ApiRoutine carries nextDueAt, and its timezone is now the always-set string the API returns.

Routine form: the live preview asks GET /api/automations/preview from #365, debounced while typing and aborted when the field changes, and shows the first of next_runs in the zone the routine will run in. A create that names no zone previews in UTC, matching the server default. If the call fails or names no run, the preview shows the schedule in words only, with no guessed time.

Dropped: the client-side next-run engine is gone from schedules-data.ts, the cron matcher port, the zoned occurrence scan, and its cache. What stays on the client is cadenceLabel's plain words and a validation check, so an invalid expression still reads as invalid instead of as words. The daylight-saving cases from your review now belong to the backend engine and its tests; the frontend tests for them are replaced with rendering tests for the instants the server returns, plus client tests for the preview call (URL, zone, and the 400 path).

Checks: frontend suite 1693 pass, 0 fail; frontend typecheck clean; biome lint clean on the changed files. Route JS for /bots and /bots/[id] measures 662K gzip locally, unchanged from the current head of this PR (the +2K over main was already noted in the PR body).

One scope note: the Schedules page never computed a next run on the client (its rows show cadence, zone, and last run), so there was nothing to switch there.

@littlestjames82-sys

Copy link
Copy Markdown
Contributor Author

Hi @abhishekgahlot2, just checking in on this one. I switched the preview over to the server side on Oct 18 as you asked, using nextDueAt from #363 and the preview endpoint from #365, and dropped the client side cron math. Head is 6d4ce80.

Happy to make any further changes if you spot something on re-review. Thanks again.

@abhishekgahlot2 abhishekgahlot2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rework, this looks good now.

@abhishekgahlot2
abhishekgahlot2 merged commit 2500626 into useagenthq:main Oct 21, 2026
21 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bots: show when a routine runs next, and read custom schedules in plain words

2 participants