Skip to content

7. Values, Types, Collections, and Interfaces

FlowScript types are not annotations added to make source code look disciplined. They describe what may cross a visible connection.

That changes how we should learn them. A string is not only a compiler category; it is a string pin that can connect to another compatible pin. An Incident is not a hidden class; it is a structured contract whose fields can become visible on the Board. An array is not merely a value that happens to contain several things; its collection shape is part of the wire’s contract.

The governing rule is simple:

If Flow-Like knows that two values are incompatible, it should reject the connection as early as possible. If an external system changes in a way the Flow could not know beforehand, execution should fail transparently at the node that first discovers the bad value and make the repair easy to find.

APIs drift, models return unexpected content, and remote systems sometimes lie about their own schemas. Reliability means using every fact we have, then making the remaining uncertainty observable.

Release check: The scalar and container spellings, interface grammar, schema projection, connection diagnostics, and Make/Break behavior in this chapter are grounded in the current parser, renderer, reconciler, pin-matching code, and catalog tests. A publication release must still verify the complete examples in Studio and capture the exact diagnostic and jump-to-node experience. Schema support is evolving; the final section distinguishes today’s readable surface from richer metadata that may remain behind it.

A Flow-Like data pin has a base data type. FlowScript gives the current built-in types these canonical spellings:

FlowScriptKind of valueTypical examples
stringTextnames, messages, URLs, identifiers
intWhole numberretry counts, status codes, quantities
floatDecimal numberscores, prices, measurements
boolBooleanenabled flags and decisions
DateDate or timestampobservation and scheduling times
PathFlow-Like storage pathfiles and managed object-store locations
bytesBinary dataencoded files, hashes, protocol payloads
StructStructured JSON-like valueAPI responses, records, configuration objects
anyGeneric data valuea boundary whose concrete data type is not yet known

Execution pins are also typed internally, but they are control connections rather than values we store in variables. They answer “what runs next?” instead of “what data is this?”

The first four types have direct literal forms:

const service = "orders"
const retries = 3
const confidence = 0.95
const productionStopped = true

Date, Path, and bytes often arrive from catalog nodes or explicitly typed boundaries. A quoted timestamp is still a string merely because it resembles a date. The distinction matters: a Date node can offer date operations, while a string can offer text operations. Conversion should be explicit when those contracts meet.

bytes represents one raw byte buffer on a Normal pin. bytes[] would be an array of buffers, not the usual representation of one file’s contents.

Struct means “an object-shaped value,” but it does not necessarily say which fields exist. That is useful at genuinely open boundaries, such as a webhook payload whose keys are selected by another system. It gives Flow-Like less information than a named interface, however.

any goes one step further. It is the textual spelling of a Generic pin: a value whose base type may specialize when it connects to something concrete. FlowScript permits this because some nodes really are generic. Array utilities, selectors, and external boundaries cannot always name a single element type in advance.

Permitted does not mean preferred. Flow-Like does not need an arbitrary style penalty for any; the workflow already makes the cost felt. With a typed value, compatible operations can be found by type, incompatible wires can be rejected, and fields can be exposed visibly. With any, the author often has to inspect, convert, or defer a decision until runtime.

The same is true for an untyped Struct. A schema-aware structure can use Make Struct (Schema) and Break Struct with visible field pins. An open structure pushes the author toward generic Get Field and Set Field operations with string keys. Both are possible. One is easier to build, read, and govern.

The base type is only half of a pin contract. Flow-Like also records the value’s shape:

  • Normal means one value.
  • Array means an ordered collection.
  • Map means values addressed by string keys.
  • Set means a collection of unique values.

FlowScript renders those shapes in familiar syntax. Top-level Board variables may intentionally have no persisted default, as in this type-focused example:

const report: string
const reports: string[]
const incidentsById: Map<string, Struct>
const affectedServices: Set<string>

The canonical FlowScript map form uses string keys. The inner type names the values. A set has one element type. For ordinary declaration and function-boundary types, the current grammar wraps one base type in one collection shape; it is not an unrestricted TypeScript generic system.

Shape participates in compatibility. A string is not a string[], and a Struct is not a Struct[]. Even Generic pins do not make that distinction meaningless: once a generic input or output declares a collection shape, current connection planning protects the scalar-versus- collection boundary rather than silently guessing.

