vlt / affordances
  • Quickstart
  • Concepts
  • Guides
  • Reference
  • For agents
  • Why Affordances
  • Quickstart
  • Benchmarks

Concepts
  • Overview
  • Actions
  • State
  • Expose
  • Guards and confirm
  • Deltas
  • Plugins

Guides
  • Overview
  • React and WebMCP
  • MCP
  • Lint
  • Security

Plugins
  • Overview

Reference
  • Overview
  • Protocol
  • Options
  • CLI
  • Media type
  • Discovery
  • Graph
  • Action descriptor
  • Call
  • Delta
  • Error codes
  • Site index
  • Security
  • Versioning
  1. Reference
  2. /
  3. Protocol

Protocol

The normative Affordances protocol, version 0. This page renders packages/protocol/spec.md from the repository at build time.

Affordances lets an agent read and operate a web page through the same URL a person uses, with the same session. A page answers in HTML for browsers and in JSON for agents. The JSON lists what the page shows (entities), what can be done there (actions), and where to go next (nav). Calling an action returns a delta: the parts of the page that changed.

This document is normative. The key words "must", "must not", "should", and "may" are used as in RFC 2119. The JSON Schemas in schemas/ are generated from the same source as the TypeScript types and are authoritative for shapes.

Media type

All Affordances documents use application/vnd.affordances+json. A server must not return an Affordances document for a request that did not ask for one.

Discovery

Per page. A client requests the page's own URL with Accept: application/vnd.affordances+json. The server responds with a graph for that page, or a not_found delta with status 404 if the page has no Affordances graph.

HTML responses for pages with a graph should advertise it in both places:

http
Link: <?affordances=1>; rel="affordances"
html
<link rel="affordances" href="?affordances=1" />

?affordances=1 is an alias for the Accept header, for clients that cannot set headers. A server that supports it must treat GET <url>?affordances=1 like GET <url> with the Affordances Accept header, and must leave affordances out of the searchParams it gives the page's state.

Per site. GET /.well-known/affordances.json returns a site index: the protocol version, the route patterns that have graphs and their action names, the supported auth modes, and optionally an MCP endpoint.

A shared cache must never serve one representation for the other: graphs send Vary: Accept and Cache-Control: private, no-store, and a server must make sure cached HTML is keyed on Accept too (with Vary: Accept, or by routing Affordances requests before the cache, as @affordances/next does with rewrites).

Graph

TypeScript
type Graph = {
  v: '0'
  page: string // concrete path: /products/heavyweight-tee
  route: string // pattern: /products/[slug]
  entities: Record<string, unknown>
  actions: ActionDescriptor[]
  nav?: { to: string; label?: string; when?: string }[]
  md?: string
}
  • entities is the page's state, keyed by name. It must not contain anything the page would not show the same user in HTML.
  • actions lists every action on the route that the requester may know about. Actions the requester can't call right now are still listed, with available: false and a reason.
  • nav lists links an agent can follow. when is a human-readable condition.
  • md is an optional Markdown rendering of the page's content.
  • A graph should stay small. About 2,000 tokens is a good ceiling, and large collections should be summarized in entities.

Action descriptor

TypeScript
type ActionDescriptor = {
  name: string // unique per route, stable across releases
  description: string
  input: JSONSchema
  output?: JSONSchema
  expose: 'public' | 'session' | 'confirm'
  guard?: string // human-readable precondition
  available: boolean // the guard, evaluated for this request
  reason?: string // why available is false
  effects?: string[] // entity keys the action changes
  readOnly?: boolean
}

expose says who may call an action:

exposeCaller
publicanyone
sessionan authenticated session, by cookie or bearer token
confirma session, and the call must carry confirm: true after the user has seen a summary

An action with no expose is not part of the protocol. It must not appear in graphs and must not be callable.

Call

http
POST <page-url>
Content-Type: application/vnd.affordances+json

{ "action": "addToCart", "input": { "sku": "TEE-BLK-M" } }
TypeScript
type Call = { action: string; input?: unknown; confirm?: boolean }

The server processes a call in this order, and stops at the first failure:

  1. Resolve the action by name on the page's route. Failure: not_found.
  2. Check the CSRF rule below and expose against the caller. Failure: unauthorized.
  3. Apply the rate limit. Failure: rate_limited.
  4. Validate input against the action's input schema. Failure: invalid_input, with issues.
  5. Evaluate the guard. Failure: unavailable, with the guard's reason as message.
  6. For confirm actions without confirm: true: confirm_required, with a summary of what will happen as message, written for the person who approves it. Nothing runs.
  7. Run the action, reload the page's state, and respond with a delta.

Delta

TypeScript
type Delta = {
  ok: boolean
  changed?: Record<string, unknown>
  output?: unknown
  actionsAdded?: ActionDescriptor[]
  actionsRemoved?: string[]
  redirect?: string
  message?: string
  error?: { code: ErrorCode; message: string; issues?: unknown }
}
  • changed holds the new value of each entity in the action's effects. When an action declares no effects, changed holds every top-level entity whose value differs after the action ran.
  • output is the action's return value, described by the descriptor's output schema when it has one.
  • actionsAdded and actionsRemoved describe changes to the action list, including availability changes. An action whose available flipped appears in actionsAdded with its new descriptor.
  • redirect is a path the person would have been sent to. The agent should request it next.
  • message is a short sentence suitable to repeat to the user.

Error codes

CodeHTTPMeaning
not_found404No such route or action
invalid_input422input failed the schema; issues says where
unauthorized401The caller may not call this action
confirm_required409Resend with confirm: true after showing message
unavailable409The guard failed; message says why
rate_limited429Too many calls; the response should carry Retry-After
internal500The action threw; message must not leak internals

Site index

TypeScript
type SiteIndex = {
  v: '0'
  name?: string
  routes: { route: string; description?: string; actions: ActionSummary[] }[]
  auth: ('cookie' | 'bearer')[]
  mcp?: string
}

// an action descriptor without this request's availability (no `available`, no `reason`)
type ActionSummary = Omit<ActionDescriptor, 'available' | 'reason'>

The index is enough to plan a task and to call an action without opening its page first: an agent that knows the route and the input can POST straight to a concrete URL of that route. It still gets unavailable or unauthorized if the guard or expose says no. Each route's description is one line on what the page is for. Input schemas may omit $schema; they are JSON Schema 2020-12.

Security

  • Default deny. Only actions that declare expose are reachable.
  • A call that authenticates by cookie must carry an Origin header that matches the site. A call with Authorization: Bearer is exempt. A server must not rely on its framework's server-action protections for Affordances calls.
  • confirm actions must not run without confirm: true. Clients acting for a person should show message and get approval first.
  • Input is validated before the guard runs, and the guard runs before the action.
  • URLs must not carry personal data. A graph must not include data that the HTML page would not show to the same user.

Versioning

v is the protocol's major version. A breaking change ships as a new major version. Additive changes, such as new optional fields, may ship within a major version, and clients must ignore fields they don't recognize. Packages declare the range of majors they support.


PreviousOverviewNextOptions

On this page

  • Media type
  • Discovery
  • Graph
  • Action descriptor
  • Call
  • Delta
  • Error codes
  • Site index
  • Security
  • Versioning
Edit this page

Make your app agent-operable

Give every page a graph and every server action an agent path: HTTP, WebMCP, and MCP, with the same session and the same checks. One command sets it up.

Read the quickstart