<!-- Mirrored from .codex/vendor/VL-Workflow-Engine/docs/workflow-spec-4.1.md (engine 4.13.0). Do not edit here; resync after engine vendor updates (scripts/sync-engine-vendor.mjs). 2026-06-11 -->

# Workflow Spec 4.1

> Spec `4.1` · Engine release `VL-Workflow-Engine 4.13.0` · Event schema `vl.workflow.run-event.v4`
>
> The authoring **spec number is `4.1`** and is stable; `4.13.0` is the **engine release** that implements it (the spec number and the engine release are intentionally distinct — the engine is the source of truth via `engine.getCapabilities()`).

## Scope

Spec `4.1` keeps the workflow graph model stable while making one orchestration change explicit:

- `Swarm` is no longer a canonical workflow step for new `4.1` graphs
- dynamic intra-round branching now belongs to native source-mode parallel `Loop`

On top of that stable spec-`4.1` graph model, engine releases `4.12`–`4.13` added an additive **DAG-complexity layer** — scored pruning (`Score`), scored dynamic growth (`Grow`), cross-DAG message channels (`Channel`), adaptive agent/edge topology (`Topology`), bounded iteration containers (`Iteration`), and runtime-generated meta-flows (`SpawnChildRun` with a computed workflow). These are new **step families**, not a new spec version. See **DAG-Complexity Steps**.

The recommended authoring model for round-based human-in-the-loop work is:

- `Set`
- `Loop` in `while` mode for rounds
- `Loop` in source-mode `parallel` for actors
- `LLM` or `Actor`
- `Pause`
- `Stop`

Other existing workflow families remain available.

## Supported Step Families

The engine reports its surface through `engine.getCapabilities()` as two tiers — `stepTypes` (canonical) and `legacyStepTypes`.

Canonical `stepTypes` (22, from `engine.getCapabilities().stepTypes`):

- `Service`
- `API`
- `Component`
- `Actor`
- `Subflow`
- `Review`
- `GraphPatch`
- `Score`      _(engine 4.12+)_
- `Grow`       _(engine 4.12+)_
- `Channel`    _(engine 4.12+)_
- `Topology`   _(engine 4.12+)_
- `LLM`
- `Set`
- `Write`
- `Download`
- `Unzip`
- `Pause`
- `Branch`
- `Loop`
- `Iteration`  _(engine 4.12+; alias of `Loop`)_
- `Stop`
- `Noop`

Aliases:

- `Fork -> Noop`
- `Check -> Branch`
- `Done -> Stop`
- `Iteration -> Loop`
- `ChildRun -> Subflow`
- `SpawnChildRun -> Subflow`

`legacyStepTypes`:

- `Swarm` validates and executes only for workflow versions `4.0` and earlier

## DAG-Complexity Steps (engine 4.12+)

These additive step families run inside ordinary spec-`4.1` graphs. Each writes its result to `_result` (plus a step-specific alias var) and supports `out` output-mapping. `Score` / `Grow` / `Topology` are the engine's objective-function surface for pruning and growth; `Channel` is the cross-DAG messaging fabric; `Iteration` is a bounded converging container.

### `Score`

Inference-time scoring + pruning decision. Computes a `0..1` score and a `decision` (`keep` / `prune` / `expand`) from weighted signals against thresholds. Three input shapes: `path` (score a trajectory array, with `aggregate`, default `mean`), `candidates` (rank a candidate array), or `node` (score + decide on a single node). Configured with `weights` / `thresholds`. Pair with a `Branch` on `=$score.decision === 'prune'` to actually prune. Writes `{ score, decision, ... }`.

### `Grow`

Scored dynamic graph growth. Proposes candidate nodes — rule-based from `context.gaps`, or pre-made specs via a `candidates` var reference (left un-evaluated so their inner `=expr` fields run when the GROWN step runs) — and turns them into `GraphPatch` operations anchored at `anchorId` / `nextId`. With `apply: true` the ops are applied live through the same path as a `GraphPatch` step and a `graph_patch_applied` event is emitted; otherwise the ops are returned for a following `GraphPatch` step. `max` caps the count. Writes `{ candidates, source, dropped, graphPatchOps, applied }`.

### `Channel`

