Components

TL;DR

  • A component is the platform's unit of capability: a typed, versioned, containerized piece of code that does one job — read an image, run a detector, normalize an audio frame, write to a queue. It is reusable across many backends and owned by one team.
  • A component declares what it consumes, what it produces, and how it can be configured — typed input streams, typed output streams, typed parameters, optional file slots. That declaration is the contract every other layer of the platform reads.
  • Components have immutable releases. Each publish creates a prerelease; promotion turns a prerelease into a released version. Backends pin specific releases, so promotion does not change any running deployment.
  • A component owns behavior. A backend owns composition. A deployment owns runtime. Keeping the three primitives separate is what lets a single capability power many products without forking the code.
  • Most components are stateless functions: input in, output out. State, virtual streams, and custom pull policies are advanced topics for the cases that need them; the basic shape covers most real components.

What a component actually is

A component is the smallest reusable unit of capability the platform models. Concretely, it is a typed function packaged as a containerized worker, with a manifest declaring what it consumes, what it produces, and how it can be configured. That declaration — the component.yml — is the contract every other layer reads: backends use it to type-check graph wiring before deploy, the catalog uses it for discovery, the runtime uses it to determine what the worker's container expects.

The granularity is deliberate. A bare function is too small to operate independently: no manifest, no container, no versioning. A whole pipeline is too large to reuse across products. A component sits at the level where one job maps to one unit that can be pinned, validated, reused, swapped, and operated on its own. The typed contract and immutable release model are what make that unit composable rather than just isolated.

The default contract is that components are stateless at the graph boundary. The function consumes typed inputs and produces typed outputs; everything else — model weights, configuration, optional state, side-channel events — is machinery the component declares when it needs it. That default keeps most components small and lets the scheduler parallelize them freely. The opt-ins exist for the cases that need them, and each one constrains how the runtime can schedule the vertex, which is the right pressure for keeping components simple.

Mental model

  ┌──── component release (immutable) ─────┐
  │                                        │
  │   component.yml      src/              │
  │   ┌─────────────┐    ┌─────────────┐   │
  │   │ language    │    │ Python or   │   │
  │   │ inputs[]    │    │ C++ code    │   │
  │   │ outputs[]   │    │             │   │
  │   │ config      │    │ implements  │   │
  │   │ files       │    │ the typed   │   │
  │   │ tags        │    │ function    │   │
  │   └─────────────┘    └─────────────┘   │
  │                                        │
  └────────────────────┬───────────────────┘
                       │  pinned by
                       ▼
                 backend vertex

The release is the immutable artifact. A backend pins it as a vertex, binds its parameters and files, and wires its streams. The same release can sit as a vertex in many backends; each backend carries its own bindings and its own graph context. Promotion records a new released row; pinning is what isolates a backend from that — a backend that pinned release_v3 keeps running v3 after v4 becomes default.

See Backends for the graph the release plugs into, and Types for the typed-stream contract its inputs and outputs satisfy.

What "typed function" actually buys you

Use this framing the first time you ask "what does the component contract guarantee".

The declared typed contract is small in the manifest — a few lines per stream — and load-bearing everywhere else. The backend's type inference uses it to reject wiring an Image output into a [BoundingBox] input. The CLI uses it to render meaningful error messages when a parameter is missing. The catalog uses it to filter components by the modalities they handle. The runtime uses it to determine what container resources the component needs at start.

The component author writes the contract once; every other party — graph authors, downstream consumers, ops teams, later maintainers — reads it. Declaring the contract moves wiring and parameter mistakes to edit time, where the backend's type inference catches them, instead of deferring them to a failed deploy. That is the practical reason the manifest is mandatory rather than optional.

The typed values themselves come from one of three places: pipelang atomic types (Double, String, Int64), composite types built from them ([BoundingBox], Maybe<String>, (Image, [Double])), or named types from the platform's type registry (Image, Tensor, AudioFrame, BoundingBox). The named types carry domain semantics: an Image is not just a tensor of bytes, it is the typed value every image-handling component agrees on, with shape, channel order, and metadata that downstream consumers read directly rather than re-deriving.

See Named types for the registry that backs Image, Tensor, and the rest.

Stateless by default, stateful when behavior needs history

Use the default; reach for state only when the next output genuinely depends on previous ones.

The default component is a stateless function: the runtime can call it many times concurrently for the same vertex, because no call depends on another. That lets the scheduler fan a single vertex out across CPU cores under load, or run independent invocations in parallel without coordinating ordering.

