Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Deck Laravel Package

promptphp/deck

Deck (formerly Prompt Deck) brings versioned AI prompt management to Laravel and PHP: store prompts as files, interpolate variables, track performance, run A/B tests, and activate versions across your app with Laravel AI SDK integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require promptphp/deck
php artisan vendor:publish --provider="PromptPHP\Deck\Providers\DeckServiceProvider"
php artisan migrate

Note that the changelog calls out tracking tables as an opt-in publish step separate from the initial config publish — after the provider publish, check what actually landed in database/migrations and re-publish by tag if the tracking migrations are missing. You don't need them unless you plan to use tracking.

Look at two things first: config/deck.php (prompts path, tracking toggle, AI SDK auto-scaffolding toggle) and resources/prompts/, which is where every prompt lives from here on.

Create your first prompt and wire it to one call site. Pick a single hard-coded system string already in your app — not your most important prompt — and move it to a file:

php artisan make:prompt order-summary
resources/prompts/order-summary/
├── metadata.json          # name, description, roles, active_version
└── v1/
    ├── metadata.json
    └── system.md

Write the prompt with {{ $variable }} placeholders in v1/system.md, then:

use PromptPHP\Deck\Facades\Deck;

$messages = Deck::get('order-summary')->toMessages([
    'tone' => 'friendly',
    'order' => $orderDetails,
]);

toMessages() returns a plain [['role' => 'system', 'content' => '...']] array, so it drops into any chat-completion call — the package is not coupled to a specific provider unless you opt into the Laravel AI SDK integration.

That's the whole first-use-case loop. The payoff isn't the facade call; it's that the next prompt change is a reviewed markdown diff instead of a PHP deploy.

Implementation Patterns

Wrap prompt access in one class per prompt. The package's API is small and string-keyed, so the failure mode is Deck::get('order-summary') scattered across fifteen files with fifteen slightly different variable bags. Keep a thin wrapper:

final class OrderSummaryPrompt
{
    public static function messages(Order $order, string $tone = 'friendly'): array
    {
        return Deck::get('order-summary')->toMessages([
            'tone' => $tone,
            'order' => $order->toPromptString(),
        ]);
    }
}

Now the prompt name and the variable contract live in one place, and swapping the backing implementation later is a one-file change.

Keep interpolation at the edge. Store only the template in the file; assemble variables in PHP at call time. Don't build prompt fragments by concatenating rendered strings — nesting prompts makes versioning and diffing meaningless.

Roles are files, not arguments. make:prompt order-summary --user --role=assistant scaffolds system.md, user.md, assistant.md. Render each with the same variable bag, since system() and toMessages() both need the full set. If a variable is only used in one role, that's still fine — but keep a single array shape so callers don't drift.

Draft in production. Creating a version never changes what's served — make:prompt on an existing prompt offers version (creates v2) or overwrite. So the day-to-day loop is:

php artisan make:prompt order-summary        # → v2, not yet live
php artisan prompt:list --all                # confirm v1 is still active
php artisan prompt:diff order-summary v1 v2  # review before promoting
php artisan prompt:test order-summary v2     # render it, catch interpolation errors
php artisan prompt:activate order-summary v2 # promote (accepts 2, '2', or 'v2')

Rollback is the same command with v1. Programmatically, Deck::activate('order-summary', 'v2') and Deck::get('order-summary', 'v2') take the same mixed version formats — pin >= 0.4.2, because the 1 vs v1 handling was fixed across 0.4.1 and 0.4.2.

A/B testing is explicit-version, not mid-request activation. activate() flips a single active version, which is global/environment state — it isn't a per-user split. To actually test, branch in code and pass the version explicitly:

$version = $user->id % 2 === 0 ? 'v1' : 'v2';

$prompt = Deck::get('order-summary', $version);
// ... run the AI call ...
Deck::track(/* prompt/version + outcome */); // after the call completes

Then attribute results by version. Deck::track() is documented as never throwing, which matters here: it runs after a completed, paid-for AI call, so a tracking failure must not blow up a request whose cost you've already incurred.

CI: treat prompts like code and rendering like a build step. Prompts live in git, so add prompt:test (and optionally prompt:list --all) to CI to catch templates that don't render or directories that don't resolve. Do not put prompt:activate in a deploy script if tracking is enabled — see the gotcha below.

