Laravel finally gave Artisan commands a real output story, and the interesting question is not "what are the new functions." It is "which contexts do they belong in." I ship custom artisan commands in my own production app, some run by humans and some by the scheduler every five minutes, and those two audiences need opposite things from terminal output. The new Laravel Prompts primitives are superb for the first audience and a trap for the second. This post covers both halves.
The release itself: Laravel Prompts v0.3.15 landed in March 2026 with five new primitives, task(), stream(), notify(), autocomplete(), and title(), extracted from the internal CLI the Laravel team built for Laravel Cloud. That origin matters. These are not speculative APIs; they were shaped by engineers watching real deployments scroll by, then generalized for the rest of us. If you are on a current Laravel release you likely already have them; otherwise:
composer require laravel/prompts

task(): long operations finally get a real UI
task() is the centerpiece. One function call gives you an animated spinner, a scrolling log area, a label you can update as phases change, and status messages that persist instead of scrolling away.
use function Laravel\Prompts\task;
task(
title: 'Deploying application',
callback: function ($task) {
$task->label('Pulling latest code');
$task->log('Running git pull origin main...');
// ... work ...
$task->label('Compiling assets');
$task->log('Running npm build...');
// ... work ...
$task->succeed('Deployment complete');
}
);
While the callback runs, the spinner animates next to the label, and every $task->log() line flows into a scrolling area beneath it. Old lines push up, recent output stays visible, and the spinner never stops, so the user always knows the process is alive. That last part is the entire psychological difference between "this is working" and "should I Ctrl+C?"
The two-level information design is the clever bit. $task->label() changes the headline ("Pulling code" → "Compiling assets" → "Running migrations") without disturbing the log stream underneath. Phase at a glance, detail on demand.
Status messages that refuse to scroll away
task(
title: 'Running migrations',
callback: function ($task) {
$task->log('42 migrations pending');
$task->warning('3 tables will be dropped');
// ... the wall of SQL output scrolls below ...
$task->succeed('Migrations applied');
}
);
$task->warning() and $task->error() render as pinned, highlighted lines above the scrolling log. They do not disappear under verbose output. I have hacked around exactly this for years with shouty ==== banners; having "3 tables will be dropped" stay on screen while SQL scrolls past is the feature working as intended.
The pcntl caveat
The animated spinner needs the pcntl PHP extension, present on macOS and virtually every Linux box, absent on native Windows. Without it you get a static fallback: same logs, same statuses, no animation. Check with:
php -m | grep pcntl
Design your output to still be useful un-animated and this never bites you.
notify(): the OS taps you on the shoulder
use function Laravel\Prompts\notify;
notify(
title: 'Database Seeded',
body: sprintf('%d records in %ds.', $count, $elapsed)
);
A native desktop notification from a PHP process: Notification Center on macOS, the notification daemon on Linux, bridged notifications under WSL. The use case is any command long enough that you switch windows: big seeds, imports, batch content jobs. Add one line at the end and stop doing the "did that finish yet" terminal check-in dance.
stream(): AI output rendered like it should be
stream() prints text progressively with a fade-in, which sounds cosmetic until you connect it to its real purpose: rendering streamed LLM responses inside artisan commands.
use function Laravel\Prompts\stream;
$stream = stream('AI Analysis');
foreach ($llmResponse as $chunk) {
$stream->append($chunk->text);
}
$stream->close();
If you are building AI-assisted tooling in Laravel, and after a year of the ecosystem sprinting in that direction, many of us are, this is the missing render primitive. I have been experimenting with the Laravel AI SDK in a real app, and terminal output was the ugliest part of that loop; stream() plus a streaming API response gives you the typewriter effect in your own CLI. Do not forget close(), which restores the cursor and finalizes the output.
autocomplete() vs suggest(): pick by user intent
autocomplete() shows inline ghost text you accept with Tab, the pattern you know from IDEs and browser address bars:
use function Laravel\Prompts\autocomplete;
$env = autocomplete(
label: 'Deploy to which environment?',
options: fn (string $value) => collect(['production', 'staging', 'preview'])
->filter(fn ($o) => str_contains($o, strtolower($value)))
->values()
->all()
);
The existing suggest() shows a dropdown of matches instead. The decision rule: suggest() when the user is browsing and needs to see options; autocomplete() when the user already knows the answer and wants fewer keystrokes. For commands experienced developers run daily, environment pickers, branch selectors, autocomplete is noticeably faster.
title(): the terminal tab tells you what it is doing
use function Laravel\Prompts\title;
title('Deploy: production');
// ... work ...
title('Deploy: complete');
It sets the terminal window or tab title. Trivial to implement, disproportionately useful if you live with six terminal tabs: the tab bar becomes a status board. Pair it with notify() and a long-running command respects your attention at both ends: the tab shows state at a glance, the notification pulls you back when it matters.
The lesson from my own commands: know your audience
Here is the part I have not seen anyone else write down, and it comes directly from operating my own tooling.
My site's generate:sitemap command renders three sitemap files and prints one plain line per file: Wrote sitemap.xml (48213 bytes). It runs two ways: occasionally by me, and every five minutes by the scheduler, plus once per deploy from the CI pipeline. When I first read the v0.3.15 release notes my instinct was to dress that command up with task(). I am glad I stopped, because it would have made the command worse.
Scheduled and CI contexts have no human watching. There is no TTY, so there is nobody for a spinner to reassure, and what you want in the log file is exactly what the command produces now: one greppable line per artifact with a byte count, because grep sitemap.xml scheduler.log | tail is how I verified cron was alive during a production incident. Spinners, fades, and scrolling regions are for humans present at the keyboard; logs are for the human who arrives three days later with a question.
So the real decision framework has a step zero that comes before choosing between primitives: who is watching this run?
- A human, interactively: use the primitives generously.
task()for anything multi-phase over a few seconds,spin()for a single short wait,progress()when iterating a known count,notify()if they might tab away. - The scheduler, a queue worker, or CI: plain, line-oriented, greppable output. One line per meaningful event, counts and byte sizes included. Your future self debugging an outage is the user.
- Both, same command: branch on it.
$this->output->isDecorated()or a simple--plainflag lets a deploy command be beautiful when run by hand and boring when run by GitHub Actions.
Prompts is polite about non-interactive environments, but "does not break in cron" is a lower bar than "is what you want in cron." Design for the reader of the log, not just the watcher of the terminal.
A rebuilt command, before and after
The before state of most deployment commands, mine included at one point: bursts of $this->info() between 30-second silences. The after state, using everything above:
use function Laravel\Prompts\task;
use function Laravel\Prompts\notify;
use function Laravel\Prompts\title;
public function handle(): int
{
title('Deploy: production');
task(
title: 'Deploying to production',
callback: function ($task) {
$task->label('Pulling latest code');
exec('git pull origin main 2>&1', $out, $code);
foreach ($out as $line) {
$task->log($line);
}
if ($code !== 0) {
$task->error('Git pull failed — aborting');
return;
}
$task->label('Installing dependencies');
exec('composer install --no-dev 2>&1', $composerOut);
$task->label('Running migrations');
Artisan::call('migrate', ['--force' => true]);
$task->log('Migrations applied.');
$task->label('Restarting queue workers');
Artisan::call('queue:restart');
$task->succeed('Deployment complete');
}
);
title('Deploy: complete');
notify(title: 'Deploy finished', body: 'Production is live.');
return self::SUCCESS;
}
Note the queue:restart phase: if your app runs supervised queue workers, a deploy that skips it leaves old code running in memory, which is its own quiet disaster; the full worker setup is in my Supervisor on Amazon Linux 2023 guide.
Why this release matters beyond the functions
Prompts launched in 2023 focused on input: select(), confirm(), text(), polished ways to ask. The output side stayed primitive, info() lines and a progress bar, which is why long PHP commands felt a decade behind Node or Rust CLI tooling. v0.3.15 completes the pair. Input and output are both first-class now, and a PHP CLI can genuinely feel like a modern tool instead of a script that happens to print.
If your artisan commands are part of an AI-driven workflow, agents running them, humans reviewing, this polish compounds; well-structured command output is also easier for a coding agent to parse when it runs your tooling, something I lean on constantly in the terminal stack I keep around Claude Code. And if the agent is the one writing the commands, Laravel Boost's MCP server gives it the application context to do it correctly.
Pick your slowest, most-avoided artisan command and give it thirty minutes: title() at the top, task() around the work, notify() at the end, and plain output preserved for any scheduled path. The command will do exactly what it did before and feel like a different tool.
I build this kind of developer tooling, custom artisan commands, deployment pipelines, CLI UX included, for teams that want their internal tools to feel as finished as their product. If yours could use that treatment, reach out and tell me what your slowest command does; it is usually a smaller project than people expect.