Every long-lived Laravel application accumulates operational scripts: re-sending a failed invoice, onboarding a tenant, backfilling a column. Too often they live in tinker sessions or in a wiki page nobody trusts. Artisan commands built with Laravel Prompts make these tasks safe, discoverable and repeatable. This tutorial walks through building one properly.
Why Prompts?
Laravel Prompts ships with the framework's Laravel\Prompts namespace. It gives command-line tools browser-like form UX: placeholders, inline validation, arrow-key selects, and spinners. Beyond looks, it reduces operator mistakes, which matters when a command touches production data.
1. Scaffold the command
php artisan make:command OnboardTenant
Define the signature with optional arguments so the command works both interactively and in automation:
protected $signature = 'tenant:onboard {name?} {--plan=} {--force}';
protected $description = 'Create a tenant and its first admin user';
2. Ask for input, only when missing
A good command prompts only for what was not passed on the command line:
use function Laravel\Prompts\text;
use function Laravel\Prompts\select;
use function Laravel\Prompts\confirm;
public function handle(): int
{
$name = $this->argument('name') ?? text(
label: 'Tenant name',
placeholder: 'Acme Inc.',
required: true,
validate: fn (string $v) => Tenant::where('name', $v)->exists()
? 'A tenant with that name already exists.'
: null,
);
$plan = $this->option('plan') ?? select(
label: 'Plan',
options: ['starter' => 'Starter', 'pro' => 'Pro', 'enterprise' => 'Enterprise'],
default: 'starter',
);
if (! $this->option('force') && ! confirm("Create {$name} on the {$plan} plan?")) {
$this->components->warn('Cancelled.');
return self::FAILURE;
}
// ...
}
The validate closure returns an error string or null. You can also pass Laravel validation rules, such as validate: ['email' => 'required|email'] style rules via the validate parameter on supported prompts.
3. Show progress for slow work
Use spin for a single step and progress for collections:
use function Laravel\Prompts\spin;
use function Laravel\Prompts\progress;
$tenant = spin(fn () => $provisioner->create($name, $plan), 'Provisioning tenant...');
progress(
label: 'Seeding default data',
steps: $seeders,
callback: fn ($seeder) => $seeder->run($tenant),
);
4. Multiselect and search for larger lists
multiselect lets operators choose several options; search and multisearch query as the user types, which suits thousands of records:
$userId = search(
label: 'Which user should be the owner?',
options: fn (string $value) => strlen($value) > 0
? User::where('email', 'like', "%{$value}%")->limit(10)->pluck('email', 'id')->all()
: [],
);
5. Make it non-interactive friendly
Commands also run in CI and from schedulers, where nobody can answer a prompt. Two habits help:
- Accept every value as an argument or option, and prompt only as a fallback.
- Use
--no-interaction; Prompts then falls back to defaults or fails fast on required values, instead of hanging. Always provide--forcestyle flags for destructive confirmations.
If a command can prompt for something critical, consider implementing PromptsForMissingInput on the command so Laravel asks for missing arguments automatically.
6. Test the command
Prompts can be faked in tests, so no terminal is needed:
use Laravel\Prompts\Prompt;
it('onboards a tenant interactively', function () {
$this->artisan('tenant:onboard')
->expectsQuestion('Tenant name', 'Acme Inc.')
->expectsChoice('Plan', 'pro', ['starter', 'pro', 'enterprise'])
->expectsConfirmation('Create Acme Inc. on the pro plan?', 'yes')
->assertSuccessful();
expect(Tenant::where('name', 'Acme Inc.')->exists())->toBeTrue();
});
it('runs without prompts', function () {
$this->artisan('tenant:onboard', ['name' => 'Beta', '--plan' => 'starter', '--force' => true])
->assertSuccessful();
});
Artisan's expectsQuestion, expectsChoice and expectsConfirmation work with Prompts. Test both the interactive path and the flag-only path.
Operational tips
- Keep business logic in an action or service class; the command only gathers input and reports output. That keeps it reusable from queues and controllers.
- Wrap multi-step writes in a database transaction.
- Log who ran destructive commands, with a correlation ID if you use structured logging.
- Use
Isolatableon commands that must never run twice concurrently.
Wrapping up
Prompts raise the quality bar for internal tooling at little cost. If your team's Laravel application relies on fragile scripts, or you need experienced Laravel engineers to tidy up operational tooling, get in touch and we can help.