Skip to content

feat(flow): persist runs — the critical path for five workstreams (#2076)#2078

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/or-flow-runs
Jul 24, 2026
Merged

feat(flow): persist runs — the critical path for five workstreams (#2076)#2078
rubenvdlinde merged 1 commit into
developmentfrom
feat/or-flow-runs

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Proposal: or-flow-runs

Summary

Make a flow run outlive the request that started it.

Why

FlowEngine takes a MarkingStoreInterface and the engine change (#2064)
deferred persisting the marking. That single deferral is what stands between us
and five separate workstreams:

Blocked Why
Wait A run that pauses must survive the request that started it
Sub-flows A parent must be suspended while a child runs
Run history Nothing to list if runs are not stored
Retry Cannot re-run what was not recorded
The Nextcloud Flow bridge A Flow operation runs inside the dispatch of the event that triggered it — often a file write — and a graph must not block that

Five things, one dependency. It is built first, not alongside.

What Changes

  • FlowRun + FlowRunMapper + openregister_flow_runs. A run stores its
    marking, its items, its context and its log.
  • FlowRunMarkingStore — a MarkingStoreInterface backed by the run row, so
    the marking survives the request.
  • FlowSuspension — thrown by a step that wants the run paused. An exception,
    not a return value: a node returns ITEMS, and smuggling "please suspend" into
    that return would mean every node author had to know about a magic item
    shape, with a forgetful node silently continuing.
  • FlowEngine::STATUS_SUSPENDED, caught before the generic Throwable
    handler so a step's onError policy never sees a pause. continue would
    otherwise skip straight past a Wait, which is the opposite of waiting.
  • FlowRunService — queue, execute, resume, persist.
  • FlowRunWorker — a per-minute job that starts queued runs, resumes due ones,
    and prunes old ones.

Design decisions

  • The marking does not advance past a suspended step. The run must resume
    ON that transition, re-entering the step that asked to wait. Advancing would
    skip it.
  • Resume uses the stored items, never a re-seed. Re-seeding from the
    subject would discard everything earlier steps produced.
  • A terminal run is never re-executed. Retry creates a new run; re-running
    the old one would repeat every side effect it already performed.
  • Retention ships in this change, not after it. Runs are operational data
    and grow without bound; this instance has already been taken down once by a
    file nobody was pruning. Default 30 days, 0 disables.

Out of scope (this change)


Verification

67 unit tests green in tests/Unit/Service/Flow (11 new), phpcs clean on every new file. The tests cover the properties that actually matter: a suspended run keeps its place in the graph, resuming carries the stored items rather than re-seeding, resumeAt is cleared once no longer suspended, the log accumulates across a pause, a terminal run is never re-executed, and a malformed flow fails the run instead of leaving it running.

Unblocks #2067 (Wait, sub-flows), #2070 (history, retry), #2068 (triggers), and lets the existing Nextcloud Flow bridge execute instead of running inline.

🤖 Generated with Claude Code

… them

The critical path of the parity programme (#2076). Five workstreams were
waiting on this one deferral: Wait, sub-flows, run history, retry, and the
Nextcloud Flow bridge executing anything at all.

- `FlowRun` + mapper + `openregister_flow_runs`, storing the marking, the
  items, the context and the log.
- `FlowRunMarkingStore` puts the marking on the RUN rather than the subject.
  Two runs over one object hold two independent markings, which is exactly why
  Symfony's subject-property stores do not fit.
- `FlowSuspension` — a step pauses the run by throwing. An exception rather
  than a return value on purpose: a node returns ITEMS, and smuggling "please
  suspend" into that return would force every node author to know about a
  magic item shape, with a forgetful node silently continuing instead.
- The engine catches suspension BEFORE the generic Throwable handler, so a
  step's `onError` policy never sees a pause — `continue` would otherwise skip
  straight past a Wait, which is the opposite of waiting. The marking is not
  advanced, so the run resumes ON the step that asked to wait.
- `FlowRunService` — queue / execute / resume. Resuming uses the stored items,
  never a re-seed, or everything earlier steps produced would be discarded. A
  terminal run is never re-executed; retry creates a new one.
- `FlowRunWorker` — per-minute job draining the queue, waking due runs, and
  pruning terminal ones. Retention ships here rather than later: runs grow
  without bound and this instance has already been taken down once by a file
  nobody was pruning.

Only a run with a `resumeAt` is woken on a timer. One waiting on a signal — a
child run, a webhook — has none, and waking it on a clock would run it before
what it waits for arrives.

67 tests green, phpcs clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 64b973a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 173/173
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-07-24 14:42 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 7535855 into development Jul 24, 2026
16 of 24 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/or-flow-runs branch July 24, 2026 14:57
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.

1 participant