feat(flow): a node that reads and writes flow state, with a capacity claim - #2221
Merged
Conversation
…claim #2219 gave a flow somewhere to remember things and #2220 let nodes reach it, but nothing could WRITE it from a graph — a flow author still had to drop into PHP. This is the node that closes that. get / set / forget are the obvious three. `claim` is the one worth having. A capacity cap — hydra's "at most ten pipelines at once", a booking, a lease — is a map of named slots where a free one must be taken by exactly one holder. Expressed with get and set that is a read-modify-write across two steps, and that is precisely the shape that lost writes in #2212. `claim` does the whole thing in one step and emits `claimed: false` rather than pretending it succeeded, so a router can branch on it and the flow author decides whether no capacity means wait, stop or escalate. `release` takes a slot number when the caller has one, and falls back to the holder when it does not — a stage that crashed knows who it was, not which slot it got. A run with NO flow state throws rather than treating every key as empty. Absent state read as "nothing claimed" would make a capacity cap wave everything through, which is the worst possible failure for this node.⚠️ Documented in the class, because it will be the first question: this is not a cross-flow lock. What makes it safe is that a scheduled flow never overlaps itself (#2218) — the same property the shell orchestrator being replaced relies on, where one supervisor holds a flock and its own slot bookkeeping is a plain check-then-write, safe only because nothing runs beside it. For coordination ACROSS flows, write an object with `onConflict: fail` and let the database arbitrate. VERIFIED LIVE, node resolved from the registry rather than constructed by hand: flow-state registered: true claim job-1 -> slot 1 claim job-2 -> slot 2 claim job-3 -> slot 3 claim job-4 -> claimed=false, slot=NULL <- the cap holds release slot 2, then job-5 -> slot 2 <- freed slot is reused Gates: phpcs clean, phpstan OK, 15,539 unit tests green (11 new). Part of #2216. This is the primitive hydra#425 task 3.5 is built on.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 555/555 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-07-31 07:36 UTC
Download the full PDF report from the workflow artifacts.
rubenvdlinde
added a commit
that referenced
this pull request
Jul 31, 2026
#2222) Flow state has existed since #2219 and nodes could write it from #2221, but nothing outside the engine could READ it — a slot table was live data nobody could show. GET /api/flow/{flowId}/state closes that. A flow with no state yet returns an empty map rather than 404. "Nothing claimed" is a perfectly good answer, and a widget should not have to special-case a flow's first tick to avoid rendering an error. This is what makes the slot dashboard possible: with the claim node writing {"slots": {"1": "issue-101", "2": null, "3": "issue-207"}}, a widget can now show which slot is running what without shelling onto the host — which is how the shell orchestrator's SLOT_DIR files could only ever be inspected. VERIFIED LIVE against the running instance: GET /api/flow/api-demo-flow/state {"id":16,"flowId":"api-demo-flow", "state":{"slots":{"1":"issue-101","2":null,"3":"issue-207"},"cursor":42}, "updated":"2026-07-31T07:34:36+00:00"} GET /api/flow/never-run-flow/state {"flowId":"never-run-flow","state":[],"updated":null} Gates: phpcs clean on the controller, phpstan OK, 15,539 unit tests green. Note on the test file: FlowControllerTest carries 15 phpcs errors on origin/development and still carries exactly 15 here — the one my change added is fixed. The rest are pre-existing and unrelated to this endpoint. Part of #2216.
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.
#2219 gave a flow somewhere to remember things and #2220 let nodes reach it — but nothing could write it from a graph, so a flow author still had to drop into PHP. This closes that.
get/set/forgetare the obvious three.claimis the one worth having.Why
claimis a single operationA capacity cap — hydra's "at most ten pipelines at once", a booking, a lease — is a map of named slots where a free one must be taken by exactly one holder. Expressed with
getandsetthat is a read-modify-write across two steps, which is precisely the shape that lost writes in #2212.claimdoes the whole thing in one step and emitsclaimed: falserather than pretending it succeeded, so a router can branch on it and the flow author decides whether "no capacity" means wait, stop or escalate.releasetakes a slot number when the caller has one and falls back to the holder when it doesn't — a stage that crashed knows who it was, not which slot it got.A run with no flow state throws
Absent state read as "nothing claimed" would make a capacity cap wave everything through — the worst possible failure for this node. It fails loudly instead.
Documented in the class, because it will be the first question. What makes it safe is that a scheduled flow never overlaps itself (#2218) — the same property the shell orchestrator being replaced relies on, where one supervisor holds a
flockand its own slot bookkeeping is a plain check-then-write, safe only because nothing runs beside it.For coordination across flows, write an object with
onConflict: failand let the database arbitrate.Verified live — node resolved from the registry, not constructed by hand
Gates
phpcs clean, phpstan OK, 15,539 unit tests green (11 new)
Part of #2216. This is the primitive hydra#425 task 3.5 is built on.