This matters visually. Imagine a node that produces Incident[] and a node that accepts one Incident. The correct graph needs an operation that selects or iterates an element. This example makes the choice explicit with For Each:

Inspect Incidents event passing a typed incidents array into For Each, with each Struct value feeding Get Field and Print Info.
The collection wire ends at For Each; only its per-item Value output crosses into the scalar Incident path.

Connecting the collection directly would hide which incident the consumer should receive. Rejecting the wire makes that decision explicit.

Array literals exist, but top-level defaults use compact canonical JSON, and an unannotated array default only establishes any[] today:

const names: any[] = ["api","billing"]
const typedNames: string[] = ["api","billing"]

The first declaration says “this is an array” without promising one element type. The second keeps the intended element contract visible. Empty arrays are even less informative, so an annotation is the honest choice when the type matters.

An object literal is a Struct, not automatically a Map. A struct has named fields that may each have different types. A map has arbitrary string keys whose values share one type. That difference decides whether we want named field pins or key-based lookup.

The current source surface has array and JSON object literals. Maps and sets are normally created and transformed through their catalog nodes rather than with JavaScript-style new Map() or new Set() expressions. That is consistent with the rest of FlowScript: construction remains a visible, declared node operation instead of arbitrary code hidden inside the source view.

FlowScript infers top-level variable types only where a literal provides an unambiguous answer. The present rules are deliberately small:

LiteralInferred declaration type
"hold"string
3int
3.0float
true or falsebool
{"system":"orders"}Struct
[1,2]any[]
nullnone; add an annotation

The renderer removes a redundant scalar annotation when the literal infers exactly the same type:

const retries: int = 3

becomes the canonical form:

const retries = 3

It retains an annotation when it carries information the literal does not. These declarations are not equivalent:

const threshold = 1
const thresholdAsFloat: float = 1

The first is an integer. The second deliberately declares a float despite using a whole-number default. Likewise, structured and array defaults keep their annotations in canonical source so their intended shape remains readable.

null cannot tell us whether the missing value will later be text, a date, an incident, or something else. FlowScript therefore rejects this:

const owner = null

and requires an explicit type. A top-level Board variable may omit its persisted default:

const owner: string

That is an unset default, not a declaration that every string pin accepts null as ordinary data. At interface-field level, nullability has a richer spelling—string | null—which we will use in the next section. Ordinary variable and boundary type references currently remain the simpler base-plus-container form.

Top-level inference is also limited to literals. FlowScript does not treat this as a hidden compile-time program:

const normalized = normalizeReport()

A top-level Board variable is persisted configuration, not the result of executing a node while the document loads. Node calls belong inside functions and events, where their output types come from catalog declarations and connected pins.

That separation gives Flow-Like two moments to catch a problem.

First, Studio checks candidate wires, and FlowScript reconciliation resolves calls against the live catalog. If a wired or anchored source contract says int and the target requires string, the new edge is rejected with a conversion diagnostic. Known shape and enforced-schema mismatches get the same treatment. Current Apply does not prove every direct literal against its target contract, so “known” must not be read as universal compile-time validation.

Second, execution validates reality. Suppose an external service used to return a customer ID as text and begins returning a number without updating any contract Flow-Like can see. No static checker can reject an unknown change. The first node whose typed operation cannot consume the value fails during the run. That may be a conversion or a later consumer if an open field lookup first produced null. Error and log evidence remain attributed to the discovering node, so the responder can jump to the operation that needs repair.

Early rejection and transparent runtime failure are not competing promises. Together they mean: catch everything knowable, and localize everything else.

A named interface turns an anonymous Struct into a readable field contract. Here is a more complete incident record:

interface AffectedSystem {
name: string;
region?: string | null = null;
}
interface IncidentRecord {
id: string;
severity: "critical" | "high" | "medium" | "low";
report: string;
source?: string | null = null;
affectedSystems?: AffectedSystem[] = [];
labels?: Map<string, string> = {};
observedAt: Date;
"external-ticket-id"?: string | null = null;
externalPayload?: any = null;
}
eventsGeneric inspectIncident(payload: Struct, incident: IncidentRecord) {
let report = incident.report
}