Declaring state changes that contract. The component asks the platform to persist memory between calls and to invoke the function one at a time per vertex, so the previous call's next state is visible to the next one. The vertex becomes history-dependent: trackers, rolling windows, session maps, conversation memory, debouncing, and "emit only after N seconds of buffered audio" all need state to behave correctly. A stateful vertex is serial per replica, which is the right pressure for keeping state out of components that do not need it.

What stays outside persisted state is the per-process machinery: model handles, database clients, compiled regexes, caches that can be regenerated on startup. Those belong in module globals or setup hooks, not in the platform's persisted state. Persisted state is for behavioral memory — the values whose loss between calls would change what the next call emits. Everything else is process-local and lives accordingly.

Side effects live behind virtual streams

Use this when the component needs to talk to the world outside the graph: HTTP, WebSocket, devices, SDK callbacks, file writes with retry semantics.

Most components consume from graph inputs, run a deterministic function, and emit to graph outputs. Some components have a harder job: they own an HTTP listener that receives traffic asynchronously, a camera thread that produces frames independently of graph timing, an LLM SDK that streams tokens back through a callback, a file writer that needs retry handling. Threading those side effects directly into the graph-facing function makes it impossible for the scheduler to determine when the function is safe to invoke.

Virtual streams isolate that work. Virtual input is a component-local queue populated by code outside the normal graph-facing tick: an HTTP listener, a device thread, a streaming SDK callback. The component can include virtual input in its pull request, so those external events still flow through the same scheduler-visible function as normal stream messages. Virtual output is the inverse: a queue of side-effect work items the component returns from a tick, which a bridge thread executes asynchronously and can feed results back through virtual input.

The discipline that makes virtual streams work is that the graph-facing function stays scheduler-visible. Side effects live behind the queues; the function reads and writes them; the bridge performs the IO. That separation is what lets a component talk to the outside world while the scheduler retains its model of when the function runs.

Releases are immutable; promotion is deliberate

Use this mental model whenever you reason about "why did my backend not pick up the new code".

Each publish creates a prerelease: a private validated build the caller can deploy in test backends. Promotion is the separate, deliberate step that turns a prerelease into a released version: the image goes to the registry, the tags declared in component.yml move (latest, default), and the version becomes deployable by everyone in the workspace's audience. The version ID does not change; only its state does. Promotion is refused until the version meets every rule of the release checklist.

Backends pin specific release IDs. Promoting a prerelease does not change any deployed backend's pinned version — the backend keeps running whatever it pinned at the time of last deploy. Rolling a backend forward to a newer release is an explicit vertex mutation followed by a redeploy. That separation is why promoting a broken version cannot, by itself, take down a running backend. The running backend is pinned; the new release is a new addressable artifact in the catalog.

See Publish semantics for the full publish + promote loop.

What a component owns and what it does not

A component owns its identity (display name plus workspace-scoped slug), its language (Python or C++), its declared input and output stream types, its typed config_schema, its optional file_schema for model and data slots, its optional generated_file_schema for outputs it commits back as workspace files, and its implementation source under src/. It also owns its discovery metadata — the catalog category and modality tags, the README and AGENT.yml that travel with the release.

It does not own anything graph-shaped. The backend it appears in, the vertex bindings of its parameters in that backend, the file IDs bound into its slots, the endpoint aliases its outputs are exposed through, the deployment that runs it, the runtime that hosts the deployment — all of those are backend-and-deployment concerns. The component is the capability; everything else is composition and runtime.

That separation is the whole reason a single component can power many products. A detector component is the same in a warehouse-safety backend, a retail-traffic backend, and a clinical-imaging backend; each backend wires it differently, binds different parameters, deploys it on different compute. The component does not need to know any of that.

The release checklist

A valid component type-checks; a good component is trustworthy from its manifest alone. The release checklist is that bar written down, and the platform holds every version to it: promotion is refused while any rule is open or failing. Components that meet it can be wired, swapped, and operated by people — and agents — who never read the implementation.

Each rule is judged against the version's current inputs and says how it is shown to hold. Auto rules are checked at publish, returning 400 with invalid fields, and recomputed on checklist reads. All seven Live rules pass together when a lease's latest completed run passed against the current execution inputs with its runner, python3 tests/test_backend.py; a run that only deployed the version proves none. Record them with ppl component checks record --lease <lease_id>. A component's tests/ holds exactly the manifest tests/live-test.yml, that runner with every test case run against the live deployment, tests/verify_files.py when output files are checked, and the fixtures and reference data they read — never a shell script, a compiled unit test or a helper script. Review rules are recorded with evidence a reviewer can check — the file and line, the measured number, the observed output. Conditional rules may be not_applicable to a component without the feature they govern, and a person may waive a rule with a reason. Publishing reopens only rules whose inputs changed. README edits preserve live proof and unrelated reviews. Promote's 409 groups remaining live, review and auto rules with commands to close each group.

Contract

  • contract.first (review) — contract designed before code. component.yml — inputs, outputs, configuration and file slots — is designed before the implementation and stays the source of truth. Graph authors, the catalog and agents read only the contract; code shaped first and described afterwards leaks its accidents into it. Meet it: Write component.yml from the task, the input and output shapes and the downstream consumers, then implement against it. Evidence: the contract decisions and why each port, parameter and slot has the shape it has.
  • contract.exact (live) — declared types match what the code emits. Every type the worker emits is declared in component.yml field for field, spelled and typed as the code writes it. A declared field the code never writes, or an emitted field the type does not declare, fails every item at run time. Meet it: Make the code write exactly the declared fields, or change the declaration to what the code means to emit. Proven by a recorded live test of the version in which every item passed type validation.
  • contract.reuse (review) — built only where nothing existing fits. A component exists only for logic worth reusing across backends or needing files, secrets or releases. A released component that fits, a built-in transformation or an inline transform vertex makes a new component a duplicate the catalog has to carry forever. Meet it: Search the catalog by capability and by input and output types, and compare contracts, not names. Evidence: the searches run and why each near fit falls short.
  • contract.standalone (review) — works and is described on its own. A component's contract and documents describe what it does with its own inputs and outputs. Another component appears only as an example of where its data can come from or go to, never as something it requires, because a graph author is free to wire it to anything that carries the type. Meet it: Remove wording that pairs the component with another one as a requirement; name the type it needs instead and keep other components to examples. Evidence: the summary and pick_when entries read without the paired component.
  • contract.typed_params (review) — parameters carry their constraints in the type. Valid values live in the type, not in prose: fixed choices are a refined String such as String<"fast" | "accurate">, optional values a Maybe, so a graph author cannot bind a value the code rejects. Meet it: Refine each config_schema type to its valid values. Evidence: each parameter whose type is a bare String or number and why its whole range is valid.
  • contract.secret_params (auto) — secrets are optional secret strings. A secret parameter is Maybe<String>, marked secret, defaulting to Nothing (), so an omitted secret is absent rather than an empty string a deploy has to refuse. Meet it: Declare every secret as type: 'Maybe<String>', secret: true, default: 'Nothing ()'.
  • contract.native_types (review) — registered types for platform data, and no one-field records. Images, audio, tensors, embeddings, masks, geometry, detections, depth and point clouds travel as the registered types, built through the SDK constructors. A hand-packed record type-checks today and breaks silently when the carrier evolves; a missing named type is a type-registry request, not an opaque inline record. Every custom record a port, parameter or state declares has at least two fields: a lone value travels as itself and a sequence of items as a list, never wrapped in a one-field custom record; a registered type travels as registered. Meet it: Emit and accept the registered types through the SDK, and unwrap any one-field custom record into the value it holds. Evidence: each port's, parameter's and state's type and, for any custom record, its fields and why no registered type carries it.
  • contract.oneof (review, conditional) — output arms are projections of one result. When several output shapes are valid, the component computes one canonical result and projects it into each oneof arm, choosing the arm once at startup from the connected sink type — never per message. Meet it: Implement one internal result and one projection per arm; resolve the arm at startup. Mark not applicable when no output is a oneof.
  • contract.files (review, conditional) — model files come through declared file slots. Weights, datasets and other files bind through file_schema slots that declare the real layout the worker loads. A worker that downloads at startup hides its real dependency, fails offline and cannot be pinned. A component serves a task family: another model its code can load through the same slot layout is another file in that slot, never another component, so the component's name, README, AGENT manifest and slot tag describe the task and the file format, never one model. A model's name, author attribution and license live on its file. Every fact that differs between models of the family (labels and the position of structural ones such as background, counts, shapes, input size, anchors, normalization, vocabulary) is read from the bound file, its metadata or a side file the slot's bundle layout declares; a fact no file carries is a parameter, defaulting to the library's own value where the library has one and required otherwise, never to one model's value, and an operating value the user may tune (a threshold) is a parameter whose default the file supplies when it carries one. The worker fails loudly when a fact is missing or disagrees with the file's tensors; the code never hard-codes one model's values. Code that fixes particular semantic classes for selection or special treatment belongs only to a component whose declared task names them; a class filter the user sets through a parameter is generic. Meet it: Declare a slot per file set with its real bundle layout and load only from it; read every per-model fact from the file, its metadata or a side file in the bundle, and take from a parameter only what no file carries and the operating values the user may tune, defaulting to the file's value when it carries one; keep model identity, attribution and license on the linked file. Evidence: each per-model fact the code uses and where it reads it. Mark not applicable when the component reads no files.
  • contract.resources (live) — hardware needs declared and true. GPU, memory and architecture needs are declared, and the declaration is what the component actually ran on. An undeclared need surfaces as a crash on the first runtime that lacks it. Meet it: Declare the hardware the worker needs; prove it with a recorded live test on a runtime matching the declaration.

