Skip to content

fix(api): reject trigger schedules that fire below a frequency floor - #6279

Open
mmabrouk wants to merge 1 commit into
mainfrom
fix/trigger-schedule-frequency-floor
Open

fix(api): reject trigger schedules that fire below a frequency floor#6279
mmabrouk wants to merge 1 commit into
mainfrom
fix/trigger-schedule-frequency-floor

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

The problem

A trigger schedule accepted any cron expression that parsed, * * * * * included. Nothing checked how often it fired.

Every fire starts an agent run in its own Daytona sandbox, so an every-minute schedule bills 1440 runs a day. It is also wrong on its own terms: an agent run routinely takes longer than a minute, so the schedule launches a new run before the previous one finishes and overlaps itself.

This is not hypothetical. A test schedule named "test every minute" was created on staging on 22 August. It ran roughly $12/day and exhausted the shared Daytona organization's credits the next day, which took down agent runs on every environment sharing that account, including a colleague's PR testing on preview.demo.

Here was the entire validation:

def _validate_schedule(expr: str) -> None:
    """Reject anything that is not a valid 5-field cron expression (UTC)."""
    if not isinstance(expr, str) or len(expr.split()) != 5:
        raise TriggerScheduleInvalid(...)
    if not croniter.is_valid(expr):
        raise TriggerScheduleInvalid(...)

The change

A minimum interval between two consecutive fires, defaulting to 15 minutes and overridable per deployment via AGENTA_TRIGGERS_SCHEDULE_MIN_INTERVAL_MINUTES.

Before After
POST /triggers/schedules/ with * * * * * 201, schedule fires 1440x/day 422, Schedule may run at most once every 15 minutes, but this one runs every 1 minute.
Same value typed in the drawer Save enabled, request sent Field shows the error, Save disabled
Editing an existing schedule to */5 * * * * Save enabled, rejected on click with a toast Save disabled

Backend. _validate_schedule is the single gate, and both create_schedule and edit_schedule already call it first, so both paths are covered by one change. The 422 detail names the floor and the actual cadence so the message is actionable rather than "invalid cron".

Web. validateSchedule in the shared cron module adds the same cadence check on top of the existing field-shape check, so the drawer rejects the value without a round-trip. The backend stays the source of truth and its message still surfaces through triggerApiErrorMessage if the two ever disagree.

One extra fix found on the way: canSubmit gated edits on isDirty alone, never on cron validity. Save looked enabled for an invalid expression in edit mode and only bounced off handleSubmit with a toast. It is now gated on a valid cron in both modes.

Measuring the gap correctly

The interesting part is that comparing two consecutive fires is not enough.

  • 0,59 * * * * fires at :00, :59, :00 — the tight gap is the second one.
  • 0,1 0 31 1 * only ever fires on 31 January. Scanning from "now" would find nothing at all, so the scan anchors on the expression's first fire, not on the clock.

Both sides walk fires from the first one, take the smallest gap, and bail early once a gap falls below the floor, so a runaway expression costs two samples rather than a full window.

Compatibility

Enforced on write only. Schedules stored before this keep firing until someone edits them, so no existing automation breaks on deploy. The four active schedules on staging are daily and weekly and sit well clear of the floor.

The web mirrors the backend default as MIN_CRON_INTERVAL_MINUTES. A deployment that lowers the backend floor must lower that constant too, or the drawer will refuse a value the API would accept. Exposing the floor over the API would remove the duplication; there is no config endpoint for the web to read today, and adding one felt out of scope here.

Tests

  • api/oss/tests/pytest/unit/triggers/test_triggers_schedule_frequency_floor.py — new. Pins the gap arithmetic, the accept/reject boundary (*/15 accepted, */14 rejected), the early exit, the env override, the message text, and that a never-firing expression like 0 0 30 2 * does not raise.
  • gatewayTriggerCron.test.ts — the same cases on the web side.
  • Two existing tests used cadences that are now below the floor and were updated: the acceptance payload default moved from */5 to */30, and a unit test's "a valid cron is accepted" case likewise.
  • A new acceptance test asserts the 422 and its body for four sub-floor expressions.

Local runs: 172 passed, 4 skipped (api/oss/tests/pytest/unit/triggers/, the skips are Postgres DAO tests with no database). 35 passed (gatewayTriggerCron.test.ts). ruff format/ruff check clean, eslint/prettier clean, tsc --noEmit clean on both touched packages.

Follow-ups, not in this PR

  • A cap on active schedules per project, which needs a DAO count and a check on every activation path.
  • Per-organization concurrent-run limits, which need shared state and a design.
  • Daytona sandboxes are labelled only code-toolbox-language: python, the SDK default. Adding stage, project, and trigger id would have made the incident above self-explaining.

Every schedule fire starts an agent run in its own sandbox, so a '* * * * *'
schedule bills 1440 runs a day and, because a run routinely outlives a minute,
overlaps itself. Nothing stopped one: _validate_schedule only checked that the
expression parsed as five fields.

Add a minimum interval, defaulting to 15 minutes and overridable via
AGENTA_TRIGGERS_SCHEDULE_MIN_INTERVAL_MINUTES. Both create and edit already
route through _validate_schedule, so both are covered, and the 422 names the
floor and the offending cadence.

