Skip to content

feat(flow): a node that reads and writes flow state, with a capacity claim - #2221

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/flow-state-node
Jul 31, 2026
Merged

feat(flow): a node that reads and writes flow state, with a capacity claim#2221
rubenvdlinde merged 1 commit into
developmentfrom
feat/flow-state-node

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

#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 / forget are the obvious three. claim is the one worth having.

Why claim is a single operation

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, which 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 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.

⚠️ Not a cross-flow lock

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 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, not 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         <- the 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.

…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.
@rubenvdlinde
rubenvdlinde merged commit f70b6dd into development Jul 31, 2026
16 of 17 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/flow-state-node branch July 31, 2026 07:32
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ a8c5c14

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.
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