Context
ADR-0031 (framework) introduced structured control-flow containers — loop, parallel, try_catch — whose bodies are nested regions carried in config (representation B): config.body for loop, config.branches[] for parallel, config.try + config.catch for try_catch. The engine executes them and folds their per-iteration/per-branch/handler step logs into the run log, and the run-observability Runs panel now nests those steps (#2667). ADR-0031 explicitly defers the remaining piece: "Designer rendering of the constructs (in ../objectui) is tracked separately." This issue is that piece.
Problem
The flow designer canvas renders loop / parallel / try_catch as flat single node cards. Their nested regions are not visualized at all, and are editable only as raw JSON:
- Icons/tones/palette entries exist (
flow-canvas-parts.tsx — loop→Repeat, parallel→GitFork, try_catch→ShieldAlert), but nothing reads config.body/branches/try/catch.
- The inspector explicitly punts (
inspectors/flow-node-config.ts:558-568): "Their bodies are nested regions … sub-graphs the flat field kinds can't model; authors edit them in the JSON source editor." Region config falls through to the "Advanced (JSON)" textarea (inspectors/FlowNodeInspector.tsx:147-149).
- The debug simulator marks
parallel/try_catch unsupported and runs loop as a single pass (previews/simulator/flow-simulator.ts:43,360-373).
- Client-side validation (
previews/flow-problems.ts) never descends into regions — single-entry/single-exit/acyclic is enforced server-side only (analyzeRegion/validateControlFlow in the spec).
So an author composing a loop body, parallel branches, or a try/catch handler is hand-editing JSON — exactly the error-prone authoring ADR-0031 set out to remove.
Current state (grounded)
The automation flow canvas is hand-rolled and dependency-free (no reactflow/@xyflow/dagre/elkjs), under packages/app-shell/src/views/metadata-admin/previews/:
| File |
Role |
FlowCanvas.tsx |
The canvas — absolutely-positioned cards over one global SVG edge layer; drag/pan/zoom; add/insert/delete. Flat nodes.map(...) node layer (:801-827). |
flow-canvas-parts.tsx |
NodeCard (fixed NODE_W×NODE_H), NODE_PALETTE, nodeIcon/nodeTone/nodeCategory (type→visual switches). |
flow-canvas-layout.ts |
computeLayout — layered longest-path, top-to-bottom; constant NODE_H; manual node.ui.{x,y} overrides; edge-path math. |
FlowPreview.tsx |
Host — owns draft/onPatch/flat selection. |
onPatch is a shallow top-level merge (StudioDesignSurface.tsx:1356-1357), so any region edit rebuilds the whole nodes array with the container's mutated config.
Prior art: FlowRunsPanel.buildStepTree (objectui#2667) already reconstructs region nesting and names it (Iteration N / Branch N / Try / Catch, regionLabel) — but it nests via indentation in a read-only log list from runtime step logs, not spatially from static config. Reuse its labeling model; the canvas problem (spatial nesting of an editable graph) is different.
Spec shapes (framework automation/control-flow.zod.ts, consumed as @objectstack/spec): FlowRegion = { nodes[≥1], edges } (single-entry/single-exit/acyclic); loop.body? (optional → legacy flat loops stay valid), parallel.branches[≥2], try_catch.try (required) + catch?.
Proposed design
Render each container as an expandable card that spatially contains a nested region sub-canvas, laid out recursively with the same computeLayout, with the container sized to bound its region(s). Deliver in phases so value lands early and the hard interactions are de-risked:
- Phase 1 — read-only nested rendering (highest value, bounded). Detect container nodes (
loop w/ config.body, parallel w/ config.branches, try_catch w/ config.try/catch) in the node-layer map; render a container variant of NodeCard with a header + collapse/expand toggle. When expanded, recursively computeLayout each region into a region-local coordinate space, draw its nodes + edges (per-region SVG group / offset transform), and size the container to bound them (parallel = N side-by-side branch columns with names; try_catch = stacked Try / Catch). Nested nodes are not yet editable (regions still JSON-edited). This requires flow-canvas-layout.ts to support variable container heights feeding back into the parent layer (today NODE_H is constant) and nested-edge routing. Collapsed containers render exactly as today.
- Phase 2 — nested selection + structured inspector. Path-based selection (
containerId → regionKey → nodeId) — flat {kind:'node', id} selection collides across regions (FlowPreview.tsx:83); route FlowNodeInspector lookup through the path. Replace the raw-JSON region block with structured editing of the selected nested node.
- Phase 3 — nested editing. Region-scoped
addNode/insertOnEdge/delete/drag (the current handlers mutate top-level arrays only, FlowCanvas.tsx:193-376); palette "+" affordances target a specific region; store nested positions in config.<region>.nodes[i].ui. Reparenting a node across a region boundary is the hardest interaction — can be last.
- Phase 4 — client-side region validation. Extend
buildFlowProblems to descend into regions and mirror the spec's analyzeRegion (single-entry/single-exit/acyclic, parallel ≥2 branches, try required) so region errors surface as canvas badges instead of only failing at server registration.
Files likely touched
FlowCanvas.tsx (node-layer + edge-layer + handlers), flow-canvas-layout.ts (recursive region layout, variable heights, nested edges), flow-canvas-parts.tsx (container NodeCard variant + collapse/expand), FlowPreview.tsx (region context + path selection + nested onPatch), inspectors/flow-node-config.ts + FlowNodeInspector.tsx (structured region editing), previews/flow-problems.ts (region validation), optionally simulator/flow-simulator.ts (region expansion), plus FlowCanvas/layout tests.
Tricky bits / open questions
- Coordinate systems — one flat frame +
node.ui.{x,y} today; nested nodes need a region-local space. Where do nested ui positions live — config.<region>.nodes[i].ui?
- Variable card heights —
NODE_H is constant; a container grows to bound its region → structural change to the layered layout + diagramSize.
- Nested edges in one SVG — the edge layer is a single global
<svg> keyed on top-level ids; region-local edges need per-region groups/offsets, plus edges that enter/leave a container.
- Collapse/expand default (collapsed for density vs expanded for authoring) and re-flow on toggle.
- Selection identity — path-based, ripples into the inspector.
onPatch contract — shallow top-level merge, so every region edit rebuilds the full nodes array.
- Back-compat — a
loop with no config.body is a legacy flat-graph loop and MUST keep rendering as a plain card (constructs are additive); parallel needs ≥2 branches; try_catch.try required, catch optional.
- Type source — the designer re-declares minimal
FlowNode/FlowEdge locally; consider importing FlowRegion/ParallelBranch from @objectstack/spec to avoid drift.
- Note:
packages/plugin-designer/ProcessDesigner.tsx is a separate BPMN swimlane designer and is not this surface — out of scope.
Acceptance (Phase 1)
Opening a flow with a loop/parallel/try_catch that has a populated region shows the region's body/branches/handlers nested inside the container card (expandable), with correct labels — no more opaque single card, and no JSON required to see the structure. Legacy flat loops and all non-container nodes render unchanged.
Scoped from an investigation of the current designer canvas; filed as the ADR-0031 "designer rendering" follow-up to objectui#2667 (Runs-panel nesting) and framework#3240 (history compaction). Suggested as a phased feature — Phase 1 is independently shippable.
Context
ADR-0031 (framework) introduced structured control-flow containers —
loop,parallel,try_catch— whose bodies are nested regions carried inconfig(representation B):config.bodyforloop,config.branches[]forparallel,config.try+config.catchfortry_catch. The engine executes them and folds their per-iteration/per-branch/handler step logs into the run log, and the run-observability Runs panel now nests those steps (#2667). ADR-0031 explicitly defers the remaining piece: "Designer rendering of the constructs (in ../objectui) is tracked separately." This issue is that piece.Problem
The flow designer canvas renders
loop/parallel/try_catchas flat single node cards. Their nested regions are not visualized at all, and are editable only as raw JSON:flow-canvas-parts.tsx—loop→Repeat,parallel→GitFork,try_catch→ShieldAlert), but nothing readsconfig.body/branches/try/catch.inspectors/flow-node-config.ts:558-568): "Their bodies are nested regions … sub-graphs the flat field kinds can't model; authors edit them in the JSON source editor." Region config falls through to the "Advanced (JSON)" textarea (inspectors/FlowNodeInspector.tsx:147-149).parallel/try_catchunsupported and runsloopas a single pass (previews/simulator/flow-simulator.ts:43,360-373).previews/flow-problems.ts) never descends into regions — single-entry/single-exit/acyclic is enforced server-side only (analyzeRegion/validateControlFlowin the spec).So an author composing a loop body, parallel branches, or a try/catch handler is hand-editing JSON — exactly the error-prone authoring ADR-0031 set out to remove.
Current state (grounded)
The automation flow canvas is hand-rolled and dependency-free (no reactflow/@xyflow/dagre/elkjs), under
packages/app-shell/src/views/metadata-admin/previews/:FlowCanvas.tsxnodes.map(...)node layer (:801-827).flow-canvas-parts.tsxNodeCard(fixedNODE_W×NODE_H),NODE_PALETTE,nodeIcon/nodeTone/nodeCategory(type→visualswitches).flow-canvas-layout.tscomputeLayout— layered longest-path, top-to-bottom; constantNODE_H; manualnode.ui.{x,y}overrides; edge-path math.FlowPreview.tsxdraft/onPatch/flatselection.onPatchis a shallow top-level merge (StudioDesignSurface.tsx:1356-1357), so any region edit rebuilds the wholenodesarray with the container's mutatedconfig.Prior art:
FlowRunsPanel.buildStepTree(objectui#2667) already reconstructs region nesting and names it (Iteration N/Branch N/Try/Catch,regionLabel) — but it nests via indentation in a read-only log list from runtime step logs, not spatially from staticconfig. Reuse its labeling model; the canvas problem (spatial nesting of an editable graph) is different.Spec shapes (framework
automation/control-flow.zod.ts, consumed as@objectstack/spec):FlowRegion = { nodes[≥1], edges }(single-entry/single-exit/acyclic);loop.body?(optional → legacy flat loops stay valid),parallel.branches[≥2],try_catch.try(required) +catch?.Proposed design
Render each container as an expandable card that spatially contains a nested region sub-canvas, laid out recursively with the same
computeLayout, with the container sized to bound its region(s). Deliver in phases so value lands early and the hard interactions are de-risked:loopw/config.body,parallelw/config.branches,try_catchw/config.try/catch) in the node-layer map; render a container variant ofNodeCardwith a header + collapse/expand toggle. When expanded, recursivelycomputeLayouteach region into a region-local coordinate space, draw its nodes + edges (per-region SVG group / offset transform), and size the container to bound them (parallel= N side-by-side branch columns with names;try_catch= stackedTry/Catch). Nested nodes are not yet editable (regions still JSON-edited). This requiresflow-canvas-layout.tsto support variable container heights feeding back into the parent layer (todayNODE_His constant) and nested-edge routing. Collapsed containers render exactly as today.containerId → regionKey → nodeId) — flat{kind:'node', id}selection collides across regions (FlowPreview.tsx:83); routeFlowNodeInspectorlookup through the path. Replace the raw-JSON region block with structured editing of the selected nested node.addNode/insertOnEdge/delete/drag (the current handlers mutate top-level arrays only,FlowCanvas.tsx:193-376); palette "+" affordances target a specific region; store nested positions inconfig.<region>.nodes[i].ui. Reparenting a node across a region boundary is the hardest interaction — can be last.buildFlowProblemsto descend into regions and mirror the spec'sanalyzeRegion(single-entry/single-exit/acyclic,parallel≥2 branches,tryrequired) so region errors surface as canvas badges instead of only failing at server registration.Files likely touched
FlowCanvas.tsx(node-layer + edge-layer + handlers),flow-canvas-layout.ts(recursive region layout, variable heights, nested edges),flow-canvas-parts.tsx(containerNodeCardvariant + collapse/expand),FlowPreview.tsx(region context + path selection + nestedonPatch),inspectors/flow-node-config.ts+FlowNodeInspector.tsx(structured region editing),previews/flow-problems.ts(region validation), optionallysimulator/flow-simulator.ts(region expansion), plusFlowCanvas/layout tests.Tricky bits / open questions
node.ui.{x,y}today; nested nodes need a region-local space. Where do nesteduipositions live —config.<region>.nodes[i].ui?NODE_His constant; a container grows to bound its region → structural change to the layered layout +diagramSize.<svg>keyed on top-level ids; region-local edges need per-region groups/offsets, plus edges that enter/leave a container.onPatchcontract — shallow top-level merge, so every region edit rebuilds the fullnodesarray.loopwith noconfig.bodyis a legacy flat-graph loop and MUST keep rendering as a plain card (constructs are additive);parallelneeds ≥2 branches;try_catch.tryrequired,catchoptional.FlowNode/FlowEdgelocally; consider importingFlowRegion/ParallelBranchfrom@objectstack/specto avoid drift.packages/plugin-designer/ProcessDesigner.tsxis a separate BPMN swimlane designer and is not this surface — out of scope.Acceptance (Phase 1)
Opening a flow with a
loop/parallel/try_catchthat has a populated region shows the region's body/branches/handlers nested inside the container card (expandable), with correct labels — no more opaque single card, and no JSON required to see the structure. Legacy flat loops and all non-container nodes render unchanged.Scoped from an investigation of the current designer canvas; filed as the ADR-0031 "designer rendering" follow-up to objectui#2667 (Runs-panel nesting) and framework#3240 (history compaction). Suggested as a phased feature — Phase 1 is independently shippable.