Repository navigation
Capability Inputs
A capability declares its inputs with three arrays of its class: $rules (what is accepted), $filters (how it is
cleaned) and $descriptions (what each input is). The same three arrays validate every call, whatever the channel,
and become the JSON Schema that AI agents, the JS client and GET /capabilities see. Since 2.1.0.
/caligrafy-quill:new-capability create_book books:write: add a book (title up to 200 characters, published year from 1450 to this year, optional summary, an existing author)
Claude writes a rule for every input, a trim filter for the text ones and a description for each, written for an
agent ("The id of its author (list_authors)").
Check before you say OK:
- every input
handle()reads is in$rules(with''when it has no rule); -
php caligrafer.php capabilities --jsonshows the schema you expect; -
php caligrafer.php call <name>with a wrong value answers a message for that field.
#[Capability(name: 'create_book', description: 'Adds a book to the Bookshelf catalog and returns it with its id and its author.', scope: 'books:write')]
class CreateBook extends Service
{
public $rules = array(
'title' => 'required|max_len,200',
'published_year' => 'required|integer|min_numeric,1450|max_numeric,current_year',
'summary' => 'max_len,2000',
'author_id' => 'required|integer|exists,authors.id'
);
public $filters = array(
'title' => 'trim',
'summary' => 'trim'
);
public $descriptions = array(
'title' => 'The title of the book',
'published_year' => 'The year it was first published, for example 1969 (not after this year)',
'summary' => 'Optional: one or two sentences about the book',
'author_id' => 'The id of its author (list_authors)'
);
public function handle(array $input, Context $context)
{
$id = Book::insert($input);
return Book::with('author')->find($id);
}
}php caligrafer.php call create_book title=Dune answers a message per field:
{
"error": {
"code": "invalid",
"message": "Some of the inputs are invalid",
"fields": {
"published_year": "The Published Year field is required",
"author_id": "The Author Id field is required"
}
}
}REST answers the same body with 422, MCP as a tool result with "isError": true, a form goes back with
errors('published_year').
Service::validate() runs before handle(), on every channel:
-
Keep only the inputs named in
$rules. Any other key the caller sends is dropped: an input without a rule is listed with''. -
Files (the
filerule, 2.19.0): web pages and REST save the uploaded file and give the capability its public URL; MCP and the command line send a URL or a path, whose extension is checked.required_file(orrequired|file) with nothing sent is refused: "The Cover field needs a file". -
Numbers (2.21.8): an input whose rule has
integer,numeric,float,min_numericormax_numericnever takes a list ortrue/false: "The Year field must be a number". -
Filter, then validate with the validation rules
(GUMP).
$filtersapply only to declared inputs. - Anything wrong throws
ServiceError::invalid([field => message]). Otherwisehandle()gets the clean inputs.
$rules with unique and exists read the database (exists,authors.id); current_year in min_numeric and
max_numeric is the year of today.
Capabilities::schema('create_book') (and schemaFromRules($rules, $descriptions)) gives an object schema with
"additionalProperties": false:
| Rule | Schema |
|---|---|
required |
listed in required
|
integer / numeric, float / boolean
|
"type": "integer" / "number" / "boolean" (otherwise "string") |
valid_email, valid_url, uuid, date
|
"format": "email", "uri", "uuid", "date"
|
max_len,N, min_len,N, exact_len,N
|
maxLength, minLength (both for exact_len) |
min_numeric,N, max_numeric,N
|
minimum, maximum (current_year is this year) |
contains_list,a;b |
"enum": ["a", "b"] |
file, required_file
|
"description": "The URL or the path of a file" (required_file also in required) |
$descriptions[input] |
"description", over the one above |
A string keeps only its format and lengths, a number only its minimum and maximum. A rule argument that is not a
number (max_len with nothing after it) is left out of the schema. The create_book above becomes:
{"type": "object", "properties": {
"title": {"type": "string", "maxLength": 200, "description": "The title of the book"},
"published_year": {"type": "integer", "minimum": 1450, "maximum": 2026, "description": "The year it was first published, for example 1969 (not after this year)"},
"summary": {"type": "string", "maxLength": 2000, "description": "Optional: one or two sentences about the book"},
"author_id": {"type": "integer", "description": "The id of its author (list_authors)"}},
"additionalProperties": false, "required": ["title", "published_year", "author_id"]}Reference: Service::validate(),
Capabilities::schema(),
Capabilities::schemaFromRules(),
Uploads.
-
An input with no rule (G52): it never reaches
handle(). List it in$rules, with''when it has no rule. -
Forgetting
php caligrafer.php clientafter changing the rules: the Vue app's functions keep the old input types.
- Validation rules: every rule and filter
- Declarative routes: file inputs
- The capability attribute · Refusing a call
- MCP for AI agents: where agents read the schema
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)