Plasm Authoring¶
Iteratively author, validate, and test a typed agent surface (Python Program declarations + typed teaching) from an API specification. This skill is the single source of truth for CGS / CML authoring inside plasm-core. The monorepo root delegates to this file; do not re-author CGS doctrine elsewhere.
The two authored files are:
domain.yaml— CGS, the semantic model. Entities, fields, relations, capability declarations (query,get,search,create,update,delete,action), top-levelvalues:registry (semantic slots), optional top-leveldata_classes:(Guardians-style information-flow labels and sink roles), and optional top-levelviews:composed read DAGs.mappings.yaml— CML, the transport wiring. HTTP / GraphQL templates per capability, plustransport: viewstubs that point at aviews:key (nomethod/pathfor those rows).- Runtime query semantics (no extra YAML file). Pagination lives on CML query mappings (
pagination:block). Hydration (default concurrent GET per query row) applies when CGS declares bothqueryandgetfor the same entity unless execution opts out. Continuations and page sizing are expressed in Plasm (typed Python declarations / host continuation handles /.take(n)where taught) — not by authoring synthetic CLI flags.
For complete schema reference (types, operators, CML grammar, variable resolution, pagination block, default query hydration, action output, views, auth schemes), read reference.md.
Companion skills¶
This skill is the authoring core. Use these companion skills for follow-on work:
- plasm-catalog-e2e-test — Hermit-first then live / sandbox transport testing.
- plasm-catalog-adversarial-verify — programming-agent adversarial prove loop (AppWorld-first): teaching audit, twin-query compression, dry/live Plasm crawl, evidence under
apis/.../e2e/. - plasm-catalog-polish — autonomous diagnostic / fix loop for an existing catalog.
- plasm-catalog-score — rubric scoring of catalog quality.
- plasm-catalog-reprint — full-cutover regeneration of a weak catalog.
- plasm-catalog-retro — post-authoring retrospective for systemic press improvements.
The companion Cursor agent at .cursor/agents/plasm-forge.md wraps this skill for autonomous catalog runs.
Domain authoring is not deterministic¶
domain.yaml is not produced by a correct-by-construction pipeline. There is no supported "OpenAPI → CGS" generator crate or script in this repo, and you must not add one as a substitute for human or LLM semantic judgement. Reasonable authors disagree on entity boundaries, which operations merge under one capability, relation shapes, abstract entities, and what belongs in the prompt-facing surface versus the wire-only edge. The same applies to values: identity: each key is a semantic slot for the catalog (gloss, prompts, validation intent) — not something you derive by collapsing every field that shares a primitive wire shape. Whether two slots share one values key or get distinct keys is an authoring judgement; default toward separate keys unless the domain meaning is intentionally one shared space (one enum, one id space, one taxonomy).
What is deterministic (after the YAML exists): CGS::validate, CML template parsing, compilation to HTTP, decoding against declared shapes, plasm-eval coverage, and similar checks. Those prove internal consistency of an authored model — not that the model is the right abstraction for an API.
Implication: Expanding an API (e.g. "full GitHub REST") is iterative authoring — repeated passes through the loop below — not flipping a codegen switch. If you need a huge RPC-shaped surface for experiments (e.g. MCP prompt size baselines), treat that as a separate artifact or fork with its own trade-offs; do not pretend it replaces a curated CGS.
The Loop¶
1. READ spec → 2. AUTHOR domain.yaml → 3. AUTHOR mappings.yaml → 4. VALIDATE → 5. E2E TEST (Hermit, then live/sandbox) → 6. EVAL COVERAGE
↑ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Authoring with an agent (Cursor / Claude / Codex)¶
Point the agent at this skill (SKILL.md + reference.md) as the single source of truth. The agent prompt should be minimal — for example: read the OpenAPI spec the user provides, then follow the loop above until domain.yaml and mappings.yaml cover the API surface the user asked for, validating and testing as in Steps 4–6.
Do not paste parallel API-specific runbooks (phased tag lists, per-vendor checklists, or duplicate rules) into the prompt; large specs are handled by repeated passes through the same loop (read a slice of the spec, extend the two YAML files, validate, test, repeat).
File Structure¶
Canonical API catalogs live under apis/ in this repo (the monorepo apis/ is only a symlink to plasm-oss/apis/):
apis/<api-name>/
domain.yaml # entities, fields, relations, capabilities (WHAT)
mappings.yaml # CML templates per capability (HOW)
README.md # commands, auth env vars, scope, sandbox info
eval/cases.yaml # natural-language eval cases for plasm-eval
fixtures/schemas/ holds test-only split catalog dirs (e.g. capability_with_input/, plasm_language_matrix*); do not use it for new REST API authoring unless you are intentionally adding a tiny fixture for tests.
NL eval cases (plasm-eval)¶
Goal-oriented harness cases live in apis/<api>/eval/cases.yaml with schema: <api> matching the directory name. Each case has a natural-language goal, soft expect: scoring, and optional covers: — a list of expression-form buckets this case is meant to exercise (e.g. query_filtered, get, chain, multi_step). Bucket IDs are snake_case and align with CGS-derived requirements.
- Coverage (deterministic, no LLM):
plasm-eval coverage --schema apis/<api> --cases apis/<api>/eval/cases.yamlprints a human- and LLM-readable report: required buckets derived from CGS vs the union of per-casecovers. Exit code is non-zero if any required bucket is missing (--warn-onlyto soften). Use--format jsonfor scripts or agent consumption. - Optional
apis/<api>/eval/coverage.yaml: onlyrequired_extra:is implemented — it adds buckets beyond the CGS-derived set. There is noexclude:override. - Scaffold:
plasm-eval scaffold --schema apis/<api>emits a commented YAML fragment (CGS-derived buckets + one example case). Use--writeto createapis/<api>/eval/cases.yaml;--forceoverwrites.
Step 1: Read the OpenAPI Spec (or GraphQL SDL / vendor docs)¶
Read the spec file directly. For large specs, read section by section — paths first, then schemas, then descriptions. Use wc -l to gauge size, then grep "^ /" to list all paths before diving in.
IMPORTANT: Do not write scripts, binaries, or "generator crates" to emit domain.yaml / mappings.yaml from OpenAPI (or any spec) as if the mapping were unique or mechanical. There is no single deterministic reduction from RPC to CGS. You must read the spec and author the domain — merging endpoints, naming entities, choosing relations, classifying parameters, and deciding scope for agents. Mechanical dumps mirror the RPC surface and bypass the judgements this skill is for; they are explicitly out of scope for canonical apis/<name>/ trees. A GraphQL mutation/query list is not a capability list — compress to user tasks per reference.md — Task-oriented catalogs.
Query parameters in particular are a non-deterministic inference step. OpenAPI specs vary enormously in how they document query semantics:
- Some use standard
parameters:within: query - Some put params in
requestBodyJSON schemas (e.g. Tavily/search) - Some use vendor extension fields (
x-*) - Some rely entirely on
descriptionprose and examples - Some leave parameters undocumented and rely on external docs
You must read all of the above — not just the canonical OpenAPI parameters array — to classify each vendor input into a capability input lane (scope / selection / controls / arguments / payload) and decide how CML transmits it (path: / query: / body / pagination:). Flat CGS parameters: / role: is a hard load error. A spec where params live in extension fields or request bodies is a harder but equally valid authoring exercise.
From the spec, identify:
- Schemas (components/schemas): these become entities. Note field names, types, required fields, enums, and
$reflinks. - Operations (paths): these become capabilities. Note HTTP method, path, parameters (path/query/body), request body schema, response schema, extension fields, and
descriptionprose. - Relations:
$reffields (Pet.category → Category), foreign key patterns (Order.petId → Pet), path nesting (/pet/{id}/tags). - Shared enums: the same enum values appearing in both a schema field and a query parameter (Pet.status and findByStatus?status= are the same
PetStatustype).
Also identify:
- Authentication: check
securitySchemesincomponentsandsecurity:at the operation or global level. Common patterns: apiKey(in:query) →api_key_queryapiKey(in:header) →api_key_headerhttp(scheme:bearer) →bearer_tokenoauth2(clientCredentials flow) →oauth2_client_credentials- Genuinely public →
scheme: none
Key principle: An OpenAPI spec describes RPC operations. You are extracting the domain model — the business entities, how they relate, and what operations exist on them. Multiple endpoints operate on the same entity. A field like petId: integer is a relation, not just a number.
Step 1.5: Task inventory (before entities)¶
Before naming entities or capabilities, list agent tasks in user language (what would a consolidated MCP or product UI expose?). Examples: "show my open bugs", "what's ENG-42 and its comments", "create a bug on Backend", "project status this week".
Each task must map to kind: search, a views: composed read, or a small set of write verbs (create / update / delete). Flag any task that would require chaining multiple capabilities without a view — that task needs a views: entry or a merged capability.
See reference.md — Task-oriented catalogs.
Step 2: Author domain.yaml¶
Write the domain model. No HTTP details here — only what exists and what you can do.
Value registry: under values:, each stable key defines a named semantic slot (kernel or profile type: plus optional target, enum:, constraints:, description, …). Entity fields and capability lane slots only value_ref: that slot — slot-level keys (required, path, …) say how this use site differs. Treat one key ↔ one intended meaning in the domain; sharing a key across sites is a deliberate merge (same gloss / semantics), never a mechanical "all strings dedupe" shortcut. See reference.md — Value domains. Obsolete string_semantics / value_format / allowed_values / type: select|date|multi_select — rejected by the loader; see reference.md — Obsolete.
CRITICAL: Versioning is mandatory¶
- Every
apis/<api>/domain.yamlmust declare top-levelversion: <n>withn > 0. - Never rely on defaults. Omitted/zero versions are invalid for authoring and packaging.
- When you change domain semantics (entities, fields, relations, capability signatures, parameter types/roles, auth contract, output/provides behavior, information-flow annotations), you must increment
version. - Treat any change that can affect prompt shape, compile/decode behavior, or runtime dispatch as a version bump event.
- If you only change prose / comments with no semantic / runtime impact, keep
versionunchanged.
description strings: On entities, capabilities, and output for side-effect actions, write concise language for an agentic surface: what the entity or operation is for in the task (goal, anchor, decision), not an inventory of typed fields and relations — the schema and teaching table already show those. Avoid tabular jargon ("row") in Teaching-table-facing prose. Avoid embedding REST paths, methods, status codes, bare http:// / https:// links, or "see GET /…" notes — those belong in mappings.yaml comments or vendor docs, not in the CGS. auth.token_url in domain.yaml is the intentional exception (machine OAuth endpoint string). Do not repeat shapes already taught by value_ref, projection provides:, input_schema unions, or parameter names — omit field / parameter descriptions when types carry the story (see reference.md — Gloss: do not restate typed structure).
No cross-catalog prose (compositional CGS): Each apis/<name>/domain.yaml is a closed semantic surface. Teaching text (description, value glosses, discovery names / terms that read as instructions) must never name another registry catalog, its entry_id, or its entity/capability ids (e.g. do not tell Venmo login to “use Supervisor.email / AccountPassword”). Sessions compose catalogs at runtime; authors annotate role semantics inside this catalog only (“account holder’s email”, “this service’s password”, “payment recipient ≠ login identity”). Cross-catalog orchestration belongs in product docs / eval harnesses — not CGS strings. Same-catalog entity names are fine when they teach composition within this file.
Agentic teaching table copy (execute / MCP teaching): The prompt renderer attaches entity description to the symbolic teaching table (projection witness / banner). Treat it as imperative surface, not a manual or vendor doc: one or two short sentences on purpose (why an agent would focus this entity) — never name relations or fields that already show up as wire-name columns, bracket projections, or typed columns (that duplicates the graph and confuses "banner" with "nav map"). Do not summarize projection contents ("includes refs to …", "typed booleans plus …") — wire names, relations, and types already do that. Do not name other capability ids, spell out call sequences ("use X then Y"), cite transport:, document HTTP error semantics, or tell agents how to seed MCP — discovery: blocks (operation_terms, target_terms, qualifier_terms on entities/capabilities), apis/<api>/README.md, and eval cases carry that operational guidance. Capability description: should state effect or when to use in domain terms; move cross-capability playbooks into discovery on the relevant capability. See reference.md — Teaching-table-facing descriptions.
values:
nv_pet_id:
type: integer
nv_pet_name:
type: string
nv_pet_status:
type: enum
enum: [available, pending, sold]
nv_category_id:
type: integer
nv_category_name:
type: string
nv_pet_find_by_status_status:
type: enum
enum: [available, pending, sold]
nv_created_at:
type: rfc3339
entities:
Pet:
id_field: id
fields:
id:
value_ref: nv_pet_id
required: true
name:
value_ref: nv_pet_name
required: true
status:
value_ref: nv_pet_status
created_at:
value_ref: nv_created_at
relations:
category:
target: Category
cardinality: one
tags:
target: Tag
cardinality: many
Category:
id_field: id
fields:
id:
value_ref: nv_category_id
required: true
name:
value_ref: nv_category_name
# Include ALL entities referenced by relations, even simple ones
capabilities:
pet_findByStatus:
kind: query
entity: Pet
selection:
- name: status
value_ref: nv_pet_find_by_status_status
required: true
pet_get:
kind: get
entity: Pet
pet_create:
kind: create
entity: Pet
pet_delete:
kind: delete
entity: Pet
Teaching projection: Python entity declarations expose typed catalog fields; use .select(...) for an explicit row projection. Primary read capabilities and provides: still describe materialization. Historical domain_projection_examples only affects the internal native teaching oracle.
String / text profiles: use kernel type: string or profile types (markdown, document, html, json_text, email, url, …) on the values: row — not string_semantics:.
Field / lane wire types: the vocabulary is kernel names (string, integer, number, boolean, array, json, entity_ref, blob, money) or core profiles (enum, multi_enum, rfc3339, iso8601_date, unix_ms, unix_sec, uuid, …) on a values: row, not inline field_type on the slot. For entity_ref, set target: EntityName on the value row. For blob, see reference.md — Blob / binary. For array, the value row has type: array and items: { value_ref: <element_key> }. enum / multi_enum require non-empty enum: on the value row.
Temporal profiles: use type: rfc3339 / iso8601_date / unix_ms / unix_sec directly — no separate value_format key. Do not put evaluation now or harness calendar dates in values: descriptions — the language card states evaluation_now (PLP-9) when temporal slots are taught.
CGS field typing checklist (strict)¶
Use this on every new or edited entity (and on capability lane / input_schema fields) so the model does not collapse to "stringly typing."
- Instants and calendar dates — If the wire is a timestamp or date, use a
values:row with the matching temporal profile (rfc3339,iso8601_date,unix_ms,unix_sec). Do not use barestringfor fields named likedate_created,date_updated,last_modified,*expires*,*_on, orlast_*when the API returns a normal machine date/time. - Enumerations — If the set of values is closed and known, use
enum/multi_enumwithenum:on the value row. If the vendor reuses a field across resources with inconsistent or extensible lifecycles, keepstringand do not force a narrow enum that rejects valid future wire values. - Foreign keys — If the value is another resource's id and that resource is in the CGS, use
type: entity_refandtarget:. - Reverse list edges (many) — When a child has
entity_refto a parent and the child's primary list query accepts a parameter that filters by that parent's id, declare acardinality: manyrelation on the parent withmaterialize: { kind: query_scoped, capability: <child_query>, param: <parent_id_param> }. Do not add arelationskey with the same name as anentity_reffield on the same entity. - Opaque bytes and file bodies — Use
type: blob. - Human text and opaque tokens — Use
type: stringor a text profile (markdown,document, …).
Apply the same rules to lane value_ref targets.
Capability kinds: query (collection filter), search (free-text relevance), get (by ID), create, update, delete, action (anything else).
Composed read models (views:)¶
When the agent-facing concept is a single read row that no single vendor endpoint returns, but it decomposes into several query / get capabilities you already modeled, you must express it in CGS:
- Add an
entities:row for that concept (oftenabstract: trueso discovery does not attach it to parent graphs until explicitly seeded). - Declare a
kind: querycapability on that entity; lanes (scope/selection/ …) are the scope inputs the composition needs. - Add
views:<key>with orderednodes(each runs an existing capability),bindmaps for node inputs, andoutputmaps that shape entity fields. - In
mappings.yaml, wire that capability withtransport: viewandview: <key>only.
Anti-pattern: Long playbook text that says "call A, then B, then aggregate" without a views: entry leaves agents without a single query symbol for the composed row. Canonical example: apis/cloudflare — SecurityOverview + security_overview_query + views.security_overview. Full grammar: reference.md — Composed read views.
Runtime schema overlay (schema_overlay:)¶
When the API has workspace-defined columns on generic rows (Fibery databases, Notion database properties, Jira custom fields per project/issue type, ClickUp custom fields on tasks), add a top-level schema_overlay: block after the bootstrap entity and schema-fetch capability exist. Do not mirror every column statically in entities: when the schema is fetched at runtime.
- Static vendor schema (GitHub, Slack, PokéAPI) → no overlay
- Multi-hop reads without dynamic columns →
views:, not overlay - Per-scope typed entities →
projection.mode: per_scope_entity(Fibery, Notion, Jira) - Shared entity + extra columns →
projection.mode: augment_base(ClickUp; Linear deferred until GraphQL exposes custom-field defs)
Authoring details, spec table, checklists, and reference catalogs: reference.md — Runtime schema overlay. Runtime behavior: monorepo docs/schema-overlay.md.
kind: action output: Every action must declare either non-empty provides: or output: with type: side_effect and a non-empty description: that states what the operation changes. There is no output.type: none. See reference.md — Action output.
Information-flow annotations (Guardians / plan flow typing)¶
After the relational model is stable, annotate data classes, sinks, and sanitizers so the host can verify agent plans before execute (plasm dry-run → verify_plan_flow). Conformance map: monorepo docs/guardians-alignment.md. Full vocabulary: reference.md — Information-flow annotations.
Catalog declares facts; tenant policy enforces rules. Register classes in data_classes:, label sensitive entity fields with data_class:, mark exfiltration/destructive input_schema params with sink_class:, and list taint breakers on sanitizes: capabilities. Output labels are derived from effective_provides × field data_class — do not duplicate on capabilities.
Annotation pass (minimum for mutating APIs):
- Add
data_classes:entries for every label and sink role you reference (untrusted,pii_*,external_send,destructive_delete, …). - Label user-generated text and PII on entity fields agents read (
body,description,comment, email fields, …). - On send/publish/post/reply/share/delete capabilities, set
sink_class:on the payload field ininput_schema. - On redact/scrub/sanitize capabilities, declare
sanitizes:for the classes they clear. - Increment
version:when flow annotations change.
Witness catalog: plasm-oss/fixtures/schemas/flow_matrix/. Unlabeled catalogs remain flow-permissive until tenant policy is pinned active.
Authentication — top-level auth: block¶
Place a single auth: block at the end of domain.yaml. The runtime reads secrets at execution time from the named environment variables — no secrets go in schema files. Use scheme: none for genuinely public APIs.
# API key in query string (e.g. RAWG ?key=..., OMDb ?apikey=..., NYT ?api-key=...)
auth:
scheme: api_key_query
param: key
env: RAWG_API_KEY
# API key as a request header (e.g. X-Api-Key)
auth:
scheme: api_key_header
header: X-Api-Key
env: MY_SERVICE_API_KEY
# Bearer token (e.g. ClickUp, Notion, Tavily)
auth:
scheme: bearer_token
env: CLICKUP_API_TOKEN
# OAuth 2.0 client credentials (e.g. Spotify)
auth:
scheme: oauth2_client_credentials
token_url: https://accounts.spotify.com/api/token
client_id_env: SPOTIFY_CLIENT_ID
client_secret_env: SPOTIFY_CLIENT_SECRET
scopes: []
# Public API with no outbound credentials
auth:
scheme: none
Query capability input lanes¶
Critical rule: only declare lane slots the API endpoint actually accepts as HTTP inputs. Read the OpenAPI operation's parameters list and description fields. Never invent selection: / scope: keys from entity fields — entity fields describe the domain object, not what the query endpoint accepts.
No selection? If the endpoint is a plain paginated resource index (e.g. PokéAPI /pokemon/ — no server-side filters, just offset/limit), declare no selection:. Pagination belongs in mappings.yaml (pagination: / page controls).
kind: query vs kind: search¶
Use kind: search when the endpoint's primary interface is a required free-text relevance query (q, query, search) that returns ranked results rather than field-filtered rows. The free-text selection param must be required: true — optional free-text list filters use kind: query. Required non-text selection (e.g. AppWorld access_token) is taught on the primary e~"<query>"{…} row; an optional-filter twin is emitted only when other optional selection slots exist. If the endpoint filters by concrete field values (status, archived, team_id), use kind: query.
Classification: OpenAPI params → lanes¶
| OpenAPI / vendor examples | Lane | CML transmission |
|---|---|---|
offset, limit, page, cursor, after, before |
(omit from CGS) or controls when taught |
CML pagination block; not brace WHERE |
status, tags[], assignees[], archived, type, shelf discriminants |
selection |
CML query: / path branch vars |
q, query, search (required free-text) |
selection on kind: search |
CML query: var |
order_by, sort_by, direction, embed, fields, locale |
controls |
CML query: var; not brace WHERE |
team_id in GET /team/{team_id}/space |
scope |
CML path: var; pair with relation materialize |
| Create/update/action body fields | payload |
CML body |
| Non-body method args (token on a get/action) | arguments |
CML query: / header / path as mapped |
Same-type list compression: Competing kind: query caps that return the same entity (identical or sibling brace shapes, different HTTP path only — including scoped+unscoped twins) are RPC residue and a hard CGS validate error. Collapse to one query with a selection discriminant (enum/profile) and CML path branching (type: if), fold scoped lists into optional scope on that query + relation materialize, or a real entity split when polarity is a different domain object. Do not teach twin brace forms or raise teaching-line caps to "escape" brace-dedupe. Full law: reference.md — Same-type list compression.
Scoped sub-resource lists: When an API has endpoints like GET /classes/{class_index}/spells alongside GET /spells, declare one kind: query with optional class scope (CML path branch). Pair the parent relation with materialize against that single query — never a second competing query on the child entity.
Range filters like ClickUp's due_date_gt / due_date_lt are separate named selection slots — declare each one individually, not as one field with operator suffixes.
Array / multi-value params (e.g. genres, assignees[], embed[]): use a values: row with type: multi_enum or type: array with items: { value_ref: <element_key> }. In mappings.yaml:
- Repeated key (
?embed=a&embed=b) — plain{ type: var, name: embed }; HTTP layer expands arrays. - CSV (
?genres=1,2,3) —{ type: join, sep: ",", expr: { type: var, name: genres } }. - Pipe (
?ids=1|2|3) —{ type: join, sep: "|", expr: { type: var, name: ids } }.
See reference.md for the full pattern catalogue (index-only, filter, rich filter, search, sort + response control).
Rows without a top-level id: Some list endpoints return objects with no id key (only nested URLs or names). Set optional id_from to a path of object keys (YAML list or dotted string, e.g. location_area.url) so decoding can still build stable _refs.
Checklist before proceeding:
- [ ] Every relation target is a defined entity
- [ ] Every
values:row withtype: enum(ormulti_enum) has non-emptyenum: - [ ] Every required selection / scope / argument slot has
required: true - [ ] No twin same-arity queries on the same entity (compress or split — see Same-type list compression)
- [ ] Capability names are unique and follow
entity_operationconvention - [ ] Every entity has either a declared
fieldsentry forid_fieldor a non-emptyid_frompath - [ ] If an entity should not teach projection brackets in teaching table, set
domain_projection_examples: false - [ ] Any multi-endpoint read summary is modeled with
views:+ syntheticquery+transport: view(not prose-only runbooks) - [ ] User-defined / workspace schema uses
schema_overlay:(not hundreds of static custom-field columns) — see reference.md — Runtime schema overlay - [ ] Every list/filter agent intent has
kind: searchwhere the vendor supports filter DSL (not a fleet of scopedquerycaps for the same entity) - [ ] Human-visible keys are
id_fieldwhere the vendor accepts them on get/create - [ ] Write surface uses domain verbs, not per-input-field mutation explosion
- [ ] Mutating APIs:
data_classes:registry + fielddata_class:on sensitive reads +sink_class:on outbound/destructive inputs (see Information-flow annotations) - [ ] Semantic annotation review: inspect inherited field/lane glosses for type-only filler; verify scope, polarity and units from the API. Check authored meaning in retrieval documents, selector evidence and full/incremental teaching, including Get operation descriptions. Follow Semantic annotation review.
- [ ] Teachability proof: dump the teaching card under realistic seeds; every task-critical list polarity and mutator is expressible from the card (discriminant or other entity) — not a phantom second query line
- [ ] Token / auth args on actions/gets do not look like list filters on sibling entities (document in entity/cap descriptions)
Scoped relation traversal (materialize)¶
When an API uses sub-resource URLs (/parent/{id}/children), set materialize on the many relation so chain traversal fills the target query's scope parameter(s). See reference.md — Scoped many-relations.
EntityRef fields¶
When a field stores another entity's ID, use type: entity_ref with target: on the value row. This enables FK navigation, reverse traversal, and cross-entity predicates. Audit every string/integer field ending in _id, Id, _key — if a matching entity exists with a get capability, it should be entity_ref. See reference.md — Foreign key fields.
Step 3: Author mappings.yaml¶
Write the transport wiring for each capability. Default is REST (method + path). GraphQL uses top-level transport: graphql with endpoint / operation / variables. Composed views use transport: view + view: <key> matching domain.yaml's views:.
pet_findByStatus:
method: GET
path:
- type: literal
value: pet
- type: literal
value: findByStatus
query:
type: object
fields:
- - status
- type: var
name: status
pet_get:
method: GET
path:
- type: literal
value: pet
- type: var
name: id
pet_create:
method: POST
path:
- type: literal
value: pet
body:
type: var
name: input
Path segments: {type: literal, value: "pet"} (static) or {type: var, name: "id"} (resolved at runtime).
Variables the engine provides:
id— entity ID (for get/delete/update/action)- Predicate field names — from query predicates in the Plasm program
input— full input object (create/update with body)- Path variables: see reference.md (primary
id, optionalpath_vars, multi-segment, createinputkeys merged into CML env)
Pagination & hydration¶
Pagination — declare only in mappings.yaml (pagination block on query capabilities). Infer style, wire param names, and JSON paths per reference.md — Pagination. The runtime merges pagination.params into follow-up HTTP requests; pagination: in CML is the single authoring surface for paging behavior.
Hydration — after a query, if the entity has both query and get, the runtime by default fetches full rows via get unless QueryExpr.hydrate = Some(false) or the engine disables hydrate. No extra CGS flag. See reference.md — Query result hydration.
Step 4: Validate¶
For split domain.yaml + mappings.yaml, pass the catalog directory apis/<api>/ to schema validate. Pointing at domain.yaml alone skips mappings.yaml and can falsely fail.
# CGS validation (catalog directory for split domain+mappings)
cargo run -p plasm-cli --bin plasm-cgs -- schema validate apis/<api>
# Optional: exhaustive mapping exercise against an OpenAPI spec
cargo run -p plasm-cli --bin plasm-cgs -- validate --spec path/to/openapi.json apis/<api>
# Smoke-load REPL + help
cargo run -p plasm-repl -- --schema apis/<api> --backend http://localhost:1080 --help
Contract completeness is distinct from schema validity¶
Before claiming a catalogue complete, audit the pinned backend contract in both directions:
- Every supported operation and selector has an explicit mapping or a documented exclusion.
- Every domain-relevant response field, nested person, collection and relationship is preserved in typed CGS or explicitly excluded with a reason. A scalar-only projection of a rich response is not complete.
- List/detail variants preserve truthful identity and cardinality. Parent-scoped children use compound identities; embedded-only rows must be executable without an invented Get endpoint.
- Advertised fields actually occur in the response or have a declared derivation. Never fabricate a thread-level field merely because its messages contain it.
- Typed temporal and numeric inputs are encoded at the CML boundary according to the backend's documented wire format.
- Validation exercises every Query and Search, secondary capabilities, and nested traversal. Record skips and separate structural validity, operation coverage, mock conformance and pinned-backend execution evidence.
Do not attribute agent failures to reasoning limits while necessary catalogue evidence is absent or unverified. Preserve an auditable list of remaining gaps; passing YAML/schema validation alone cannot close it.
Step 5: End-to-End Testing¶
Hand off to plasm-catalog-e2e-test, which is the operational source of truth for the testing ladder:
- Hermit against the OpenAPI spec when one exists in the README or source docs.
- Live API when credentials and rate-limit headroom exist.
- Vendor sandbox / test mode as a substitute when live calls would mutate real data or are otherwise unsafe.
Skips must be recorded with a reason; representative Plasm expressions and outcomes belong in the evidence the e2e skill emits.
Step 6: Eval Coverage¶
cargo run -p plasm-eval -- coverage --schema apis/<api> --cases apis/<api>/eval/cases.yaml
Fix missing buckets by adding eval cases (not by softening coverage). For LLM conformance runs, see plasm-oss/crates/plasm-eval/README.md.
Failure Modes (quick reference)¶
When transport tests fail, check:
CmlError::VariableNotFound→ mapping var name doesn't match the engine env. Fixnamein mappings.yaml.DecodeError→ response shape mismatch. The engine normalizes bare arrays automatically; checkpath:/derive:on entity fields.404→ path doesn't match spec. Check--backendincludes the server prefix.401/403→ wrongauth:scheme or missing env var.
Then fix domain.yaml or mappings.yaml and re-run from Step 4.
When you cannot model the API faithfully¶
If the desired API shape cannot be modeled with today's CGS + CML + runtime (missing expressiveness, not just tediousness), stop. Document the gap as a short blocker note (what shape is needed, which capability/entity breaks, which validator or runtime behavior is insufficient). Do not patch plasm-core, plasm-cml, plasm-runtime, or validators yourself to "unstick" the mapping unless explicitly directed in a separate task.
After a difficult or interesting catalog, run plasm-catalog-retro to capture systemic improvements.
Embedded execution gate: follow Embedded identity proof and observation ownership. Require pack/codec identity checks and an abstract direct/scoped/hydrated relation witness. Report backend-schema evidence, mock evidence and real-backend evidence separately; operation coverage alone never establishes complete response or mutation semantics.
When a catalogue declares prerequisites, verify the deployment manifest covers every requirement and exercise federated discovery bootstrap. Direct engine/catalogue tests do not establish deployment closure.