Repository navigation
Capability Attribute
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 *.
/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).
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']);
}
}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.
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().
-
servicewithout--scope(G76): it writesscope: '', which any token may call. Give a scope, or write it in the attribute right away. -
webOnly: trueon 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().
- Services, capabilities and MCP: the whole chapter
- Your first capability: the short tour
- Capability inputs · Refusing a call · The context
- API reference: services and capabilities
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.
Why Caligrafy: the benefits, the measurements, and what it is not yet
-
Quickstart with Claude: describe it, approve it, ship it
or the Quickstart: the same three steps by hand, in 5 minutes
- How Caligrafy works: the ideas in 5 minutes
- Your first capability: build a feature, page included
-
Capabilities in depth
-
#[Capability]: every option - Scheduled jobs: capabilities on a timer
- Notifications: web push and email
- Background jobs: capabilities run later
- Migrations: database changes, in order
- The cache
-
Testing your app
- Dependency injection
- Validation rules
-
Web pages
- Views and page components
- Design styles: another look, one setting
- Declarative routes: every request, in one line
-
Route::page·Route::form·Route::auth·Route::webhook·Route::download·Route::redirect·Route::action·Route::snap· capability routes - How to export a list as CSV
- Controllers: moving one to a declared route · how to, step by step
- Forms and helpers
-
Vue apps
-
Mobile and tablet
-
REST API
-
AI agents
-
Snap Cards and the assistant
- Payments: Stripe and crypto
- Face detection and face login
- OpenAI: chat and images · Assistants
- Open models: Llama, Mistral...
- Chatbots with Watson
- Machine learning in the browser
- Metadata and social cards · search and analytics
-
Putting it online: the production image, the database at every start
-
API reference: every class and method
-
Routing ·
Route·AuthRoutes· pages ·Controller· capabilities -
Data ·
Model·Query· sign-in · jobs ·Notify· payments · AI -
Interfaces · Snap ·
Snap\Cards·Snap\Conversation· testing · tooling · helper functions
-
Routing ·
-
Caligrafer: every command
- Project · build · data · agents · jobs and tests · maintenance · deploy
- Installation, configuration and Caligrafer
- Routing · Request · Controllers · Views
- Vue: flow of information · routes · validations
- Security and upgrading
- Release notes: 2.26 · 2.25 · 2.24 · 2.23 · 2.22 · 2.21 · 2.20 · 2.19 · 2.18 · 2.17 · 2.16 · 2.15 · 2.14 · 2.13 · 2.12 · 2.11 · 2.10 · 2.9 · 2.8 · 2.7 · 2.6 · 2.5 · 2.4 · 2.3 · 2.1 · October 2026 · changelog
- Testing Caligrafy (contributors)