Behavior

  • behavior.one_output (live) — one output per valid input. Every valid input yields exactly one value on every output port. Absence is said explicitly — a Nothing, an empty list, a documented value — never by skipping a tick, because downstream vertices schedule against each emission and a skipped tick stalls the graph. Meet it: Emit on every port for every input, with explicit absence. Proven by a recorded live test whose runner checks one output per input.
  • behavior.fail_loud (review) — invariants asserted, failures passed on whole. Impossible states are asserted at the boundary and trusted inside; there is no catch-and-continue and no fallback value standing in for a missing real one. An upstream service's failure reaches the output with its complete error object, so the cause is visible where it surfaces. Meet it: Assert at the boundary, remove silent fallbacks, return the full upstream error. Evidence: the file and line of each boundary assertion and error path.
  • behavior.logging (auto) — logs only through ppl::logger. A C++ component logs only through ppl::logger, so every line it writes reaches the platform's logs with its level and source. Its own C/C++ files under src/ (symlinks read through to what they name) do not use std::cout, std::cerr, std::clog, printf, wprintf (and their v-forms), puts, putchar, perror, dprintf / write / writev on descriptor 1 or 2, std::print / std::println / std::vprint_unicode with a format string first, or the stdout / stderr handles (fprintf(stderr, …), fputs(…, stdout)). Comments and string literals are not checked; the check is lexical and does not expand macros or resolve symbols. A directory under src/ that carries its own license file — named LICENSE, LICENCE, COPYING or UNLICENSE, alone or followed by . and a suffix (LICENSE.txt, LICENSE.hnswlib) — is vendored third-party code and is exempt with everything below it. A C++ component that breaks the rule is refused at upload. Meet it: Replace each write with ppl::logger::log (info) or ppl::logger::elog (error) from "pipelogic/logger/log.hpp", or ppl::logger::dlog (debug) from "pipelogic/logger/dlog.hpp". Vendor third-party code in its own directory under src/ together with its license file.
  • behavior.no_stall (live) — never blocks a shared stream. The component never holds a stream other vertices read. Anything that stops reading or holds input is a configuration choice documented as a hazard, because the worker cannot see the graph it is placed in. Meet it: Prove it with a recorded live test where one source feeds both this component and a plain passthrough probe, and the probe keeps receiving.
  • behavior.state (review) — stateless unless output depends on history. Persisted state holds only behavioral memory — values whose loss changes the next output. Model handles, clients and caches live per process. Declaring state serializes the vertex, so state that is not needed costs throughput. Meet it: Keep the component stateless, or justify each persisted field by the output it changes.
  • behavior.side_effects (review, conditional) — outside calls behind virtual streams. HTTP, devices, SDK callbacks and other outside work run behind virtual streams so the graph-facing function stays visible to the scheduler. Retries are bounded and documented. Meet it: Move outside calls behind virtual input and output; bound and document retries. Mark not applicable when the component calls nothing outside the graph.
  • behavior.live_params (review) — live-changeable only where safe. A parameter is mutable on a running deployment only when changing it mid-stream leaves every output correct. Meet it: Mark mutable only the parameters whose change the code applies safely between items.
  • behavior.upstream_parity (live, conditional) — as good and as fast as the code it wraps. A component wrapping an upstream model or library matches or beats the upstream code's output quality and speed on the same hardware. A wrapper that is worse than the code it wraps has no reason to exist. The reference is the upstream inference path for the declared task, every branch it takes on input properties and its preprocessing, decoding, suppression and thresholds included; steps that serve one upstream demo or deployment scene beyond the component's declared task (scene heuristics, filters fixed on classes the task does not name) are not part of it and are not carried into the component. Meet it: Measure output and latency against the upstream inference path on the same inputs and hardware in a recorded live test. Evidence: the upstream entry point used as the reference, the lease and the comparison. Mark not applicable when nothing upstream is wrapped.

