Automations - first roundtrip for forecasts - #2290
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
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Documentation build overview
138 files changed ·
|
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
…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>
Manual test walkthroughA reviewer can exercise the whole roundtrip with this. It is written against this branch only: Start a worker in a second terminal, and pick a sensor that already holds history so a forecast has flexmeasures jobs run-worker --name automations-demo --queue forecasting1. The command explains itselfflexmeasures add automation --helpThe help stays focused on the automation: the asset, the name, the recurrence and its timezone, the 2. Validation refuses what could never work# a recurrence that matches no real date
flexmeasures add automation --asset 242 --name "Impossible" --cron "0 0 30 2 *" --sensor 913
# → Error: Invalid value for '--cron': '0 0 30 2 *' does not match any possible date.
# six fields instead of five
flexmeasures add automation --asset 242 --name "Six fields" --cron "0 0 * * * *" --sensor 913
# → Error: ... must contain exactly five fields (minute, hour, day of month, month, and day of week).
# a timezone that does not exist
flexmeasures add automation --asset 242 --name "Bad tz" --cron "0 6 * * *" \
--timezone Europe/NotAmsterdam --sensor 913
# → Error: Invalid value for '--timezone': Timezone 'Europe/NotAmsterdam' does not exist.Nothing is created by any of these. The first is the interesting one: 3. Create one, inactive, and read it backflexmeasures add automation \
--asset 242 \
--name "Campus power forecast" \
--cron "0 6 * * *" \
--timezone Europe/Amsterdam \
--inactive \
--sensor 913
# → Successfully created inactive automation '...' (ID: N) to compute forecasts for asset 242,
# recurring per cron string '0 6 * * *' in timezone 'Europe/Amsterdam'.The listing is sortable and shows the recurrence in natural language alongside the timezone it is Details shows what the automation would read and write. Both are sensor 913 here, because a The same information is available over the API:
The detail response carries
4. The recurrence belongs to the automation, not the serverflexmeasures edit automation --id N --timezone Asia/Seoul
# → Successfully updated automation '...' (ID: N): timezone: 'Europe/Amsterdam' → 'Asia/Seoul'.
flexmeasures edit automation --id N --timezone Europe/Amsterdam5. Run itflexmeasures edit automation --id N --cron "* * * * *" --activate
flexmeasures jobs run-automations
# → Automation N ('Campus power forecast') queued 1 forecasting job(s) for asset 242.Run it again within the same minute: flexmeasures jobs run-automations
# → the occurrence was already claimed; nothing is queuedThat is the at-most-once guard. An occurrence is claimed before it is queued, and the claim also 6. See where the job came fromJobs record how they were created — via the CLI, the API, or an automation, with the automation's Once the worker has run it, the forecast is ordinary belief data: The sensor's page also lists the automations that write to it, and its data source can be inspected 7. Catching up after downtimeLeave the automation active and due, but stop running the runner for a few minutes. Then run it A newly created automation starts from its own creation and does not replay history, and the cursor 8. Clean upflexmeasures edit automation --id N --deactivate
flexmeasures delete automation --id N --forceDeleting an automation removes only its definition. Forecasts it already produced are ordinary What this deliberately does not cover
|
|
@Flix6x can you review this |
Flix6x
left a comment
There was a problem hiding this comment.
Note, I'm just posting these comments, and will pick up a first round of changes myself.
Flix6x
left a comment
There was a problem hiding this comment.
Note, I'm just posting these comments, and will pick up a first round of changes myself.
Flix6x
left a comment
There was a problem hiding this comment.
Note, I'm just posting these comments, and will pick up a first round of changes myself.
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>
| Here is how you create an automation in the CLI, asking for daily (at 6 AM) forecasts of sensor 12: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| flexmeasures add automation --asset 3 --name "Daily PV forecasts" --cron "0 6 * * *" --timezone Europe/Amsterdam --sensor 12 |
There was a problem hiding this comment.
There is nothing in the CLI command that tells it that it should be forecasting, which feels off. I imagine this example will be changed in ongoing follow-up PRs when flexmeasures add automation can be used for scheduling and reporting, too. Perhaps those PRs offer a clue as to how this example could be made more explicit within this PR already.
There was a problem hiding this comment.
Made explicit: the example now passes --type forecasts. That option already exists on this branch (default forecasts, click.Choice(Automation.SUPPORTED_TYPES)), and it is exactly what #2293 and #2297 use to select schedules and reports — so spelling it out here is the clue you expected, rather than something new. 4051dc2
| The stored data generator is required while the automation exists, so its data source cannot be deleted until the automation is removed. | ||
|
|
||
| The forecaster and its configuration are stored on a data source. | ||
| Pass ``--source`` to reuse the data source of an existing forecaster, in which case ``--forecaster`` and ``--config`` (and the individual configuration options) are not needed — the data source already determines them. |
There was a problem hiding this comment.
Up until this point, it wasn't clear that --forecaster and --config were needed, so this comes as a bit of a surprise.
There was a problem hiding this comment.
Introduced them before they are waved away: the paragraph now says a forecast automation accepts everything flexmeasures add forecast accepts, naming --forecaster and --config, and only then that --source makes them unnecessary. 4051dc2
| The runner is a stateless command, executed once a minute by cron (see below), so it needs a durable record of how far each automation has got. | ||
| That record is one UTC timestamp per automation, its *cursor*: the scheduled time of the most recent run the automation has committed to. | ||
| Runs at or before the cursor are never queued again. | ||
| Before queueing any jobs, the runner advances the cursor to the run it is about to queue, and saves it. | ||
| The cursor therefore records that a run was claimed, not that queueing or the forecast itself succeeded. | ||
|
|
||
| Keeping a single moving timestamp, rather than a record per run, is what makes the behaviour below fall out: a runner that has been down catches up by moving the cursor straight to the latest due run, and two runners started in the same minute cannot queue the same run twice, because the cursor is advanced with a conditional update that only one of them can win. | ||
|
|
||
| A new automation starts from its creation minute and does not replay runs from before it existed. | ||
| Changing its cron expression or timezone, or reactivating it, restarts from the time of that change. | ||
| Deactivated automations do not accumulate catch-up work. | ||
| After upgrading an existing installation, runs scheduled before the upgrade are not replayed. | ||
|
|
||
| If the runner misses one run, it queues that run once when it resumes. | ||
| If it misses several forecast runs, it queues only the latest one: moving the cursor straight to that run leaves the older ones behind, rather than replaying stale forecasts. | ||
| Timing parameters that default to the run time are resolved when this catch-up run is actually queued, producing a current forecast. | ||
|
|
||
| Daylight-saving-time transitions follow wall-clock semantics. | ||
| If the clock skips a scheduled local time in spring, that run happens once at the transition boundary. | ||
| If a scheduled local time occurs twice in autumn, the first instance is the canonical run and the repeated instance is not queued again. |
There was a problem hiding this comment.
Most of this feels like a developer's note more worthy of being in an appendix than in the main body explaining forecasting automations. It isn't even specific to forecasting.
There was a problem hiding this comment.
Agreed, and you are right that it is not forecasting-specific. The cursor mechanics and the daylight-saving-time rules are now an appendix on the new page, prefaced with a note that it is bookkeeping you do not need in order to use automations. The main body keeps only the user-visible consequence: after downtime, just the latest missed run is queued. 4051dc2
| Each due automation then queues its forecasting jobs. | ||
| Each scheduled run receives at most one automatic queueing attempt. | ||
| If the process crashes, or queueing fails after creating some jobs, that run is not retried automatically, because a retry could duplicate partial work. | ||
| Recording each run and its outcome, which is what safe retries would need, is out of scope here (see `issue #2393 <https://github.com/FlexMeasures/flexmeasures/issues/2393>`_). |
There was a problem hiding this comment.
This reads like a todo. Should it really be in the docs?
There was a problem hiding this comment.
Dropped — it was a todo, and belongs on the issue rather than in the docs. I kept the preceding sentence, that a failed attempt is not retried automatically, since that is behaviour users have to plan around. 4051dc2
| .. _automating_forecasts: | ||
|
|
||
| Automating forecasts | ||
| -------------------- |
There was a problem hiding this comment.
I think this whole section is better suited in features/automations.rst, since most of it will apply to scheduling and reporting automations, too, in the future.
There was a problem hiding this comment.
Moved to documentation/features/automations.rst, and rewritten to talk about automations in general with forecasts as today's only type. features/forecasting.rst keeps a short pointer under the same automating_forecasts label, and the new page is in the Features toctree. 4051dc2
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>
…ations Context: - Picking up the review changes from #2290, in which scheduling_cursor became cursor, automation "occurrences" became "runs", and the automations documentation was pulled out of the forecasting page. - This branch already has a features/automations.rst of its own, covering all three automation types, how to manage them and who may, which is the better page. Change: - Kept this branch's automations page and dropped the one #2290 added, folding in the part only that one had: an appendix on the cursor, catch-up and daylight-saving-time rules. - Replaced the claim that a Redis guard is what prevents queueing twice, which predates the durable cursor from #2396, with the at-most-once and catch-up behaviour. - Kept a forecast-specific section in features/forecasting.rst, since the automations page links back to it, now passing --type forecasts explicitly and introducing --forecaster and --config before saying --source makes them unnecessary. - Fixed two calls to _canonical_occurrence_time, renamed to _canonical_run_time upstream, which git merged without a conflict and which would have raised NameError. - Removed a leftover stub of _asset_and_ancestor_ids, replaced upstream by the shared helper in data/queries/generic_assets. - Merged the CLI changelog entries and regenerated the OpenAPI specs. Signed-off-by: F.N. Claessen <felix@seita.nl>
…s-ui-polish Context: - Picking up the review changes from #2290 and the automations documentation page as it now stands. Change: - Kept this branch's wording for the automations listing endpoint, which describes its batched job counts, with the cursor no longer called a scheduling cursor. - Kept both ancestor and descendant query helpers in the automations service imports. - Dropped the API changelog entry for timezone and cursor, folded into the endpoint entry upstream, and kept this branch's job_stats entry. - Moved the automation management endpoints from v3.0-32 to v3.0-33, so all of this unreleased automation work is described in one version section. Signed-off-by: F.N. Claessen <felix@seita.nl>
…plates Context: - Picking up the review changes from #2290 and the automation work below this branch. - Both sides had a check that refuses forecaster options on a schedules automation, in two generations: this branch still guessed from configuration values that differ from their default, while upstream only counts options actually given on the command line. Change: - Kept this branch's report template handling and upstream's stricter check, rather than either side wholesale, which would have dropped one or reverted the other. - 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>


