Skip to content

Core API

Commands

Define typed commands for AI agents

Commands#

Commands are the core building block of a Surf API. Each command has a name, description, optional parameters, and a handler function.

TypeScript
const surf = await createSurf({
name: 'My Store',
commands: {
search: {
description: 'Search products by keyword',
params: {
query: { type: 'string', required: true, description: 'Search term' },
limit: { type: 'number', default: 10 },
},
returns: { type: 'object' },
tags: ['catalog', 'public'],
hints: {
idempotent: true,
sideEffects: false,
estimatedMs: 50,
},
run: async ({ query, limit }) => {
return db.products.search(query, { limit })
},
},
},
})

๐Ÿ’ก Tip: The hints object helps AI agents make smart decisions โ€” for example, knowing a command is idempotent means it's safe to retry on failure.

Execution Hints#

The hints.execution field tells agents where a command runs. This is critical for window.surf โ€” it determines whether a command executes locally in the browser or goes to the server.

| Value | window.surf | HTTP / CLI | Description | |-------|---------------|------------|-------------| | "any" (default) | โœ… Local handler or server | โœ… Server handler | Works everywhere | | "browser" | โœ… Local handler only | โŒ Returns error | UI-only, modifies live browser state | | "server" | โœ… Proxied to server | โœ… Server handler | Needs auth, DB, business logic |

execution: "browser" โ€” Declaration-only commands

Commands with execution: "browser" don't need a run() handler on the server. They're declaration-only in the server config โ€” listed in the manifest for discovery, but the actual handler lives client-side via useSurfCommands (React) or registerCommand (@surfjs/web).

TypeScript
const surf = await createSurf({
commands: {
'canvas.addCircle': {
description: 'Add circle to canvas',
params: {
x: { type: 'number', required: true },
y: { type: 'number', required: true },
radius: { type: 'number', default: 50 },
fill: { type: 'string', default: '#000' },
},
hints: { execution: 'browser', sideEffects: true },
// No run() โ€” browser-only command
// The handler is registered client-side
},
ย 
'order.create': {
description: 'Create order',
params: {
sku: { type: 'string', required: true },
quantity: { type: 'number', default: 1 },
},
hints: { execution: 'server', sideEffects: true },
run: async (params, ctx) => {
const order = await db.orders.create(ctx.claims!.userId, params)
return order
},
},
ย 
'product.search': {
description: 'Search products',
params: {
query: { type: 'string', required: true },
limit: { type: 'number', default: 10 },
},
hints: { execution: 'any', idempotent: true },
run: async ({ query, limit }) => {
return db.products.search(query, { limit })
},
},
},
})

When a headless agent or CLI tries to call a browser-only command, they get a clear error:

TypeScript
$ surf test myapp.com canvas.addCircle --x 400
โš ๏ธ canvas.addCircle requires browser execution
Open the site and run: await window.surf.execute('canvas.addCircle', { x: 400 })

See Architecture & Execution Models for the full breakdown of execution modes and command strategies.