Evidence: 1 file:line witnesses as of 2026-08-18. Confidence: derived, written from the seams and the estate's failures rather than from citations; treat each rule as a design call and argue with it. What changes it: the first module built against this doc (
../PLAN.mdforge-1) and its instance breakdown.
What a module says about itself, machine-readably, so an app, an agent, or a
future workflow composer can discover what it does, what it needs, what it
gives back, and which version answered. No instance has one. versable-runner's
GET /usage, a plain-text guide generated from the live method registry and
aimed at LLM agents (src/services-api/app/usage.py), is the closest thing,
and it is the right instinct pointed at the wrong reader: a person or a model
can read prose; code cannot.
Owner ruling 2026-08-17: composability is supported from the start even if the composer is not built, and standalone must work. The manifest is how both are true at once. It costs a module one JSON document; without it, the first attempt at composition means reading every module's source.
Audience: anyone building a module (you publish one), an app that calls modules (you read them), or the future composer.
The rule#
Every module serves GET /manifest, unauthenticated, and everything a
caller needs to call it correctly is in there. A capability that is not in
the manifest does not exist; a param the manifest does not list is rejected;
an error code the manifest does not list is a bug.
Same stance as versable-builder's barrel: "if it is not exported there, it
does not exist" (versable-builder/docs/KIT-HANDBOOK.md:10-11).
What is in it#
{ "module": "content", stable module id "module_version": "2.3.0", semver of this deployment "contract_version": "0.1", which Foundry contract this conforms to "build": { "commit": "…", "built_at": "…", "branch": "main" }, name and version are module and module_version "env": "prod", "capabilities": { "content.generate": { "summary": "Marketing copy from a template, with web research", "params": { …JSON Schema… }, validated on create "item": { …JSON Schema… }, shape of items[].input "output": { …JSON Schema… }, shape of a result outcome's output "error_types": { "RESEARCH_QUALITY_LOW": { "label": "Research too thin", "description": "…" }, "TEMPLATE_INVALID": { "label": "Template invalid", "description": "…" } }, "review_reasons": { "UNVERIFIABLE_CLAIM": { "label": "Unverifiable claim", "description": "…" }, "INPUT_MISSING": { "label": "Input missing", "description": "…" } }, reserved code, declared because requires uses it "confidence": { "self": true, "judges": ["judge.copy"], "authoritative": "self" }, "requires": [ { "field": "part.type", a field-registry id "need": "required", "from": ["caller", "parttype.match"], "on_missing": "needs_review:INPUT_MISSING" } ], "provides": [ "content.title", "content.description", "content.bullets" ], "variants": { "research": { "options": ["self", "extractor"], "default": "self" } }, "ceilings": { "max_items": 10000, "max_concurrent_jobs_per_caller": 4, "max_concurrent_jobs_per_tenant": 8, "max_item_bytes": 262144 }, "settings": [ "max_attempts", "item_timeout_s", "callback_url", "callback_events" ], "cost_model": { "unit": "item", "typical_usd": 0.012, "meters": ["llm.tokens", "research.calls"] }, "log": { "levels": ["debug", "info", "warn", "error"], "default": "info" }, "facets": ["user", "workflow_run"], attribution keys GET /jobs counts live "outputs": { "catalog-rows": { "shape": { …JSON Schema… }, one row per settled item "version": "1", names the materialization (canon/15) "reads": ["output.title", "output.description", "output.bullets"], "partial": true, "formats": ["csv", "xlsx", "jsonl"] } } }, "judge.copy": { … a judge is a capability like any other; its item schema is another capability's output … } }, "surface": { "routes": [ "POST /jobs", "GET /jobs/{job_id}", "GET /jobs/{job_id}/outcomes", … ], METHOD then path, as module-surface writes them the normative list is contracts/manifest.md "page_size": { "default": 100, "max": 1000 }, "callbacks": { "events": ["job.state", "item.outcome", "usage"], "signature": "hmac-sha256", "max_batch": 500 } }, "identity": { "verifier": "jwks", "issuer": "https://id.versable.ai", "audience": "content", "scopes": ["jobs:submit", "jobs:read", "jobs:cancel", "stats:read", "stats:admin"] }, "runtime": { "queue": "cloudtasks", "storage": "gcs", "in_process_supported": true, "topology": "api-worker" }, "retention": { "inputs": { "dev": "7d", "prod": "90d" }, "outcomes": { "dev": "30d", "prod": "1y" }, "artifacts": { "dev": "7d", "prod": "90d" }, "logs": { "dev": "7d", "prod": "30d" }, "usage": { "dev": "never", "prod": "never" }, "caches": { "dev": "ttl", "prod": "ttl" } }}Field by field, the ones that carry weight:
params,item,outputas JSON Schema. These are what the create route validates with and what a Catalog Output Template targets. That a per-module UI can be generated from them (customers "won't be writing json configs ever") is available, not required: the kit'sSchemaFormships in its next bump (owner ruling 2026-08-18) and renders the declared JSON Schema subset itslintSchemaaccepts, but the contract stays wide (full draft 2020-12; the same ruling), so a schema outside the subset is rendered by a hand-built form or the JSON view, never rejected. A module runs the lint when it wants a generated form (../contracts/manifest.md§ Validation names the fixtures and the two rules that hold regardless of vocabulary). What the manifest can promise now is fields, not layout: each field the module reads or writes carries a presentation hint (type family, unit, tone) that maps onto kit cell types, and the app renders rows with the kit's column factories.error_typesandreview_reasonsas closed sets. An app builds rules and UI against codes, not messages. Each code carries thelabela status pill shows and thedescriptiona tooltip shows, so an app never invents human text for a module's code. A new code is a manifest change and a version bump.confidence. Whether the capability self-reports, which judges it knows how to attach, and which score is authoritative for the outcome'sconfidencefield (03-jobs-and-state.md).requiresandprovides. The composability hooks.requiresnames inputs the capability needs and where they can come from: a caller can supplypart.typedirectly, or aparttype.matchjob's output can. Both names are field-registry ids, which is what makes the two agree; arequiresorprovidesentry that is not a registry id is banned. The module does not care which; the future composer does.providesnames what this capability puts on an item, in a vocabulary shared across modules (contracts/manifest.md§ The field registry holds the field names). Standalone callers ignore both and pass what they have.variants. Different coverage for the same purpose, selectable per job: research done by the module itself, or delegated to a prior extractor step; confidence from self or from a judge.14-graceful-degradation.md.ceilings. What429and422mean here: the job and concurrency limits that shed load, and the per-item size that gets rejected outright.cost_model. What the usage events will contain and a typical cost, so a caller can estimate before submitting and an app can build a price on it (08-usage-and-credits.md).facets. WhichattributionkeysGET /jobsreturns live counts for, so an app can build a filter bar without guessing which keys are cheap to count.useris always facetable and carries the reserved{id, label}shape.outputs. The transforms this capability offers over its own outcomes: a name, the outcome fields it reads, the shape it produces, and whether it will run on a job that is still going. Absent means the module serves raw outcomes and nothing else (15-outputs-and-transforms.md). Areadsentry may end in.*, meaning every key under that prefix; a capability whose output columns are named by a person at run time has no other way to declare them at build time.retention. How long each class of bytes survives, per environment, stated where a caller can read it rather than promised in a runbook (05-storage-and-persistence.md). A manifest without it is invalid.contract_versionandmodule_version. Which contract, which build. A caller pins the contract version it was written against and can tell from the manifest whether the module still speaks it.
Versioning#
Three numbers, three meanings:
| Number | Changes when | Who reads it |
|---|---|---|
contract_version | this doc set changes a required route or shape | every client, once |
module_version | the module ships; semver: patch for fixes, minor for a new capability or additive field, major for a breaking schema or removed capability | apps deciding when to upgrade; the degradation rules |
| capability schema | a capability's params, item, or output schema changes shape | callers of that capability; the UI generated from it |
Breaking route changes bump the path prefix (/v2/jobs); the old prefix
keeps serving until every caller has moved, and the manifest lists both.
Two majors of one module can run side by side as two deployments
(14-graceful-degradation.md), each with its own manifest.
/build-info is {commit, built_at, branch?, name, version, contract_version},
snake_case, one shape for modules and apps alike
(contracts/module-surface.md). Its name and version are this manifest's
module and module_version, so the two agree by construction rather than by
discipline. What the manifest adds is everything /build-info has no room
for: which capabilities that build offers and what they accept. One route says
which build is running, the other says what it promises. speedway and
walmart-mvp both serve a build route (/build-info, /api/build-info); no
instance serves a manifest.
For a person or an agent#
GET /guide is an optional route rendering the manifest as prose with
examples: the runner's existing guide is good and agents use it. It is
generated from the manifest, never hand-written, so it cannot drift. The
route is named /guide and not /usage because /usage/events is the usage
route; versable-runner serves the guide at /usage today, which is the
collision (contracts/module-surface.md). /usage may alias /guide for
one release.
Do-nots#
- Do not accept a capability, param, or setting the manifest does not list.
- Do not emit an error code or review reason the manifest does not list.
- Do not hand-write the prose guide; generate it from the manifest.
(versable-runner generates
/usagefrom the registry, which is the right half of this) - Do not change a schema without bumping
module_version, and do not remove a capability without a major bump and a period of running both. - Do not require a caller to read source to learn what a module needs.