Description
An automation is a recurring task defined on an asset. This first roundtrip covers forecasts: the
automation decides when work is due and queues it; the existing forecasting worker still computes
the forecast and stores it as timed beliefs. Nothing about how a forecast is computed changes.
Data model. A new
automationtable, with per-asset ownership (cascade delete), atype(
forecastsfor now), aname, acronstrrecurrence, an IANAtimezonein which that recurrenceis interpreted, a
cursor, anactiveflag, agenerator_idpointing at the data sourcethat holds the forecaster configuration, and JSONB
parametersfor the per-run forecast parameters.CLI.
flexmeasures add automation— create one, defaulting to daily at midnight inFLEXMEASURES_TIMEZONE. The forecaster configuration is stored on a data source; the forecastparameters are validated and stored on the automation.
--sourcereuses an existing forecaster'sdata source, in which case the options that source already determines are refused.
flexmeasures edit automation— rename, re-schedule (--cron), change the--timezone, activateor deactivate.
flexmeasures delete automationflexmeasures jobs run-automations— queue jobs for whatever is due. Run once per minute fromcron or another host scheduler.
Recurrence and timezones. Each automation's cron expression is interpreted in its own IANA
timezone, so moving the server or changing
FLEXMEASURES_TIMEZONEdoes not silently rescheduleexisting automations. Daylight saving is handled explicitly: a local time skipped in spring runs once
at the transition boundary, and a local time that occurs twice in autumn is not queued twice.
Catching up. Each automation stores a UTC cursor, so restarting the runner does not
lose runs that fell during downtime. Several missed runs are coalesced into the latest
useful forecast rather than replayed one by one, and timing parameters that default to the run time
resolve when the caught-up job is queued, so the result is a current forecast. A new automation
starts from its own creation and does not replay history.
At most once. A run is claimed before it is queued, with a compare-and-swap that also
requires the recurrence, the timezone and the active flag to be unchanged since the run was
read. Concurrent runners therefore cannot queue the same run twice, and an edit made between
reading and claiming invalidates the claim rather than clobbering the rebased cursor. A failed
attempt is not retried automatically, because it may already have queued some jobs — durable run
records and safe retries are tracked in #2393.
Validation. A recurrence must be a five-field cron expression that matches at least one real
date, so
0 0 30 2 *is refused rather than accepted as an automation that can never fire. Atimezone must be a real IANA name.
API.
[GET] /assets/(id)/automationsand[GET] /assets/(id)/automations/(automation_id)listand inspect an asset's automations, including the sensors each reads from and writes to, its
timezone and cursor, and counts of recently created jobs per status. A new
[GET] /sources/(id)returns the full record of one data source, including the attributes wheredata generators keep their configuration. Asset job entries carry
created_viaprovenance.UI. The asset gains an Automations page: a sortable listing, and a details modal linking to the
sensors an automation reads from and writes to. A sensor's own page lists the automations that feed
it, and its data source can be inspected as a full record.
Provenance. Every queued job records how it came about — via the CLI, the API, or an automation
(with the automation's id) — so the status page can tell them apart.
documentation/changelog.rstLook & Feel
Automations defined on an asset get their own page. The listing is sortable, describes each
recurrence in natural language, and shows the timezone that recurrence is interpreted in. The
Schedules and Reports tabs are placeholders until #2293 and #2297:
An automation's details show the timezone its cron expression is interpreted in, its cursor, the
data generator holding the forecaster configuration, and the sensors it reads from and writes to,
each linking to its own page. Recently created jobs are counted per status:
How to test
See the manual test walkthrough in the PR comments. In short: create an automation with
flexmeasures add automation, activate it, runflexmeasures jobs run-automations, and confirm aforecasting job was queued and appears on the asset's status page.
Automated coverage:
Further improvements
the at-most-once guard rather than risking duplicate jobs.
FlexMeasures schedules the runner inside the RQ ecosystem, is a natural follow-up.
restricting them to sensors their creator can access, follows in CRUD for automations in the API and UI #2294.
Related items
Closes #2288. Closes #2392 (timezone-aware recurrence and catch-up, merged in from #2396).
Part of the automations story #2334. Followed by #2293 (schedules as automations).
Sign-off