diff --git a/skills/bootstrap/SKILL.md b/skills/bootstrap/SKILL.md
index e22bb4f..cea4c51 100644
--- a/skills/bootstrap/SKILL.md
+++ b/skills/bootstrap/SKILL.md
@@ -123,8 +123,17 @@ more.
### 3.1 Check for an existing record
-Read `.postman/resources.yaml` for `localResources` and `workspace.id`.
-Populated → go to step 4. Absent or empty → run init.
+Read `.postman/resources.yaml` for `workspace.id`, and for `cloudResources`
+if you need the existing bindings.
+
+`workspace.id` non-empty → the repo is already initialised, go to step 4.
+File missing, or `workspace.id` empty → run init.
+
+Route on `workspace.id` alone. `postman init` writes only the `workspace`
+stanza; `cloudResources` is added later by `postman workspace push` and
+`postman mock push`, so an initialised repo that has not pushed yet has a
+valid `workspace.id` and no `cloudResources` at all. Treating that as
+"absent" re-runs init over a repo that is already bound.
### 3.2 Run init
diff --git a/skills/collection-schema-v3/SKILL.md b/skills/collection-schema-v3/SKILL.md
index a9fcbe4..11be154 100644
--- a/skills/collection-schema-v3/SKILL.md
+++ b/skills/collection-schema-v3/SKILL.md
@@ -1,6 +1,6 @@
---
name: collection-schema-v3
-description: The reference for the git-native v3 collection file format — one YAML file per request/folder/example under postman/collections/, plus postman/environments/. Read before writing, editing, or generating any file in either directory by hand, or before debugging a `collection lint` failure. Covers the HTTP request/example/definition schema, environment schema, and the YAML/naming rules that make files parse — GraphQL, gRPC, WebSocket, Socket.IO, MQTT, MCP, and LLM request schemas are non-HTTP protocols and live in reference/other_protocols.md, read only when a collection actually uses one.
+description: The reference for the git-native v3 collection file format — one YAML file per request/folder/example under postman/collections/, plus postman/environments/. Read before writing, editing, or generating any file in either directory by hand, or before debugging a `collection lint` failure. Covers the HTTP request/example/definition schema, environment schema, and the YAML/naming rules that make files parse — GraphQL, gRPC, WebSocket, Socket.IO, MQTT, MCP, and LLM request schemas are non-HTTP protocols and live in reference/other_protocols.md, read only when a collection actually uses one. Also read before writing any other on-disk Postman entity by hand — a dataset, a local mock server, workspace globals, a spec, a document or .postman/resources.yaml — since none of those use $kind and each has its own discriminator; reference/other_entities.md has the complete field lists.
---
# Collection Schema (v3, Git-Native)
@@ -206,3 +206,444 @@ variables:
- [Other request protocols](reference/other_protocols.md) — GraphQL, gRPC,
WebSocket, Socket.IO, MQTT, MCP, and LLM request schemas.
+- [Other entities and full field lists](reference/other_entities.md) — datasets,
+ local mocks, globals, specs, documents, SDKs, `.postman/resources.yaml`, and the
+ complete key list for every request kind.
+
+---
+
+## Postman v3 on disk — entities beyond HTTP collections
+
+Supplements `collection-schema-v3`, which covers the collection directory layout, HTTP
+requests, examples, environments, YAML quoting and naming. This covers what that file
+does not: the other entity types, the fields whose types are easy to get wrong, and what
+`lint` will not catch.
+
+Complete field lists — every key of every request kind and every entity — are in
+[reference/other_entities.md](reference/other_entities.md). Read it rather than guessing
+a field name: an unrecognised key is dropped in silence, so a guess costs you a working
+request with no error to show for it.
+
+### Verify with `workspace lint`, not `collection lint`
+
+Unrecognised keys are **dropped, not rejected** — a misspelled or v2.1-carryover key is
+silently discarded and the entity runs without it. `collection lint` is the one verb that
+will not tell you: `environment lint` and `globals lint` report an unknown field as
+`ENV007`, and `workspace lint` reports one in a collection. Only `collection lint` stays
+quiet:
+
+| Written | `collection lint` | `workspace lint` |
+|---|---|---|
+| `body: {mode, raw}` | error `FMT015` | error |
+| `$kind` missing or unknown | error `FMT015` / `FMT014` | error |
+| `event:` instead of `scripts:` | **silent** | warning `FMT206` |
+| `metod:` instead of `method:` | **silent** | warning `FMT206` |
+| `dataMode:` / `data:` | **silent** | warning `FMT206` |
+| any other unrecognised key in a request | **silent** | warning `FMT206` |
+| an unrecognised key in `definition.yaml` | **silent** | warning `FMT208` |
+
+`collection lint` reports `errorCount: 0, warningCount: 0` for all of those — it does not
+run the unrecognised-field rules at all, so `-f warning` does not help. This is specific
+to collections: an unknown key in an environment or globals file *is* reported, by both
+`environment lint`/`globals lint` and `workspace lint`.
+
+```
+warning [FMT206]: Unrecognized field "metod" is not part of the Postman Collection
+Format v3 schema and will be ignored. (path: /metod)
+```
+
+**So always finish with `postman workspace lint`.** It names every key that will be
+thrown away, covers every entity in the repo plus the workspace manifest, and is the only
+lint verb with `--fix` and `info`/`hint` severities. Treat a clean `collection lint` as
+necessary, never sufficient.
+
+For scripts specifically, `postman collection run
--no-report-events` is the
+end-to-end proof: `requests`, `test-scripts` and `assertions` should all be non-zero,
+since a dropped `scripts` block leaves assertions at 0 while the run still reports green.
+
+Pass `--no-report-events`. The run itself needs no login, but without that flag the CLI
+uploads its run report to the cloud afterwards, which fails when you are not signed in
+and turns a passing local run into a non-zero exit.
+
+### Key lists accept a map as well as an array
+
+`headers`, collection `variables` and `auth.credentials` take either form, and both lint
+clean:
+
+```yaml
+headers:
+ Content-Type: application/json
+variables:
+ base_url: https://api.example.com
+auth:
+ type: bearer
+ credentials:
+ token: '{{TOKEN}}'
+```
+
+Prefer the map: the Postman app writes maps when it round-trips a collection, so
+array-form entries get rewritten on the next sync and show up as diff noise. Use the
+array form only when an entry needs `description` or `disabled`, which a map cannot
+carry:
+
+```yaml
+headers:
+ - key: X-Trace
+ value: '1'
+ description: Enables verbose server logging
+ disabled: true
+```
+
+In the array form, quote values that look numeric or boolean — `value: "400"`.
+
+### `queryParams` and the url query string must agree
+
+This one is symmetric and easy to trip. A url carrying `?a=1&b=2` with no `queryParams`
+warns, and a `queryParams` block whose url has no query string warns too:
+
+```text
+warning [FMT209] query parameters in url are out of sync with the queryParams
+field; keep the url query string and queryParams consistent.
+```
+
+So write both, in step:
+
+```yaml
+url: '{{base_url}}/search?q=shoes&page=2'
+queryParams:
+ q: shoes
+ page: '2'
+```
+
+`collection lint` reports this as a warning, not an error, so a green `errorCount` will
+not tell you about it — `postman workspace lint` surfaces it in normal output.
+
+### Only collections use `$kind`
+
+Each other entity lives in its own directory with its own discriminator. Getting this
+wrong is the most common first-attempt error on these files.
+
+| Entity | Path | Discriminator |
+|---|---|---|
+| collection | `postman/collections//` | `$kind: collection`, `$kind: -request` |
+| environment | `postman/environments/.environment.yaml` | none — `name` + `values` |
+| globals | `postman/globals/.globals.yaml` | none — `name` + `values` |
+| dataset | `postman/datasets//.dataset.yaml` | **`type: dataset`** |
+| mock | `postman/mocks//config.yaml` | **`version: 1`** |
+| spec | `postman/specs/.yaml` | none — plain OpenAPI/AsyncAPI |
+| document | `postman/documents/.md` | none — plain Markdown |
+
+**Filenames are validated, not just the directory.** `postman/environments/Staging.yaml`
+is rejected — *"Not an environment file. Expected a file ending in .environment.yaml,
+.environment.yml, .environment.json"* — and `globals.yaml` likewise needs
+`.globals.yaml`. A spec is the exception: it may be a single file or a directory.
+
+#### environment and globals
+
+```yaml
+name: Dev
+values:
+ - key: base_url
+ value: https://api.example.com # value must be a string
+ enabled: true
+ type: default # or "secret"
+```
+
+A secret can be marked either way — `type: secret`, or `secret: true` alongside
+`type: default`. Both are recognised.
+
+Globals are the same shape, conventionally `name: Globals`. An empty `values: []` is
+valid. Validate with `postman environment lint ` / `postman globals lint `
+— note these take `-o/--output` where `collection lint` takes `-r/--reporter`.
+
+#### dataset
+
+The minimum, which is what `postman dataset create --name X` writes:
+
+```yaml
+type: dataset
+name: "Users"
+id: 3f2b6c1e-9d4a-4f81-bc37-5e0a71d9c402 # uuid4
+data_dir: .resources
+```
+
+With a file source and a view over it:
+
+```yaml
+type: dataset
+name: "Sample Fixture"
+id: d7c81f43-2e95-4a08-b6d1-9f3e5c07a284
+data_dir: .resources
+sources:
+ - from: embedded_datasource
+ datasource:
+ name: Records
+ id: 5b6a92c8-3d17-4e50-a9f2-81c4e7b0d365
+ format: json # or csv, xlsx, mysql
+ source_type: static # static for files, dynamic for databases
+ location:
+ type: local_filesystem # or mysql
+ value: rows.json # relative to data_dir
+views:
+ - from: embedded_view
+ view:
+ name: Records_default_view
+ id: e2f47a91-6b08-4c3d-95a7-1d8e0f62b4c3
+ query: SELECT id, name FROM Records ORDER BY id
+```
+
+A database source adds `ingest: false`, `dsKey`, `slug` and `location.table`, and
+`location.value` is a DSN. There is no `dataset lint`: use `postman dataset get `
+for structure, and `postman dataset query -q ""` to run SQL against the
+source, which also proves the data parses.
+
+#### mock
+
+`postman mock generate --name X` writes `config.yaml` plus a scenario `.js`:
+
+```yaml
+version: 1
+id: a41c8e05-7f23-4b96-8d0e-3c6a9b25f718
+name: Probe Mock
+slug: probe-mock
+protocol: http
+port: 4500
+scenarios:
+ - name: default
+ path: ./default.js
+ default: true # only on the default; omit it elsewhere rather than setting false
+```
+
+Scenario `id` is optional. A scenario file is a plain Node HTTP server — nothing
+validates its contents, so match the shape the CLI emits:
+
+```js
+const http = require('http');
+const PORT = process.env.PORT || 4500;
+
+const server = http.createServer((req, res) => {
+ const url = new URL(req.url || '', 'http://localhost');
+ const pathname = url.pathname;
+
+ // @endpoint GET /health
+ if (req.method === 'GET' && pathname === '/health') {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify({ status: 'ok' }));
+ }
+
+ res.writeHead(404, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: 'Endpoint not defined' }));
+});
+
+server.listen(PORT);
+```
+
+Branch on `req.headers['x-mock-response-code']` to let a caller select an error path
+from a single scenario. Validate by running `postman mock run `, or drive
+it from a collection with `postman collection run --mock `.
+
+#### The workspace manifest — let the CLI own it
+
+`.postman/resources.yaml` maps local paths to cloud ids. **Do not hand-author it.**
+`postman init` creates it, and `workspace push` / `mock push` add and update the
+mappings. The only routine hand-edit is deleting a line after a cloud entity is removed,
+since a cloud delete leaves the mapping behind.
+
+When you do need to read or repair it: collections map to a **directory**, every other
+entity maps to a **file**, paths are relative to `.postman/` so they begin `../`, and ids
+come either owner-prefixed (`12345678-`) or as bare 24-character hex — both work.
+
+```yaml
+workspace:
+ id: 6d3e9a17-4c80-4f25-b1a9-7e02c58d3f64
+cloudResources:
+ collections:
+ ../postman/collections/My API: 12345678-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
+ environments:
+ ../postman/environments/Dev.environment.yaml: 12345678-6d3e9a17-4c80-4f25-b1a9-7e02c58d3f64
+ datasets:
+ ../postman/datasets/users/users.dataset.yaml: d7c81f43-2e95-4a08-b6d1-9f3e5c07a284
+```
+
+An entity that exists in the cloud but is missing here makes `workspace push` plan a
+*create*, which then fails `404 The target resource was not found` and leaves the cloud
+copy stale.
+
+### Request kinds beyond HTTP
+
+`folder` and `collection` share `$kind: collection` — a folder's
+`.resources/definition.yaml` is distinguished only by sitting deeper in the tree, and may
+also carry `order`.
+
+**`$kind` must match the file type.** `http-example` is rejected in a `*.request.yaml`
+and accepted in a `*.example.yaml` under
+`.resources/.resources/examples/`. Only `*.request.yaml` files are
+discovered as requests; a request-shaped file under any other name is skipped silently.
+
+#### Script `type` differs by scope
+
+- on a request: `beforeRequest`, `afterResponse`
+- on a collection or folder: protocol-prefixed — `http:beforeRequest`,
+ `http:afterResponse`, `graphql:beforeQuery`, `grpc:beforeInvoke`, …
+
+A bare value at collection scope, or a prefixed one at request scope, is dropped
+silently.
+
+#### Fields that are strings when they look like structures
+
+- `mcp-request.message` — a JSON-RPC envelope as a **string**; `{{variables}}`
+ interpolate inside it.
+- `websocket-request.messages` and `mqtt-request.messages` — a **string** path to a
+ messages directory holding `*.message.yaml` files, alongside the `examples`
+ convention. An array fails with `Invalid input: expected string, received array`.
+- `llm-request.mcpConfig` — a **string** containing a JSON **array**, not nested YAML
+ and not `mcp.json`'s `mcpServers` object.
+
+#### `mcp-request`
+
+Requires `$kind` and `transport`, and `transport` selects which other keys are legal.
+The only values are `stdio` and `sse`.
+
+```yaml
+$kind: mcp-request
+transport: stdio # command + env; no url
+command: uvx some-mcp-server
+env:
+ API_KEY: '{{KEY}}'
+message: |-
+ {"method": "tools/call", "params": {"name": "do_thing", "arguments": {}}}
+order: 1000
+```
+
+```yaml
+$kind: mcp-request
+transport: sse # url + headers; no command
+url: https://mcp.example.com/sse
+headers:
+ Authorization: 'Bearer {{KEY}}'
+message: |-
+ {"method": "tools/list"}
+```
+
+#### `llm-request`
+
+Only `$kind` is required, so lint cannot catch a half-written one. `config` needs
+`model` and `provider`, both free-form strings with no fixed list, so a typo lints clean.
+
+> Prompt entries take **`contentType`**, not `type`. Both lint clean, so nothing warns
+> you — but `contentType` is the defined field and `type` is unrecognised, so it is
+> dropped and the content type is lost. This is the single most common error in the
+> whole format: `type` reads natural and is silently wrong.
+
+```yaml
+$kind: llm-request
+url: https://api.openai.com/v1/chat/completions
+config:
+ model: gpt-5
+ provider: openai
+settings:
+ temperature: 0.2
+ maxToken: 2048
+ maxSteps: 16 # raise it when a long tool chain must complete
+userPrompts:
+ - id: '1'
+ value: Summarise the last 10 orders
+ timestamp: 1758412800000 # a number — do not quote it
+ active: true
+ contentType: text
+mcpConfig: |-
+ [
+ {
+ "name": "my-server",
+ "config": {"command": "uvx", "args": ["my-mcp-server"]},
+ "enabled": true,
+ "disabledTools": []
+ }
+ ]
+enabledTools:
+ - some_tool
+auth:
+ type: bearer
+ credentials:
+ token: '{{OPENAI_API_KEY}}'
+order: 1000
+```
+
+`clientTools`, for tools the model may call without an MCP server, is
+`[{id, tools: [{id}]}]`.
+
+#### Per-kind `settings` blocks
+
+Each kind has its own closed `settings` list — they do **not** share the HTTP request's
+keys. Writing a key from the wrong kind means it is dropped in silence.
+
+| Kind | `settings` keys |
+|---|---|
+| `http-request` | `protocolVersion` (`auto`\|`http1`\|`http2`), `strictSSL`, `followRedirects`, `maxRedirects`, `disabledSystemHeaders` |
+| `socket.io-request` | `version` (**`"2"`**\|**`"3"`**\|**`"4"`**, strings), `path`, `handshakeTimeout`, `retryCount`, `retryDelay`, `strictSSL` |
+| `websocket-request` | `handshakeTimeout`, `retryCount`, `maxPayload`, `retryDelay`, `strictSSL` |
+| `mqtt-request` | `cleanSession`, `keepAlive`, `autoReconnect`, `connectionTimeout`, `strictSSL` |
+| `llm-request` | `temperature`, `maxToken`, `maxSteps`, `topP`, `topK`, `seed`, `presencePenalty`, `frequencyPenalty`, `repetitionPenalty`, `contextWindow`, `streamResponse`, `streamTools`, `responseFormatJSON` |
+| `graphql-request` | `disabledSystemHeaders` |
+| `grpc-request` | `secureConnection`, `strictSSL`, `serverNameOverride`, `maxResponseMessageSize`, `includeDefaultFields`, `connectionTimeout` |
+| `mcp-request` | `version`, `requestTimeout`, plus `strictSSL` and `sessionTimeout` on `sse` only |
+
+**`disabledSystemHeaders` takes a closed list of lowercase header names**, not free
+text: `cache-control`, `postman-token`, `content-type`, `content-length`, `accept-encoding`, `user-agent`, `connection`, `accept`, `host`. `- User-Agent`
+is rejected outright.
+
+**Quote a numeric-looking value only where the field is a string.** The two fields
+named `version` are the trap, because they disagree:
+
+| Field | Type | Write |
+|---|---|---|
+| `socket.io-request` → `settings.version` | **string** | `version: '4'` |
+| `mqtt-request` → `version` (top level, not in `settings`) | **number** | `version: 5` |
+
+`version: 4` is rejected for Socket.IO and `version: '5'` is rejected for MQTT — the
+only MQTT values are `4` and `5`. Beyond those, `timestamp`, `port`, `order`,
+`maxToken`, `retryCount`, `handshakeTimeout` and `qos` are all numbers, and quoting
+*them* fails with `Invalid input: expected number, received string`.
+
+#### `graphql-request`
+
+Has no `queryParams` field — put query-string parameters in the `url`. Its `schema`
+block is a tagged union; write exactly one branch:
+
+```yaml
+schema:
+ source: auto # or none
+```
+```yaml
+schema:
+ source: file
+ location: ./schema.graphql
+```
+
+#### `grpc-request`
+
+`url` plus `methodPath` (e.g. `helloworld.Greeter/SayHello`), with `methodDescriptor`
+for an inline definition, `message` for the request body, and `metadata` for per-call
+headers.
+
+### Examples
+
+An example's `response` **requires** `body`, so a 204 still needs
+`body: {type: text, content: ''}` rather than omitting it. `response.body.type` accepts
+`text`, `json`, `xml`, `html`, `javascript` and `yaml` — there is no `urlencoded` on a
+*response*, so carry a form-encoded payload as `type: text` with a matching
+`Content-Type` header. Request bodies do have `urlencoded`.
+
+### Specs
+
+A spec may be one file or a directory, but **`$ref` does not resolve across documents**.
+Splitting `index.yaml` and `components.yaml` fails whichever form you use —
+`'#/components/schemas/X'`, `'./components.yaml#/components/schemas/X'` and
+`'components.yaml#/components/schemas/X'` all report *"does not exist"*. Keep
+`components` inline in the same document as the paths that reference it.
+
+`postman spec lint` reports governance findings alongside validity ones, and it reports
+them at **ERROR** severity. A perfectly valid spec with no `security` block still fails
+with *"Security field is not defined"*, issue type `Governance`. So severity alone does
+not tell you whether the spec is well-formed — read the `issue type` column, and treat
+only `Validation` findings as format errors.
diff --git a/skills/collection-schema-v3/reference/other_entities.md b/skills/collection-schema-v3/reference/other_entities.md
new file mode 100644
index 0000000..82f9f07
--- /dev/null
+++ b/skills/collection-schema-v3/reference/other_entities.md
@@ -0,0 +1,518 @@
+# Field reference: every v3 entity and request kind
+
+Read this when authoring something the main guidance only names. Every field list here
+is complete for its kind — a key not listed is not recognised, and the linter will
+**strip it in silence** rather than report it.
+
+## Auth
+
+`auth` is available on a collection, a folder, a request and an example. It has two
+shapes. No auth:
+
+```yaml
+auth:
+ type: noauth
+```
+
+Anything else takes `type` plus `credentials`:
+
+```yaml
+auth:
+ type: bearer
+ credentials:
+ token: '{{TOKEN}}'
+```
+
+`type` is one of `basic`, `bearer`, `apikey`, `digest`, `oauth1`, `oauth2`, `jwt`,
+`awsv4`, `hawk`, `ntlm`, `asap`, `edgegrid`, `azure`, `sasl`, `inherit`. Use `inherit`
+to take the parent folder's or collection's auth explicitly; omitting `auth` entirely
+does the same thing.
+
+`credentials` keys are the same names the Postman app shows for that auth type —
+`username`/`password` for `basic`, `token` for `bearer`, `key`/`value`/`in` for
+`apikey`, `accessKey`/`secretKey`/`region`/`service` for `awsv4`. They are not
+validated against the auth type, so a wrong key name is dropped and the request goes
+out unauthenticated. `credentials` accepts a map or an array of `{key, value}`; prefer
+the map.
+
+## Request body
+
+`body` is one shape chosen by `type` — write that branch's keys and nothing else.
+
+```yaml
+body: {type: none}
+```
+```yaml
+body:
+ type: json # or text, xml, html, javascript
+ content: |-
+ {"id": 1}
+```
+```yaml
+body:
+ type: urlencoded
+ content:
+ grant_type: client_credentials # map, or array of {key, value, disabled?}
+```
+```yaml
+body:
+ type: formdata
+ content:
+ - key: file
+ type: file
+ src: ./payload.bin
+ - key: note
+ type: text
+ value: hello
+ contentType: text/plain
+```
+```yaml
+body:
+ type: file
+ content:
+ src: ./upload.bin # an object with src, not a bare path
+```
+
+`type: json` is the one to reach for — it does not reformat the content, so write the
+JSON as a block scalar. A *response* body inside an example has a narrower type list:
+see Examples below.
+
+## Scripts
+
+```yaml
+scripts:
+ - type: afterResponse
+ code: |-
+ pm.test('ok', () => pm.response.to.have.status(200));
+```
+
+`type` is `beforeRequest` or `afterResponse` on a request, and protocol-prefixed on a
+collection or folder (`http:beforeRequest`, `http:afterResponse`,
+`graphql:beforeQuery`, `grpc:beforeInvoke`, …). A script entry may also carry
+`language`, `packages`, and `requests`. `language` takes exactly one value,
+**`text/javascript`** — `language: javascript` is a hard error,
+`Invalid input: expected "text/javascript"`. Omitting it is fine.
+
+Writing `event:` instead of `scripts:` — the v2.1 spelling — lints clean and is
+dropped, leaving the request with no scripts and a run with 0 assertions.
+
+## Request kinds
+
+`$kind` is required on all of these, and it is the *only* required field except on
+`mcp-request`, which also requires `transport`. So for every other kind a half-written
+request lints clean. Fields are listed complete; `id`, `name`, `description` and `order`
+are available on all of them and omitted below.
+
+### `http-request`
+
+`url`, `method`, `headers`, `queryParams`, `pathVariables`, `body`, `auth`, `scripts`,
+`examples`, `settings`.
+
+`settings`: `protocolVersion` (`auto`|`http1`|`http2`), `strictSSL`,
+`followRedirects`, `maxRedirects`, `disabledSystemHeaders`.
+
+`disabledSystemHeaders` is a closed list of **lowercase** header names, not free text:
+`cache-control`, `postman-token`, `content-type`, `content-length`, `accept-encoding`, `user-agent`, `connection`, `accept`, `host`. `User-Agent` is rejected —
+`Invalid option: expected one of "cache-control"|…|"host"`.
+
+### `graphql-request`
+
+`url`, `query`, `variables` (a **string** of JSON), `headers`, `auth`, `schema`,
+`scripts`, `examples`, `settings`.
+
+There is **no `queryParams`** — put query-string parameters in the `url`.
+
+`settings`: `disabledSystemHeaders` only (same closed list as HTTP — see above).
+
+`schema` is one shape chosen by `source`:
+
+```yaml
+schema: {source: none}
+```
+```yaml
+schema: {source: auto}
+```
+```yaml
+schema:
+ source: file
+ location: ./schema.graphql
+```
+```yaml
+schema:
+ source: api
+ apiId:
+ versionId: # releaseId optional
+```
+```yaml
+schema:
+ source: specification
+ specificationId:
+```
+
+### `grpc-request`
+
+`url`, `methodPath` (e.g. `helloworld.Greeter/SayHello`), `methodDescriptor` (an inline
+proto definition), `message` (`{content}`), `metadata` (per-call headers; map or
+array), `auth`, `schema`, `scripts`, `examples`, `settings`.
+
+`settings`: `secureConnection`, `strictSSL`, `serverNameOverride`,
+`maxResponseMessageSize`, `includeDefaultFields`, `connectionTimeout`.
+
+### `websocket-request`
+
+`url`, `headers`, `queryParams`, `messages`, `examples`, `settings`.
+
+`messages` and `examples` are **strings** — relative paths to directories, not arrays.
+An array fails with `Invalid input: expected string, received array`.
+
+`settings`: `handshakeTimeout`, `retryCount`, `maxPayload`, `retryDelay`, `strictSSL`.
+
+### `socket.io-request`
+
+`url`, `headers`, `queryParams`, `events`, `messages`, `examples`, `settings`.
+
+```yaml
+events:
+ - name: chat:message
+ subscribeOnConnect: true
+ description: Inbound chat traffic
+```
+
+`settings`: `version` (the **strings** `'2'`, `'3'`, `'4'`), `path`,
+`handshakeTimeout`, `retryCount`, `retryDelay`, `strictSSL`. Bare `version: 4` is
+rejected — `Invalid option: expected one of "2"|"3"|"4" (path: /settings/version)`.
+
+### `mqtt-request`
+
+`url`, `clientId`, `version`, `topics`, `lastWill`, `properties`, `auth`, `messages`,
+`examples`, `settings`.
+
+`version` is a **number**, either `4` or `5`, and it sits at the top level rather than
+in `settings`. Do not quote it — that is the opposite of `socket.io-request`, whose
+`settings.version` is the string `'2'`, `'3'` or `'4'`.
+
+```yaml
+topics:
+ - name: sensors/+/temp
+ qos: 1 # a number: 0, 1 or 2
+ subscribe: true
+lastWill:
+ topic: status/client
+ payload: offline
+ type: text
+ qos: 0
+ retain: true
+```
+
+`properties`: `sessionExpiryInterval`, `receiveMaximum`, `maximumPacketSize`,
+`requestResponseInformation`, `userProperties`.
+
+`settings`: `cleanSession`, `keepAlive`, `autoReconnect`, `connectionTimeout`,
+`strictSSL`.
+
+### `mcp-request`
+
+Requires `$kind` **and** `transport`, which selects the legal keys. Common to both:
+`message`, `auth`.
+
+| `transport` | Its keys | `settings` |
+|---|---|---|
+| `stdio` | `command`, `env` (map or array) | `version`, `requestTimeout` |
+| `sse` | `url`, `headers` | `version`, `requestTimeout`, `strictSSL`, `sessionTimeout` |
+
+There are no other transports. `message` is a JSON-RPC envelope as a **string**;
+`{{variables}}` interpolate inside it.
+
+### `llm-request`
+
+`url`, `config`, `userPrompts`, `systemPrompts`, `mcpConfig`, `enabledTools`,
+`clientTools`, `auth`, `settings`.
+
+`config` takes `model` and `provider`, both free-form strings with no fixed list, so a
+typo lints clean and fails at run time.
+
+Prompt entries are `{id, value, timestamp, active, contentType}`. **`contentType`, not
+`type`** — `type` lints clean and is stripped, losing the content type.
+
+`mcpConfig` is a **string containing a JSON array**, not nested YAML and not
+`mcp.json`'s `mcpServers` object. `clientTools`, for tools the model may call with no
+MCP server behind them, is `[{id, tools: [{id}]}]`.
+
+`settings`: `temperature`, `maxToken`, `maxSteps`, `topP`, `topK`, `seed`,
+`presencePenalty`, `frequencyPenalty`, `repetitionPenalty`, `contextWindow`,
+`streamResponse`, `streamTools`, `responseFormatJSON`. Raise `maxSteps` when a long tool
+chain has to run to completion.
+
+## Messages
+
+A `messages` directory holds `*.message.yaml` files. Each kind has different required
+fields — this is the most common mistake in this area.
+
+```yaml
+$kind: websocket-message # requires content + contentType
+content: '{"op":"subscribe"}'
+contentType: json # text | json | xml | html | hex | base64
+```
+
+```yaml
+$kind: socket.io-message # requires eventName + args
+eventName: chat:message
+acknowledgement: true
+args:
+ - content: '{"text":"hi"}'
+ contentType: json
+```
+
+```yaml
+$kind: mqtt-message # requires content + contentType + topic
+topic: sensors/1/temp
+content: '21.5'
+contentType: text # text | json | hex | base64
+qos: 1
+retain: false
+```
+
+## Examples
+
+Examples live in `.resources/.resources/examples/*.example.yaml`, with
+`$kind` matching the request's protocol — `http-example`, `graphql-example`,
+`grpc-example`, `websocket-example`, `socket.io-example`, `mqtt-example`. An
+`*-example` kind is rejected in a `*.request.yaml`, and a `*-request` kind is rejected
+in a `*.example.yaml`.
+
+`http-example` requires `$kind` and `response`, and `response` requires `body`:
+
+```yaml
+$kind: http-example
+name: 200 OK
+request:
+ method: POST
+ url: https://api.example.com/orders
+ headers:
+ Content-Type: application/json
+ body:
+ type: json
+ content: '{"sku":"A1"}'
+response:
+ statusCode: 200
+ statusText: OK
+ headers:
+ Content-Type: application/json
+ body:
+ type: json
+ content: '{"id":"o_1"}'
+order: 1000
+```
+
+A 204 still needs a body — `body: {type: text, content: ''}`. Response `body.type`
+accepts only `text`, `json`, `xml`, `html`, `javascript` and `yaml`; there is **no
+`urlencoded` on a response**, so carry a form-encoded payload as `type: text` with a
+matching `Content-Type` header. Request bodies do have `urlencoded`.
+
+## Datasets
+
+```
+postman/datasets/users/
+├── users.dataset.yaml
+└── .resources/
+ └── rows.json
+```
+
+```yaml
+type: dataset # not $kind
+name: "Users"
+id: 3f2b6c1e-9d4a-4f81-bc37-5e0a71d9c402 # uuid4
+data_dir: .resources
+sources:
+ - from: embedded_datasource
+ datasource:
+ name: Records
+ id: 8a1d0e77-5c62-4b19-9f03-2d6e4a8b7c15
+ format: json # json | csv | xlsx | mysql
+ source_type: static # static for files, dynamic for databases
+ location:
+ type: local_filesystem
+ value: rows.json # relative to data_dir
+views:
+ - from: embedded_view
+ view:
+ name: Records_default_view
+ id: c94e5b20-71af-4d36-8e52-0b7c6d1934fa
+ query: SELECT id, name FROM Records ORDER BY id
+```
+
+`sources` and `views` are optional — `type`, `name`, `id` and `data_dir` alone is a
+valid dataset, and is what `postman dataset create --name X` writes.
+
+A database source instead sets `format: mysql`, `source_type: dynamic`,
+`ingest: false`, a `dsKey` and a `slug`, with `location.type: mysql`, `location.value`
+a DSN and `location.table` the table name.
+
+A view's `query` is SQL over the source `name`, not over the file name.
+
+There is no `dataset lint`. `postman dataset get ` checks the structure offline.
+`postman dataset query -q ""` runs SQL against the source, which also proves
+the data parses and the query resolves — note the `-q` flag is required, and this one
+call contacts the server.
+
+## Mocks
+
+```
+postman/mocks/probe-mock/
+├── config.yaml
+└── default.js
+```
+
+```yaml
+version: 1 # not $kind
+id: a41c8e05-7f23-4b96-8d0e-3c6a9b25f718
+name: Probe Mock
+slug: probe-mock
+protocol: http
+port: 4500
+scenarios:
+ - name: default
+ path: ./default.js
+ default: true # only on the default one; elsewhere omit it
+```
+
+Scenario `id` is optional. Nothing validates the `.js`, so match the shape
+`postman mock generate --name X` emits — a plain Node server reading `PORT` from the
+environment, one `// @endpoint METHOD /path` marker per route:
+
+```js
+const http = require('http');
+const PORT = process.env.PORT || 4500;
+
+const server = http.createServer((req, res) => {
+ const url = new URL(req.url || '', 'http://localhost');
+ const pathname = url.pathname;
+
+ // @endpoint GET /health
+ if (req.method === 'GET' && pathname === '/health') {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify({ status: 'ok' }));
+ }
+
+ res.writeHead(404, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: 'Endpoint not defined' }));
+});
+
+server.listen(PORT);
+```
+
+Branch on `req.headers['x-mock-response-code']` to let a caller pick an error path out
+of one scenario. Run it with `postman mock run `, or drive it from a
+collection with `postman collection run --mock `.
+
+## Environments and globals
+
+```yaml
+name: Dev
+values:
+ - key: base_url
+ value: https://api.example.com # always a string
+ enabled: true
+ type: default # or secret
+```
+
+Globals are identical, conventionally `name: Globals`. `values: []` is valid.
+
+A secret value can be marked as `type: secret`, or as `secret: true` alongside
+`type: default` — both are recognised fields.
+
+Unlike `collection lint`, `environment lint` and `globals lint` **do** report an
+unrecognised key, as `warning [ENV007] Unrecognized field "values.0."`. So a clean
+environment lint is stronger evidence than a clean collection lint.
+
+The **filename suffix is enforced**: `*.environment.yaml` under
+`postman/environments/` and `*.globals.yaml` under `postman/globals/`.
+`postman/environments/Staging.yaml` is rejected outright — *"Not an environment file.
+Expected a file ending in .environment.yaml, .environment.yml, .environment.json"*.
+
+Validate with `postman environment lint ` and `postman globals lint `.
+Both take `-o/--output` for the report format, where `collection lint` takes
+`-r/--reporter`.
+
+## Specs
+
+`postman/specs/.yaml`, or `postman/specs//` as a directory — both are
+accepted on disk. The content is plain OpenAPI or AsyncAPI with no Postman wrapper.
+
+**`$ref` does not resolve across documents.** Splitting `index.yaml` and
+`components.yaml` fails whichever spelling you use —
+`'#/components/schemas/X'`, `'./components.yaml#/components/schemas/X'` and
+`'components.yaml#/components/schemas/X'` all report *"does not exist"*. Keep
+`components` inline in the same document as the paths that reference it.
+
+`postman spec lint` reports governance findings next to validity ones, at **ERROR**
+severity. A valid spec with no `security` block still reports *"Security field is not
+defined"* with issue type `Governance`, so a non-zero error count does not mean the spec
+is malformed. Read the `issue type` column and treat only `Validation` findings as
+format errors.
+
+## Documents
+
+`postman/documents/.md`, plain Markdown, no front matter and no wrapper. Nothing
+validates a document; it is pushed and pulled as-is.
+
+## SDKs
+
+An SDK is configured in `.postman/config.json`, not under `postman/`. Generate with
+`postman sdk generate`, which is also the only validation.
+
+## The workspace manifest
+
+`.postman/resources.yaml` maps local paths to cloud ids. **Do not hand-author it** —
+`postman init` creates it and `workspace push` / `mock push` maintain the mappings. The
+one routine hand-edit is deleting a stale line after an entity is deleted in the cloud,
+since that leaves the mapping behind.
+
+Collections map to a **directory**; every other entity maps to a **file**. Paths are
+relative to `.postman/`, so they begin `../`. Ids come either owner-prefixed
+(`12345678-`) or as bare 24-character hex — both are accepted.
+
+```yaml
+workspace:
+ id: 6d3e9a17-4c80-4f25-b1a9-7e02c58d3f64
+cloudResources:
+ collections:
+ ../postman/collections/My API: 12345678-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
+ environments:
+ ../postman/environments/Dev.environment.yaml: 12345678-6d3e9a17-4c80-4f25-b1a9-7e02c58d3f64
+ globals:
+ ../postman/globals/Globals.globals.yaml: 12345678-99999999-8888-7777-6666-555555555555
+ datasets:
+ ../postman/datasets/users/users.dataset.yaml: d7c81f43-2e95-4a08-b6d1-9f3e5c07a284
+ mocks:
+ ../postman/mocks/probe-mock/config.yaml: a41c8e05-7f23-4b96-8d0e-3c6a9b25f718
+ specs:
+ ../postman/specs/orders.yaml: cccccccc-dddd-eeee-ffff-000000000000
+```
+
+An entity that exists in the cloud but is missing from this file makes `workspace push`
+plan a *create*, which fails `404 The target resource was not found` and leaves the
+cloud copy stale. A mapping whose path no longer exists raises `WSP014` from
+`workspace lint`.
+
+## Validating each entity
+
+| Entity | Command |
+|---|---|
+| collection | `postman collection lint -r json` |
+| environment / globals | `postman environment lint ` / `postman globals lint ` |
+| spec | `postman spec lint ` |
+| dataset | `postman dataset get `, then `postman dataset query -q ""` |
+| mock | `postman mock run ` |
+| sdk | `postman sdk generate` |
+| document | none |
+| everything, plus the manifest | `postman workspace lint` |
+
+`postman workspace lint` is the broadest check, the only one with `--fix` and
+`info`/`hint` severities, and the only one that reports unrecognised fields in a
+*collection* (`FMT206` in a request, `FMT208` in `definition.yaml`). `collection lint`
+stays silent about those, so run `workspace lint` before you call anything done.
+Environment and globals lint do report their own unknown fields (`ENV007`). `postman collection run --no-report-events` is
+the only way to prove scripts survived. The run needs no login; the flag suppresses the
+post-run report upload, which is the only part that reaches the cloud and the only reason
+a local run would fail on authentication.
diff --git a/skills/collection-schema-v3/reference/other_protocols.md b/skills/collection-schema-v3/reference/other_protocols.md
index 3a21b8b..f8a14ba 100644
--- a/skills/collection-schema-v3/reference/other_protocols.md
+++ b/skills/collection-schema-v3/reference/other_protocols.md
@@ -77,8 +77,10 @@ types — for the common case, the HTTP schema in the parent
- `url` — string.
- `order` — optional.
- `config` — `{model, provider}`.
-- `userPrompts` — array of `{id, value, timestamp, active, type: "text"}`.
-- `systemPrompts` — array of `{id, value, timestamp, active, type: "text"}`.
+- `userPrompts` — array of `{id, value, timestamp, active, contentType: "text"}`.
+- `systemPrompts` — array of `{id, value, timestamp, active, contentType: "text"}`.
+ The field is `contentType`, not `type`: `type` lints clean and is then stripped,
+ so the content type is silently lost.
- `mcpConfig` — optional string (JSON config).
- `enabledTools` — optional array of strings.
- `auth` — `{type, credentials}`.