This one declaration carries several independent facts:

  • id, severity, report, and observedAt are required fields.
  • source may be absent, and when present may contain a string or null.
  • affectedSystems is an array of another named structured type.
  • labels is a string-keyed map whose values are strings.
  • severity declares four literal alternatives in its schema rather than an unrestricted string.
  • the external ticket key is quoted because its hyphens cannot be written as an identifier; and
  • externalPayload admits uncertainty exactly where it enters, without weakening the rest of the record.

Optionality, nullability, and a default are different statements:

source?: string | null = null;

The ? says the property is not required. | null says null is an allowed value when the property exists. = null records a default in the generated schema. A field can be required and nullable, or optional but non-null when supplied. FlowScript does not collapse those choices into one vague idea of “maybe.”

Inside interfaces, the current surface supports named types, nested arrays, string-keyed maps, unions, string-literal alternatives, null, and any. Parentheses disambiguate an array whose element is itself a union:

interface Evidence {
observations?: (string | null)[] = [];
}

Without the parentheses, string | null[] would mean a string or an array of nulls, not an array whose individual elements can be either.

The parser turns an interface into JSON Schema metadata. When one interface references another, the generated schema carries the referenced definition. A variable, function boundary, or event boundary using IncidentRecord therefore still becomes a Struct pin at runtime, but one with a specific schema attached.

This is structural information, not object-oriented behavior. IncidentRecord has no constructor, prototype, or arbitrary methods. It describes fields on a wire. Method-shaped syntax still resolves to a catalog node or a FlowScript function with a compatible receiver.

The practical reward for typing a Struct appears on the canvas.

Given an open Struct, Flow-Like knows that the value is object-shaped but not which keys it contains. A generic Get Field node therefore needs both the Struct and a string such as "report". A generic Set Field node needs the same kind of key. Rename the remote field and that string may remain plausible until a run reaches it.

Given IncidentRecord, the schema can drive field-shaped nodes instead:

Typed incident Struct connected to Break Struct, which exposes Id, Report, and ObservedAt output pins.
Break Struct adopts the IncidentRecord schema and turns its three fields into visible, typed outputs.

Break Struct adopts the concrete schema from its producer and creates a field output pin with the most specific type it can currently derive for each property. Make Struct (Schema) does the inverse: it exposes field inputs derived from the consumer’s schema and produces the structured value. Field selection becomes part of the visible graph rather than a bag of unverified string keys.

FlowScript can render that graph compactly:

let report = incident.report
let ticketId = incident["external-ticket-id"]

Plain identifier-like fields use dot syntax. Property names containing spaces, hyphens, or other characters that cannot form an identifier use bracketed string syntax. A numeric or dynamic bracket remains a collection index, so incidents[0] and incident["external-ticket-id"] keep different meanings.

The compact expression does not hide arbitrary reflection. Depending on the live graph, the renderer can be describing a Break Struct field output or a catalog field-access node. Applying the text must resolve that expression back to a compatible graph operation. Chapter 9 will follow that lowering in detail.

Schema propagation is what keeps the field information useful after the first wire. Current catalog behavior carries a typed array’s item schema through common operations such as Get Element and For Each, so a downstream Break Struct can still expose the element’s fields. Nested Struct fields receive standalone sub-schemas where the catalog can derive them. A generic passthrough must not overwrite a concrete schema with an “open object” marker.

Schema evolution should preserve the author’s intent, too. The current Make/Break implementation does not silently delete a field pin while it still has a wire. If a new producer schema removes the wired name field, the stale pin and its connection remain, the new fields are added, and the node receives an error naming what is still connected but no longer declared. The author can jump to that node, insert a conversion or select the renamed field, and repair the Flow without first reconstructing a vanished wire.

That is the concrete answer to “what happens when an interface changes?” There is no single rule based only on whether a field was added, removed, or renamed. Flow-Like uses the information available at each boundary:

  • a new optional field may require no repair at an open or schema-adopting boundary, although two enforced whole-schema contracts can still compare unequal;
  • a known type or schema contradiction is rejected before a new edge is applied;
  • a removed field that is still wired is retained and reported rather than silently erased; and
  • an unannounced external change can only be discovered at runtime, where the first node unable to consume the changed value exposes it.