The web mirrors the check in validateSchedule so the drawer refuses the value
before submitting, and Save is now gated on a valid cron in edit mode too, not
only on create.

Enforced on write only: schedules stored before this keep firing until edited.
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug labels Aug 25, 2026
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Blocked Blocked Aug 25, 2026 3:56pm

Request Review

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 8716d9d8-b190-45cb-b78e-ac7bafd270d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added minimum schedule cadence validation, defaulting to 15 minutes.
    • Added configurable cadence limits for trigger schedules.
    • Schedule validation now checks both cron syntax and how frequently a trigger will run.
  • Bug Fixes

    • Prevented creation and editing of schedules that fire more frequently than the allowed interval.
    • Improved validation messages with clear, human-readable interval details.
    • Updated schedule previews and next-run information to reflect the enhanced validation.

Walkthrough

The change adds a configurable minimum cron firing interval. Backend validation scans upcoming occurrences. Frontend validation mirrors the default 15-minute floor. Schedule forms now block invalid schedules in create and edit flows. Tests cover cadence detection, boundaries, errors, and configuration.

Changes

Schedule frequency-floor validation

Layer / File(s) Summary
Backend configuration and cadence enforcement
api/oss/src/utils/env.py, api/oss/src/core/triggers/service.py
Adds TriggersConfig with a positive integer interval, defaulting to 15 minutes. Backend schedule validation scans cron occurrences and rejects intervals below the configured floor.
Backend cadence validation coverage
api/oss/tests/pytest/unit/triggers/test_triggers_schedule_frequency_floor.py, api/oss/tests/pytest/acceptance/triggers/test_triggers_schedules.py, api/oss/tests/pytest/unit/triggers/test_triggers_schedules_refresh.py
Tests cadence calculation, malformed expressions, threshold boundaries, configured floors, rejection responses, and updated 30-minute valid schedules.
Frontend cadence utilities and exports
web/packages/agenta-entities/src/gatewayTrigger/core/cron.ts, web/packages/agenta-entities/src/gatewayTrigger/index.ts, web/packages/agenta-entities/tests/unit/gatewayTriggerCron.test.ts
Adds minimum-gap calculation, interval formatting, the 15-minute default, validateSchedule, public exports, and unit coverage.
Schedule form validation integration
web/packages/agenta-entity-ui/src/gatewayTrigger/drawers/ScheduleBuilderField.tsx, web/packages/agenta-entity-ui/src/gatewayTrigger/drawers/schedule/ScheduleForm.tsx
Uses validateSchedule for field validation and requires valid schedules before submission in create and edit flows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to beb9b

A narrow client-side validation gap allows some leap-day schedules below the 15-minute floor to appear valid and reach the API, where they should still be rejected. The impact is limited to inconsistent validation and user experience, but the leap-day case should receive explicit owner follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant ScheduleForm
  participant validateSchedule
  participant TriggerAPI
  participant _validate_schedule

  ScheduleForm->>validateSchedule: validate cron expression and cadence
  validateSchedule-->>ScheduleForm: return client validation result
  ScheduleForm->>TriggerAPI: submit valid schedule
  TriggerAPI->>_validate_schedule: validate configured frequency floor
  _validate_schedule-->>TriggerAPI: accept or reject schedule
  TriggerAPI-->>ScheduleForm: return creation result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.88% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting trigger schedules that fire more frequently than the configured frequency floor.
Description check ✅ Passed The description directly explains the schedule frequency-floor problem, backend and web changes, compatibility behavior, and test coverage.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/trigger-schedule-frequency-floor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ad3c2e6-99d2-48b1-a46d-60cf56ff834b

📥 Commits

Reviewing files that changed from the base of the PR and between a09a0f0 and beb9b82.

📒 Files selected for processing (10)
  • api/oss/src/core/triggers/service.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/acceptance/triggers/test_triggers_schedules.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_schedule_frequency_floor.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_schedules_refresh.py
  • web/packages/agenta-entities/src/gatewayTrigger/core/cron.ts
  • web/packages/agenta-entities/src/gatewayTrigger/index.ts
  • web/packages/agenta-entities/tests/unit/gatewayTriggerCron.test.ts
  • web/packages/agenta-entity-ui/src/gatewayTrigger/drawers/ScheduleBuilderField.tsx
  • web/packages/agenta-entity-ui/src/gatewayTrigger/drawers/schedule/ScheduleForm.tsx

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

Comment on lines +200 to +202
export function smallestCronGapMinutes(expression: string): number | null {
const runs = nextCronRuns(expression, GAP_SAMPLE_COUNT)
if (runs.length < 2) return null

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Detect leap-day schedules before accepting them.

nextCronRuns stops after 366 days. With time frozen at 2026-03-01, 0,1 0 29 2 * fires twice one minute apart on February 29, 2028. This function returns null, so validateSchedule accepts a schedule below the cadence floor. Extend cadence sampling to reach valid leap-day occurrences without an unbounded minute scan. Add this case as a regression test.

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6279.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6279-902be6d
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-25T16:08:40.475Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant