Installation
This page covers installing Ghostwriter in a Laravel app with a Filament panel, registering it on the panel, and running the queue it needs.
Requirements
- PHP 8.3 or later
- Laravel 12 or 13
- Filament 5 (with Livewire 4)
- An API key for Anthropic (Claude), OpenAI (ChatGPT) or Google (Gemini). See API keys.
- A queue worker, for model calls (see The queue)
- Optional: an OpenAI or Gemini key, to make images
- Optional: the GD PHP extension, for image placeholders. Without it, image fields a draft leaves empty are left empty.
- Optional: the Imagick PHP extension, to send the model smaller copies of large images. Without it, large images are skipped.
Install the plugin
composer require 1994/ghostwriter-filament
php artisan ghostwriter:install
Composer installs 1994/ghostwriter-core (~0.2.0) from Packagist with it. You don't need to add a repository.
Ghostwriter talks to Anthropic, OpenAI and Gemini itself, through 1994/ghostwriter-core. It doesn't use the Laravel AI SDK (laravel/ai). If you installed that only for Ghostwriter, you can remove it. Ghostwriter still reads a key from config/ai.php if its own variable is empty (see API keys), so move the key to .env before you delete that file.
ghostwriter:install publishes config/ghostwriter.php and Ghostwriter's migrations, then asks to run the migrations. Pass --no-migrate to run them yourself later, or --force to overwrite files you published before.
Ghostwriter's stylesheet is a Filament asset. If your deploy doesn't already do it, publish assets after installing or updating:
php artisan filament:assets
Register the plugin on a panel
In the panel's provider, add the plugin with the resources it writes for:
use NineteenNinetyFour\Ghostwriter\Filament\GhostwriterPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(
GhostwriterPlugin::make()
->resources([
PostResource::class => fn ($query) => $query->where('status', 'published'),
PageResource::class,
CaseStudyResource::class,
])
->canManage(fn (User $user): bool => $user->is_admin)
);
}
Resources. Ghostwriter only reads and writes for the resources you list. A closure narrows a resource to the records that count as published. Ghostwriter learns from those records. Without a closure every record counts, so add one when a resource holds drafts.
Who manages Ghostwriter. ->canManage() says who may change the settings, hide Get started and delete a piece someone else started. Filament has no roles of its own, so without it everyone who can use Ghostwriter manages it, except on panels with tenancy, where nobody does. See Permissions.
The other plugin options (navigation, the image button, the widget) are listed in Configuration.
Add Ghostwriter to your resource pages
Add the WritesWithGhostwriter trait to each resource's Create and Edit pages:
use NineteenNinetyFour\Ghostwriter\Filament\Concerns\WritesWithGhostwriter;
class CreatePost extends CreateRecord
{
use WritesWithGhostwriter;
}
class EditPost extends EditRecord
{
use WritesWithGhostwriter;
}
The trait does four things:
- It adds Write with Ghostwriter to the Create page and Edit with Ghostwriter to the Edit page.
- It puts drafts and images into the form.
- It reopens the panel after a reload.
- On the Create page, it links the new record to the piece whose draft went into it. Editing the record later carries on that conversation. This happens in
afterCreateWritesWithGhostwriter(), which Filament calls after your ownafterCreate(), so a page can still have its ownafterCreate().
If a page already defines getHeaderActions(), add Ghostwriter's action to your own list:
use NineteenNinetyFour\Ghostwriter\Filament\Actions\GhostwriterAction;
protected function getHeaderActions(): array
{
return [
GhostwriterAction::make(),
DeleteAction::make(),
];
}
To start from the list page too, add the ListsWithGhostwriter trait. It puts Write with Ghostwriter beside New. Or add GhostwriterAction::make() to the list page's own header actions. Either one opens the Create page with the panel open.
use NineteenNinetyFour\Ghostwriter\Filament\Concerns\ListsWithGhostwriter;
class ListPosts extends ListRecords
{
use ListsWithGhostwriter;
}
Add your API key
Add one writing key to .env, such as ANTHROPIC_API_KEY=sk-ant-.... API keys covers every service, including the free options.
Who can use it
Anyone who can use the panel can use Ghostwriter, unless you define the useGhostwriter gate. See Permissions.
The queue
Every model call runs as a queued job, and the page checks back for the result. A call takes from a few seconds to a few minutes.
Send Ghostwriter's jobs to a queue of their own with GHOSTWRITER_QUEUE, and run a worker for it:
GHOSTWRITER_QUEUE=ghostwriter
php artisan queue:work --queue=ghostwriter,default --timeout=960
Why 960 seconds. A call that finds the provider busy is tried again, up to 3 attempts in all (see Busy providers and retries). So each job is allowed its time limit × 3 + 60 seconds. With the default time limit of 300 seconds, that's 960. If you raise the time limit, raise --timeout with it.
Keep the queue connection's retry_after above that too: in config/queue.php, or DB_QUEUE_RETRY_AFTER for the database driver. Otherwise a slow job can be started a second time.
If nothing has picked a job up after 30 seconds, the page says "Still waiting for a queue worker to pick this up. Is php artisan queue:work … running?", with the queue it is waiting on.
With the sync driver, jobs run after the response has been sent, so the page doesn't wait. This is fine for local work, but a long draft holds a PHP process until it's done.
Navigation
Ghostwriter adds one Ghostwriter item to the panel's navigation, with the Ghostwriter logo. Its pages are tabs across the top:
- Get started (until it's done or hidden)
- Overview
- Content plan
- Voice guide
- Image style
- Settings (only for people who manage Ghostwriter)
On a narrow screen the tabs become a menu.

Change this on the plugin:
use Filament\Pages\Enums\SubNavigationPosition;
GhostwriterPlugin::make()
->navigationGroup('Content') // put the Ghostwriter item in one of your groups
->navigationSort(30) // and place it among the items there
->subNavigationPosition(SubNavigationPosition::Start) // list the pages down the side instead of across the top
To list each page in the navigation as its own item, use ->navigation(cluster: false). The pages then go in a group called "Ghostwriter", unless you name another with navigationGroup() (or pass null for no group). The pages' addresses (/admin/ghostwriter/...) are the same either way.
The logo is a Blade icon set you can use too: ghostwriter-mark (outline) and ghostwriter-mark-solid.
Updating
composer update 1994/ghostwriter-filament
php artisan migrate
php artisan filament:assets
Read the changelog before updating. 1.0.0 is the first release, so it has no upgrade steps.
Uninstalling
-
Remove the plugin from your panel providers.
-
Remove the traits and
GhostwriterActionfrom your pages. -
Roll back or drop the
ghostwriter_tables. -
Run:
composer remove 1994/ghostwriter-filament