Plugin getting started
Build and enable a minimal local Signals plugin in about ten minutes — folder layout, composer.json, signals.yaml, PluginBase, and CLI lifecycle.
Prerequisites
- A working Signals install on this branch (Composer deps, migrations, Vite not required for CLI checks)
- PHP 8.5 CLI available as
php - Permission to create directories under
plugins/
You do not need to publish a Composer package. Local discovery scans plugins/{name}/ and vendor-nested plugins/{vendor}/{name}/ (see Plugin folder structure).
1. Create the package directory
mkdir -p plugins/acme/hello/{src,database/migrations}
2. composer.json
{
"name": "acme/hello",
"description": "Minimal Signals plugin quickstart",
"type": "library",
"autoload": {
"psr-4": {
"Acme\\Hello\\": "src/"
}
},
"extra": {
"signals": {
"plugin": "Acme\\Hello\\HelloPlugin"
}
}
}
Discovery requires:
- Composer
namematchingsignals.yamlpackage - Non-empty
extra.signals.pluginFQCN that extendsApp\Sdk\PluginBase - Package-root
signals.yaml
Local packages are loaded on demand by PluginClassLoader from the package’s autoload.psr-4 map — you do not need to require the package from the application composer.json.
3. Minimal signals.yaml
package: acme/hello
name: Acme Hello
version: 0.1.0
signals_version: "^1.0"
permissions:
- acme.hello.view
hooks: []
slots:
- slot: dashboard.widgets
component: signals.stat-card
priority: 90
permission: acme.hello.view
settings:
- key: greeting
type: string
label: Greeting
default: Hello from Acme
rules:
- nullable
- string
- max:255
network: []
Permissions must use the Composer vendor prefix (acme.). Full rules: Plugin manifest.
When registering permissions in PHP, you may pass optional capabilities tags (money, operations, crm, admin, reporting, destructive) so the role builder wizard can place them under the right high-level toggles. Tag destructive grants explicitly.
Check your manifest as you type with the Manifest validator & builder — it runs the same ManifestValidator the install flow uses, and can generate this whole file from a form.
4. Plugin entry class
plugins/acme/hello/src/HelloPlugin.php:
<?php
namespace Acme\Hello;
use App\Sdk\PluginBase;
use App\Sdk\Signals;
use App\Sdk\Slots\SlotComponentType;
use App\Services\Plugins\PluginRegistrar;
class HelloPlugin extends PluginBase
{
public function register(PluginRegistrar $registrar): void
{
$registrar->permission('acme.hello.view', [
'label' => 'View Acme Hello',
'description' => 'See the Acme Hello dashboard widget.',
'group' => 'Integrations',
'capabilities' => ['admin'],
]);
$package = $this->package;
$registrar->slot(
'dashboard.widgets',
SlotComponentType::View,
'signals.stat-card',
static function (array $context) use ($package): array {
return [
'label' => 'Acme Hello',
'value' => (string) Signals::setting($package, 'greeting', 'Hello'),
'color' => 'blue',
'hint' => 'Quickstart plugin',
];
},
90,
'acme.hello.view',
);
}
}
Optional lifecycle hooks on PluginBase: install(), enable(), disable(), uninstall(), onUpdate(), boot().
5. Validate, install, enable
php artisan signals:plugin list
php artisan signals:plugin check acme/hello
php artisan signals:plugin install acme/hello
php artisan signals:plugin enable acme/hello
CLI actions (from PluginCommand): list, install, remove, enable, disable, check.
check runs manifest validation. install creates the plugins row, runs guarded migrations (none in this minimal package), seeds permissions and settings defaults, then calls PluginBase::install(). enable sets status enabled and calls enable().
Enable takes effect on the next application boot. PluginLifecycleManager::enable() does not hot-register hooks or slots. Restart PHP-FPM / Octane workers (or the next Herd request cycle after a process recycle) before expecting the dashboard widget.
6. Verify in admin + dashboard
- Open Admin → Settings → Plugins (
/admin/settings/plugins, routeadmin.settings.plugins). - Confirm
acme/hellois listed as enabled; expand for overview / permissions / slots / settings. - Open the dashboard. With a user that can
acme.hello.view, thedashboard.widgetsslot should render ansignals.stat-cardlabelled Acme Hello.
Edit the greeting under the plugin’s generated settings form, save, reload the dashboard after the next boot if you also toggled enable state.
7. Iterate
Typical loop:
- Edit
signals.yamland/orHelloPlugin::register(). php artisan signals:plugin check acme/hello- Reload the app process (required after enable/disable; usually enough after code edits in local PHP-FPM).
- Re-test the slot / settings.
When you add tables, declare them under tables: as plugin_* names and put migrations in database/migrations/ — the lifecycle guard rolls back any batch that creates a non-plugin_ table. See Plugin folder structure.
Next reading
| Guide | Why |
|---|---|
| Folder structure | Canonical tree and Xero layout |
| Manifest reference | Every YAML section and validation message — including nav, pages[], notifications[], events[] |
| UI slots & navigation | Slot types, live slot names, modals, nav groups + pages, palette, recents, notifications |
| Plugin events | Provided events, Signals::emit(), self-subscription |
| LLM / agent guide | Copy-paste agent instructions |
| Examples | signals/slots-demo kitchen-sink reference, signals/xero-sync, acme-example fixture |