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}`.