Backend operations
TL;DR
- A backend is not a static file — it is the result of a sequence of typed mutations applied to an empty starting state. The operation log is the source of truth; the graph state is derived.
- Every surface — App, CLI, SDK, REST API — speaks the same operation set. A change made through the CLI shows up identically through the App; there is one history regardless of which surface produced it.
- The operations form a small typed vocabulary:
add-vertex,connect,change-parameter,add-file,change-endpoint-alias,change-version,delete-vertex,disconnect, and a handful of others. Each one is recorded with its inverse, so the log is inspectable and undoable. - Three meta-operations cover history management:
undoreverts the last N operations,compactrewrites the log into the minimum sequence producing the same final graph,operationsbrowses what happened. - Parameter values are pipelang literals (not JSON), typed against the declaration the component made in its
config_schema. A value whose type does not match the slot is rejected at edit time, not at deploy.
What backend operations are
A backend is an event-sourced graph: a stream of small typed operations applied to an empty starting state, with the current graph derived by replaying them. The operation log is the source of truth; the graph is the projection.
Each operation is narrow — one vertex, one edge, one parameter — and is validated at the moment it lands. The platform records every operation alongside its inverse, so the log is both inspectable and undoable. Because the graph is the replay of the log rather than a file kept in sync with it, concurrent edits merge as independent operations rather than overwriting each other, and the graph at any past point is exactly the prefix of operations up to that point.
This is the standard event-sourcing trade-off. The platform owns the mutation surface — there is no out-of-band file to edit directly — and in exchange every change is validated before it lands, recorded with its inverse, and reversible. Undo reverts recorded operations; history answers who changed what and when; "the graph as of last Tuesday" is a concrete replay rather than a guess.
The CLI verbs, the App's graph editor, the REST API, and any SDK all produce the same operation rows against the same log. No surface has a separate low-level path that bypasses it. That uniformity is what lets you edit a backend from one tool without breaking what another tool sees.
Mental model
empty
│ add-vertex v1 (pinned to release X)
│ add-vertex v2 (pinned to release Y)
│ add-vertex v3 (pinned to release Z)
│ connect v1.out0 → v2.in0
│ connect v2.out0 → v3.in0
│ change-parameter v2.threshold = 0.7
│ add-file v2.weights = <file_id>
│ change-endpoint-alias v3.image_out → preview
▼
current backend graph
Each line is one recorded operation. The graph the App renders, the graph ppl backend get returns, and the graph the deploy step validates are all the replay of that log. Editing the same backend from the CLI, the UI, or a script writes new rows to the same log; the surface that initiated a change is not recorded as special.
Validation happens before an operation lands. A connect whose source and destination types do not unify is rejected and not recorded; the graph stays valid by construction. A change-parameter whose literal value does not match the declared type is rejected at the call. An add-file whose file type does not match the slot is rejected. The log only ever contains operations the platform accepted, which is why "is the current graph valid" is the same question as "what landed in the log".
See Backends for the graph the log produces, and Types for the validation contract.
The operation vocabulary
Use this as the catalog of "what verbs exist".
The operation set is intentionally small. Almost every backend edit fits one of the verbs below, and the platform rejects anything outside this vocabulary.
Graph topology — operations that change which vertices exist and how they wire together. add-vertex adds a component instance to the graph (pinning a specific release version). delete-vertex removes a vertex and cascades any disconnects. connect wires (from-vertex, from-output) to (to-vertex, to-input). disconnect removes the inbound edge on a specific input — inputs accept at most one upstream edge, which is why disconnect takes the destination. change-version migrates a vertex to a different release of the same component, keeping its existing bindings as long as the new release's types are compatible.
Vertex bindings — operations that change a vertex's configuration. change-parameter sets a typed config parameter to a pipelang literal. add-file binds a file to one of the vertex's file_schema slots (passing --file "" clears the binding). add-constraint pins or refines a vertex's generic-type bindings when type inference cannot decide on its own.
Vertex metadata — operations that affect how the vertex is named or rendered. change-alias renames the vertex's display alias. change-layout sets the (x, y, w, h) position in graph-view tools. change-metadata sets a free-form metadata key.
Endpoint exposure — operations that affect how external callers reach the vertex. change-endpoint-alias maps a component-declared endpoint name to a stable role string the application resolves at runtime. change-host-port-binding maps a container port to a host port for direct access. change-volume-mount mounts a volume into the container.
Runtime overrides — operations that tweak how a vertex runs when deployed. change-service-config overrides a (service, key) value on a vertex's depends_on declarations (e.g. swap a serving service's model name or pull strategy). These are escape hatches for the cases where the component's defaults need to be overridden at the backend level.
Every operation is one row in the log. Operations the platform rejects (a type-incompatible connect, a parameter literal of the wrong type, an alias on an endpoint the component does not declare) are not recorded at all — the graph stays valid because invalid operations never land.
Undo, compact, browse
Use these three verbs to operate on the log itself rather than on the graph.
ppl backend undo <bid> <count> reverts the last N operations in reverse order. The undo is itself recorded as new operations, so the trail of what was done, reverted, and re-done is preserved rather than rewritten. Use it to walk back the most recent edits without disturbing earlier ones.
ppl backend compact <bid> rewrites the recorded log into the minimum sequence of operations producing the same final graph. Superseded parameter values are dropped, undone changes are dropped, and connect/disconnect churn collapses into the edges that actually exist; the graph itself is unchanged. After compact, the earlier operations are no longer in the log, so undo cannot reach back through them. Compact suits a backend that has accumulated long edit history and a log too noisy to skim — the final graph is preserved, the superseded operations are dropped.
ppl backend operations <bid> browses the log: who changed what and when, how many operations to undo, and why a backend looks the way it does.
A common combined pattern: operations to see what happened recently, undo to walk back the bad edits, continue editing from the corrected state.
Batch mutations with patch
Use this when many small operations should land together or not at all.
ppl backend patch <bid> --file ./changes.json applies a JSON batch of operations as a single transaction. The first failure aborts and the whole batch is rolled back, so either all of the edits land or none of them do. Adding --dry-run (or dry_run: true in the file) previews the result without committing — useful when the batch is generated by automation and the caller wants to know whether it would type-check before sending it for real.
The transactional shape matters because issuing each operation as its own CLI call has no atomicity guarantee. A script that issues twelve operations one at a time and fails on the tenth leaves the backend with nine applied; patch ensures either all twelve apply or none do.
How parameter literals work
Use this framing whenever change-parameter rejects what looks like a sensible value.
Parameter values are pipelang literals, not JSON. The --type flag declares which type the value should match; the --value is the literal in pipelang syntax. The platform validates that the literal parses against the type and that the type matches the component's declared parameter type from its config_schema.
The grammar is small and specific: strings double-quoted, tuples in parentheses, lists in brackets, records in braces, lowercase booleans, decimal floats. JSON-looking values like "true" (a quoted string) or 1 (an integer where a Double is expected) are rejected at the change-parameter call. The full grammar lives in the Type syntax reference.
Shell quoting also matters. Single-quote the full literal in shell invocations so the shell does not eat parentheses, brackets, or quotes before the pipelang parser sees them:
ppl backend change-parameter <bid> --vertex 2 --name threshold \
--type Double --value '0.7'
See Types for the type system the validation runs against.
What the log lets you do
Use this framing to understand which capabilities are downstream consequences of the event-sourced design.
The event-sourced shape is what makes several other platform capabilities possible. Undo is one. Reproducibility is another: the same log produces the same graph on replay, every time. Audit is a third: the log is the authoritative record of every change, including who made it and when. Test-and-verify loops depend on it, because a backend with a pinned operation history makes "the version tested yesterday" addressable. Forking a backend with ppl backend create --source <bid> copies the log to a new object; the fork starts identical and diverges through its own subsequent operations.
The design requires that every change goes through an operation. There is no out-of-band path for hand-editing the graph and no way to bypass the log. In exchange, the log is the single place to look for what changed, and every downstream capability builds on that property.
Where this fits
Backend operations are the platform's atomic edit alphabet. Every higher-level workflow — assembling a graph, swapping a model, exposing a new endpoint, rolling back a change, forking for parallel environments — decomposes into a sequence of these typed operations. The vocabulary is small enough to learn once and large enough to express the work, and the event-sourced log is the authoritative record of what has happened.
Related
- Backends — the conceptual layer these operations build.
- Types — what
connectandchange-parametervalidate against. - File binding — what
add-filebinds and the config it carries. - Solutions — how vertices and edges become a shipped product.
- Secrets — binding a
secret: trueparameter to a workspace secret. - Deployments — the runtime side; the log is the input, the deployment is the output.
- Type syntax reference — full pipelang literal grammar.