feat(flow): JSONLogic expressions, plus Filter and Wait (#2069)#2079
Merged
Conversation
… 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.
Expressions are how one step reads another's output. Without them a flow can move data but never decide anything about it. Uses `jwadhams/json-logic-php`, which openconnector already relies on for synchronisation and endpoint conditions — moved into OpenRegister so the engine does not depend on a leaf app. One expression language for the fleet. An expression sees a flow-shaped document: the current item's `json` and `binary`, its index, the step's item count, the run context and the subject. So one authored expression applies per item, with no loop drawn by the author. Decisions worth keeping: - An expression that cannot be evaluated returns null, and a condition treats null as FALSE. A branch whose condition failed must not be taken, and throwing would abort a run mid-graph with side effects half applied. The failure belongs at save time instead — `validateConfig()` rejects it. - A date operator given nonsense returns null, never "now". Silently substituting the current time would corrupt data. - `FilterNode` re-pairs survivors to their ORIGINAL input index, so an item can still be traced back after the drop. - `WaitNode` runs TWICE: the marking does not advance past a suspended step, so it sees `context.resuming` on the way back and passes items through. A time that cannot be read at run time passes through rather than suspending on a moment that will never arrive. The custom operator set is deliberately small — strings, dates, arrays, coalesce/toJson/fromJson. Each exists because its absence would push an author toward arbitrary code, which is the gap this whole decision keeps closed. What JSONLogic genuinely cannot do stays undone until the sandboxed sidecar (#2066). 88 tests green (21 new), phpcs clean.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ❌ | ✅ 174/174 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-07-24 15:13 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposal: or-flow-expressions
Summary
Give flows an expression engine, and the first three nodes that need one.
Why
Expressions are how one step reads another's output — what a condition
branches on and what a field is set from. Without them a flow can only move
data, never decide anything about it.
Decision: JSONLogic, and the gap that leaves
jwadhams/json-logic-php, which openconnector already uses forsynchronisation and endpoint conditions. One expression language for the
fleet, not two. The dependency moves to OpenRegister so the engine does not
depend on a leaf app.
n8n uses a full JavaScript engine. Matching that means running user-authored
code inside the Nextcloud process — full
OC\Server, database and filesystemaccess from a text field in a flow editor. That trade is refused.
So JSONLogic is a ceiling by decision, and it genuinely cannot express
loops with state, parsing, or crypto. The route to those is the optional
sandboxed sidecar (#2066), never a relaxation here.
What Changes
FlowExpression— evaluation with a flow-shaped data document:json,binary,itemIndex,itemCount,context,subject.trim, split/join, replace, regex match; date formatting and arithmetic;
array unique/sort/length;
coalesce,toJson,fromJson. Deliberatelysmall — each exists because its absence would push an author toward a Code
node.
FilterNode— keeps the items whose condition holds. The simplest proofthat an item list is a list.
WaitNode— the node run persistence was built for. Suspends on the way in,passes through on the way back.
Design decisions
could not be evaluated must not be taken.
fail their condition, not abort a run mid-graph with side effects half
applied. The failure belongs at save time —
isValid(), called fromvalidateConfig().survives the drop.
the node sees
context.resumingon the way back and lets items through. Anunreadable time at run time passes through rather than suspending forever.
Out of scope
If/Switchmulti-branch routing, which needs labelled outputs on an edge(Flow parity: take over n8n's flow logic (If/Switch, Merge, Loop, Wait, error branch, sub-flows) #2067).
Verification
88 unit tests green (21 new),
phpcsclean. Covers scope (json/itemIndex/itemCount/context), falsiness of unevaluable conditions, save-time validity, every custom operator, filter provenance across a drop, matching nothing as a legitimate outcome, and Wait's suspend / resume / bare-seconds / absolute-moment paths.Builds on #2078 (run persistence), which
WaitNodedepends on entirely.🤖 Generated with Claude Code