Malformed schedule dict silently disables scheduling with no visible error
Summary
Query.outdated_queries() (redash/models/__init__.py) can silently set a query's schedule.disabled = True when the schedule JSON is malformed — with zero indication anywhere (API response, UI, logs visible to the org admin) that anything went wrong. The only symptom is stale query results, discovered much later.
Root cause
The schedule column has no server-side shape validation on write — POST /api/queries/{id} with {"schedule": {...}} accepts and echoes back whatever JSON is sent, even a partial object like {"interval": 86400}.
But the scheduler's outdated_queries() reads the schedule with direct dict indexing, not .get():
if query.schedule["until"]:
...
if should_schedule_next(
retrieved_at, now,
query.schedule["interval"],
query.schedule["time"],
query.schedule["day_of_week"],
query.schedule_failures,
):and should_schedule_next() parses time with a strict unpack:
hour, minute = time.split(":")So on the very next scheduler tick after saving an incomplete schedule (missing until/time/day_of_week), or one where time includes seconds ("07:00:00" instead of "07:00"), either a KeyError or ValueError is raised. That exception is caught by a bare except Exception in outdated_queries():
except Exception as e:
query.schedule["disabled"] = True
db.session.commit()
message = ("Could not determine if query %d is outdated due to %s. "
"The schedule for this query has been disabled." % (query.id, repr(e)))
logging.info(message)
sentry.capture_exception(...)This is only logging.info + Sentry (if configured) — nothing is exposed via the query API, the dashboard, or any user-facing surface. A schedule that looked fine seconds earlier silently flips to disabled: true within one scheduler tick (~a minute), and stays that way until someone happens to notice stale data and manually re-inspects the schedule JSON.
Impact
Anyone driving schedules through the API (rather than exclusively the UI, which always sends the full shape) can trip this without realizing it — we hit it across 9 separate queries feeding two production dashboards, and it recurred after an initial "fix" because the repair itself re-wrote an incomplete schedule object.
Suggested fixes (any of these would help)
- Validate
scheduleshape onPOST /api/queries/{id}and reject/normalize incomplete or malformed objects instead of accepting them silently, or - Use
.get()with defaults inoutdated_queries()/should_schedule_next()instead of direct indexing, so a partial schedule just runs with sane defaults rather than crashing, or - At minimum, surface the auto-disable somewhere visible — e.g. include
schedule_failures-style info in theGET /api/queries/{id}response, or a dashboard/query-list badge — so it isn't purely a server log line.
Version
Encountered against a Redash instance following the master-branch outdated_queries() implementation as of 2026-08.
Source: getredash/redash