The FBP Language/ Control & Virtual Ports

Control & Virtual Ports

Control & Virtual Ports

Virtual ports

Every node always has a set of virtual ports — control ports that carry lifecycle and status events rather than data. They are wired like any other port:

SelectProtocol out [clear] -> :start ClearCommands
Source :error -> err Sink

The names carry a leading : to distinguish them from data ports. They are materialized by the runtime on every node; they do not appear in the node's ports definition.

:start — the virtual input port

:start is a virtual input port that every node auto-creates on connect. When the flow runs, it sends an empty IIP to :start on every startable node — a node whose input ports are all filled (a node with no inputs is always startable). This is how no-input nodes get kicked off.

Virtual output ports

Every node carries these virtual output ports:

Port Fires when
:complete The node completes a run
:start The node starts a run
:shutdown The node shuts down
:error The node raises an error
:statusUpdate The node's status changes
:contextUpdate The node's context is set
:contextClear The node's context is cleared
:nodeTimeout An async node exceeds its timeout
:stop (vestigial — see below)

Two of them have special behavior:

  • :error only writes a packet when a consumer is wired. If the port has no connections, the same error is raised as a node error event instead of a packet. This keeps error wires explicit: wire :error to a handler and errors flow as data; leave it silent and they surface as events.
  • :shutdown only fires if the node defines an on.shutdown handler. A node without one never emits :shutdown, even when the runtime tears it down.

In a normal run, :complete, :start, and :statusUpdate fire; the rest fire only when their condition occurs. The FBP protocol advertises just two of them to tooling: the :start input and the :complete output on every component.

@fn — logic as data

A port declared type: 'function' carries a live reference, not data. A function value travels through the graph by reference — never JSON-serialized — which is how a node can accept code and use it internally.

The .fbp side of a function port is a context feed onto it:

( ... ) -> @fn PrepareCommand(array/map)

The ( ... ) is a function body string. It lands on the node's fn input port, and because that port is type: 'function', the body is compiled into a live function when the port fills. The port name is incidental — @ only means "context, not a link/IIP"; the interesting part is that the target input port is declared type: 'function'.

A node needs an input port under that name; feeding a function into a port the node does not have raises an error. This is the function-as-data mechanism: inline logic without authoring a node file.

The inert surface

Two link-syntax features parse but are not wired to runtime behavior. A flow that uses them runs exactly as if the syntax were absent:

MyProcess(test) *out -> in[MyProcess] Consume(consume)
  • *out parses to source.setting.pointer: true — the pointer.
  • in[MyProcess] parses to target.setting.sync: 'MyProcess'sync.

Together they were a language-level mechanism for lockstep delivery (a node consuming several ports runs on (x₁, y₁) then (x₂, y₂), never on a lone x). That concept was re-implemented as a node-level library, @fbpx/group (SendGroup / Sync / RecvGroup), and the old .fbp syntax was left behind — parsed for compatibility, consumed nowhere. The runtime still registers both settings when it sees them, but nothing reads them, so they are best avoided in new flows.

Similarly, :stop is a vestigial port: the runtime declares it on every node, but no node code ever emits it — there is no clean node-level "stop" lifecycle to hang it on. Node teardown is :shutdown; error recovery is a reset, not a stop. The flow-level stop (flow.stop()) works, but it is the whole flow stopping, not a per-node port.

Edit this page