vlt / affordances
  • Quickstart
  • Concepts
  • Guides
  • Reference
  • Overview
  • Getting started
  • Files and conventions
  • Routing
  • Route handlers
  • Plugins
  • Sessions
  • With an agent
  • By hand
  • 1. Install
  • 2. Wrap the config
  • 3. Add the route handler
  • 4. Ignore the manifest
  • 5. Describe a page
  • 6. Mark the controls
  • 7. Check it
  • Next
  1. Guides
  2. /
  3. Next.js
  4. /
  5. Getting started

Getting started

Add Affordances to an existing Next.js app, either by handing your coding agent a prompt or by following the steps yourself.

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). There are two ways to set it up.

With an agent

Paste this prompt into Claude Code, Cursor, or any coding agent. It points the agent at these docs as Markdown and tells it what to build:

Agent prompt
Help me make this Next.js app agent-operable with Affordances. Start by reading https://affordances.vercel.app/llms.txt, which lists every page of the Affordances docs. Any page is available as Markdown by adding .md to its URL (for example https://affordances.vercel.app/docs/quickstart.md), and https://affordances.vercel.app/llms-full.txt has the whole site in one file.

1. Follow https://affordances.vercel.app/docs/guides/nextjs/getting-started.md: install @affordances/next, @affordances/react, and zod, then run `vlx @affordances/cli init`. It writes the route handler, wraps next.config.ts with withAffordances, and generates the manifest.
2. Next to each page an agent should operate, add a state.ts (what the page shows) and an actions.ts with defineAction. Set `expose` on every action an agent may call, and use 'confirm' for anything that spends money or can't be undone.
3. Check your work with curl -H 'Accept: application/vnd.affordances+json' on the page's URL, and read https://affordances.vercel.app/docs/guides/nextjs/sessions.md and https://affordances.vercel.app/docs/guides/security.md before exposing anything that needs a session.

Only use APIs that appear in the docs.

When it's done, check its work the same way you would your own: read a graph with curl.

By hand

The steps below set up one page. init does steps 2 to 4 for you; they're spelled out so you know what it changed.

1. Install

Terminal
vlt install @affordances/next @affordances/react zodnpm install @affordances/next @affordances/react zodpnpm add @affordances/next @affordances/react zodyarn add @affordances/next @affordances/react zodbun add @affordances/next @affordances/react zoddeno add npm:@affordances/next npm:@affordances/react npm:zod

2. Wrap the config

withAffordances generates the manifest and adds the rewrites that send agent requests to the route handler. Your pages and their HTML are untouched.

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

const nextConfig: NextConfig = {}

export default withAffordances(nextConfig, { name: 'My app' })

name is what agents see in /.well-known/affordances.json.

3. Add the route handler

Every agent request ends up here. It's one line, in app/ or src/app/:

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

The handler finds the generated manifest on its own: withAffordances points it there.

Route handlers covers its options.

4. Ignore the manifest

withAffordances writes .affordances/manifest.ts every time Next loads the config, and again in next dev when an actions or state file changes. It's generated, so leave it out of git:

.gitignore
.affordances

Or skip steps 2 to 4 and let init do them:

Terminal
vlx @affordances/cli initnpx @affordances/cli initpnpm dlx @affordances/cli inityarn dlx @affordances/cli initbunx @affordances/cli initdeno run -A npm:@affordances/cli init

5. Describe a page

Next to a page, state.ts returns what the page shows, and actions.ts defines what can be done on it:

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

export default defineState(async () => ({ entries: await entries.list() }))
app/guestbook/actions.ts
'use server'

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: (input) => entries.add(input),
})

sign is still a server action, so the page uses it like any other.

6. Mark the controls

Wrap the control a person uses for an action in <Affordance>, so agents that read the DOM, and the devtools, know which button does what:

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

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

<Affordance> renders no box of its own (display: contents) and changes nothing about the button. It adds data-affordances-action="sign" around it. The button carries the action with formAction, so what's marked is exactly what submits.

When the whole form is the control, wrap the form instead. Pass input for values the control already fixes, such as the product a button adds:

TSX
<Affordance action={addToCart} input={{ sku: variant.sku }}>
  <form action={addToCart}>
    <button>Add to cart</button>
  </form>
</Affordance>

In a client component, pass name="sign" instead of action: there, a server action arrives without the metadata <Affordance> reads its name from. The require-affordance lint rule warns about any control that calls a server action outside an <Affordance>, and React and WebMCP adds the provider that turns these actions into browser tools.

7. Check it

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

Terminal
curl -H 'Accept: application/vnd.affordances+json' localhost:3000/guestbook

The response is the page's graph: its entities and the sign action with its input schema. Call the action at the same URL:

Terminal
curl localhost:3000/guestbook \
  -H 'Content-Type: application/vnd.affordances+json' \
  -d '{ "action": "sign", "input": { "name": "Ada", "message": "Hello" } }'

The response is a delta with the new entries.

The CLI does both with a readable summary, including roughly how many tokens the page costs an agent:

Terminal
vlx @affordances/cli open localhost:3000/guestbook
vlx @affordances/cli call localhost:3000/guestbook sign --input '{"name":"Ada","message":"Hello"}'

Next

  • Sessions: let agents act as the signed-in user.
  • Expose: choose who may call each action, and which need a person's approval.
  • MCP: connect Claude or Cursor to the app.

PreviousNext.jsNextFiles and conventions

On this page

  • With an agent
  • By hand
  • 1. Install
  • 2. Wrap the config
  • 3. Add the route handler
  • 4. Ignore the manifest
  • 5. Describe a page
  • 6. Mark the controls
  • 7. Check it
  • 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