ObjectStackObjectStack

Control Flow

Control Flow protocol schemas

@module automation/control-flow

Structured control-flow constructs (ADR-0031) — the native + AI-authored

flow model: a loop container, a parallel block, and structured

try/catch/retry. Unlike BPMN's gateway/boundary/token graph (kept in the

protocol for interop only), these constructs are **well-formed by

construction**, locally composable, and statically analyzable — the right

substrate for LLM authoring (ADR-0010/0011).

Representation — decision: (B) nested sub-structure

ADR-0031 flagged two ways to carry structured containers in the flat

nodes[]+edges[] model:

  • (A) marker-delimited scoped regions (a container node + a scope-end

marker; the body is the edges between them in the main graph), or

  • (B) the container node carries a nested mini-flow in its config.

We adopt (B). Each container holds its body as a self-contained

FlowRegionSchema (config.body for loop, config.branches[] for

parallel, config.try/config.catch for try_catch). The reasons:

  1. Well-formed by construction — a nested region is its own graph, so

single-entry is intrinsic; there are no scope markers to balance and no

way to "leak" an edge across a boundary. Validation is local.

  1. The shared engine traversal stays untouched — the container executor

runs its own body via a scoped helper; the main DAG traverseNext never

learns about scope markers (important under the multi-agent discipline

around engine.ts). The container's ordinary out-edges remain the

"after-loop / after-block" continuation.

  1. Cleaner AST for AI — ADR-0031 calls (B) "the cleaner long-term AST,"

and AI authoring is the design center.

Existing flat-graph loops (a loop node with no config.body) keep their

legacy behavior — the constructs are additive, activated only when the

nested structure is present.

The canonical construct type ids are LOOP_NODE_TYPE (loop,

pre-existing), PARALLEL_NODE_TYPE (parallel), and

TRY_CATCH_NODE_TYPE (try_catch). These are distinct from the BPMN

interop node types (parallel_gateway / join_gateway / boundary_event),

which remain author-invisible interchange representations.

Source: packages/spec/src/automation/control-flow.zod.ts

TypeScript Usage

import { FlowRegion, LoopConfig, ParallelBranch, ParallelConfig, TryCatchConfig } from '@objectstack/spec/automation';
import type { FlowRegion, LoopConfig, ParallelBranch, ParallelConfig, TryCatchConfig } from '@objectstack/spec/automation';

// Validate data
const result = FlowRegion.parse(data);

FlowRegion

Properties

PropertyTypeRequiredDescription
nodesObject[]Region body nodes (single-entry/single-exit sub-graph)
edgesObject[]optionalRegion body edges

LoopConfig

Properties

PropertyTypeRequiredDescription
collectionstringTemplate/variable resolving to the array to iterate
iteratorVariablestringoptionalLoop variable holding the current item
indexVariablestringoptionalOptional loop variable holding the current index
maxIterationsintegeroptionalHard cap on iterations (clamped to the engine ceiling)
bodyObjectoptionalLoop body region (omit for legacy flat-graph loops)

ParallelBranch

Properties

PropertyTypeRequiredDescription
namestringoptionalBranch label
nodesObject[]Branch body nodes
edgesObject[]optionalBranch body edges

ParallelConfig

Properties

PropertyTypeRequiredDescription
branchesObject[]Branch regions executed concurrently; implicit join at block end

TryCatchConfig

Properties

PropertyTypeRequiredDescription
tryObjectProtected region
catchObjectoptionalHandler region run when the try region fails
errorVariablestringoptionalVariable holding the caught error in the catch region
retryObjectoptionalOptional retry policy for the try region

On this page