The principle matters more than pretending every provider follows perfect versioning: catch the change wherever it becomes knowable, and take the responder directly to the place that must convert or adapt it.

Types protect edges, but the protection is intentionally based on facts—not wishful inference. The following matrix is a useful working model:

What Flow-Like knowsCurrent behavior
Known source and target contracts have different base types, such as int and stringReject the new connection and request an explicit conversion
Scalar versus collection shapeReject where the declared shapes contradict one another
Two incompatible concrete schema contractsReject when the pins require those contracts to be enforced
A direct literal has the wrong input typeStudio may warn, but Apply does not yet validate every literal; the consumer may reject it at runtime
One side is genuinely Generic or declares an open Struct shapePermit specialization or schema adoption where that node contract allows it
A typed Make/Break field disappears while still wiredPreserve the wire and report the stale field on the node
An external value violates a fact the Board did not knowFail at runtime with node-attributed error evidence

any is therefore an escape from a fact we do not possess, not an escape from every collection rule. An open Struct schema similarly says “these fields are not fixed”; it must not be mistaken for a different concrete object schema. Make and Break boundary pins are special because they adopt the connected Struct schema dynamically.

Schema compatibility is not currently a full structural-subtyping theorem. Where two pins enforce concrete schemas, the safe comparison is generally canonical contract equality, with explicit schema-adopting boundaries as exceptions. Authors should not assume that a record with three fields can always flow into a two-field input merely because it looks like a TypeScript subtype. The node declaration decides whether its schema is descriptive, enforced, or open.

The Board stores schema metadata as JSON Schema text or as a reference to it. FlowScript interfaces project the most readable portion of that metadata into source. Today that readable surface handles object properties, requiredness, supported defaults, named definitions, arrays, string-keyed maps, supported unions and literal strings, null, any, and date/date-time formats.

Some JSON Schema carries more than this grammar can say: validation bounds, titles and descriptions at every level, pattern properties, or compositions such as oneOf and allOf may exceed the readable interface projection. The legacy @schema("…") decorator remains an escape hatch for an object schema that cannot be represented as a named interface. It is exact, but it is not pleasant prose and should not be the first choice for an ordinary record.

There is an important preservation nuance. When a Board already contains a richer live schema, the renderer can show its representable interface projection while the reconciler retains the exact schema on an unchanged Board-to-source-to-Board round trip. The visible interface does not necessarily prove that every hidden constraint could be recreated from a new standalone text file. Preserving known Board metadata and authoring every possible JSON Schema feature are different capabilities.

Collection surfaces have release boundaries as well. Top-level and function/event type references currently support Normal, array, Map<string, T>, and Set<T> shapes around a single base type. Interface fields support richer nested arrays, maps, and unions, but Set<T> is not yet part of the interface-field grammar. Current Make/Break field inference is narrower still: an enum-only literal field can materialize as Generic, while a map-shaped property can materialize as an object-shaped Struct. The root interface contract remains available, but those derived field-pin types are not yet as precise as the source. Treat these facts as release observations, not as a manifesto against future type-system growth.

Named-type lookup has another current edge: a top-level, function, or event type name that does not resolve to a declared interface can fall back to a schema-less Struct. A misspelled interface name is therefore not guaranteed to be rejected today. Keep publication examples under catalog-aware reconciliation tests instead of treating parser acceptance as nominal type safety.

For publication, any example near these edges should pass three checks against the named release:

  1. parse and render the FlowScript;
  2. apply it with the release’s catalog and inspect the resulting Board pins and schemas; and
  3. render that Board back to FlowScript and compare the meaningful contract.

If a shape fails one of those checks, report the smallest reproducible source, the release, the expected field or container metadata, and the actual result. Do not turn one unsupported round trip into folklore that “FlowScript can never support this.” The language is evolving, and honest release notes are more useful than permanent-sounding limitations.

Types in FlowScript serve one purpose across both views: make the workflow easier to construct, harder to connect incorrectly, and faster to repair when reality changes. The next chapter uses those contracts to navigate the catalog—the large, typed node library that gives FlowScript its real capabilities.