Inputs and Outputs

In FlowMaker, node IO is explicit and versioned as part of flow and box definitions.

  • inputs: inbound ports (id, name)
  • outputs: outbound ports (id, name)

A connection always links:

  • source.nodeId + source.outputId
  • to destination.nodeId + destination.inputId

Why explicit ports matter

  • Validation before run: invalid port mappings fail early during spec gathering.
  • Safer refactors: changing a box IO contract forces visible flow changes.
  • Predictable routing: each edge in the graph corresponds to one declared output→input path.

Canonical TypeScript model

interface FlowNodeDef {
  id: string;
  boxVersionId: string;
  inputs: { id: string; name: string }[];
  outputs: { id: string; name: string }[];
  options?: Record<string, any>;
}

interface FlowConnection {
  source: { nodeId: string; outputId: string };
  destination: { nodeId: string; inputId: string };
}

Runtime initialization contract

When a run starts, each node receives only the connected ports through INIT_FLOW_ELEMENT:

interface FlowBoxInitParams {
  runtimeContext: {
    flowBoxId: string;
    jobId: string;
    nodeId: string;
    usedBy?: string;
  };
  connectedIO: {
    input: string[];
    output: string[];
  };
  options: any;
}

This minimizes ambiguity in workers: each box knows exactly which input/output channels are active for that run.

[INFO!tune/CONNECTED IO ONLY] connectedIO is intentionally runtime-scoped: workers should process only declared connected ports and ignore assumptions about optional ports not wired in the current graph.

Concrete flow wiring example

const flowDef = {
  nodes: [
    {
      id: 'src-meter',
      boxVersionId: 'industream/source/modbus/1.2.0',
      inputs: [],
      outputs: [{ id: 'telemetry', name: 'Telemetry stream' }],
    },
    {
      id: 'pipe-normalize',
      boxVersionId: 'industream/pipe/normalize-energy/2.0.0',
      inputs: [{ id: 'raw', name: 'Raw values' }],
      outputs: [{ id: 'normalized', name: 'Normalized values' }],
    },
    {
      id: 'sink-historian',
      boxVersionId: 'industream/sink/timeseries/3.1.0',
      inputs: [{ id: 'in', name: 'Input data' }],
      outputs: [],
    },
  ],
  connections: [
    {
      source: { nodeId: 'src-meter', outputId: 'telemetry' },
      destination: { nodeId: 'pipe-normalize', inputId: 'raw' },
    },
    {
      source: { nodeId: 'pipe-normalize', outputId: 'normalized' },
      destination: { nodeId: 'sink-historian', inputId: 'in' },
    },
  ],
};

Real-life IO patterns

  • One-to-many fan-out: one output sends to both storage and alerting sinks.
  • Many-to-one merge: several source streams converge into one correlation pipe.
  • Branch by responsibility: one pipe output for archival, one for real-time decisions.

Common pitfalls to avoid

  • Reusing human-readable names (name) instead of stable IDs (id) in runtime logic.
  • Versioning a box without updating downstream port expectations.
  • Connecting ports that are semantically incompatible (e.g., event stream vs batch payload) even if structural types match.

[ERROR!rule/CONTRACT VIOLATION] If a box upgrade changes port IDs or semantics, treat it as a breaking change and migrate flows explicitly; silent remapping by label leads to misrouted data.

Runtime references

  • platform-backend/docs/runtime-v2.md (FlowDef, FlowNodeDef, FlowConnection)
  • platform-backend/docs/runtime-v2.md (INIT_FLOW_ELEMENT payload)
  • platform-backend/src/runner/run-node.class.ts
  • sdk/typescript/src/flowbox-init-params.ts