Skip to content

FlowPad and the actor model

Every visible pane in FlowPad is a Matrix actor. The package was rewritten to Pattern B — separated shell-element + headless actor — to make it possible for an external script, an agent, or another package to drive FlowPad through the bus without going near the DOM.

This is the substrate's actor model surfaced inside one vertical. FlowPad does not invent new primitives; it composes the substrate's three primitive types (Feed / Service / Component) into a developer-facing workspace. Every pane is a Component (a typed actor with accepts/emits/streams); every infrastructure child (db, scheme, vlm-eval) is a Service; every transport-log line that streams into the dock is a Feed observation.

Source: projects/matrix-3/packages/flowpad/.claude/rules/webapp-compliance.md (the package's own compliance file). The 12 named rules live in projects/architecture/MATRIX-COMPONENT-DEVELOPER-GUIDE.md §8.

Pattern B in FlowPad

FlowpadAppShell  : extends MatrixActorHtmlElement (DOM owner)
   └── wraps ──▶  FlowpadApp : extends MatrixActor (logic owner)

The shell handles the DOM (template, slots, click/keyboard handlers). The headless FlowpadApp actor handles the data: it owns the example list, the selected example index, the target mount, and the per-stage results map (FlowpadApp.ts:205-208). They communicate only through the bus; the shell never calls a method on the actor and vice versa.

A trimmed view of FlowpadApp (projects/matrix-3/packages/flowpad/src/FlowpadApp.ts:101-130):

ts
export class FlowpadApp extends MatrixActor {
  static description = 'FlowPad — visual pipeline editor for composing data flows across actors.';

  static accepts = {
    runFlow: { exampleIndex: 'number?' },
    runFromCode: { code: 'string', modeHint: 'string?', sourceFormatHint: 'string?' },
    selectExample: { index: 'number' },
    setTargetMount: { mount: 'string' },
    loadExample: { name: 'string' },
    clearResults: {},
    'snapshot.tree.oracle': {},
  };

  static emits = {
    flowStarted:      { flowId: 'string' },
    executionPlanned: { flowId: 'string', language: 'string', irKind: 'string', adapterPath: 'string' },
    flowCompleted:    { flowId: 'string', result: 'unknown' },
    flowError:        { flowId: 'string', error: 'string' },
    exampleLoaded:    { name: 'string', code: 'string' }
  };
}

Every static accepts entry has a parameter schema; every emits entry names its payload shape. This is the Actor Communication Contract (Rule 3 of the project rules): every op and field declares itself, so introspection tools can build menus and oracles automatically.

Children mount as bus addresses

When onBootstrap runs, FlowpadApp registers eleven service children. Each becomes an addressable mount under the FlowPad mount (FlowpadApp.ts:212-246):

ts
const infrastructureServices: Array<[string, new () => MatrixActor]> = [
  ['db',             MockDbService],
  ['flow-runner',    FlowRunnerService],
  ['security-realm', SecurityRealmService],
  ['topic-claims',   TopicClaimService],
  ['lisp',           LispEval],
  ['scheme',         SchemeEval],
  ['bootstrap',      BootstrapNode],
  ['vlm-router',     VLMRouter],
  ['vlm-join',       VLMJoinCoordinator],
  ['vlm-atomizer',   VLMAtomizer],
  ['vlm-eval',       VLMEntryPoint],
];

for (const [mountId, serviceCtor] of infrastructureServices) {
  await this.addChild(mountId, serviceCtor);
}

// Spawn 5 bootstrap nodes for parallel execution
for (let i = 0; i < 5; i++) {
  await this.addChild(`vlm-node-${i}`, VLMBootstrapNode);
}

If FlowPad's mount is flowpad-page, the mounted children answer at flowpad-page.db, flowpad-page.scheme, flowpad-page.vlm-eval, etc. The DSL's ./db and ./scheme shorthand resolves against the calling component's mount root — so a flow editor inside FlowPad addresses these children with a relative prefix and no environmental hardcoding.

What "Snapshot Tree Oracle" exposes

onSnapshotTreeOracle (FlowpadApp.ts:264-304) is FlowPad's contribution to discovery: it returns the page's mount, accepted ops with their parameter shapes, emitted event names, and the cognitiveState (selectedExample, targetMount, availableExamples). Agents that target FlowPad as a $TARGET use this to learn which ops they can call.

Pattern B compliance rules (FlowPad-specific)

The package's compliance file says it explicitly:

  • NEVER use innerHTML to create <mx-*> elements — destroys actor lifecycle.
  • NEVER call methods on child actors directly — use sendToChild(id, op, payload).
  • ALWAYS declare static accepts (Record format) and static emits.
  • ALWAYS set name attribute on child elements BEFORE DOM insertion.
  • NEVER call headless actor methods from the shell — communicate via bus only (CQRS).
  • Root must be wrapped in <matrix-dsl-host>.

The shells in src/components/ (PipelineEditorShell, ResultsPanelShell, TransportLogShell, FlowpadActorTree, FlowpadExampleList, FlowpadPackageBrowser, FlowpadSidebar, FlowpadControlShell, FlowpadAppShell) all extend MatrixActorHtmlElement and follow this rule. Their actor halves all extend MatrixActor and declare static accepts/static emits.

Where flows actually execute

onRunFromCode (FlowpadApp.ts:342-371) is where typed code becomes a running flow. It compiles the source into a CanonicalIR, derives a runner context (this._context!.derive('_runner')), and calls executeCanonicalIR(executionContext, compiled.ir). The CanonicalIR captures language (Flow DSL, LISP, Scheme, JSON-command, etc.), the steps array, and the adapterPath — the chain of actor mounts the input value will pass through.

See also

Source: projects/matrix-3/packages/flowpad/src/FlowpadApp.ts:101-425.