Skip to content

Row compute (artifact plane)

Plasm programs operate on rows: JSON objects materialized from catalog queries, relation hops, and prior bindings. Row compute is postfix syntax that transforms those rows in the plan executor (in-memory), distinct from predicates on the catalog plane that compile to HTTP/CML.

See also plasm-language-definition.md for full grammar and binding rules.

Two planes

Plane Surface When to use
Catalog e1{p71="open"} on a query/get Reduce data at the API; predicates become query parameters or CML filters.
Row rows.filter{p71="open"} or rows.filter(p71="open") Filter, sort, group, or aggregate rows already fetched into the session artifact.

Use catalog filters when the API supports them and you want fewer round-trips. Use row filters when refining a binding, combining results from multiple steps, or when the field is not a query parameter.

Row filter

Equivalent forms:

open = items.filter{owner="alice"}
open = items.filter(owner="alice")
  • Comma-separated clauses are implicit AND.
  • Comparison operators match entity brace queries: =, !=, >, <, >=, <=, ~ (contains).
  • v1: flat comparisons only (no OR/NOT/relation exists).

Disambiguation: In postfix/program RHS, .filter immediately followed by { or ( is row compute. In path expressions, .filter followed by an identifier (no {/() remains a relation name.

Invalid: rows{owner="alice"} on a binding label — use rows.filter{…}.

group_by and aggregate

Canonical:

by_owner = LangItem.group_by(owner, n=count)
by_team = LangItem.group_by(owner, team, n=count, total=sum(score))

Sugar: LangItem.group_by(owner)group_by(owner, count=count).

Output shape: One JSON field per group key (wire/dotted name), plus aggregate columns (n, total, …). Not a single generic "key" column.

aggregate without a key applies functions over all rows: all = items.aggregate(n=count).

Chaining order

Postfix applies left-to-right on the written expression (a.limit(10).sort(x) → sort after limit). Recommended SQL mental model:

source → .filter{…} → .group_by(…) → .sort(…) → .limit(n) → [fields] → <<TAG

group_by and aggregate change the row schema (terminal for relation-dot continuation on that label). After group_by, output columns are the group keys plus aggregate names (n, total, …). A chained .sort(n, desc) sorts on those aggregate columns — not on fields of the original catalog entity.

filter, sort, and limit on a binding that still carries the catalog entity shape validate against that entity’s fields.

Relation-dot vs row compute

Syntax Meaning
issues.labels / issues.r# Catalog relation hop (may fan out HTTP per source row when issues is plural). Use opaque r# or wire from teaching exemplars — not bare homograph p# in nav position. Scoped query_scoped_bindings map parent fields into capability params with catalog typing — see Relation binding proofs (dry plasm on hole-IR alone is not enough for live plasm_run).
Paginated parent issues = e1{…} All API pages are materialized into the binding by default (runtime page cap). Use .limit(n) or .page_size(n) on the read to bound; MCP page(pgN) on a later binding only pages that step — not a substitute for a full-repo histogram before group_by.
issues.filter{owner="alice"} Row compute on materialized issue rows
issues => { … } Derive map over rows — not a relation hop

See plasm-language-definition.md. => is only for derive maps and for_each on bindings; relation hops use .r#/wire, not =>.

Not in v1

  • OR/NOT in row filters; .having{…}; .derive() postfix.
  • HTTP push-down of row filters (optimizer may add later without changing surface meaning).
  • rows{…} as a row-local filter shorthand.

Federation

Row compute field paths are validated against the qualified entity of the upstream surface or binding (same catalog as e# in the teaching table). Use the correct session e# when the same wire entity name appears in multiple catalogs.