Openwork · API Governance Rules
Openwork — openwork rules
Spectral linting rules defining API design standards and conventions for Openwork.
5 Rules
error 2
warn 3
Published by Openwork
Served by the provider at https://github.com/different-ai/openwork/blob/70c948c2abfc9b875c2971c6306c1603ac62f587/.spectral.yaml; the copy below was fetched from there.
Rule Categories
ow
Rules
error
ow-operation-security
Every operation must declare `security` (an empty array marks a public route).
$.paths[*][get,post,put,patch,delete,head,options,trace]
error
ow-operation-has-4xx
Protected operations must document at least one 4xx response.
$.paths[*][get,post,put,patch,delete][?(@.security && @.security[0])].responses
warn
ow-200-has-content
200 responses must document their body; use 204 for empty responses.
$.paths[*][*].responses['200']
warn
ow-timestamps-date-time
Properties ending in `At` must be RFC 3339 `string` / `date-time`.
$..properties[?(@ && @property.match(/At$/) && !@.anyOf && !@.oneOf && !@.$ref)]$..properties[?(@property.match(/At$/))].anyOf[?(@ && @.type != 'null')]
warn
ow-list-pagination
List GET operations must accept `cursor` + `limit` and return `nextCursor`.
$.paths[*].get
Spectral Ruleset
# harvested from https://github.com/different-ai/openwork/blob/70c948c2abfc9b875c2971c6306c1603ac62f587/.spectral.yaml on 2026-10-09 — a Spectral ruleset published in the provider's own GitHub repository (different-ai/openwork); found by GitHub code search, fetched verbatim
x-method: harvested
x-stamped: 2026-10-09
x-source-url: https://github.com/different-ai/openwork/blob/70c948c2abfc9b875c2971c6306c1603ac62f587/.spectral.yaml
# Spectral ruleset for the Den API OpenAPI document (packages/docs/openapi.json).
#
# This is the single API-quality gate: `pnpm api:lint` regenerates nothing, it
# lints the committed snapshot; CI regenerates first (drift check) and then
# lints. Errors fail the build; warnings are ratcheted against
# .spectral-baseline.json and may not increase.
#
# Rule intent, exemptions and the protocol-adapter boundary are documented in
# docs/api-style.md. Keep the two files in sync.
extends: [[spectral:oas, recommended]]
rules:
# ---- spectral:oas severity adjustments -------------------------------------
operation-tag-defined: error
operation-tags: error
operation-description: warn
oas3-unused-component: warn
info-contact: error
# prefixItems tuples are valid OpenAPI 3.1 / JSON Schema 2020-12 but the
# bundled array-items rule still expects `items`; keep the check without
# failing the build on the two tuple schemas.
array-items: warn
# ---- OpenWork house rules ---------------------------------------------------
# Every operation states how it is authenticated. `security: []` is the
# explicit marker for a public route; omitting the key is not allowed even
# though document-level security would apply, because generators and
# scanners commonly ignore the document-level default. Routes without a
# describeRoute() surface here.
ow-operation-security:
description: Every operation must declare `security` (an empty array marks a public route).
message: "{{path}} has no `security` array; declare one in describeRoute() (use [] for public routes)."
severity: error
given: "$.paths[*][get,post,put,patch,delete,head,options,trace]"
then:
field: security
function: defined
# Protected operations (non-empty security) document at least one 4xx so
# clients learn the failure envelope. Public health/metadata routes are
# exempt through their empty security array.
ow-operation-has-4xx:
description: Protected operations must document at least one 4xx response.
message: "{{path}} is protected but documents no 4xx response."
severity: error
given: "$.paths[*][get,post,put,patch,delete][?(@.security && @.security[0])].responses"
then:
function: schema
functionOptions:
schema:
type: object
anyOf:
- required: ["400"]
- required: ["401"]
- required: ["402"]
- required: ["403"]
- required: ["404"]
- required: ["405"]
- required: ["406"]
- required: ["408"]
- required: ["409"]
- required: ["410"]
- required: ["412"]
- required: ["413"]
- required: ["415"]
- required: ["422"]
- required: ["423"]
- required: ["428"]
- required: ["429"]
# A 200 without `content` leaves the response body undocumented. Use 204 when
# there is genuinely no body.
ow-200-has-content:
description: 200 responses must document their body; use 204 for empty responses.
message: "{{path}} is a 200 with no `content`; document the body or return 204."
severity: warn
given: "$.paths[*][*].responses['200']"
then:
field: content
function: truthy
# Timestamps end in `At` and are RFC 3339 `date-time` strings. Nullable
# timestamps are expressed as anyOf [date-time, null]; the second `given`
# checks the non-null branch.
ow-timestamps-date-time:
description: Properties ending in `At` must be RFC 3339 `string` / `date-time`.
message: "{{property}} ends in `At` but is not a string with format date-time (see docs/api-style.md#timestamps)."
severity: warn
given:
- "$..properties[?(@ && @property.match(/At$/) && !@.anyOf && !@.oneOf && !@.$ref)]"
- "$..properties[?(@property.match(/At$/))].anyOf[?(@ && @.type != 'null')]"
then:
function: schema
functionOptions:
schema:
type: object
properties:
type:
const: string
format:
const: date-time
required: [type, format]
# Collections paginate with cursor + limit and return nextCursor. A GET whose
# response carries `items` or that accepts `limit`/`offset` is a list.
ow-list-pagination:
description: List GET operations must accept `cursor` + `limit` and return `nextCursor`.
message: "{{path}} looks like a list but does not implement cursor + limit + nextCursor pagination (see docs/api-style.md#pagination)."
severity: warn
given: "$.paths[*].get"
then:
function: schema
functionOptions:
schema:
# A GET is a list when it accepts limit/offset or returns `items`.
if:
anyOf:
- required: [parameters]
properties:
parameters:
contains:
type: object
required: [in, name]
properties:
in: { const: query }
name: { enum: [limit, offset] }
- required: [responses]
properties:
responses:
required: ["200"]
properties:
"200":
required: [content]
properties:
content:
required: [application/json]
properties:
application/json:
required: [schema]
properties:
schema:
required: [properties]
properties:
properties:
required: [items]
then:
required: [parameters, responses]
properties:
parameters:
allOf:
- contains:
type: object
required: [in, name]
properties: { in: { const: query }, name: { const: cursor } }
- contains:
type: object
required: [in, name]
properties: { in: { const: query }, name: { const: limit } }
responses:
required: ["200"]
properties:
"200":
required: [content]
properties:
content:
required: [application/json]
properties:
application/json:
required: [schema]
properties:
schema:
required: [properties]
properties:
properties:
required: [nextCursor]
# ---- Documented exceptions ----------------------------------------------------
# Protocol adapters follow their RFC verbatim and are exempt from house naming,
# pagination and timestamp rules (they still need security, summary, tags and
# responses). Pointers are JSON-pointer escaped (`/` -> `~1`) and, as Spectral
# does, percent-encode path parameter braces (`{` -> `%7B`, `}` -> `%7D`).
overrides:
# Audit export is a bounded NDJSON/CSV attachment, not a JSON list envelope.
# It accepts cursor + limit and returns X-Audit-Next-Cursor until exhausted;
# X-Audit-Snapshot-Sequence pins the committed snapshot. Route/schema and
# transport tests verify these headers survive the API and browser proxy.
- files:
- "**/*.json#/paths/~1v1~1audit~1export"
rules:
ow-list-pagination: "off"
# SCIM 2.0 (RFC 7643/7644): PascalCase resources, startIndex/count paging,
# `meta.lastModified` timestamps.
- files:
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1Schemas"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1Schemas~1urn:ietf:params:scim:schemas:core:2.0:Group"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1ResourceTypes"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1ResourceTypes~1Group"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1Groups"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1Groups~1%7BgroupId%7D"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1Users"
- "**/*.json#/paths/~1api~1auth~1scim~1v2~1Users~1%7BuserId%7D"
rules:
ow-list-pagination: "off"
ow-timestamps-date-time: "off"
operation-description: "off"
# OAuth 2.0 / OIDC discovery (RFC 8414, OIDC Discovery), dynamic client
# registration (RFC 7591, snake_case fields) and protected-resource metadata
# (RFC 9728): public by specification, bodies defined by the RFCs.
- files:
- "**/*.json#/paths/~1.well-known~1oauth-authorization-server"
- "**/*.json#/paths/~1.well-known~1oauth-authorization-server~1api~1auth"
- "**/*.json#/paths/~1.well-known~1openid-configuration"
- "**/*.json#/paths/~1.well-known~1openid-configuration~1api~1auth"
- "**/*.json#/paths/~1api~1auth~1.well-known~1oauth-authorization-server"
- "**/*.json#/paths/~1api~1auth~1.well-known~1openid-configuration"
- "**/*.json#/paths/~1register"
- "**/*.json#/paths/~1api~1auth~1oauth2~1register"
- "**/*.json#/paths/~1api~1auth~1oauth2~1authorize"
- "**/*.json#/paths/~1.well-known~1oauth-protected-resource"
- "**/*.json#/paths/~1.well-known~1oauth-protected-resource~1mcp"
- "**/*.json#/paths/~1.well-known~1oauth-protected-resource~1mcp~1agent"
- "**/*.json#/paths/~1.well-known~1oauth-protected-resource~1mcp~1admin"
- "**/*.json#/paths/~1mcp~1.well-known~1oauth-protected-resource"
- "**/*.json#/paths/~1mcp~1agent~1.well-known~1oauth-protected-resource"
- "**/*.json#/paths/~1mcp~1admin~1.well-known~1oauth-protected-resource"
- "**/*.json#/paths/~1oauth~1client-metadata.json"
rules:
ow-list-pagination: "off"
ow-timestamps-date-time: "off"
ow-operation-has-4xx: "off"
# Liveness probes have no failure envelope beyond the status code.
- files:
- "**/*.json#/paths/~1health"
- "**/*.json#/paths/~1ready"
rules:
ow-operation-has-4xx: "off"
# Deprecated tombstones intentionally answer only 410 (skill hubs, memory
# save). DELETE /v1/memory/{id} answers 401/404 only and is listed here for
# the same reason; whether it should also document a 2xx is tracked as a
# follow-up in docs/api-style.md.
- files:
- "**/*.json#/paths/~1v1~1skill-hubs"
- "**/*.json#/paths/~1v1~1skill-hubs~1%7BskillHubId%7D"
- "**/*.json#/paths/~1v1~1skill-hubs~1%7BskillHubId%7D~1skills"
- "**/*.json#/paths/~1v1~1skill-hubs~1%7BskillHubId%7D~1skills~1%7BskillId%7D"
- "**/*.json#/paths/~1v1~1skill-hubs~1%7BskillHubId%7D~1access"
- "**/*.json#/paths/~1v1~1skill-hubs~1%7BskillHubId%7D~1access~1%7BaccessId%7D"
- "**/*.json#/paths/~1v1~1memory"
- "**/*.json#/paths/~1v1~1memory~1%7Bid%7D"
rules:
operation-success-response: "off"
# Where a member's Automations can run: their own desktops, capped by
# DESKTOP_TARGET_LIST_LIMIT, plus the cloud flag. A bounded snapshot, not a
# paged collection. lastSeenAt is epoch milliseconds like every other
# Automation timestamp (AutomationRun, Automation, AutomationRevision).
- files:
- "**/*.json#/paths/~1v1~1automation-runners"
rules:
ow-list-pagination: "off"
# Distinct analytics dimension values (project labels) for one selector:
# bounded by the organization's labels and read whole by the desktop and Den
# web, so it is not a paginated collection.
- files:
- "**/*.json#/paths/~1v1~1telemetry~1dimensions"
rules:
ow-list-pagination: "off"
- files:
- "**/*.json#/components/schemas/AutomationDesktopTarget/properties/lastSeenAt"
rules:
ow-timestamps-date-time: "off"
Work with this as data
Every ruleset here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for spectral rules
4 MCP tools reach this
find_rulesBrowse and filter every ruleset in the catalog.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.
Call it yourself
curl for this page
This ruleset
curl "https://apis.io/api/v1/rules/openwork-openwork-spectral-rules"
All spectral rules
curl "https://apis.io/api/v1/rules?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.