Skip to content

Actor integration and IR pipeline

This page describes the contract between FlowPad and the actors it executes flows against.

CanonicalIR

projects/matrix-3/packages/flowpad/src/runtime/queryPipeline.ts defines the IR (queryPipeline.ts:42-60):

ts
export type CanonicalIR = IQueryExecutionIR | IComponentSchemaIR;

export interface IQueryExecutionIR {
  kind: 'query-plan';
  language: QuerySourceLanguage;
  source: string;
  input: unknown;
  steps: QueryStep[];
  adapterPath: string[];
}

export interface IComponentSchemaIR {
  kind: 'component-schema';
  language: QuerySourceLanguage;
  source: string;
  sourceFormat: SerializationFormat;
  schema: IComponentSchema;
  adapterPath: string[];
}

QueryStep is one of:

ts
type QueryStep = IRemoteCallStep | ILocalMapStep | ILocalFilterStep;

interface IRemoteCallStep { kind: 'remote-call'; mount: string; operation: string; }
interface ILocalMapStep   { kind: 'local-map';   expression: string; }
interface ILocalFilterStep{ kind: 'local-filter'; expression: string; }

adapterPath is a flat list of mount addresses the input value will pass through, in order. Status panes can render this as breadcrumbs without re-walking steps.

Source languages

QuerySourceLanguage covers everything the editor or an upstream caller might supply:

flow-dsl, flow-json, lisp, scheme,
json-command, json-payload, sql,
serialization-html, serialization-fluent, serialization-json,
serialization-sexpr, serialization-topics

detectLanguage (runtime/queryPipeline.ts:97+) chooses based on sourceFormatHint, then modeHint, then a JSON-shape probe.

How a typed flow becomes an IR

mermaid
sequenceDiagram
    participant Editor as PipelineEditor
    participant Shell as FlowpadAppShell
    participant App as FlowpadApp
    participant Pipeline as queryPipeline.ts

    Editor->>Editor: detect mode (flow / lisp)
    Editor->>App: requestRun { code, mode }
    App->>Pipeline: compileSourceToCanonicalIR({ source, defaultTargetMount, modeHint })
    Pipeline-->>App: { language, ir, adapterPath }
    App->>App: emit executionPlanned
    App->>Pipeline: executeCanonicalIR(context, ir)
    Pipeline-->>App: result
    App->>App: emit flowCompleted

For LISP/Scheme input, the editor first wraps the source as flow().fromValue({ "eval": null, "expr": "..." }).viaRemote("./vlm-eval", "eval"). The wrapper is not stored back in the editor — it's a runtime transformation invisible to the user. After wrapping, the rest of the pipeline is identical to Flow-mode execution.

Actor contract for viaRemote targets

For a remote-call step viaRemote(mount, operation), FlowPad will:

  1. Resolve ./mount against the calling actor's mount root.
  2. RequestReply.execute(context, mount, operation, currentInput, { timeoutMs })currentInput is the previous step's output.
  3. Use the result as the next stage's input.

The target actor must:

  • Declare the operation in static accepts with a parameter schema that's compatible with currentInput.
  • Reply via the standard Matrix request/reply pattern ($reply.{correlationId} envelope on the bus).

Per-stage timeout policy is the same as documented in Running flows: 30s cross-root, 10s VLM, 5s otherwise.

Bootstrapped service contracts

The eleven services FlowpadApp mounts as children all conform to the contract above. Examples:

ts
// MockDbService accepts:
{ Query: { table: 'string', limit: 'number?' } }

// SchemeEval accepts:
{ eval: { source: 'string?', expr: 'any?' } }

// SecurityRealmService accepts:
{ getDefaultPolicy, mintCapability, attenuate, revokeCapability,
  validateCapability, listCapabilities, cascadeDemo }

The full list lives in each service's source file under src/services/.

The FlowRunner path

onRunFlow (the example-execution path, separate from onRunFromCode) uses FlowBuilder (from @open-matrix/core/flow/FlowBuilder) to construct a plan, then sends it to the flow-runner child:

ts
// FlowpadApp.ts:373-412
const builder = FlowBuilder.start(this._context!);
let chain = builder.fromValue(input);
for (let i = 1; i < example.stages.length; i++) {
  const stage = example.stages[i];
  if (stage.kind === 'remote-rr') {
    chain = chain.viaRemote(mount, stage.op);
  } else if (stage.kind === 'local-map') {
    chain = chain.mapLocal(stage.fn);
  }
}
const plan = chain.build();
const result = await RequestReply.execute(
  this._context!,
  this._context!.mount + '.flow-runner',
  'RunFlow',
  { plan, input, flowId }
);

FlowRunnerService (src/services/FlowRunnerService.ts) is what receives that RunFlow op. The two paths exist because onRunFlow predates onRunFromCode — they will likely converge as the IR becomes more capable. Today, onRunFromCode goes through executeCanonicalIR directly (no flow-runner round-trip), onRunFlow goes through the runner service.

Five-representation parity

When the editor input parses as a component schema (HTML, Fluent, S-expr, Topics, or JSON), compileSourceToCanonicalIR produces an IComponentSchemaIR instead of a query plan. The pipeline can convert any one of those formats to any other using @open-matrix/core/serialization — see the DSL examples 'Serialize: S-expr', 'Serialize: HTML', 'Serialize: Fluent', 'Serialize: Topics', 'Serialize: JSON'. This is the codepath for treating FlowPad as a serialization workbench rather than a pipeline runner.

Targeting agents and oracles

FlowpadApp.onSnapshotTreeOracle (FlowpadApp.ts:264-304) returns the page's surface as a tree:

ts
{
  mount,
  tag: 'flowpad-app',
  accepts: [ { op, params: [[name, type], ...] }, ... ],
  emits: [...],
  cognitiveState: { selectedExample, targetMount, availableExamples },
  children: [ { mount, tag, accepts, emits, ... }, ... ],
}

An LLM or external orchestrator that has FlowPad as $TARGET calls snapshot.tree.oracle (or introspect) to learn the available ops and the example library. From there it can synthesise runFromCode calls.

See also

Source: projects/matrix-3/packages/flowpad/src/runtime/queryPipeline.ts and projects/matrix-3/packages/flowpad/src/FlowpadApp.ts:336-412.