Bidirectional multi-DAG communication via named FIFO queues on the execution context, **shared across parallel branches / sub-DAGs** (child contexts delegate to the same `_channels`) and checkpoint-serialized. Requires a `channel` name + `action`:

- `send` — enqueue `message` (or `in`) → `{ channel, size, sent }`
- `recv` — dequeue oldest; `wait: true` polls up to `maxWaitMs` (default `2000`, `pollMs` default `5`) so a concurrent branch's `send` becomes visible; `required: true` throws on timeout, else returns `fallback`
- `drain` — dequeue all remaining as an array
- `peek` — read oldest without removing

Emits `channel_send` / `channel_recv`. The empty-value key is `fallback` (not `default`, which is reserved as a `Branch` default-case edge target).

### `Topology`

Adaptive agent/edge pruning via the Scorer. `action`:

- `select` — rank candidate agents by `candidate.signals`, keep top-`keep` survivors (prune the rest). With `apply: true`, the selected agents are GraphPatched in as parallel children of `anchorId` (default = this step) and run immediately → `{ selected, pruned, ranked, applied, keptCount, prunedCount }`
- `prune-edges` — rank candidate comm `edges`, keep the high-value ones → `{ kept, pruned }`

Configured with `weights` / `thresholds`. Put scoring inputs under `candidate.signals` so they don't collide with step-spec fields.

### `Iteration`

Bounded `generate → test → fix` container — sugar that aliases `Loop`. Bound with `maxRounds`, `until`, and/or `while`; specifying **both** `while` and `until` is rejected at validation. Use it for an explicit converging loop (produce a candidate, test it with tool/verifier gates, repair, repeat until it passes or the round budget is exhausted).

### Meta-Flows (`SpawnChildRun`)

`SpawnChildRun` aliases `Subflow` for execution, but its `workflow` field may be a computed expression (`workflow: "=$generatedFlow"`) resolving a **runtime-generated workflow object** — a flow that builds and runs another flow. Surfaced as `features.workflowOfWorkflows` / `features.spawnChildRun`, distinct from a static `Subflow` reference.

## Host-Owned Capabilities

Host-owned capabilities are not workflow step families. They are environment
surfaces exposed by Agent-OS, VLC, Agent App, AI Chat, or another embedding host
through tools, adapters, or a host capability manifest.

VL local preview is host-owned:

- do not add `Preview`, `BrowserPreview`, `VLPreview`, or similar step families
  to the engine
- call preview through host tools or adapters when a workflow needs it
- return preview diagnostics, screenshots, render-session URLs, and artifacts as
  normal host tool output or `ResultEnvelope` data
- keep browser runtime details outside checkpoints and engine validation

If preview needs standardization later, standardize the host tool/capability
manifest rather than embedding browser/runtime details in the workflow engine.

## Preferred Round-Orchestration Shape

```text
Set_Init
  -> Loop_Rounds (while: $done !== true)
       -> Loop_Actors (source: $actors, mode: parallel)
            -> LLM_ActorWork / Actor_ActorWork
       -> Set_CollectRound
       -> Pause_Human
       -> Set_ApplyDecision
  -> Stop_End
```

This shape is normative for new `4.1` workflows that previously would have used `Swarm`.

## Loop

`Loop` supports three execution shapes:

- `while` mode
- source-mode `serial`
- source-mode `parallel`

### Common Loop Locals

Loop children can read:

- `_item` for source-mode loops
- `_index` for both source-mode and while-mode loops
- `_iterDir` for both source-mode and while-mode loops

### `BREAK`

`next: "BREAK"` remains valid inside loop children.

Semantics:

- in serial and while loops, it ends the whole loop
- in parallel loops, it prevents not-yet-started branches from launching

### Parallel Loop Branch Control

Source-mode parallel loops may dynamically change their active branch set while running.

#### `spawnSibling(item)`

Available inside a parallel loop child context.

Normative behavior:

1. the engine appends a new sibling branch to the same loop
2. the new branch receives the same loop children
3. the new branch gets a fresh `ChildExecutionContext`
4. the new branch receives `_item = item`
5. the loop does not complete until every initial branch and every spawned branch reaches a terminal state

Constraints:

- nested dynamic spawn is not supported
- a dynamically spawned branch cannot itself spawn another branch

#### `exitBranch(reason?)`

Available inside a parallel loop child context.

