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

Prompt Deck Laravel Package

veeqtoh/prompt-deck

Manage AI prompts as versioned files in Laravel and PHP. Organise agent instructions, compare prompt performance, and activate the right version with variable interpolation, tracking, A/B testing, and AI SDK integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish (5 minutes)

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

Note the vendor string: the package was renamed at v0.4.0 from veeqtoh/prompt-deck / Veeqtoh\PromptDeck to promptphp/deck / PromptPHP\Deck. Any tutorial, blog post, or Laravel News snippet older than that uses the dead namespace — don't copy use Veeqtoh\PromptDeck\Facades\Deck; into a fresh project.

Where to look first, in this order

  1. config/deck.php after publishing — this is the only real knob surface (prompts path, tracking toggle, agent auto-scaffolding toggle). Read it before writing code.
  2. resources/prompts/ — the directory is the data model. Understand metadata.json (prompt-level, holds active_version) vs v1/metadata.json (version-level) before you touch anything.
  3. php artisan prompt:list --all — cheapest way to confirm the loader sees what you think it sees.
  4. https://deck.promptphp.com/ for anything past the README; the repo also ships Mintlify MDX docs under docs/.

First real use case — pull a hard-coded system prompt out of a controller and make it editable without a deploy:

php artisan make:prompt order-summary

Edit resources/prompts/order-summary/v1/system.md:

You are a {{ $tone }} customer service agent.
Summarise the following order for the customer: {{ $order }}.

Then:

use PromptPHP\Deck\Facades\Deck;

$prompt   = Deck::get('order-summary');            // active version
$messages = $prompt->toMessages(['tone' => 'friendly', 'order' => $orderDetails]);
// [['role' => 'system', 'content' => '...']]

toMessages() is the point of the package for most teams: it hands you an array you can drop straight into any chat-completion call, or into the Laravel AI SDK.

Confirm your starting state before shipping anything. Tracking defaults to off as of 0.4.6. If you want performance data you must set DECK_TRACKING_ENABLED=true and publish/run the tracking migrations — they are a separate opt-in step, not part of the default publish. The old behaviour (tracking on whenever APP_DEBUG=false) was exactly the bug that made production installs throw no such table: prompt_versions on the first Deck::get().

Implementation Patterns

Draft → verify → promote (the load-bearing workflow). Creating a version never changes what the app serves. That asymmetry is the whole design, and you should lean on it:

php artisan make:prompt order-summary        # choose "version" → writes v2/
php artisan prompt:diff order-summary 1 2    # review the text delta   (check `artisan list prompt` for exact flags)
php artisan prompt:test order-summary 2      # render with sample vars
php artisan prompt:activate order-summary v2 # or just: 2

Roll back by activating v1. Keep this as a documented runbook; it is the main reason to adopt the package over a config/prompts.php array.

Don't scatter prompt names through the codebase. The facade is convenient, which makes it easy to end up with Deck::get('order-summary') in nine places and no idea which call sites depend on which roles. Wrap it:

final class OrderSummaryPrompt
{
    public function messages(array $vars): array
    {
        return Deck::get('order-summary')->toMessages($vars);
    }
}

Now prompt renames, version pins, and variable-shape changes are one-file edits, and you have a natural seam for tests.

Pin a version explicitly for anything that isn't a live experiment. Deck::get('order-summary', 'v2') bypasses active_version entirely. Use it for deterministic jobs (batch backfills, eval harnesses, cron) where an operator flipping the active version in production should not silently change a queued job's behaviour. Reserve the unversioned call for the paths you actually want to A/B.

A/B testing is a composition of two features, not an API. There is no Deck::variant() helper — you select a version yourself (cohort, feature flag, tenant) and pass it to get(), then record the outcome with Deck::track(). Keep the selection in one place so the cohorts stay balanced:

$version = $user->inExperiment('order-summary-v2') ? 'v2' : 'v1';
$prompt  = Deck::get('order-summary', $version);
// ... AI call ...
Deck::track(/* prompt name, version, latency, tokens, outcome */);

Tracking is what makes the comparison meaningful; without it you have two prompts and opinions. Call track() after the AI call returns — it is deliberately non-throwing because the money is already spent by then.

