Skip to content

Capability Attribute

Dory Azar edited this page Nov 15, 2026 · 2 revisions

A capability is a class of application/services/ that extends Caligrafy\Service and carries the attribute #[Capability(...)]. The attribute says what the capability is called, what it does, who may call it and how the channels treat it. This page lists every option; each one has a page of its own. Since 2.1.0 (webOnly 2.21.0, async 2.23.0).

Since 2.26.0: cache: <seconds> (with readOnly: true and public: true: a visitor's answer may be kept by shared caches, see performance); and an empty scope on a write capability means a token needs the scope named after the capability, or *.


With Claude

/caligrafy-quill:new-capability delete_book books:write destructive: only administrators (level 1)

Claude runs php caligrafer.php service <Folder>/<Name> --scope <scope> and writes the attribute: a description written for an AI agent, the scope, and readOnly, destructive, level or public from what you asked.

Check before you say OK: php caligrafer.php capabilities lists it with the scope (or public) and the flag you expect (read, writes, destroys).


By hand

php caligrafer.php service Bookshelf/DeleteBook --scope books:write writes the class with its name in snake_case (DeleteBook becomes delete_book). Complete the attribute:

<?php

use Caligrafy\Capability;
use Caligrafy\Context;
use Caligrafy\Service;
use Caligrafy\ServiceError;

#[Capability(
    name: 'delete_book',
    description: 'Deletes a book of the catalog for good, with its loans and its tags. Returns the id of the deleted book.',
    scope: 'books:write',
    level: 1,
    destructive: true
)]
class DeleteBook extends Service
{
    public $rules = array('id' => 'required|integer');

    public $descriptions = array('id' => 'The id of the book to delete');

    public function handle(array $input, Context $context)
    {
        if (Book::where('id', $input['id'])->delete() === 0) {
            throw ServiceError::notFound('There is no book #' . $input['id']);
        }
        return array('deleted' => (int) $input['id']);
    }
}

Every option

The arguments of the attribute's constructor, in their order (framework/core/Capability.php):

Option Type, default What it does Learn more
name string, required snake_case: a letter, then letters, digits and _, 64 characters at most; unique in the application. How REST, MCP, the JS client, the command line and the routes name it. Any other name throws InvalidArgumentException Calling a capability from code
description string, '' one or two sentences: what it does and what it returns. Agents read it as the tool description MCP for AI agents
scope string, '' the token scope it needs, resource:action (books:write). '': any token Scopes and access
level int, 0 the minimum value of the users' permissions column. 0: anyone signed in Scopes and access
readOnly bool, false it changes nothing: not audited when it succeeds, can be page data and a download, readOnlyHint for agents Read-only and destructive
destructive bool, false it deletes or overwrites data: destructiveHint, agents ask the user first Read-only and destructive
public bool, false anyone may call it, without signing in or a token Scopes and access
mcp bool, true false keeps it out of the MCP tools (one only a browser can use, such as a push subscription). REST still reaches it MCP for AI agents
webOnly bool, false true (2.21.0): only Route::action() reaches it; it may read the session and sign people in Web-only capabilities
async bool, false true (2.23.0): a slow capability; Snap runs it in the background as a Track card: Task. Other channels call it as before Async capabilities

The class itself declares the inputs ($rules, $filters, $descriptions) and handle(): Capability inputs. Its constructor gets what it uses from the container.

php caligrafer.php service <Folder>/<Name> [--scope <scope>] [--read-only] writes scope and readOnly. There is no flag for destructive, level, public, mcp, webOnly or async: write them in the attribute.


How the attribute is found

Caligrafy\Capabilities::all() builds the registry once per request: it loads the files of application/services/ that mention #[Capability] (with the class map, 2.18.0) and the framework's own capabilities of framework/services/, then keeps every class that extends Service, is not abstract and carries the attribute. Two classes that declare the same name stop the registry with a LogicException naming both. A class outside application/services/ (a package, a test) is added with Capabilities::register(MyService::class).

Reference: Capability, Capabilities::all(), Capabilities::register().


Common mistakes

  • service without --scope (G76): it writes scope: '', which any token may call. Give a scope, or write it in the attribute right away.
  • webOnly: true on a feature agents or programs need (G143): they get "There is no capability". Keep the work in a plain capability and let the web-only one call it with $this->capability().

Related pages

Caligrafy: Describe it. Approve it. Ship it. Your idea turned into a complete app: every feature built once, for web pages, Vue apps, a REST API and AI agents, and online.

Start here

Why Caligrafy: the benefits, the measurements, and what it is not yet

  1. Quickstart with Claude: describe it, approve it, ship it

    or the Quickstart: the same three steps by hand, in 5 minutes

  2. How Caligrafy works: the ideas in 5 minutes
  3. Your first capability: build a feature, page included

Capabilities

Experiences

Data and people

  1. Models
  2. Relationships
  3. Queries: filters, paging, related rows
  4. Sign-in, users and tokens

Built-in features

Put it online

Build with AI

Reference

Clone this wiki locally