Normative behavior:

1. only the current branch exits
2. sibling branches continue unaffected
3. the loop still waits for the rest of the branch set
4. the runtime emits `loop_branch_exited`

#### `next: "EXIT_BRANCH"`

Inside a parallel loop child step, `next: "EXIT_BRANCH"` is a declarative alias for `ctx.exitBranch()`.

Validation rule:

- `EXIT_BRANCH` is only valid inside children of a source-mode parallel `Loop`

### Adapter Runtime Visibility

When `Actor` or `LLM` runs inside a parallel loop branch, the runtime passes branch control to the adapter:

- `loopId`
- `branchIndex`
- `branchId`
- `spawnSibling(item)`
- `exitBranch(reason)`

This is how hosts can upgrade from "round-to-round actor adjustment" to "in-round self-splitting / self-exit".

## Pause And Human Steering

`Pause` remains the canonical way to insert human steering between rounds.

Recommended pattern:

- gather round outputs into normal workflow variables
- pause on a regular `Pause_*` node
- resume with `resumeResultTarget`
- apply the decision in `Set_*`
- let the next `while` evaluation decide whether another round is needed

## Checkpoint / Resume

Checkpoint version `3` adds `loopState`.

Normative behavior:

1. active parallel loops persist their branch set in `checkpoint.loopState`
2. dynamically spawned branches survive JSON round-trips
3. in-flight branches resume as pending work
4. the resume entry point is the loop node, not an individual branch child
5. `loopProgress` remains present for compatibility with pre-`4.1` checkpoints and static-loop resumes

## Events

All events use event schema `vl.workflow.run-event.v4` and are additive.

Spec-`4.1` loop events:

- `loop_branch_spawned`
- `loop_branch_exited`

Engine 4.13 telemetry events:

- `step_retrying` — a step is being retried (retry telemetry)
- `step_cached` — a step result was served from the opt-in result cache

Channel events (engine 4.12+):

- `channel_send`
- `channel_recv`

See `docs/workflow-event-schema.md` for the full event catalog (review/human-gate, preflight/step-guard, graph-patch, and legacy swarm families).

## Runtime Graph Snapshot

Runtime graph snapshot is a read-only API surface layered over existing `4.1`
execution state. It does not add a step type or change execution semantics.

Normative behavior:

1. the engine may expose `engine.getRuntimeSnapshot(ctxOrCheckpoint, options?)`
2. the snapshot schema is `vl.workflow.runtime-graph.v1`
3. snapshots are built from workflow definition, current execution context or checkpoint, `graphPatches`, `loopState`, child runs, result envelopes, and generic artifact refs
4. snapshots must reflect steps added, updated, or removed by `GraphPatch`
5. snapshots must expose active parallel-loop branches persisted in checkpoint v3 `loopState`
6. Agent App identity, host control-plane rows, VL surface state, preview sessions, and product UI state remain host-owned

The capability manifest advertises this support with:

```js
features.runtimeGraphSnapshot === true
```

## Legacy Swarm Compatibility

`Swarm` is removed from `4.1` validation and authoring, but runtime compatibility remains for older graphs:

- use workflow `version: "4.0"` if a graph still contains `Swarm`
- upgrade to `4.1` only after replacing `Swarm` with native `Loop` / `Pause` orchestration

The host-side swarm knowledge adapter remains exported only so older `4.0` flows can still be replayed.


## VisualLogic Step (Event-Panel Logic)

> Additive extension to spec `4.1` — an **opt-in custom step**, not a core step family. Flows that never use it are unaffected. Implemented by `createVisualLogicHandler` in `VL-Workflow-Engine` (`lib/visual-logic.js`, release `4.13.0+`) and registered by the host as `customHandlers.VisualLogic`. Engine core dispatch is unchanged.

A `VisualLogic` node carries logic authored **graphically in the Event Panel** (the iVX `VxEventPanel`) rather than as prompt or hand-written code. The same logic exists in four aligned layers:

1. **VL method** (`logic.source`) — human-authored source of truth, e.g. `METHOD processFile(input) …`.
2. **Event-Panel AST** (`logic.events`) — the renderable native event AST the Event Panel reads/writes (`getEvents()` / `onChange`). Every visible block carries a unique `ln`; formula refs use the `$cbParams` shape.
3. **Compiled JS** (`logic.compiledJs`) — the runtime artifact produced by `VxEventPanel.ast2js(...)`.
4. **ctx runtime** — the engine executes the compiled JS through a host capability bridge.

