Types
TL;DR
- A type is the contract the platform uses to decide whether one component's output can feed another component's input. Types are checked at edit time, at deploy time, and at every place a value flows through the system.
- Types come in three categories: atomic types (
Int32,Double,String,Bool, …), composite types built from other types (lists, tuples, records, optionals, sums), and named types from the registry (Image,Tensor,AudioFrame,BoundingBox, …) that carry typed binary payloads with their metadata. - Identifier case is significant. Lowercase identifiers (
t,a,frame) are generic type variables; uppercase identifiers (Image,Double,BoundingBox) are atomic types or named types. The platform unifies generics against the concrete types connected to them at edit time. - There is no implicit coercion. A
Stringcannot become anImage. If two slots disagree, the platform rejects the connection and reports the upstream type, the downstream type, and where unification fails. The fix is a converter component. - Parameter values are pipelang literals, not JSON — strings are double-quoted, tuples use parentheses, lists use brackets, records use braces. The literal must match the declared type of the parameter; mismatches are rejected at the change-parameter call.
What a type is on this platform
A type is the declared shape of a value that moves between components: the contract that says what an output slot produces and what an input slot accepts. AI workflows compose components from different teams, model frameworks, and generations of the platform into the same backend, so each wire needs a shared description of the data crossing it — raw bytes versus a structured tensor, a pre-resized image versus one the consumer must resize, a bounding box list in pixel coordinates versus normalised. The type is that description.
Every component declares the type of each input slot, each output slot, each configuration parameter, and each file slot. Every backend wires those slots with the typed contract enforced. The platform's type inference runs across the whole graph at edit time and rejects any mutation that would violate a contract — at the moment the mutation is attempted, before deploy.
Component authors write the contract; everyone downstream — graph authors, future maintainers, AI agents driving the platform from the CLI — reads it from the manifest without reading the component's source. The manifest is the spec.
Mental model
A pipelang type is the shape language for every value the platform reasons about. Three categories cover everything:
atomic composite named
───────── ──────────────── ─────────
Int32, UInt32 [Image] Image
Int64, UInt64 (Double, String) AudioFrame
Float, Double {x: Double, y: Double} Tensor
Bool, Char Image | AudioFrame BoundingBox
String, Bytes [BoundingBox] Polygon<Double>
Atomic types are the language-level scalars: ten numeric and value types that match what most type systems recognise. Bytes is the only deliberate escape — opaque binary blob, used sparingly when no richer type fits.
Composite types are built up from other types using constructors. Lists [t], tuples (a, b, c), records {x: A, y: B}, sum types a | b. A composite type can wrap any other type, including another composite type — [[Image]] is a list of lists of images.
Named types live in the platform's type registry. Image is not a tensor of bytes; it is the named type every image-handling component agrees on, with shape, channel order, and metadata that downstream consumers can rely on without re-deriving. Same for AudioFrame (sample rate, channel count, encoding), Tensor (shape, dtype), BoundingBox (coordinates, frame reference). The named registry is the bridge between the typed contract and the domain semantics.
See Named types for the registry behind Image, Tensor, and the rest.
Generics use lowercase identifiers
Use this framing whenever you wonder "is t a placeholder or a real type".
Identifier case is significant in pipelang. A type written with a lowercase leading letter (t, a, frame) is a generic type variable: a placeholder the platform unifies against the concrete type it ends up connected to. A type with an uppercase leading letter (Image, Double, BoundingBox) is a concrete primitive or a named type — not a placeholder.
The casing rule is what makes components polymorphic. A passthrough buffer declared [t] → [t] works on [Image], [AudioFrame], [Tensor], or any other element type — the platform unifies t consistently across the component's declaration. The buffer author writes the component once; the graph author reuses it everywhere.
Case selects the meaning. Writing [T] (uppercase) declares a slot that accepts a named type called T, which probably does not exist in the registry — the platform rejects it with "unknown named type". Writing [t] declares a generic slot. The rule: lowercase = variable; uppercase = concrete.
The check runs at edit time
Use this framing whenever you wonder why the platform rejected a connect call.
When a backend wires two vertices, the platform's type inference runs across the proposed edge — and across the whole graph — and decides whether the connection is sound. A successful wire means the source's output type unifies with the destination's input type, taking into account any generic variables that need to instantiate consistently. A failed wire means the platform rejects the operation and reports the upstream type, the downstream type, and the point where unification fails. The operation is not recorded; the graph stays valid.
The check covers every place types appear. Connecting an Image output into a [BoundingBox] input is rejected. Setting a parameter to a literal that does not match the declared type is rejected at change-parameter. Binding a file whose file_type: does not match the consuming slot is rejected at add-file. Changing a vertex's pinned release to a newer version whose I/O types do not unify with downstream wiring is flagged before deploy. The graph the platform deploys is by construction the same shape the platform validated.
There is no implicit coercion. To feed a String value into an Image slot, place a converter component between them; the platform does not bridge the gap silently. Edit-time checking moves type mismatches off the runtime path, where silent coercion in code-based composition systems otherwise surfaces them.
Open types, and how they resolve
Use this framing whenever you wonder why a generic component or a oneof[…] slot is allowed to stay "undecided" while you build.
Edit-time checking does not force every type to be concrete — that is what makes generic and polymorphic components possible. A buffer declared [t] → [t] keeps t open until a concrete stream is wired to it; an oneof[Image, DepthImage] slot stays an unresolved bound until the surrounding connections force it to one variant. The platform unifies these open types against the concrete types they connect to as the graph is built. A tagged bound oneof t[…] goes further — it correlates, pinning every same-tag occurrence to the same variant, so one choice fixes the rest.
Refinements narrow rather than open. Int32<0..=255>, String<"a" | "b" | "c">, Int64<%8>, and String<email> attach a predicate to an already-concrete base, and they travel with the type through inference as part of its identity — Int64 and Int64<%8> are distinct types. Because the base of a refinement is always concrete, a refinement never keeps a type unresolved; it only constrains the values the type admits. (Changing or removing a parameter's refinement re-types the parameter, and the platform clears any stored value the new type would no longer accept.)
Deploy requires every type concrete
Use this framing whenever you wonder what changes between editing a backend and deploying it.
The edit-time check is permissive about open types; deploy is not. Deploying a backend runs strict inference across the entire graph, and every wire and every configuration parameter must resolve to a fully concrete type. Anything still open — a leftover type variable, a pack variable, an unresolved oneof[…] bound — fails the deploy rather than starting a container with an undetermined contract. An unresolved bound is reported as needing a concrete pin.
This is the payoff of typing the graph at all: a backend cannot reach runtime with an ambiguous shape on any wire. By deploy time, every generic has been bound and every oneof has been narrowed to a single variant by the connections around it. If a slot was never constrained — a generic component wired to nothing concrete, a bounded slot the graph never pins — the fix is to complete the wiring before deploying. The system that runs is exactly the one strict inference fully resolved.
Pipelang literals are not JSON
Use this framing the first time you reach for change-parameter and the platform rejects what looks like valid JSON.
Parameter values are pipelang literals, not JSON. The grammar is small but specific: strings are double-quoted, tuples use parentheses with positional elements, lists use brackets, records use braces with named fields, booleans are lowercase, floats must contain a decimal point (1.0, not 1), and the empty tuple () is the canonical "no value" / unit.
A few literal shapes worth remembering:
- Strings:
"google/vit-base-patch16-224". Always double-quoted. - Numbers:
42(integer),0.5(float — note the decimal),0x2a(hex),-1. - Booleans:
true,false. Lowercase. - Tuples:
(0.0, 0.0, 0.0). Single-element tuples need a trailing comma:(42,). - Lists:
[0.1, 0.2, 0.3],[(8, 0.1), (4, 0.3)]. Trailing comma is optional. - Records:
{x: 1.0, y: 2.0}. Named fields, optional trailing comma.
Shell invocations should single-quote the full literal so the shell does not eat parentheses, brackets, or quotes before pipelang parses them:
ppl backend change-parameter <bid> --vertex 3 --name threshold \
--type Double --value '0.5'
ppl backend change-parameter <bid> --vertex 3 --name class_thresholds \
--type '[(UInt64, Double)]' --value '[(8, 0.1), (4, 0.3)]'
The full grammar — every literal form, the JSON-vs-pipelang differences, the named-type registry rules, the generics constraints — lives in the Type syntax reference.
When to reach for which shape
Use this section as a quick guide to the type categories.
Atomic types are right for the obvious cases — numbers, strings, booleans, opaque bytes. Use them as configuration parameters, as scalar outputs, as elements inside richer composite types.
Records are right when the data is structural (no opaque bytes, no media metadata) and no catalog named type fits. A {x: Double, y: Double, depth: Double} for a 3D point is clearer than three separate parameters. Records are inline-defined; they exist as the type you wrote rather than as a named entry in the registry.
Lists and tuples are right when the value is a collection. Lists are homogeneous ([BoundingBox] is a list of bounding boxes); tuples are heterogeneous positional ((Image, [Double], String) carries three values of different types). Reach for tuples when the positions are meaningful; reach for records when the positions need names.
Sum types (a | b) are right when a slot genuinely handles two unrelated shapes — a media demultiplexer that emits either Image or AudioFrame, a multimodal model whose output depends on its input. Sum types should be the deliberate choice, not a way to dodge defining a richer named type.
Named types are right for everything domain-specific that has metadata travelling with bytes — images with shape and channel order, audio with sample rate, tensors with shape and dtype. The named registry is what lets components from different teams agree on what an Image is without re-deriving the contract every time.
Generics (t, a) are right for shape-preserving components — buffers, samplers, gates, splitters, transformations that work on any element type. The same component is reused across Image, AudioFrame, Tensor, and any other type the graph wires through it.
Where this fits
The type system is the contract the rest of the platform builds on. Backends rely on it to reject bad wiring at edit time. Component releases rely on it to make immutability meaningful — the typed contract is the version's public face. Deployments rely on it to know what containers expect at start. Proof loops rely on it to know what shape an output should have. Applications rely on it to bind UI needs to endpoint shapes.
Untyped streams with ad-hoc per-component validation are the common alternative, and they push wiring mismatches to runtime. Typing moves the check to edit time: authors declare the contract once, and the rest of the system composes against it.
Related
- Components — where the typed contract is declared.
- Backends — where typed edges connect and where mutations are validated.
- Named types — the registry behind
Image,Tensor,AudioFrame. - Transformations — typed inlined atomic types between vertices.
- Type syntax reference — full literal grammar.
- Quickstart — route picker for your first task.