Repository navigation
Refusing a Call ServiceError
A capability refuses a call by throwing a Caligrafy\ServiceError. It never prints an error, redirects or picks an
HTTP status itself: each channel turns the same refusal into its own form, a status and a JSON error for REST, a tool
error for an agent, the errors of a form for a page. Since 2.1.0.
Say when the capability must refuse:
/caligrafy-quill:new-capability lend_book loans:write: lend a book to a reader; refuse when the book is already out
Claude throws ServiceError::notFound() for a book that does not exist and ServiceError::conflict() when it is lent.
Check before you say OK: php caligrafer.php call <name> ... with the case that must be refused prints
{"error": {"code": ...}} with the code you expect, and exits with 1.
use Caligrafy\ServiceError;
public function handle(array $input, Context $context)
{
$book = Book::find($input['book_id']);
if (!$book) {
throw ServiceError::notFound('There is no book #' . $input['book_id']);
}
if (Loan::where('book_id', $input['book_id'])->whereNull('returned_at')->exists()) {
throw ServiceError::conflict('This book is already lent');
}
// ...
}The invalid inputs of $rules are refused before handle() runs, with a message per field: you throw invalid only
for what the rules cannot say.
throw ServiceError::invalid(['due_at' => 'The due date must be after today']);| Throw | Code | Status | Default message | When |
|---|---|---|---|---|
ServiceError::invalid(['field' => 'message'], $message) |
invalid |
422 | Some of the inputs are invalid | an input is wrong |
ServiceError::unauthenticated($message) |
unauthenticated |
401 | Sign in or send a personal access token | nobody is signed in |
ServiceError::forbidden($message) |
forbidden |
403 | You are not allowed to do this | this caller may not do it |
ServiceError::notFound($message) |
not_found |
404 | Not found | the thing does not exist |
ServiceError::conflict($message) |
conflict |
409 | (a message is required) | it clashes with the current state |
new ServiceError($kind, $message = '', $fields = []) does the same; any other kind throws InvalidArgumentException.
On the error: $e->kind, $e->fields, $e->getMessage(), $e->status() and $e->toArray(), which is
{"error": {"code": "invalid", "message": "...", "fields": {...}}} (fields only when there are some).
The framework throws the same errors itself: not_found for an unknown capability, unauthenticated and forbidden
("This needs the scope books:write") from the access checks,
invalid from the input rules.
| Channel | What the caller gets |
|---|---|
REST (Route::capability, Route::capabilities) |
the status of the kind and $e->toArray() as JSON; 401 also sends WWW-Authenticate: Bearer
|
| MCP | a tool result with "isError": true and $e->toArray() as its text, so the agent can read the reason and correct itself |
php caligrafer.php call |
$e->toArray() as JSON, exit code 1
|
| The JS client | a CapabilityError with status, code, message and fields
|
Route::form, Route::action (a browser) |
invalid or conflict: back, with errors(), old(), flash('error'); unauthenticated: the sign-in page; forbidden: 403 [ERROR 1006]; not_found or anything else: 404 [ERROR 1005]
|
Route::form, Route::action (a JSON request) |
the status of the kind and the REST error |
Route::page data |
a visitor goes to sign in (with the authentication module), otherwise 403 or 404; with 'notFound' => 'Folder/view' that view, its $message the refusal |
| Snap | an Ask card for invalid (the fields to fix), a notice for the others |
Every refusal is written to the audit log, its outcome
the code and its detail the message. Any other exception is written with the outcome error and the exception's class,
then thrown on: it is a bug, not a refusal.
Reference: ServiceError,
Capabilities::call().
-
Declarative routes: forms:
errors(),old()andflash()in the view - REST API · MCP for AI agents · The JS client
- The context: who calls, to decide what to refuse
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)