Schedules as automations - #2293
Conversation
Automations are recurring tasks (for now: computing forecasts) defined per asset. The recurrence is defined by a cron string, and the work to be done is defined by a data generator (linked through a data source) together with the parameters to call it with. Includes a migration for the new table, and new dependencies on croniter (cron matching/validation) and cron-descriptor (natural-language recurrence descriptions). Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
- `flexmeasures add automation` creates an automation (active by default), validating the forecast parameters with the forecast parameter schema and storing the forecaster config on a data source. - `flexmeasures edit automation` edits the name, recurrence (cron string) or activation status. - `flexmeasures delete automation` deletes an automation. - All three record their events in the asset's audit log. - `flexmeasures jobs run-automations` queues jobs for all automations due this minute (to be run once per minute, e.g. via cron), with a Redis-based guard against duplicate runs within the same minute. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Data generators can now be told how their queued jobs got triggered (via the CLI, the API or an automation), and the train-predict pipeline stores this on the jobs as meta data. The asset's status page shows it in a new 'Created Via' column of the jobs table. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
GET /api/v3_0/assets/<id>/automations lists the automations defined on an asset (without generator and parameters details). GET /api/v3_0/assets/<id>/automations/<automation_id> additionally provides the parameters, data generator info and counts of recently created jobs per job status. Both are documented in the OpenAPI specs. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
/assets/<id>/automations shows the asset's automations in a tabbed view (schedules and reports tabs are prepared but deactivated), with per-row details (parameters, data generator, job counts) loaded asynchronously into a modal. The page is linked in the breadcrumbs dropdown and links to the status page, where recent jobs are listed. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
CI runners have no locale set (POSIX), which made cron-descriptor render 'At 06:00' while dev environments with an en_US-style locale rendered 'At 06:00 AM'. Request 24-hour format explicitly so the description is deterministic across environments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxkeq64jtENY7fiWjwUsVS
- Escape automation names (and other user-controlled strings) in the Automations page and the status page's jobs table, closing two stored HTML/script injection sinks. - Wipe parameter state on the (possibly shared) cached data generator before each automation run, so automations sharing a generator data source don't pollute each other's runs. - Count automation job stats under the forecast target sensor(s) from the automation's parameters, which may belong to a different asset. - Release the per-minute Redis guard when a run fails, so a retry within the same minute can still queue jobs. - Return 404 (as documented) for nonexistent automation ids on the detail endpoint, and check permissions on the asset, so automation ids can no longer be enumerated across accounts via 403-vs-422 differences. - Use ondelete=SET NULL for the generator FK: deleting a data source no longer silently deletes automations. - Delegate Automation ACL to the asset's ACL instead of duplicating it. - Extract the config/parameters assembly shared by `add forecasts` and `add automation` into a helper (which no longer drops falsy config values). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Completes the previous commit, whose staged files were dropped by an interrupted pre-commit run: template escaping, shared-generator state reset, job stats under target sensors, Redis guard release on failure, 404 for nonexistent automations, SET NULL generator FK, ACL delegation, and the shared CLI config/parameters assembly helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
The scheduling job creators accept an optional trigger dict (stored as job meta data), like the forecasting pipeline already does. The API trigger endpoint records origin API; the CLI and automations follow in the next commit. The status page's 'Created Via' column picks this up automatically. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Automations now also support the 'schedules' type: - `flexmeasures add automation --type schedules` validates the parameters as a schedule trigger message (per the AssetTriggerSchema, as accepted by the API trigger endpoint, without the asset id). The schedule 'start' may be omitted, in which case each run schedules from the run time (floored to the message's resolution, if given) — a fixed start draws a warning. - The runner dispatches schedules automations to the same job creators as the API trigger endpoint (sequential or simultaneous), recording trigger meta data (origin automation) on the queued jobs; `flexmeasures add schedule --as-job` now records origin CLI. - Job stats for schedules automations are counted from the scheduling job cache (asset-level wrap-up jobs and per-sensor device jobs). - The UI automations page's Schedules tab is now enabled, with automations filtered by type per tab. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Documentation build overview
139 files changed ·
|
…essage format PR #2303 makes click report the validation message rather than the offending value, which changes the exact wording of this error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Merge current main, resolve the shared forecasting and documentation changes, regenerate the lockfile, and move the automation migration after the current migration head. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Reject cron expressions with seconds, year fields, or aliases because the automation runner executes once per minute. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Test valid five-field expressions and reject unsupported seconds, year, and alias formats. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Keep the per-minute Redis guard after failures because an attempt may already have queued some forecast jobs. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Verify that retrying a failed partial queueing attempt does not create duplicate jobs. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Convert YAML dates to ISO strings, accept empty files, and report non-object config or parameter files as usage errors. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Test YAML dates and timestamps, empty files, and invalid top-level list values for automation options. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Hide automation names and IDs from asset job responses when the current user cannot read the source automation. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Verify inaccessible automation provenance is redacted while authorized callers still receive the full identity. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Show a persistent API error instead of presenting failed automation requests as an empty list. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Check that the automations page renders the warning target and hides the table when loading fails. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Avoid interpreting cron wildcard asterisks as RST italic markup when generating OpenAPI documentation. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <149331360+BelhsanHmida@users.noreply.github.com>
…differ from their default The guard against combining forecaster options with --type schedules compared the forecaster against its default, so naming the default forecaster explicitly passed silently and the automation was created, leaving the impression that the option had applied to a schedule automation. Ask which options were actually given on the command line instead, the way the --source conflict check already does, and name the offending options in the error rather than listing every option it could have been. Signed-off-by: Mohamed Belhsan Hmida mohamedbelhsanhmida@gmail.com
Manual test walkthroughEverything below was run against a development database on this branch. It assumes the forecast Pick an asset that describes its own flexibility, so a minimal trigger has something to work with — flexmeasures jobs run-worker --name schedules-demo --queue scheduling1. A minimal trigger is enoughThe parameters of a schedule automation are a trigger message: what echo 'duration: "PT12H"' > trigger-message.yml
flexmeasures add automation \
--asset 25 \
--name "Hourly schedules" \
--cron "0 * * * *" \
--timezone Europe/Amsterdam \
--type schedules \
--inactive \
--parameters trigger-message.yml
# → Successfully created inactive automation 'Hourly schedules' (ID: N) to compute schedules
# for asset 25, recurring per cron string '0 * * * *' in timezone 'Europe/Amsterdam'.Note it needs no forecaster and no data source: 2. The schedule starts when the run startsOmitting A fixed echo 'start: "2025-11-16T00:00:00+01:00"' > trigger-fixed.yml
echo 'duration: "PT12H"' >> trigger-fixed.yml
flexmeasures add automation --asset 25 --name "Fixed window" --type schedules \
--inactive --parameters trigger-fixed.yml
# → Warning: the schedule 'start' is fixed, so each run will compute the same period.
# Omit 'start' to schedule from the run time instead.
# → Successfully created inactive automation 'Fixed window' (ID: M) ...3. Forecaster options do not applyA schedule automation is not computed by a forecaster, so anything configuring one is refused and flexmeasures add automation --asset 25 --name "Wrong options" --type schedules \
--parameters trigger-message.yml --forecaster SomeOtherForecaster
# → Error: --forecaster cannot be combined with --type schedules: a schedule automation is not
# computed by a forecaster.
flexmeasures add automation --asset 25 --name "Wrong options" --type schedules \
--parameters trigger-message.yml --train-period P7D
# → Error: --train-period cannot be combined with --type schedules: ...Naming the default forecaster is refused as well, since it was still given: flexmeasures add automation --asset 25 --name "Wrong options" --type schedules \
--parameters trigger-message.yml --forecaster TrainPredictPipeline
# → Error: --forecaster cannot be combined with --type schedules: ...The check asks which options were passed rather than comparing their values against their defaults, A parameters file that is not a mapping is reported plainly rather than failing later: echo 'just a string, not a mapping' > bad.yml
flexmeasures add automation --asset 25 --name "Bad yaml" --type schedules --parameters bad.yml
# → Error: The --parameters file must contain a YAML or JSON object at the top level.4. Run it, and see a scheduling jobflexmeasures edit automation --id N --cron "* * * * *" --activate
flexmeasures jobs run-automations
# → Automation N ('Hourly schedules') queued 1 scheduling job(s) for asset 25.The job lands on the scheduling queue, not the forecasting one, and carries the same provenance For a sequential schedule the automation queues one job per device plus the wrap-up job, and the job 5. The Schedules tabThe Schedules tab is now populated, alongside Forecasts. Each type has its own listing, and the 6. Clean upflexmeasures edit automation --id N --deactivate
flexmeasures delete automation --id N --force |
Context: - Review of #2396 asked what the "scheduling cursor" is, how an automation is "watermarked" (watermarks do not update), and why the field is needed at all. - "scheduling" also collides with FlexMeasures' scheduling machinery (the "scheduling" queue, StorageScheduler), which this field has nothing to do with. - The feature is unreleased, so the column, the API field and the migration can still be renamed without a compatibility burden. Change: - Renamed Automation.scheduling_cursor to Automation.cursor, and get_initial_scheduling_cursor to get_initial_cursor. As a column on the automation table, it reads as an automation's cursor without further qualification, like the neighbouring timezone column. - Replaced the "watermark" wording everywhere with what the field holds: the scheduled time of the most recent run the automation committed to, advanced just before queueing, and therefore not a record of success. - Said "run" instead of "occurrence" throughout the automation code, matching the vocabulary already used for run time, run-automations and the automation-run guard key. - Explained in the migration why both columns are added nullable and backfilled before NOT NULL, and why the backfilled cursor is one minute before the upgrade. - Regenerated the OpenAPI specs. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Automation.scheduling_cursor became Automation.cursor, and automation "occurrences" became "runs". Change: - Updated the field name in the automation fixtures and assertions, and the UI assertion on the "Cursor (UTC)" heading. - Renamed the coalescing and spring-forward test cases to speak of runs. Signed-off-by: F.N. Claessen <felix@seita.nl>
…ence" Context: - Review of #2396 found "the cursor is a watermark", "migration watermark", "the cursor is committed" and "durable run records" unclear, and asked why the cursor is needed at all. - The docs already spoke of a run time, run records and run-automations, so "occurrence" was a second word for the same thing. Change: - Introduced the cursor by the problem it solves: the runner is a stateless once-a-minute command, so it needs a durable record of how far each automation has got. - Stated what it holds, that it advances before queueing (so it is not a record of success), and that keeping one moving timestamp instead of a record per run is what produces the catch-up and concurrency behaviour described below it. - Replaced the "migration watermark" sentence with what an upgrade actually does to existing automations. - Linked issue #2393 where the docs referred to "durable run records". - Applied the suggested wording for the "add automation" command summary. Signed-off-by: F.N. Claessen <felix@seita.nl>
…tion Context: - Review of #2290 asked to move the automation API entries to a new v3.0-33 section, and noted that a revision to a CLI command introduced in the same version does not warrant its own entry. Change: - Moved the automation and data source entries from v3.0-32 to a new "v3.0-33 | September 1, 2026" section. - Folded the timezone and cursor entry into the entry introducing the automation endpoints, applying the same reasoning as for the CLI changelog, and described the cursor in terms of the run it points at. - Folded the --timezone and catch-up entry into the two CLI entries introducing the commands it revises. - Folded the #2396 entry in the main changelog into the #2290 entry it refines, listing both PRs. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - This branch had shortened the underline of "v3.0-32 | August 11, 2026" from 26 to 24 characters, one short of the 25-character title, which makes docutils warn that the title underline is too short. Change: - Set the underline to exactly the title length. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Reviewing #2290 turned up three issues in how the automations service handles shared state and asset trees. Change: - run_automation now works on a copy of the data generator, like resolve_automation_sensors already did. The generator is cached on the data source, which several automations may share, so setting the job trigger on the shared instance would attribute jobs to the wrong automation as soon as anything runs concurrently. - Moved the upward tree walk to asset_and_ancestor_ids in data/queries/generic_assets, and expressed asset_is_in_subtree in terms of it, so the two copies of that walk introduced by this branch became one. - Added get_automations_involving_sensor, which considers every automation rather than only those on the sensor's asset and its ancestors, because a regressor may live anywhere in the tree. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - PostgreSQL does not index a foreign key by itself, and automations are looked up by asset on an asset's automations page and when finding the automations that feed a sensor. - Reviewing #2290 also showed that a bool would be read as a sensor ID, as bool is a subclass of int. Change: - Added an index on automation.asset_id, in a new migration rather than in the migration that creates the table, so a database that already ran that one still gets the index. - Excluded bools from the integer branch of DataGenerator._resolve_sensors. Signed-off-by: F.N. Claessen <felix@seita.nl>
…esolved Context: - On the error path, the automation details endpoint called resolve_automation_sensors and then get_automation_sensors, which calls resolve_automation_sensors again and swallows the error, so a broken automation set up its data generator and loaded its parameters twice. Change: - Log the reason and fall back to empty sensor lists directly, which is what the second call amounted to. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - An automation refers to its sensors by ID inside its parameters, which no foreign key protects. Deleting such a sensor left the automation looking healthy while failing on its next run, with the reason visible only in the runner's output. - A data source is protected from this by a foreign key, so the sensors were the remaining gap. Change: - flexmeasures delete sensor now lists the automations that read from or write to each sensor before asking for confirmation. The deletion is still allowed, as the host may well intend it. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - test_invalid_cron_does_not_hide_other_due_automations failed whenever an earlier test in the session had built an app: creating one reconfigures logging and replaces the root handlers, after which pytest's caplog captures nothing. Reproduced with utils/tests/test_job_utils.py::test_app_queues_use_custom_global_and_queue_job_timeout running first. - The condition is pre-existing and hits any test that reads caplog afterwards, including data/tests/test_utils.py::test_schema_mismatch_log_record_is_deduplicated on main, which already uses caplog.at_level. So at_level is not a workaround: the handler is gone, not merely filtered. - The test's behavioural assertion passed throughout; only the log assertion failed. Change: - Assert on the logger itself rather than on caplog, which makes the test independent of what ran before it. - Cover that deleting a sensor names the automations using it, including a regressor-only sensor, which get_automations_feeding_sensor does not find. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - flexmeasures delete sensor now warns which automations use a sensor. Change: - Added a CLI changelog entry, as delete sensor is a pre-existing command rather than one introduced in this version. - Folded the behaviour into the automations entry in the main changelog, which already covers this feature. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Only uv.lock conflicted. Both sides listed flexmeasures' own dependencies, main having added limits and this branch croniter and cron-descriptor. - The two lockfiles were also written by different uv versions, which normalise environment markers differently, so the sides disagreed on nearly every line rather than only on those three packages. Change: - Took main's uv.lock and regenerated it with uv 0.10.9, the version that wrote it (see #2451, which pins this and will later move everything to 0.12.7). That adds cron-descriptor and nothing else: 16 insertions, no deletions, and no package re-versioned. - Resolving it by hand was not viable: keying on the package name drops the Python 3.10 halves of version-split entries such as pint 0.24.4, and keying on the whole line keeps both marker spellings of every package. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Review of #2290 found the automations section sitting under forecasting, although most of it will apply to scheduling and reporting automations too. - The same review found the CLI example silent about what it automates, --forecaster and --config referred to before being introduced, the daylight-saving-time rules reading as a developer's note in the main body, and the pointer to issue #2393 reading as a todo. Change: - Moved the section to features/automations.rst, split into creating, running and viewing automations, and left a pointer in features/forecasting.rst. Wrote it in terms of automations in general, mentioning forecasts as today's only type. - Made the example pass --type forecasts explicitly, which is the option the follow-up PRs use to distinguish schedules and reports. - Introduced --forecaster and --config before the sentence that says --source makes them unnecessary. - Moved the cursor and daylight-saving-time rules to an appendix, marked as bookkeeping you do not need in order to use automations. - Dropped the sentence pointing at issue #2393, keeping the statement that a failed attempt is not retried, which is the part users need. Signed-off-by: F.N. Claessen <felix@seita.nl>
…edule-automations Context: - Picking up the review changes on #2290: the scheduling_cursor column became cursor, automation "occurrences" became "runs", and the automations documentation moved to its own page. Change: - Kept this branch's prepare_schedule_trigger_message and dropped its private _asset_and_ancestor_ids, which #2290 replaced with the shared asset_and_ancestor_ids in data/queries/generic_assets. - Kept the pointer that features/forecasting.rst now holds, including this branch's cross-reference to automating_schedules. - Merged the changelog entries: dropped the #2396 entry that #2290 folded into its own, and carried "computing forecasts or schedules" into the fuller CLI wording. - Regenerated the OpenAPI specs rather than merging the generated file by hand. Signed-off-by: F.N. Claessen <felix@seita.nl>
…ons-crud Context: - Picking up the review changes from #2290 and #2293, in which scheduling_cursor became cursor, "occurrences" became "runs", and the automations documentation moved to its own page. Change: - Updated this branch's update_automation service, which git merged without a conflict because the two sides touched different regions, but which still called get_initial_scheduling_cursor and assigned to automation.scheduling_cursor. Both would have failed at runtime. - Kept the edit logic in the service, so cli/data_edit.py neither imports get_initial_cursor nor rebases the cursor itself. - Kept both automation imports in cli/data_delete.py, one for deleting an automation and one for warning which automations use a sensor. - Said "run" instead of "occurrence" in the update_automation docstring and the automations UI template. - Regenerated the OpenAPI specs. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - The separate revision added for this index branched off 9f2b6e1d4a73, but so does c63896a97a8e on the branches stacked on top of this one. Merging this branch down therefore left two alembic heads, and `flexmeasures db upgrade` fails on multiple heads. That broke the Docker build job on #2293, #2294, #2297 and #2299, while this PR itself stayed green with its single head. - Adding the index in its own revision was meant to spare a database that had already run 9f2b6e1d4a73. Breaking the upgrade on four stacked PRs is the greater harm, so that trade-off no longer holds. Change: - Folded the index into 9f2b6e1d4a73, which every branch in the stack shares, and dropped the separate revision. No branch gains a head. - Anyone whose database already ran 9f2b6e1d4a73 will not have the index; recreate the database or add the index by hand. Signed-off-by: F.N. Claessen <felix@seita.nl>
…sts' into HEAD Signed-off-by: F.N. Claessen <felix@seita.nl>
* feat: add Automation data model Automations are recurring tasks (for now: computing forecasts) defined per asset. The recurrence is defined by a cron string, and the work to be done is defined by a data generator (linked through a data source) together with the parameters to call it with. Includes a migration for the new table, and new dependencies on croniter (cron matching/validation) and cron-descriptor (natural-language recurrence descriptions). Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * feat: CLI commands to manage and run automations - `flexmeasures add automation` creates an automation (active by default), validating the forecast parameters with the forecast parameter schema and storing the forecaster config on a data source. - `flexmeasures edit automation` edits the name, recurrence (cron string) or activation status. - `flexmeasures delete automation` deletes an automation. - All three record their events in the asset's audit log. - `flexmeasures jobs run-automations` queues jobs for all automations due this minute (to be run once per minute, e.g. via cron), with a Redis-based guard against duplicate runs within the same minute. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * feat: record on forecasting jobs how they were created Data generators can now be told how their queued jobs got triggered (via the CLI, the API or an automation), and the train-predict pipeline stores this on the jobs as meta data. The asset's status page shows it in a new 'Created Via' column of the jobs table. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * feat: API endpoints to list an asset's automations GET /api/v3_0/assets/<id>/automations lists the automations defined on an asset (without generator and parameters details). GET /api/v3_0/assets/<id>/automations/<automation_id> additionally provides the parameters, data generator info and counts of recently created jobs per job status. Both are documented in the OpenAPI specs. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * feat: UI page listing an asset's automations /assets/<id>/automations shows the asset's automations in a tabbed view (schedules and reports tabs are prepared but deactivated), with per-row details (parameters, data generator, job counts) loaded asynchronously into a modal. The page is linked in the breadcrumbs dropdown and links to the status page, where recent jobs are listed. Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * test: cover automations CLI, API and UI Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * docs: document automations Part of #2288 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * docs: changelog entry for automations Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * fix: render cron descriptions in 24-hour format regardless of locale CI runners have no locale set (POSIX), which made cron-descriptor render 'At 06:00' while dev environments with an en_US-style locale rendered 'At 06:00 AM'. Request 24-hour format explicitly so the description is deterministic across environments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxkeq64jtENY7fiWjwUsVS * fix: address code review findings for automations - Escape automation names (and other user-controlled strings) in the Automations page and the status page's jobs table, closing two stored HTML/script injection sinks. - Wipe parameter state on the (possibly shared) cached data generator before each automation run, so automations sharing a generator data source don't pollute each other's runs. - Count automation job stats under the forecast target sensor(s) from the automation's parameters, which may belong to a different asset. - Release the per-minute Redis guard when a run fails, so a retry within the same minute can still queue jobs. - Return 404 (as documented) for nonexistent automation ids on the detail endpoint, and check permissions on the asset, so automation ids can no longer be enumerated across accounts via 403-vs-422 differences. - Use ondelete=SET NULL for the generator FK: deleting a data source no longer silently deletes automations. - Delegate Automation ACL to the asset's ACL instead of duplicating it. - Extract the config/parameters assembly shared by `add forecasts` and `add automation` into a helper (which no longer drops falsy config values). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * fix: address code review findings for automations (remaining files) Completes the previous commit, whose staged files were dropped by an interrupted pre-commit run: template escaping, shared-generator state reset, job stats under target sensors, Redis guard release on failure, 404 for nonexistent automations, SET NULL generator FK, ACL delegation, and the shared CLI config/parameters assembly helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * test: assert on the cron validation failure without pinning click's message format PR #2303 makes click report the validation message rather than the offending value, which changes the exact wording of this error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX * data/schemas: restrict automations to five-field cron Reject cron expressions with seconds, year fields, or aliases because the automation runner executes once per minute. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * data/schemas/tests: cover automation cron field count Test valid five-field expressions and reject unsupported seconds, year, and alias formats. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * cli/jobs: retain automation guard after queueing failure Keep the per-minute Redis guard after failures because an attempt may already have queued some forecast jobs. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * cli/tests: cover partial automation queue failure Verify that retrying a failed partial queueing attempt does not create duplicate jobs. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * cli: normalize YAML forecasting option files Convert YAML dates to ISO strings, accept empty files, and report non-object config or parameter files as usage errors. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * cli/tests: cover automation YAML option files Test YAML dates and timestamps, empty files, and invalid top-level list values for automation options. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * data/services: redact inaccessible automation provenance Hide automation names and IDs from asset job responses when the current user cannot read the source automation. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * api/v3_0/tests: cover automation provenance authorization Verify inaccessible automation provenance is redacted while authorized callers still receive the full identity. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * ui/assets: distinguish automation load failures Show a persistent API error instead of presenting failed automation requests as an empty list. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * ui/tests: cover automation load error state Check that the automations page renders the warning target and hides the table when loading fails. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * utils/docs: preserve standalone asterisks in RST conversion Avoid interpreting cron wildcard asterisks as RST italic markup when generating OpenAPI documentation. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * utils/tests: cover RST cron wildcard conversion Verify cron wildcards remain unchanged while ordinary italic markup is still converted. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * docs/forecasting: clarify automation execution contract Document five-field cron expressions, at-most-once queueing attempts, and the automations API endpoint. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * changelog: record automation API and runner contract Record the automation endpoints, authorization-aware provenance, and five-field runner behavior. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * api/docs: show job creation provenance Include the created_via field in the asset jobs OpenAPI example. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test: keep forecast CLI stub compatible with job provenance Add the trigger method required by the forecasting CLI to the regressor parsing test double. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * fix: require valid automation generators Require every automation to reference a data generator and prevent deleting a data source while an automation still depends on it, matching the retention policy for belief and annotation sources. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test: cover automation generator retention Verify referenced generators cannot be deleted, generator references cannot be cleared, and automation API fixtures always use valid generators. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * fix: constrain forecast automation outputs Allow forecast output only on the automation asset or its descendants and revalidate that relationship before every scheduled run, including explicit sensor-to-save targets. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test: cover forecast automation output scope Cover same-asset, child, grandchild, ancestor, and unrelated output targets, explicit sensor-to-save behavior, and runtime revalidation after an asset is moved. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * docs: explain forecast automation ownership rules Document output-sensor scope, runtime relationship checks, and the requirement to retain a generator while its automation exists. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * fix: merge automation and main migration heads Join the automation and main Alembic branches so installations have a single database upgrade target. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * data/models: let data generators report their input and output sensors Context: - Review of #2290 asked for a data generator property listing the sensors it reads from and writes to, so that automations can link to those sensors (and, later, check the creating user's permissions on them) Change: - Added input_sensors and output_sensors to DataGenerator (empty by default), implemented for Forecaster from its regressors and target sensor - Added the same properties to Automation, resolved from its data generator configured with the automation's own parameters - Added get_automations_feeding_sensor to look up automations by output sensor Signed-off-by: F.N. Claessen <felix@seita.nl> * data/models/forecasting: only announce a pipeline run when actually running it Context: - 'flexmeasures jobs run-automations' logged 'Starting Train-Predict Pipeline' for every automation, while it only queues the cycles as jobs Change: - Log that line at debug level when running with as_job, where the workers running the cycles log their own start Signed-off-by: F.N. Claessen <felix@seita.nl> * cli: default the automation recurrence to daily, and reject options that --source already determines Context: - Review of #2290: --cron should not be required, and --forecaster/--config are redundant with --source, whose data generator attributes already hold both (get_data_generator silently ignores them when a source is given) Change: - Default --cron to '0 0 * * *' (daily at midnight) - Abort when --source is combined with --forecaster, --config or any of the forecaster configuration options, naming the conflicting options Signed-off-by: F.N. Claessen <felix@seita.nl> * api/v3_0: report an automation's input and output sensors Context: - The automation details modal should link to the sensors an automation feeds Change: - Added input_sensors and output_sensors (id and name each) to GET /assets/<id>/automations/<automation_id> Signed-off-by: F.N. Claessen <felix@seita.nl> * api/v3_0: add an endpoint for one data source Context: - The sensor page should be able to show the full record of a data source, including the attributes in which data generators store their configuration Change: - Added GET /sources/<id>, with the same access rules as listing sources - Let _serialize_source optionally include the attributes and unset fields Signed-off-by: F.N. Claessen <felix@seita.nl> * api/v3_0: regenerate the OpenAPI specs Context: - The specs are generated from the endpoint docstrings by a pre-commit hook Change: - Regenerated after adding the data source endpoint and the automation's input and output sensors Signed-off-by: F.N. Claessen <felix@seita.nl> * ui: link an automation's details to its sensors, and make the listing sortable Context: - Review of #2290: the details modal should link to the sensors an automation feeds, with its data source pre-selected there, and the listing was not sortable Change: - Show the input and output sensors in the details modal, linking to /sensors/<id>?source=<generator id> - Enabled ordering, sorting the rendered columns on separate values (the ISO timestamp, the activation status and the cron string), newest first Signed-off-by: F.N. Claessen <felix@seita.nl> * ui: show a sensor's data source record and the automations feeding it Context: - Review of #2290: the sensor page should be able to show all details of a data source, and list the automations that write data to the sensor Change: - Added an info button next to the source selector, opening a modal with the full data source record - Pre-select the source given in the source query parameter, so that links from an automation land on its own source - List the automations feeding the sensor (those the user may read), linking to the automations page of their asset - Added user_can_read to the UI's permission helpers Signed-off-by: F.N. Claessen <felix@seita.nl> * tests: cover the automation and data source review follow-ups Context: - New behaviour from the #2290 review needs regression coverage Change: - CLI: the daily default recurrence, the --source conflict, and an automation's input and output sensors - API: an automation without a data generator reports no sensors; the new data source endpoint, its access rules and its 404 - UI: the source query parameter reaches the page, and automations feeding a sensor are listed on it Signed-off-by: F.N. Claessen <felix@seita.nl> * docs: describe the automation and data source follow-ups Context: - The #2290 review changed user-facing CLI, API and UI behaviour Change: - Documented the daily default recurrence and reusing a forecaster via --source - Documented the links between automations and the sensors they feed - Added API change log entries for the automations and data source endpoints - Extended the changelog entry of #2290 Signed-off-by: F.N. Claessen <felix@seita.nl> * api/v3_0: regenerate the OpenAPI specs after merging Context: - The merge combined endpoint docstring changes from both sides Change: - Regenerated the specs Signed-off-by: F.N. Claessen <felix@seita.nl> * cli: only reject configuration options that were actually given with --source Context: - The guard added on this branch compared against the assembled config, which always holds the schema defaults of the list-valued options, so any use of --source was rejected Change: - Detect the conflicting options from click's parameter sources, so --source on its own works again while explicitly given configuration options still abort - Name the conflicting options in the error message Signed-off-by: F.N. Claessen <felix@seita.nl> * data/services: only consider automations that could feed a sensor Context: - Listing the automations feeding a sensor sets up a data generator per candidate, which does not need to happen for every automation in the database Change: - Narrowed the candidates to automations on the sensor's asset or an ancestor, which is where an automation writing to it must live Signed-off-by: F.N. Claessen <felix@seita.nl> * tests: follow the merged automation behaviour Context: - Automations now always have a data generator, and the --source guard also covers the forecaster configuration options Change: - Assert the sensors an automation with a generator reads from and writes to - Cover a configuration option conflicting with --source Signed-off-by: F.N. Claessen <felix@seita.nl> * cli: keep mypy happy about click 8 attributes types-Flask pins types-click 7.1, whose stubs shadow the inline types that click ships itself. Those stubs predate ParameterSource and Context.get_parameter_source, both added in click 8.0, so mypy rejected the --source conflict detection and pre-commit failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * cli: keep the automation help focused on the automation `add automation` reuses the forecast schemas, so Click rendered every forecaster and pipeline option in its help, burying the options that describe the automation itself. Accept those options still, but hide them, and let the parameters file supply a field the schema requires, so --sensor no longer has to be repeated on the command line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * data/models: count a source-filtered regressor as an input sensor SensorIdOrReferenceField deserializes a regressor that filters on sources into a SensorReference rather than a Sensor, which _resolve_sensors skipped, so those regressors were missing from a data generator's input sensors. The source filters only narrow down which beliefs are read from a sensor, not which sensor is involved, so the wrapped sensor counts as an input just like a plain sensor ID does. This matters beyond the sensor links in the UI: the input and output sensors are meant to carry the access checks for automations administered through the API, where a missing input sensor means a missing permission check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * data/services: do not report no sensors when an automation's sensors are unknown Working out an automation's sensors could fail for several reasons, and every one of them was reported as no sensors at all. That is fine for the sensor links in the UI, but the same answer is meant to carry the access checks for automations administered through the API, where no sensors reads as nothing to check, so a broken automation would pass every check on the sensors it involves. Split the two uses: resolve_automation_sensors raises AutomationSensorsUnknown, while get_automation_sensors keeps reporting none for display. The broad exception handler is narrowed to the failures that can actually occur here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Feat automation timezones catchup (#2396) * feat: add timezone-aware automation catch-up Store each automation's IANA timezone and a durable UTC scheduling watermark. Canonicalize daylight-saving transitions, coalesce missed forecasts, and claim occurrences before queueing while retaining the existing at-most-once failure policy. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test: cover timezone-aware automation scheduling Exercise timezone defaults and validation, independent timezone evaluation, normal and missed occurrences, DST gaps and folds, durable cursor claims, inactive automations, API/UI exposure, and the existing Redis failure guard. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * docs: explain automation timezone and catch-up semantics Document per-automation timezone snapshots, scheduling watermarks, downtime coalescing, daylight-saving behavior, reconfiguration boundaries, and the at-most-once retry limitation. Refresh the published automation API examples. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * changelog: record automation timezone and catch-up support Announce the new CLI options, additive automation response fields, persistent scheduling progress, and DST-aware catch-up behavior for issue #2392. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * docs: link automation catch-up changelog to PR Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> --------- Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * fix(data/schemas): reject cron expressions without dates Validate that a syntactically correct five-field recurrence can produce an actual calendar occurrence, preventing impossible dates such as February 31 from entering the automation table through either CLI or API input. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * fix(data/services): isolate invalid recurrences and stale claims Keep one legacy or corrupted recurrence from aborting global discovery, and make occurrence claiming an atomic comparison against the active state, recurrence, timezone, and cursor observed by the runner so concurrent edits cannot queue stale work. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * fix(api/v3_0): protect automation sensor details Require read access to every resolved input and output sensor before returning full automation details, ensuring the derived metadata and raw parameters cannot reveal cross-organisation sensor information to an asset reader. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test(cli): cover impossible recurrence input Exercise the CLI validation path with a syntactically valid recurrence that can never match, while updating the runner fixture to carry the scheduling snapshot required by atomic occurrence claims. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test(data/services): cover resilient automation claims Verify that corrupt recurrences are isolated and that deactivation, deletion, recurrence edits, timezone edits, or cursor movement after discovery prevent a stale claim, while non-execution name edits remain harmless. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * test(api/v3_0): cover private automation dependencies Build an automation with a supplier-owned regressor and confirm that a plain member who may read the automation asset receives a generic denial without the inaccessible sensor name. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> * data/models: name an automation's cursor after what it points at Context: - Review of #2396 asked what the "scheduling cursor" is, how an automation is "watermarked" (watermarks do not update), and why the field is needed at all. - "scheduling" also collides with FlexMeasures' scheduling machinery (the "scheduling" queue, StorageScheduler), which this field has nothing to do with. - The feature is unreleased, so the column, the API field and the migration can still be renamed without a compatibility burden. Change: - Renamed Automation.scheduling_cursor to Automation.cursor, and get_initial_scheduling_cursor to get_initial_cursor. As a column on the automation table, it reads as an automation's cursor without further qualification, like the neighbouring timezone column. - Replaced the "watermark" wording everywhere with what the field holds: the scheduled time of the most recent run the automation committed to, advanced just before queueing, and therefore not a record of success. - Said "run" instead of "occurrence" throughout the automation code, matching the vocabulary already used for run time, run-automations and the automation-run guard key. - Explained in the migration why both columns are added nullable and backfilled before NOT NULL, and why the backfilled cursor is one minute before the upgrade. - Regenerated the OpenAPI specs. Signed-off-by: F.N. Claessen <felix@seita.nl> * tests: follow the automation cursor rename Context: - Automation.scheduling_cursor became Automation.cursor, and automation "occurrences" became "runs". Change: - Updated the field name in the automation fixtures and assertions, and the UI assertion on the "Cursor (UTC)" heading. - Renamed the coalescing and spring-forward test cases to speak of runs. Signed-off-by: F.N. Claessen <felix@seita.nl> * docs: explain the automation cursor, and say "run" instead of "occurrence" Context: - Review of #2396 found "the cursor is a watermark", "migration watermark", "the cursor is committed" and "durable run records" unclear, and asked why the cursor is needed at all. - The docs already spoke of a run time, run records and run-automations, so "occurrence" was a second word for the same thing. Change: - Introduced the cursor by the problem it solves: the runner is a stateless once-a-minute command, so it needs a durable record of how far each automation has got. - Stated what it holds, that it advances before queueing (so it is not a record of success), and that keeping one moving timestamp instead of a record per run is what produces the catch-up and concurrency behaviour described below it. - Replaced the "migration watermark" sentence with what an upgrade actually does to existing automations. - Linked issue #2393 where the docs referred to "durable run records". - Applied the suggested wording for the "add automation" command summary. Signed-off-by: F.N. Claessen <felix@seita.nl> * docs/changelog: give the automation API changes their own version section Context: - Review of #2290 asked to move the automation API entries to a new v3.0-33 section, and noted that a revision to a CLI command introduced in the same version does not warrant its own entry. Change: - Moved the automation and data source entries from v3.0-32 to a new "v3.0-33 | September 1, 2026" section. - Folded the timezone and cursor entry into the entry introducing the automation endpoints, applying the same reasoning as for the CLI changelog, and described the cursor in terms of the run it points at. - Folded the --timezone and catch-up entry into the two CLI entries introducing the commands it revises. - Folded the #2396 entry in the main changelog into the #2290 entry it refines, listing both PRs. Signed-off-by: F.N. Claessen <felix@seita.nl> * docs/changelog: restore the v3.0-32 underline to full length Context: - This branch had shortened the underline of "v3.0-32 | August 11, 2026" from 26 to 24 characters, one short of the 25-character title, which makes docutils warn that the title underline is too short. Change: - Set the underline to exactly the title length. Signed-off-by: F.N. Claessen <felix@seita.nl> * data/services: address review findings on the automations service Context: - Reviewing #2290 turned up three issues in how the automations service handles shared state and asset trees. Change: - run_automation now works on a copy of the data generator, like resolve_automation_sensors already did. The generator is cached on the data source, which several automations may share, so setting the job trigger on the shared instance would attribute jobs to the wrong automation as soon as anything runs concurrently. - Moved the upward tree walk to asset_and_ancestor_ids in data/queries/generic_assets, and expressed asset_is_in_subtree in terms of it, so the two copies of that walk introduced by this branch became one. - Added get_automations_involving_sensor, which considers every automation rather than only those on the sensor's asset and its ancestors, because a regressor may live anywhere in the tree. Signed-off-by: F.N. Claessen <felix@seita.nl> * data/models: index the automation asset foreign key Context: - PostgreSQL does not index a foreign key by itself, and automations are looked up by asset on an asset's automations page and when finding the automations that feed a sensor. - Reviewing #2290 also showed that a bool would be read as a sensor ID, as bool is a subclass of int. Change: - Added an index on automation.asset_id, in a new migration rather than in the migration that creates the table, so a database that already ran that one still gets the index. - Excluded bools from the integer branch of DataGenerator._resolve_sensors. Signed-off-by: F.N. Claessen <felix@seita.nl> * api/v3_0: work out an automation's sensors once when they cannot be resolved Context: - On the error path, the automation details endpoint called resolve_automation_sensors and then get_automation_sensors, which calls resolve_automation_sensors again and swallows the error, so a broken automation set up its data generator and loaded its parameters twice. Change: - Log the reason and fall back to empty sensor lists directly, which is what the second call amounted to. Signed-off-by: F.N. Claessen <felix@seita.nl> * cli: warn which automations a sensor deletion would break Context: - An automation refers to its sensors by ID inside its parameters, which no foreign key protects. Deleting such a sensor left the automation looking healthy while failing on its next run, with the reason visible only in the runner's output. - A data source is protected from this by a foreign key, so the sensors were the remaining gap. Change: - flexmeasures delete sensor now lists the automations that read from or write to each sensor before asking for confirmation. The deletion is still allowed, as the host may well intend it. Signed-off-by: F.N. Claessen <felix@seita.nl> * tests: cover the sensor deletion warning, and stop depending on caplog Context: - test_invalid_cron_does_not_hide_other_due_automations failed whenever an earlier test in the session had built an app: creating one reconfigures logging and replaces the root handlers, after which pytest's caplog captures nothing. Reproduced with utils/tests/test_job_utils.py::test_app_queues_use_custom_global_and_queue_job_timeout running first. - The condition is pre-existing and hits any test that reads caplog afterwards, including data/tests/test_utils.py::test_schema_mismatch_log_record_is_deduplicated on main, which already uses caplog.at_level. So at_level is not a workaround: the handler is gone, not merely filtered. - The test's behavioural assertion passed throughout; only the log assertion failed. Change: - Assert on the logger itself rather than on caplog, which makes the test independent of what ran before it. - Cover that deleting a sensor names the automations using it, including a regressor-only sensor, which get_automations_feeding_sensor does not find. Signed-off-by: F.N. Claessen <felix@seita.nl> * docs/changelog: record the sensor deletion warning Context: - flexmeasures delete sensor now warns which automations use a sensor. Change: - Added a CLI changelog entry, as delete sensor is a pre-existing command rather than one introduced in this version. - Folded the behaviour into the automations entry in the main changelog, which already covers this feature. Signed-off-by: F.N. Claessen <felix@seita.nl> * docs: give automations their own page Context: - Review of #2290 found the automations section sitting under forecasting, although most of it will apply to scheduling and reporting automations too. - The same review found the CLI example silent about what it automates, --forecaster and --config referred to before being introduced, the daylight-saving-time rules reading as a developer's note in the main body, and the pointer to issue #2393 reading as a todo. Change: - Moved the section to features/automations.rst, split into creating, running and viewing automations, and left a pointer in features/forecasting.rst. Wrote it in terms of automations in general, mentioning forecasts as today's only type. - Made the example pass --type forecasts explicitly, which is the option the follow-up PRs use to distinguish schedules and reports. - Introduced --forecaster and --config before the sentence that says --source makes them unnecessary. - Moved the cursor and daylight-saving-time rules to an appendix, marked as bookkeeping you do not need in order to use automations. - Dropped the sentence pointing at issue #2393, keeping the statement that a failed attempt is not retried, which is the part users need. Signed-off-by: F.N. Claessen <felix@seita.nl> * data/migrations: index the automation asset FK without adding a revision Context: - The separate revision added for this index branched off 9f2b6e1d4a73, but so does c63896a97a8e on the branches stacked on top of this one. Merging this branch down therefore left two alembic heads, and `flexmeasures db upgrade` fails on multiple heads. That broke the Docker build job on #2293, #2294, #2297 and #2299, while this PR itself stayed green with its single head. - Adding the index in its own revision was meant to spare a database that had already run 9f2b6e1d4a73. Breaking the upgrade on four stacked PRs is the greater harm, so that trade-off no longer holds. Change: - Folded the index into 9f2b6e1d4a73, which every branch in the stack shares, and dropped the separate revision. No branch gains a head. - Anyone whose database already ran 9f2b6e1d4a73 will not have the index; recreate the database or add the index by hand. Signed-off-by: F.N. Claessen <felix@seita.nl> --------- Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> Signed-off-by: F.N. Claessen <felix@seita.nl> Signed-off-by: Mohamed Belhsan Hmida <149331360+BelhsanHmida@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com> Co-authored-by: Mohamed Belhsan Hmida <149331360+BelhsanHmida@users.noreply.github.com>
Context: - #2290 was squash-merged, so main now carries its 78 commits as one. This branch already contains those commits individually, but git cannot see that across the squash, and a normal merge conflicted in 15 files. - Checked before resolving: main's tree is byte-identical to #2290's final commit (06bf14b), that commit is an ancestor of this branch, and so is main's pre-squash tip. So this branch already contains everything main has, and main brought nothing of its own. Change: - Recorded the merge while keeping this branch's tree, rather than replaying 40 hunks of a conflict that only exists because the parent was squashed. - Verified afterwards that no file has content on main which is missing here; the differences are this branch's own work, such as generalising the job cache lookup beyond forecasts. Signed-off-by: F.N. Claessen <felix@seita.nl>
Flix6x
left a comment
There was a problem hiding this comment.
Notes to self (will pick them up now).
| @@ -0,0 +1,29 @@ | |||
| """Allow schedule automations without a generator. | |||
There was a problem hiding this comment.
Did we consider the alternative, which I guess would be making the Scheduler a DataGenerator? I'd like to discuss implications on this comment thread before making a decision.
There was a problem hiding this comment.
Discussed, nothing changed here yet — here is the analysis, so we can decide before I touch anything.
Where we stand today. Scheduler is almost a DataGenerator already: schedulers are registered
in app.data_generators["scheduler"] (app.py:177), and DataSource.data_generator accepts type
"scheduler" and even says "Only the classes Scheduler, Forecaster and Reporters are DataGenerator's"
(data_sources.py:437). But Scheduler does not subclass it — only Reporter and Forecaster do.
So the alternative is not a small change: it means a _config_schema/_parameters_schema split for
schedulers, _clean_parameters, and compute conforming to the base class.
Would it remove the nullable column? I do not think so, and that is the crux.
For a forecast automation, the data source is the choice the user made up front: this forecaster,
with this configuration. --source even lets you point at an existing one. Storing a pointer to it
is natural, and the config is static by construction — changing it yields a different source.
A schedule has no equivalent up-front choice:
- The scheduler class is resolved per run, from the asset, by
find_scheduler_class(asset_or_sensor),
or from aschedulerspec inside the flex config. The user never names it when creating the automation. - Its data source is created by the scheduling job, with the class version current at run time. Pinning
a generator source when the automation is created would freeze the class and its version, so an
automation would keep scheduling with an old scheduler version after an upgrade, unlike the same
trigger message sent through the API. - The config/parameters split does not map either. A trigger message is per-run input (
flex-model,
flex-context,duration), not static config. Treating it as config would fragment sources: a
soc-at-startthat differs per run would spawn a new data source each time.
So even with Scheduler as a DataGenerator, a schedule automation would have nothing to point at
when it is created, and generator_id would still have to be nullable — the check constraint
(type != 'forecasting' OR generator_id IS NOT NULL) is exactly the shape of that asymmetry.
Recommendation. Keep the nullable column here, and make Scheduler a DataGenerator on its own
merits, in its own PR — it changes how every scheduler data source is created, which is a scheduling
change rather than an automations one, and it would not be blocked by anything in this PR. Happy to
open an issue for it (or to do it first, if you would rather have that order).
There was a problem hiding this comment.
Discussing this in this comment thread instead.
I am interested to learn more about potential resolutions to these complexities.
Issue 1
The scheduler class is resolved per run, from the asset, by find_scheduler_class(asset_or_sensor),
or from a scheduler spec inside the flex config. The user never names it when creating the automation.
There is an existing mechanism for API users to pick the scheduling model, though.
Issue 2
Its data source is created by the scheduling job, with the class version current at run time. Pinning
a generator source when the automation is created would freeze the class and its version, so an
automation would keep scheduling with an old scheduler version after an upgrade, unlike the same
trigger message sent through the API.
Does it have to? Can't the automation create a new source when the code is updated to a new version of e.g. the StorageScheduler (if it is not pinned to a specific --source)?
Issue 3
The config/parameters split does not map either. A trigger message is per-run input (flex-model,
flex-context, duration), not static config. Treating it as config would fragment sources: a
soc-at-start that differs per run would spawn a new data source each time.
I imagine it should still be possible to:
- create a scheduling automation in case the flex-model and flex-context only contain sensor references (or fixed values, but no time series specs; and no fields like soc-at-start which are kind of slated for deprecation anyway), so they do form a static config. The duration should be part of the automation instead of the data source.
- or just use the Scheduler.source.id as the generator ID. I still don't see why it would need to be nullable?
There was a problem hiding this comment.
You are right on 1 and 2; my framing there was too absolute. Issue 3 is where I think the real
question sits, and it turns out to be sharper than I put it.
Issue 1 — conceded
find_scheduler_class reads custom-scheduler off the asset or sensor attributes
(data/services/scheduling.py:969), so the class is resolvable when the automation is created, not
only per run. My "the user never names it" was wrong.
One caveat that survives: it is an asset attribute, so it can be changed after the automation
exists. Resolving it once at creation time either freezes it (the automation keeps using the class
the asset named back then) or has to be re-resolved per run. Which is really your issue 2.
Issue 2 — conceded, at a price worth naming
Yes, the runner could re-resolve and write a new generator_id when __version__ changes. What
that costs is the column's meaning. For a forecast automation, generator_id is user intent: the
forecaster and config chosen at creation, optionally reused via --source, and documented as
required for as long as the automation exists (the source cannot be deleted). If the runner rewrites
it for schedules, the same column is user intent in one row and runner-maintained cache in the next
— including for that deletion rule, where the "protected" source would be one nobody chose.
That is a design cost, not an impossibility. It is a fair trade if it buys a meaningful column.
Which brings me to 3, where I no longer think it does — unless we do your 3.1.
Issue 3 — a scheduler data source carries no config today
This is the bit I under-stated. A scheduler's source is not built like a DataGenerator's:
DataGenerator.data_source(data/models/data_sources.py:244) puts the dumped config into
attributes["data_generator"]["config"], so the config is part of the source's identity — a
different config is a different source. That is what makesgenerator_idmeaningful for forecasts.Scheduler.get_data_source_info(data/models/planning/__init__.py:222) returns only
model(class name),version(__version__) andname(__author__). The call site,
make_schedule(data/services/scheduling.py:837and884), looks that up withget_data_source
and never passes attributes.
So today, every StorageScheduler schedule in an instance shares one data source per version,
whatever its flex-model. That makes your 3.2 possible but hollow: Scheduler.source.id would give us
a non-null column that records the algorithm and its version, and nothing at all about what the
automation actually computes. Non-nullable, but not the same column as for forecasts.
Your 3.1 is the version I would defend, precisely because it fixes that: if a schedule automation
requires a flex-model and flex-context free of per-run values (sensor references and fixed
quantities, no time series specs, no soc-at-start), then the flex config genuinely is static
config, and it belongs in the source's config attribute like any other DataGenerator. Timing
(start, duration, resolution) stays on the automation, which matches _clean_parameters, whose
fields_to_remove is already ["start", "end", "resolution", "belief_time"].
Two consequences to weigh before we commit to it:
- It changes scheduler provenance product-wide, not just for automations. Once the flex config
is part of source identity, scheduler sources fragment per distinct config, where today one source
per scheduler version serves everything. That affects anyone attributing or filtering saved
schedules by source id, and it is a change to scheduling, arriving through an automations PR.
(It also argues, separately, for filtering by source type rather than id.) - It makes automations a strict subset of triggers. A trigger message the API accepts could be
un-automatable, and we would owe a precise error naming the field that made it non-static. That is
defensible whilesoc-at-startis on its way out, but it is a real asymmetry, and worth being
deliberate about rather than discovering it in the error message.
Where that leaves us
My preference, in order:
- Land this PR as it stands (nullable +
type != 'forecasting' OR generator_id IS NOT NULL). The
constraint is a visible, temporary statement of the asymmetry rather than a hidden one. - Do
Scheduleras aDataGeneratorwith a real_config_schema— your 3.1 — in its own PR, on
its own merits, judged as the scheduling-provenance change it is. - When that lands, drop the constraint and make
generator_idNOT NULL for both types. One
migration, and the asymmetry is gone for the right reason.
What I would rather not do is 3.2 now: it removes the nullable column but leaves generator_id
meaning two different things depending on the row.
If you would rather not merge the asymmetry at all, I will do 3.1 first and rebase this PR onto it —
just say so, since it is the larger change and it touches how every scheduler source is created.
Happy to open the issue either way, with the two consequences above written up as the open questions.
There was a problem hiding this comment.
About the two consequences:
It changes scheduler provenance product-wide, not just for automations.
I see how this affects users/hosts using source filters. I was about to propose fragmenting the scheduler source only for use within automations, to not affect existing usage of the scheduler. However, I want to note that the scheduler source already updates from time to time, for instance, with every FM release that bumps the scheduler version. In other words, already regularly breaking installations that supposedly filter by scheduler source ID makes me less conservative about taking care not to break such installations.
It makes automations a strict subset of triggers.
I agree.
If you agree, please create a new PR on top of this PR's branch that implements 3.1 and resolves the asymmetry. Make sure the PR's base branch is feat/2288-schedule-automations rather than main.
There was a problem hiding this comment.
Agreed on the churn argument — a source id that already changes on every version bump is not a
stable thing to filter on, so config-fragmenting it product-wide is a difference of degree, not kind.
I withdraw that objection, and I would rather do it product-wide than only inside automations: a
scheduler source that means one thing for automated schedules and another for ad-hoc ones is the
same two-meanings problem we are trying to remove from generator_id.
One thing 3.1 has to decide before I write it, because it changes the design rather than the code.
Which config goes into the source?
A schedule's effective configuration is not in the trigger message. Scheduler.collect_flex_config
(data/models/planning/__init__.py:260) merges the passed flex-model and flex-context with
asset.get_flex_model() and asset.get_flex_context() from the asset tree, at compute time. So:
- If the source's config is the trigger message, this PR's flagship example —
duration: "PT12H"
on an asset that describes its own flexibility — has an empty config. Every minimal-trigger
automation in the instance would share one source while computing entirely different schedules.
That is exactly the hollowness I objected to in 3.2, moved one level up. - If the source's config is the resolved config, it is meaningful, and it changes whenever
somebody edits the asset's flex-model or flex-context through the ordinary asset API — nothing to
do with the automation.
I think the second is right, and that the consequence is a feature rather than a cost: if a battery's
power-capacity changes, schedules computed before and after should carry different sources. That
is what "config is part of source identity" means.
But it settles the shape of generator_id: it becomes runner-maintained, re-resolved per run,
exactly as you proposed in issue 2 — now for config drift as well as version drift. Non-null and
always meaningful, but derived rather than chosen, so the rule that an automation's generator cannot
be deleted needs rethinking for derived sources. I am fine with that; I want it to be a decision
rather than a side effect.
Two practical wrinkles, flagged not blocking
- Resolving the config when the automation is created means running the merge outside a compute call,
andScheduler.__init__warns that timing and flex-model are intertwined ("E.g. start and
flex_model[soc_at_start] are intertwined",planning/__init__.py:163). Forbidding per-run fields
should defuse this, but it is where I expect the sharp edges. - Making
generator_idNOT NULL needs the existing schedule-automation rows backfilled with a
resolved source, which is app logic in a migration. I would rather resolve them on the automation's
next run and tighten the column in a follow-up migration than run scheduler resolution inside
Alembic.
Unless you read the first point differently, I will implement it that way — resolved config, sources
fragmenting product-wide, generator_id derived and refreshed per run — in a PR based on
feat/2288-schedule-automations.
There was a problem hiding this comment.
Implemented in #2464, based on feat/2288-schedule-automations.
Two things came up while building it that changed the shape of the work, both now resolved in the PR:
A scheduling request spawns several schedulers. With the config in the source, each device job of
a sequential schedule resolved a source from its own slice of the flex-model, and
[GET] /sensors/(id)/schedules/(uuid) then answered UNKNOWN_SCHEDULE for every device after the
first — the request's job knows one source, and the device's schedule was under another. So
create_sequential_scheduling_job now resolves the request's source once and hands it to each device
job: one request, one generator run, one source.
The CLI hands over a deserialized flex config. flexmeasures add schedule passes quantities and
sensor objects, not their serialized form, so recording the config raised
TypeError: Object of type Quantity is not JSON serializable on nine tests. Values that JSON cannot
hold are now recorded by how they print, so the configuration is still described rather than lost.
On the deletion rule we discussed in the session: it is enforced by nothing but the plain foreign
key, and the honest change is to what the documentation claims it does, not to the schema. It now
reads as the source describing the automation's configuration, which is true for both types, rather
than as protecting a configuration the user chose.
Full suite green: 1788 passed, 12 skipped, 4 xfailed. Migration exercised up, down and up again on a
sandboxed Postgres.
Resolve the test_run_automations conflict by keeping main's freeze_server_now fixture, and rejoin the migration branches with a merge revision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs Signed-off-by: F.N. Claessen <claessen@seita.nl>
The scheduling chapter now points to the automations chapter, the way the forecasting chapter already does, and the details of what a schedule automation stores live next to the rest of the automations documentation. The changelog entry is merged with the one for forecasting automations, into a single entry on automations. That entry had stayed in the v1.0.0 section although PR #2290 was merged after v1.0.0 was tagged, so the merged entry moves to v1.1.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs Signed-off-by: F.N. Claessen <claessen@seita.nl>
Queue names and job types call these tasks "forecasting" and "scheduling", so the automation types now do too: 'forecasts' becomes 'forecasting' and 'schedules' becomes 'scheduling'. An automation's type is now the name of the queue its jobs go to, so the runner no longer maps one to the other. Automations were merged after v1.0.0 was tagged, so no released CLI or API surface changes. A migration renames the values of existing rows, in both directions, and recreates the check constraint that requires a data generator for forecast automations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs Signed-off-by: F.N. Claessen <claessen@seita.nl>
Like the automations entry it accompanied, it was written while v1.0.0 was the open section, but PR #2290 was merged on 2026-09-01, after the v1.0.0 tag of 2026-08-26. Neither the v1.0.0 nor the v1.0.0rc5 tag contains it, and it is the last entry in that section that postdates the tag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs Signed-off-by: F.N. Claessen <claessen@seita.nl>
Description
Automations can now compute schedules on a recurring basis, alongside forecasts. The automation
still only decides when work is due and queues it; the existing scheduling machinery computes the
schedule and records it.
Parameters are a trigger message. A schedule automation stores what the
[POST] /assets/(id)/schedules/triggerendpoint accepts, without the asset id, validated againstAssetTriggerSchemawhen the automation is created. Use the canonical API field names, includingflex-model,flex-contextandforce-new-job-creation.The schedule moves with the clock. Omit
startand each run schedules from its own run time,floored to the message's
resolutionwhen given and otherwise to the minute. A fixedstartisaccepted, but then every run computes the same period, so the CLI warns when creating such an
automation.
Minimal triggers inherit stored flex config. The flex-model and flex-context can live on the
asset tree instead of in the trigger message, so
duration: "PT12H"on its own is a completeautomation for an asset that already describes its own flexibility. The scheduler resolves the rest
the same way it does for a triggered schedule.
Task names. An automation's type now names the task, like the rest of the codebase does:
forecastingandscheduling, rather thanforecastsandschedules. The type is therefore also the name of the queue the jobs go to. A migration renames the values of existing rows.No data generator. A forecast automation points at a data source holding its forecaster
configuration; a schedule automation has nothing equivalent, so
generator_idis now nullable and adatabase constraint requires it only for forecasts.
Provenance. Scheduling jobs record how they came about in the same way forecasting jobs do:
{"origin": "automation", "automation_id": <id>}, so a scheduled job queued by an automation isdistinguishable from one triggered through the API or the CLI. Job statistics count scheduling jobs
for schedule automations, including the per-device jobs of a sequential schedule.
CLI.
flexmeasures add automation --type schedulingtakes the trigger message through--parameters. Forecaster options are refused for schedule automations — the check asks whichoptions were actually given on the command line, rather than comparing their values against their
defaults, so naming the default forecaster is refused too instead of passing silently. A
--parametersor--configfile that is not a mapping is reported as such rather than failingobscurely later.
UI. The Schedules tab on an asset's Automations page is now populated, with one listing per
automation type. Reports remain a disabled placeholder until #2297.
documentation/changelog.rstLook & Feel
The Schedules tab is now populated, with its own listing alongside the forecasts one. Here an hourly
schedule automation on a battery, recurring in Europe/Amsterdam. Reports remains a placeholder until
#2297:
How to test
See the manual test walkthrough in the PR comments.
Further improvements
and then looking for the fields that name where results go. That agrees with the scheduler's own
resolution for the shapes that matter — a device's
sensor, and itsconsumptionandproductionoutputs — and errs towards reporting more rather than fewer, which is the safedirection once the same resolution guards who may create an automation (CRUD for automations in the API and UI #2294). Checking outputs
against what a scheduler actually returns at run time is tracked in Check the sensors a scheduler actually writes to, instead of predicting them when an automation is created #2421.
occurrence is worth scheduling at all once its window has passed — is still open.
Related items
Closes #2370. Part of the automations story #2334. Stacked on #2290, followed by #2294.
Sign-off