A project groups 1..N flows. A flow's project is optional, so flows without one
stay fully valid, and the association is a plain String column - this codebase
has no JPA relations, and the hottest entity is not the place to introduce the
first one.
Ownership and visibility
- Projects are strictly owner-scoped and report another user's project as 404,
not 403: unlike a flow, whose existence is not secret, a project is private
workspace structure.
- projectId and projectName are disclosed only to the flow's owner. A published
flow inside a project stays readable by everyone - membership is organizational
structure, not access control - but a project name can itself be sensitive, so
a non-owner sees the flow as unassigned.
- Assignment gets its own PUT /flows/{id}/project rather than a field on the flow
body: that body is the full-replace PUT the editor issues on every save, so a
project carried there would be silently dropped on each save. A finalized flow
stays reassignable, since finalizing is irreversible and must not freeze a flow
out of reorganization forever.
Deleting a project deletes its flows, finalized ones included, and requires
confirm=true. Refusing the cascade was not an option: finalizing cannot be
undone, so a project holding one finalized flow could never be deleted by any
API. Executions are kept - each snapshots its own copy of the flow graph.
Shared context
Values a project shares with its flows, readable as ${{project.x}} and as
#project['x'] in conditional expressions. Three changes make that work, each
silent if missed: the project. prefix is preserved by ExecutionTemplateResolver
(otherwise keys publish as ${{vars.project.x}} and never resolve), admitted by
PlaceholderInputs (otherwise the placeholder becomes a dangling block input), and
bound in the SpEL context. Values are frozen into the execution snapshot at
creation, so editing a project never rewrites a run that already happened, and
they are applied only when the person running owns the project.
Project runs
POST /projects/{id}/execute creates one execution per flow, sharing a
projectRunId, and refuses the lot if any flow is not executable - a half-created
run group is worse than a clear refusal. POST .../runs/{runId}/start then runs
them one at a time in the project's order, each step starting only when the
previous succeeded. The run keeps no state of its own: its position is derived
from the executions, so there is no second status machine to drift out of sync.
A failed step stops the run and leaves the rest untouched, so it can be resumed.
Project values pre-fill matching global inputs - same name and type, never
overwriting a user's own value - which is what lets a run start without a
per-flow round trip.
Also fixes an ordering hazard in AuthService: account deletion deliberately
preserves finalized flows, so their project assignment is now cleared before the
projects are removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>