Build

  • build.pinned (auto) — dependencies pinned exactly. Every dependency the manifest declares — src/requirements.txt for Python, xmake_packages for C++ — is pinned to one exact version, and packages the build system already provides are not restated, so a rebuild resolves the same packages. A system package the build image provides (pkgconfig::, cmake::, system::, apt::) takes its version from the image and names none. Meet it: Pin each requirement as pkg==X.Y.Z and each xmake package as name X.Y.Z (conan::name/X.Y.Z); write a system package as its bare name (pkgconfig::name); pick the build_system that carries the runtime's own packages.
  • build.license (auto) — license declared. The component declares the license its own code ships under, so whoever adds it to a backend knows what they may do with it. A model or other file bound through a file slot carries its own license on that file; the component license never covers or restates it. Meet it: Set the component license (SPDX id or text) to the license of the component's code alone; set each model file's license on the file.
  • build.no_secrets (auto) — no secret in source. No credential appears in source, manifest or assets. Secrets reach the worker only as secret parameters bound to workspace secrets. Meet it: Remove the credential, revoke it, and take it through a secret parameter.

Docs

  • docs.true (review) — documents say exactly what the code does. The README, the AGENT manifest and every description in component.yml are exactly true of the code. A document that oversells or drifts is a bug as severe as wrong output, because agents quote it to justify wiring. Meet it: Read each statement against the source. Evidence: the statements checked and the code each rests on.
  • docs.two_audiences (auto) — a README for people and an AGENT manifest for agents. The README explains how the component fits into pipelines and its caveats; the AGENT manifest says when to pick it, when not to, and how it behaves. Both pass the document rules. Meet it: Write both documents; ppl component rules --surface=readme and --surface=agent list what each must hold.
  • docs.hazards (review) — every hazard documented. Every way the component can stall a graph, drop data, cost money or fail is a caveat or behavior note, not only its happy path. Meet it: List the failure, stall, cost and limit cases from the code and document each. Evidence: the hazards found and where each is documented.
  • docs.refs (auto) — declared names referenced by role. Every role marker in the README resolves to what the manifest declares, and parameters are named through :param: markers rather than as bare words, so a rename cannot leave a stale name behind. Meet it: Use :param:, :type:, :input:, :output: and :component: markers for declared names; fix or drop each marker that resolves to nothing.

Look

  • look.name (review) — named by what it does. The display name starts with an imperative verb and its object, with the engine in parentheses — "Build Vector Index (FAISS)", not "Vector Store (HNSW)" — so the catalog reads as a list of capabilities. The engine is the library or runtime the code runs, never a model or checkpoint bound through a file slot. Meet it: Rename the component to verb + object (Engine), with no model name in it.
  • look.icon (auto) — its own icon. The component carries its own icon, never the placeholder, so it is recognizable in every catalog row. Meet it: Draw a 512×512 icon — one silhouette, two or three flat colors, no text — and set it as the component avatar.
  • look.tags (auto) — category and modality tags. The component carries at least one category and one modality, the filters the catalog is browsed by. Meet it: Set the component's categories and modalities.

Proof

  • proof.tests (live) — self-contained tests pass against the version. The component's own tests cover empty, degenerate, malformed, boundary, large and real inputs and every output arm, asserting real values, and pass against the published version. Meet it: Ship the tests with the component and run them as the live test's runner. Proven by a recorded live test whose runners all exited 0.
  • proof.live (live) — output compared on chosen input. The published version ran live on input whose content was chosen for it, and its output content was compared with what that input should become. A ready deployment or a returned file id shows the graph ran, not what it produced. Meet it: Run a live test on a fixture you chose and compare the collected output. Evidence: the lease and the comparison.

Where this fits

Components are the platform's load-bearing primitive for what code does. Backends compose them; deployments run them; the catalog discovers them; the type system constrains how they wire together; the release model keeps their behavior reproducible over time. Every other concept on the platform exists to make components reusable, composable, operable, or discoverable, so that a single capability serves many products instead of being reimplemented as bespoke glue for each one.

The discipline the platform asks of component authors is small: declare the typed contract accurately, default to stateless, reach for state and virtual streams only when the basic shape cannot express the contract, and treat releases as immutable. In return, the platform provides graph type-checking, catalog discovery, runtime scheduling, deployment, observability, and proof loops. The author writes the function; everything around it is the platform's responsibility.

Related

  • Backends — wire released components into a typed graph.
  • Types — the contract that catches bad wiring before runtime.
  • Named types — the registry behind Image, Tensor, AudioFrame.
  • Publish semantics — the publish and promote loop in detail.
  • File schema — declare the files a component consumes and produces.
  • Build systems — how the platform turns source into a validated image.
  • Solutions — component + backend + deployment + surface, end-to-end.
  • Quickstart — the route picker for your first task.

Was this page helpful?