Laravel AI SDK users get the most leverage. Add HasPromptTemplate to an agent and stop writing instructions():

class OrderAgent extends Agent
{
    use HasPromptTemplate; // instructions() and promptMessages() provided
}

make:agent also scaffolds a matching prompt directory by default (toggle it in config/deck.php if you don't want every agent creating one). If an agent needs conditional instructions, override instructions() and delegate to Deck::get(...) rather than abandoning the trait.

Treat prompt files as reviewed source. They live in resources/prompts/, so they get code review, blame, and CI for free. Add a cheap CI gate: run php artisan prompt:list --all and fail the build if it errors or reports a prompt you didn't expect. That catches malformed names and stray directories that would otherwise fail at runtime in production.

Testing. There's no documented Deck::fake(). The robust approach is to point the configured prompts path at a tests/fixtures/prompts tree and assert on rendered strings, so your tests don't break every time someone edits the production prompt wording. Assert the shape ($messages[0]['role'] === 'system') in integration tests and exact content only in a dedicated prompt test.

Gotchas and Tips

Database wins over metadata.json once you've activated with tracking on. This is the single most surprising behaviour change in 0.4.6: after a tracked activation, editing active_version in the file and deploying no longer changes what is served. Activation is environment state; the file is only the bootstrap default. Consequences: staging and production have independent active versions; a fresh environment boots from the file; and "I changed the JSON, why is it still serving v1?" is a support ticket you will write for yourself. Check the DB (or prompt:list) before debugging anything else.

Upgrade 0.4.6-era installations carefully. Before that release, Deck::activate() only issued an UPDATE against prompt_versions, a table nothing ever inserted into — so runtime version switching appeared to work but recorded nothing. prompt_versions.user_prompt also became nullable, so existing tracking installs must re-publish and re-run migrations. If you're on 0.4.x < 0.4.6, treat activation as untested in your environment.

Tracking failures are silent by design. Post-0.4.6, DB interactions degrade to metadata.json with a single logged warning rather than throwing. So a broken tracking table looks like "it works, but the dashboard is empty." If your A/B data is empty, check storage/logs/laravel.log for one warning, not for an exception. And note caching gives no protection: the active version is resolved before the cache is consulted, so a bad DB hits you on the very first Deck::get() of every request path.

Prompt names are validated — keep them plain kebab-case. Names containing .. or a directory separator throw InvalidPromptNameException (this closed a path-traversal hole). make:prompt Support/Reply used to scaffold an unresolvable nested support/reply; both the generator and the loader now share one pattern. Use make:prompt support-reply. The pattern is anchored with \z, so a trailing newline in a name is also rejected.

Version directories must be exactly v<N>. The matcher is now anchored to the directory name; previously /v(\d+)$/ treated rev2, dev3, and archive-v9 as versions 2, 3, and 9 — prompt:list --all advertised them and loading then failed. If you're on an old version, don't use names like that; if you're organising prompt archives, keep notes out of the version-numbered directories.

Interpolation is not Blade. {{ $var }} is a simple substitution pass, not a template engine — no @if, no {{ $a ?? 'x' }}, no escaping semantics you can rely on. Two practical consequences: (1) if your prompt needs to show a literal {{ $variable }} as an example for the model, it will be interpolated away — test that case explicitly rather than assuming; (2) never interpolate untrusted user text into a system-role prompt without thinking about prompt injection. The package versions and renders prompts; it does not sandbox them.

Pin the version in composer.json. This is 0.x with a recent namespace-breaking rename, zero dependents, and a fast changelog (four releases in the 0.4 line, several of them bug fixes for things that would have bitten you). "promptphp/deck": "^0.4.6" and read UPGRADE.md before any minor bump. The upside of that churn is a maintainer who fixes real production bugs quickly; the downside is that a careless composer update can move your API surface.

Extension points are thin — plan accordingly. You get config/deck.php, the publishable migrations, the AI SDK trait, and the filesystem layout itself. There's no driver interface or custom storage adapter, so if you need prompts in a database, from a remote service, or in a non-resources/ location, you're building an abstraction above Deck, not inside it. Decide that before you write fifty call sites, not after.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource