Skip to content

Automations - first roundtrip for forecasts - #2290

Open
Flix6x wants to merge 79 commits into
mainfrom
feat/2288-automations-for-forecasts
Open

Automations - first roundtrip for forecasts#2290
Flix6x wants to merge 79 commits into
mainfrom
feat/2288-automations-for-forecasts

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 11, 2026

Copy link
Copy Markdown
Member

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 automation table, with per-asset ownership (cascade delete), a type
(forecasts for now), a name, a cronstr recurrence, an IANA timezone in which that recurrence
is interpreted, a cursor, an active flag, a generator_id pointing at the data source
that holds the forecaster configuration, and JSONB parameters for the per-run forecast parameters.

CLI.

  • flexmeasures add automation — create one, defaulting to daily at midnight in
    FLEXMEASURES_TIMEZONE. The forecaster configuration is stored on a data source; the forecast
    parameters are validated and stored on the automation. --source reuses an existing forecaster's
    data source, in which case the options that source already determines are refused.
  • flexmeasures edit automation — rename, re-schedule (--cron), change the --timezone, activate
    or deactivate.
  • flexmeasures delete automation
  • flexmeasures jobs run-automations — queue jobs for whatever is due. Run once per minute from
    cron 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_TIMEZONE does not silently reschedule
existing 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. A
timezone must be a real IANA name.

API. [GET] /assets/(id)/automations and [GET] /assets/(id)/automations/(automation_id) list
and 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 where
data generators keep their configuration. Asset job entries carry created_via provenance.

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.

  • Added changelog item in documentation/changelog.rst

Look & 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:

The Automations page of an asset, listing forecast automations with their recurrence, timezone and job counts, with the Schedules and Reports tabs disabled

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:

The details of one automation, showing its timezone, cursor, data generator, the sensors it reads from and writes to as links, its stored parameters, and its recent job counts

How to test

See the manual test walkthrough in the PR comments. In short: create an automation with
flexmeasures add automation, activate it, run flexmeasures jobs run-automations, and confirm a
forecasting job was queued and appears on the asset's status page.

Automated coverage:

pytest \
  flexmeasures/cli/tests/test_automations.py \
  flexmeasures/data/tests/test_automations_fresh_db.py \
  flexmeasures/data/tests/test_automation_scheduling_fresh_db.py \
  flexmeasures/data/schemas/tests/test_automations.py \
  flexmeasures/api/v3_0/tests/test_automations_api.py

Further improvements

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

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on code under GPL or another incompatible license.

Flix6x and others added 7 commits July 11, 2026 15:06
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
@socket-security

socket-security Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcron-descriptor@​2.1.0100100100100100

View full report

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rbix8k1JfeUWNXEmHEZVpX
Flix6x and others added 3 commits July 11, 2026 16:09
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
@BelhsanHmida
BelhsanHmida self-requested a review July 31, 2026 01:22
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>
@BelhsanHmida

Copy link
Copy Markdown
Contributor

Manual test walkthrough

A reviewer can exercise the whole roundtrip with this. It is written against this branch only:
schedules and reports are still "coming soon", and automations are created from the CLI
(administering them through the API and UI arrives in #2294).

Start a worker in a second terminal, and pick a sensor that already holds history so a forecast has
something to train on. The examples use asset 242 and its power sensor 913.

flexmeasures jobs run-worker --name automations-demo --queue forecasting

1. The command explains itself

flexmeasures add automation --help

The help stays focused on the automation: the asset, the name, the recurrence and its timezone, the
type, and how the forecaster is configured. Every option flexmeasures add forecast accepts is
still accepted, but is kept out of the help so it does not bury the options that describe the
automation itself.

Options:
  --asset GENERICASSETIDFIELD  ID of the asset to automate a recurring task
                               for.  [required]
  --name TEXT                  Name of the automation.  [required]
  --cron CRONFIELD             Recurrence as a standard five-field cron
                               expression, e.g. "0 6 * * *" for daily at
                               06:00. The expression is interpreted in the
                               automation timezone. Defaults to daily at
                               midnight.  [default: 0 0 * * *]
  --timezone TIMEZONEFIELD     IANA timezone in which to interpret --cron,
                               e.g. "UTC" or "Europe/Amsterdam". Defaults to
                               FLEXMEASURES_TIMEZONE.  [default:
                               (FLEXMEASURES_TIMEZONE)]
  --type [forecasts]           Type of task to automate.  [default: forecasts]
  --inactive                   Add this flag to create the automation in
                               deactivated state.
  --forecaster TEXT            Forecaster class registered in
                               flexmeasures.data.models.forecasting or in an
                               available flexmeasures plugin. Defaults to
                               TrainPredictPipeline. Use the command
                               `flexmeasures show forecasters` to list all the
                               available forecasters. Cannot be combined with
                               --source, which already determines the
                               forecaster.
  --source DATASOURCEIDFIELD   DataSource ID of the `Forecaster`. The
                               forecaster class and its configuration are read
                               from the data source's data generator
                               attributes, so --forecaster and --config are
                               not needed (or allowed) with it.
  --config FILENAME            Path to the JSON or YAML file with the
                               configuration of the forecaster. Cannot be
                               combined with --source, which already
                               determines the configuration.
  --parameters FILENAME        Path to the JSON or YAML file with the forecast
                               parameters (passed to the compute step on each
                               run of the automation).
  --help                       Show this message and exit.

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: 0 0 30 2 * is a perfectly
well-formed cron expression, and without this check it would be accepted as an automation that can
never fire.

3. Create one, inactive, and read it back

flexmeasures 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'.
/assets/242/automations

The listing is sortable and shows the recurrence in natural language alongside the timezone it is
interpreted in. On this branch the Schedules and Reports tabs are present but disabled.

Details shows what the automation would read and write. Both are sensor 913 here, because a
forecast is saved to the sensor it forecasts unless --sensor-to-save says otherwise, and each
sensor links to its own page.

The same information is available over the API:

/api/v3_0/assets/242/automations              # the listing
The automations listing over the API
/api/v3_0/assets/242/automations/<N>          # one automation, in full

The detail response carries input_sensors, output_sensors, timezone, scheduling_cursor,
recurrence_description, the generator holding the forecaster configuration, the stored
parameters, and job_stats counting recently created jobs per status.

One automation in full, over the API

4. The recurrence belongs to the automation, not the server

flexmeasures edit automation --id N --timezone Asia/Seoul
# → Successfully updated automation '...' (ID: N): timezone: 'Europe/Amsterdam' → 'Asia/Seoul'.

0 6 * * * in Asia/Seoul is 21:00 UTC the day before, so moving the server or changing
FLEXMEASURES_TIMEZONE does not silently reschedule anything. Changing the timezone also rebases
the scheduling cursor, so the automation does not immediately catch up on occurrences that only
exist in the new timezone's past. Put it back:

flexmeasures edit automation --id N --timezone Europe/Amsterdam

5. Run it

flexmeasures 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 queued

That is the at-most-once guard. An occurrence is claimed before it is queued, and the claim also
requires the recurrence, the timezone and the active flag to be unchanged since it was read — so two
runners cannot queue the same occurrence, and an edit made in between invalidates the claim instead
of being overwritten.

6. See where the job came from

/assets/242/status

Jobs record how they were created — via the CLI, the API, or an automation, with the automation's
id — so a job queued here is distinguishable from one triggered by hand.

Once the worker has run it, the forecast is ordinary belief data:

/sensors/913

The sensor's page also lists the automations that write to it, and its data source can be inspected
as a full record.

7. Catching up after downtime

Leave the automation active and due, but stop running the runner for a few minutes. Then run it
once. It queues a single job, not one per missed minute: several missed occurrences are coalesced
into the latest useful forecast, and timing parameters that default to the run time resolve when the
job is queued, so the result is a current forecast rather than a replay of an old window.

A newly created automation starts from its own creation and does not replay history, and the cursor
lives in the database, so restarting the runner does not lose due occurrences.

8. Clean up

flexmeasures edit automation --id N --deactivate
flexmeasures delete automation --id N --force

Deleting an automation removes only its definition. Forecasts it already produced are ordinary
beliefs and stay.

What this deliberately does not cover

@BelhsanHmida

Copy link
Copy Markdown
Contributor

@Flix6x can you review this

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note, I'm just posting these comments, and will pick up a first round of changes myself.

Comment thread documentation/api/change_log.rst Outdated
Comment thread documentation/api/change_log.rst Outdated
Comment thread documentation/cli/change_log.rst Outdated
Comment thread documentation/cli/change_log.rst Outdated
Comment thread documentation/cli/commands.rst Outdated
Comment thread documentation/cli/commands.rst Outdated

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note, I'm just posting these comments, and will pick up a first round of changes myself.

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note, I'm just posting these comments, and will pick up a first round of changes myself.

Flix6x added 11 commits August 31, 2026 17:38
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>
Comment thread documentation/features/forecasting.rst Outdated
Comment on lines +229 to +233
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Comment thread documentation/features/forecasting.rst Outdated
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Up until this point, it wasn't clear that --forecaster and --config were needed, so this comes as a bit of a surprise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Comment thread documentation/features/forecasting.rst Outdated
Comment on lines +248 to +267
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Comment thread documentation/features/forecasting.rst Outdated
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>`_).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This reads like a todo. Should it really be in the docs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Comment on lines +217 to +220
.. _automating_forecasts:

Automating forecasts
--------------------

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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>
Flix6x added a commit that referenced this pull request Sep 1, 2026
…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>
Flix6x added a commit that referenced this pull request Sep 1, 2026
…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>
Flix6x added a commit that referenced this pull request Sep 1, 2026
…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>
Flix6x added a commit that referenced this pull request Sep 1, 2026
…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>
Flix6x added a commit that referenced this pull request Sep 1, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automations - first roundtrip for forecasts Add per-automation timezones and catch-up semantics

2 participants