Extending
Synapse is built to be extended from the host app: swap models, add flow node types, function helpers and editor blocks, plug in your own AI invoker, and react to events. None of this requires forking the package.
This page is the overview — the cross-cutting model and the seams shared by every extension. The detailed how-tos live in three sub-pages:
In this section
- Flows — register a custom flow node (a step on the canvas): implement
FlowNodeHandler+ProvidesNodeDefinition, register viaPageBuilder::registerNode(). - Functions — register a custom function helper (a callable in the expression sandbox): a
CapabilityDefinition(kindhelper) + a callable, registered viaPageBuilder::registerHelper(); plus the callable-FunctionRegistrypath. - Components — register a custom draggable block: a
SectionBlock, registered viaPageBuilder::registerComponent(). Also the seam for open-core premium component packs.
The capability-registry model
Nodes, helpers and components all follow the same shape: a small registry the package seeds with its built-ins and a host app / third-party package adds to from a service provider's boot(). Register once and the addition surfaces everywhere — the builder UI, the validator, and the machine-readable capability catalogue — with no core change.
- Nodes →
NodeRegistry, viaPageBuilder::registerNode(). - Helpers →
HelperRegistry, viaPageBuilder::registerHelper(). - Components →
ComponentRegistry, viaPageBuilder::registerComponent().
Nodes and helpers describe themselves with one shared value object, CapabilityDefinition — the same class, differing only by kind (KIND_NODE / KIND_HELPER) — which is why a single PageBuilder::capabilities() call returns them together. Components are SectionBlocks and are mapped into that same catalogue as kind: 'component'.
The PageBuilder facade
The package registers a PageBuilder facade (auto-aliased via composer.json → extra.laravel.aliases), fronting PageBuilderManager. It exposes page rendering plus the whole extensibility seam:
use Andre\AiPageBuilder\Facades\PageBuilder;
$html = PageBuilder::render($page); // fully-rendered (cached) HTML for a published Page
PageBuilder::forget($page->slug); // bust the render cache for a slug
PageBuilder::registerNode($handler); // add a custom flow node → Flows
PageBuilder::registerHelper($definition, $fn); // add a custom function helper → Functions
PageBuilder::registerComponent($block); // add a custom draggable block → Components
PageBuilder::components(); // the serialized block list for the GrapesJS block manager
PageBuilder::capabilities(); // the merged node + helper + component catalogue (MCP/AI tool list)Each register* call is documented on its sub-page above. For data, flows and AI, use the dedicated services directly (RecordQuery, FlowManager, BuildPlanApplier, etc.) as shown throughout these docs.
Swapping a model
Every model is resolved through config('ai-page-builder.models.*') (see Configuration), so you can subclass one and point the config at your class to add behavior:
// app/Models/AppPage.php
namespace App\Models;
use Andre\AiPageBuilder\Models\Page as BasePage;
class AppPage extends BasePage
{
protected static function booted(): void
{
static::saved(fn (self $page) => /* your hook */);
}
}// config/ai-page-builder.php
'models' => [
'page' => \App\Models\AppPage::class,
// …
],The services, controllers, the AI applier and the Filament resources all read the class from config, so your subclass is used everywhere. Keep the table columns and route-key contract intact (e.g. Page resolves by slug).
The AiInvoker contract
The flow engine and the AI app builder don't hard-depend on the gateway — they depend on Andre\AiPageBuilder\Flow\Contracts\AiInvoker:
interface AiInvoker
{
public function available(): bool;
/**
* @param array<string,mixed> $args values for the integration prompt placeholders
* @param array<int,array<string,mixed>> $messages conversation turns (role/content)
* @param array<string,mixed> $opts per-call options
*/
public function invoke(string $integration, array $args = [], array $messages = [], array $opts = []): string;
}The default binding is GatewayAiInvoker (routes through the AI OpenRouter Gateway when installed; throws if not). Bind your own to use a different backend — or a fake in tests:
use Andre\AiPageBuilder\Flow\Contracts\AiInvoker;
$this->app->bind(AiInvoker::class, \App\Ai\MyInvoker::class);Your implementation backs both the ai_invoke flow node and the AppBuilderService (which calls invoke($app_builder_slug, ['app_context' => …], $conversation) and expects the model's reply text back).
Events & observers
Collection writes → flows
Every collection record write goes through the dynamic Record model, which the package observes with RecordObserver (registered in packageBooted()). On created/updated/deleted it forwards { event, collection, record } to FlowDispatcher::dispatchCollectionEvent(), which fans out to matching collection-triggered flows. Re-attached each boot (Eloquent ties observers to the event dispatcher, which is fresh per app instance).
To react to collection writes in your own code, observe the same model:
use Andre\AiPageBuilder\Models\Record;
Record::observe(\App\Observers\MyRecordObserver::class);$record->pbModelKey tells you which collection the row belongs to. (Re-register on each boot for the same reason the package does.)
Standard model events
The package's own models (Page, Flow, etc.) are ordinary Eloquent models — hook their lifecycle events as usual (e.g. via a subclass per Swapping a model, or Page::saved(...)). The render cache is already busted on page save/delete by the package.
That's the overview. Continue with Flows, Functions, or Components — or back to the docs index.