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
  • Install and initialize
  • Wrap the config
  • Describe the page
  • Add the provider
  • Read the graph
  • Call the action
  • Next

Quickstart

Give one page of a Next.js 16 app a graph and an action, then read and call it with curl.

You need a Next.js 16 app using the App Router, and zod 4 (or any schema library that implements Standard Schema and Standard JSON Schema).

Install and initialize

Terminal
vlt install @affordances/next @affordances/react zod
vlx @affordances/cli initnpm install @affordances/next @affordances/react zod
npx @affordances/cli initpnpm add @affordances/next @affordances/react zod
pnpm dlx @affordances/cli inityarn add @affordances/next @affordances/react zod
yarn dlx @affordances/cli initbun add @affordances/next @affordances/react zod
bunx @affordances/cli initdeno add npm:@affordances/next npm:@affordances/react npm:zod
deno run -A npm:@affordances/cli init

init does four things, and skips any that are already done:

  • writes the route handler at app/api/affordances/graph/[[...path]]/route.ts (or under src/app);
  • wraps the default export of next.config.ts with withAffordances;
  • adds .affordances to .gitignore;
  • generates .affordances/manifest.ts from your actions.ts and state.ts files.

The route handler it writes is one line:

app/api/affordances/graph/[[...path]]/route.ts
export { GET, POST } from '@affordances/next/route'

Wrap the config

If your config's default export isn't a plain identifier, init asks you to wrap it yourself:

next.config.ts
import type { NextConfig } from 'next'
import { withAffordances } from '@affordances/next/config'

const nextConfig: NextConfig = {}

export default withAffordances(nextConfig)

withAffordances regenerates the manifest when Next loads the config, and again in next dev whenever an actions or state file changes. It also adds the rewrites that send agent requests for a page to the route handler. Your pages and their HTML are untouched.

Describe the page

Affordances reads two files next to a page. state.ts returns what the page shows:

app/guestbook/state.ts
import { defineState } from '@affordances/next'
import { entries } from '@/lib/db'

export default defineState(async () => ({
  entries: await entries.list(),
}))

actions.ts is a regular server actions file. Each defineAction export is a server action your components can call, and, because it sets expose, an action agents can call too:

app/guestbook/actions.ts
'use server'

import { refresh } from 'next/cache'
import { z } from 'zod'
import { defineAction } from '@affordances/next'
import { entries } from '@/lib/db'
import guestbook from './state'

export const sign = defineAction({
  name: 'sign',
  description: 'Add a message to the guestbook',
  input: z.object({ name: z.string().min(1), message: z.string().max(280) }),
  state: guestbook,
  expose: 'public',
  effects: ['entries'],
  run: async (input, ctx) => {
    await entries.add(input)
    // a form submit re-renders the page; agents get a delta instead
    if (ctx.via === 'ui') refresh()
  },
  message: ({ input }) => `Signed as ${input.name}`,
})

Use it in the page like any server action. Wrapping the form in <Affordance> marks it for agents that read the DOM:

app/guestbook/page.tsx
import { Affordance } from '@affordances/react'
import { sign } from './actions'

export default function Page() {
  return (
    <Affordance action={sign}>
      <form action={sign}>
        <input name="name" />
        <textarea name="message" />
        <button>Sign</button>
      </form>
    </Affordance>
  )
}

Add the provider

<AffordancesProvider> loads the current page's graph in the browser and registers each available action as a WebMCP tool. Give it the current path, and refresh the server-rendered UI after an agent changes something:

app/agents.tsx
'use client'

import { usePathname, useRouter } from 'next/navigation'
import { AffordancesProvider } from '@affordances/react'

export const Agents = ({ children }: { children: React.ReactNode }) => {
  const pathname = usePathname()
  const router = useRouter()
  return (
    <AffordancesProvider path={pathname} onDelta={() => router.refresh()}>
      {children}
    </AffordancesProvider>
  )
}

Render <Agents> around {children} in your root layout.

Read the graph

Start the dev server and ask for the page as an agent would:

Terminal
curl -H 'Accept: application/vnd.affordances+json' localhost:3000/guestbook
JSON
{
  "v": "0",
  "page": "/guestbook",
  "route": "/guestbook",
  "entities": { "entries": [] },
  "actions": [
    {
      "name": "sign",
      "description": "Add a message to the guestbook",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": { "type": "string", "minLength": 1 },
          "message": { "type": "string", "maxLength": 280 }
        },
        "required": ["name", "message"]
      },
      "expose": "public",
      "available": true,
      "effects": ["entries"]
    }
  ]
}

Call the action

Terminal
curl localhost:3000/guestbook \
  -H 'Content-Type: application/vnd.affordances+json' \
  -d '{ "action": "sign", "input": { "name": "Ada", "message": "Hello" } }'
JSON
{
  "ok": true,
  "changed": { "entries": [{ "name": "Ada", "message": "Hello" }] },
  "message": "Signed as Ada"
}

The response is a delta: the new value of each entity the action declared in effects, and the message to repeat to the user. /.well-known/affordances.json now lists /guestbook and its sign action.

Next

  • Actions covers every defineAction option.
  • Expose explains session and confirm, and Sessions shows how to share a session resolver with createAffordances.
  • MCP connects Claude or Cursor to the site.

PreviousWhy AffordancesNextBenchmarks

On this page

  • Install and initialize
  • Wrap the config
  • Describe the page
  • Add the provider
  • Read the graph
  • Call the action
  • Next
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