### Node shape

```json
{
  "id": "VisualLogic_ProcessFile",
  "type": "VisualLogic",
  "in": { "srcPath": "=$input.srcPath", "outPath": "Process/out.md", "title": "Summary" },
  "tools": ["files.readText", "agent.summarize", "files.writeText", "ledger.append"],
  "logic": {
    "runtime": "ctx",
    "lang": "vl-method",
    "source": "METHOD processFile(input) …",
    "events": [{ "eventId": "event_run", "name": "run", "ast": { "op": "block", "args": ["…"] } }],
    "compiledJs": "async function runEvent(ctx) { … }"
  },
  "out": { "$summary": "=_result.returnValue" }
}
```

- Step IDs follow `VisualLogic_<name>` (the custom-handler dispatch keys on the step type).
- `in` is evaluated and exposed as `ctx.input.<key>`; those keys are also the node's `run` event parameters in the Event Panel.

### Runtime contract — `runtime: "ctx"` (canonical, "Option B")

`logic.compiledJs` is `async function runEvent(ctx) { … }`. The `ctx` the engine provides:

- `ctx.input.<name>` — the evaluated `in` values.
- `await ctx.callMethod(objId, method, paramsObject)` — invoke a component method; routed to the host capability bridge. **Gated by `step.tools`**: an `"objId.method"` not listed throws.
- `ctx.getProp(objId, prop)` / `ctx.setProp(objId, prop, value)` — component property access (host-provided adapters).
- `ctx.utils.*` — non-native helpers (`fnEntries`, `fnStep`, `fnJsonObj`, `obj_item`, `arr_item`); arithmetic, comparison, logic and `await` use native JS.
- The return value of `runEvent` becomes `_result.returnValue` for `out` mapping.

A legacy `runtime: "sys"` is also accepted for `$sys`-style compiled blocks; `ctx` is canonical for new graphs.

### Capability palette — declared by the flow `manifest`

The flow's top-level `manifest` is the **single decoupled source of truth** for both rendering and runtime:

- `manifest.components` — a componentMap keyed by component **type**; each declares its `methods` (with `params` + `return`) and `events`. Drives both the Event Panel's method palette and the engine's method validation.
- `manifest.objects` — the capability **instances** a node may call: `{ id, type, capability }`. `id` is what `ctx.callMethod` targets; `capability` (or the `cap-`-stripped `type`) selects the host implementation.

The node's own `run` event parameters are its `in` keys (node-specific). Governance is **two layers**: `step.tools` (per-node allow-list, enforced by the engine handler) **and** the manifest declaration (the object + method must be declared, enforced by the host bridge). An undeclared object, method, or capability **throws** — no silent skip, no fallback.

### Host registration

```js
const { createVisualLogicHandler } = require('vl-workflow-engine');
new Engine(workflow, {
  customHandlers: {
    VisualLogic: createVisualLogicHandler({
      capability: ({ objId, method, params, ctx, step }) => /* route to a real host capability */,
      getProp, setProp, utils,   // optional ctx.* adapters
      compile,                   // optional: (eventAst, { step, ctx }) => js, used when logic.compiledJs is absent
    }),
  },
});
```

The host capability bridge maps each `objId.method` to a real capability — e.g. workspace file I/O, an agent/LLM call, a run-ledger append — respecting the manifest and `step.tools`. File writes are recorded as run artifacts.

### Authoring surface

The workflow editor renders a `VisualLogic` node's `logic.events` live in the Event Panel, **isolated in an iframe**: the Ant Design–based panel communicates with the editor only via a namespaced `postMessage` contract (`vlpanel:mount` / `change` / `compile` / `compiled` / `ready`), so it stays fully decoupled, never pollutes the host theme, and can run standalone. Editing updates the AST; **Compile** runs `ast2js` to refresh `logic.compiledJs` (Option-B); the host executes that compiled code at run time via the `ctx` runtime above. Thus a VisualLogic flow is "ready as soon as the pieces are in place": its node logic is defined upfront, renders in real time, compiles for real, and runs standalone.