Laravel AI SDK integration is one trait. On an agent, use HasPromptTemplate and delete instructions(); the trait supplies instructions() and promptMessages() from the matching prompt directory. make:agent will auto-scaffold the prompt dir (toggleable in config), which is the smoothest path — the agent and its prompt are created together and stay named consistently.

In tests, point the prompts path at fixtures. Override the configured prompts directory to a tests/fixtures/prompts tree so tests assert on known templates, and use Deck::get($name, 'v1') explicitly rather than relying on whatever active_version happens to be at the time.

Gotchas and Tips

Tracking now defaults to off. As of 0.4.6 the default flipped, because the old default (on whenever APP_DEBUG=false) meant the natural production install threw no such table: prompt_versions on the first Deck::get(). If you want tracking, set DECK_TRACKING_ENABLED=true and publish and run the tracking migrations. If you published config/deck.php before upgrading, your file wins and you're unaffected — but check it rather than assuming.

Once a version has been activated with tracking enabled, the database beats metadata.json. This is the biggest day-to-day trap: editing active_version in the file and deploying no longer changes what's served. Activation becomes environment state; the file is only the bootstrap default. Decide which environment "owns" activation and document it, or you'll spend an afternoon debugging a deploy that "didn't take." The 0.4.6 fix to Deck::activate() (it previously issued an UPDATE that matched no rows, so runtime switching silently did nothing) is what makes this real — before that, runtime activation was a no-op.

Prompt names are validated, and you should let them be. Names containing .. or a directory separator now throw InvalidPromptNameException, because they were previously interpolated straight into filesystem paths. Never build a prompt name from user input, and use kebab-case: make:prompt Support/Reply used to scaffold a nested support/reply that the loader refuses to resolve and prompt:list shows as a broken entry.

Version directories must literally be v1, v2. The matcher was fixed in 0.4.6 to anchor against the directory name, so rev2, dev3, and archive-v9 are no longer mistaken for versions. Corollary: v2-draft or v2-final won't resolve either. Use make:prompt to create versions instead of hand-creating directories.

Interpolation escaping is worth a five-minute check. The {{ $variable }} syntax is Blade-flavoured, which in Laravel means HTML-escaping by default — pass a value containing ', &, or < and verify what actually reaches the model. This matters most for user-supplied content interpolated into prompts. Also test what happens on a missing variable before you rely on it; a silently empty placeholder produces a subtly wrong prompt rather than an error.

Caching won't save you from a broken active-version lookup. The active version is resolved before the cache is consulted, so a missing table or bad row fails on every request regardless of cache warmth. This is precisely why 0.4.6 degrades every DB interaction to metadata.json with a single logged warning instead of throwing — if you see that warning in laravel.log, your prompts are falling back to file state and activation isn't being honoured.

Watch what lands in the tracking tables. prompt_versions stores prompt content and has a nullable user_prompt column (nullable so activation can be recorded without content). If you interpolate customer data into prompts and enable tracking, you're persisting that data in your database. Decide deliberately.

Debugging order that saves time: a prompt that appears in prompt:list --all but fails to load is a name/directory mismatch; a prompt that loads but renders wrong is interpolation; a prompt that renders right but behaves wrong in the AI SDK is the agent/trait wiring, not the prompt. prompt:test isolates the first two from the third.

This is 0.x, single-maintainer, with zero dependents and one namespace-breaking rename already behind it (veeqtoh/prompt-deck → promptphp/deck, Veeqtoh\PromptDeck → PromptPHP\Deck at 0.4.0). Pin the version in composer.json, read UPGRADE.md before minor bumps, and clear cached config/bootstrap after upgrading so stale class references don't linger. The per-prompt wrapper class from the patterns section is also your escape hatch: if the package stalls, you rewrite one class and keep the prompt files, which are just markdown.

Extension points worth knowing: the configurable prompts path (point it at a shared or package-provided directory rather than always resources/prompts), arbitrary roles via --role=, and per-version metadata.json for descriptions — which is what makes prompt:list and prompt:diff legible months later. Fill in the description when you create a prompt; future-you reviewing a v3 diff will thank you.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums