Command Line Console Plugins in Joomla
Most people meet Joomla through its web interface: the front end visitors see, and the administrator area you log in to. But there is a second, quieter door into the very same site, one that needs no browser at all: the command line.
This article explains how Joomla's CLI (Command Line Interface) and its console plugins really work. It starts with the basics for site owners and administrators who just want to run a command, then moves on to the technical details for developers who want to build their own commands and ship them as a plugin.
One PHP entry point that boots the whole CMS without a single HTTP request.
The goal is simple: help you understand the Joomla CLI well enough to use it safely, automate your maintenance, and extend it with your own commands.
1. The Basics
1.1 What is the Joomla CLI?
The CLI is a way to run Joomla from a terminal instead of a browser. You type a command, Joomla runs it, prints the result as text, and exits. There is no page, no template, and no visitor.
The single entry point is one file in your site:
cli/joomla.php
You run it with PHP on the server, like this:
php cli/joomla.php list
That command boots the entire Joomla application in the background, then prints a list of every command it knows. The web site and the CLI share the same code, the same database, and the same configuration. The only difference is how you reach them.
1.2 Why a Command Line at All?
Some jobs are simply easier, faster, or safer without a browser:
- Automation. A scheduled job (cron) can run a command every night with no human present.
- Maintenance. Clear the cache, run database fixes, or take the site offline in one line.
- Bulk work. Add many users, install extensions, or reindex search without clicking through screens.
- Recovery. When the back end is locked or broken, the CLI may still let you reset a password or bring the site back up.
- Speed. A command skips the template, the modules, and the rendering, so it starts and finishes quickly.
Think of the CLI as a service entrance to your Joomla site: the same building, the same rooms, but a door meant for staff and machines rather than the public.
1.3 The Shape of a Command
Every Joomla command follows the same readable pattern. The name uses colons to group related actions:
php cli/joomla.php extension:install --path=/tmp/mod_hello.zip
| |
| └─ options (start with --)
└─ the command name (group:action)
- The command name tells Joomla what to do, for example
cache:cleanoruser:add. - Arguments are values the command expects in a fixed position.
- Options start with
--and can appear in any order, for example--path=....
This naming is not decoration. The part before the colon is a namespace, so all the extension:* commands belong together and can be listed as a group.
2. Running Your First Commands
2.1 list and help
Two built-in commands tell you everything else. Start here on any site:
php cli/joomla.php list # show every available command
php cli/joomla.php list extension # show only the extension:* commands
php cli/joomla.php help user:add # show how one command works
The help command prints the description, the arguments, and the options for a single command. When you are unsure what a command needs, ask it with help before you run it.
2.2 The Global Options
A set of options works on every command, because the application defines them once for all:
| Option | Short | What it does |
|---|---|---|
--help |
-h |
Show help for the command |
--quiet |
-q |
Silence all output |
--verbose |
-v, -vv, -vvv |
Show more detail (more letters, more detail) |
--version |
-V |
Print the application version |
--no-interaction |
-n |
Never ask questions; use defaults or fail |
--ansi / --no-ansi |
- | Force or disable coloured output |
The --no-interaction option matters for automation. A command that normally asks "Are you sure?" will hang forever in a cron job unless you tell it not to ask.
2.3 Reading the Result: the Exit Code
A command prints text for humans, but it also returns a number for machines. This exit code is how a script knows whether the command worked:
| Exit code | Meaning |
|---|---|
0 |
Success |
1 |
Failure (the command ran but the job did not succeed) |
2 |
Invalid (wrong arguments or options) |
In a shell you can check it with echo $? right after the command. Cron and deployment tools use this number to decide whether to continue or stop.
3. A Tour of the Core Commands
Joomla 6 ships with a useful set of commands out of the box. You do not install anything; they are part of core. Here are the families you will use most.
3.1 Cache, Configuration and Site State
| Command | What it does |
|---|---|
cache:clean |
Clear the system cache (add expired to remove only stale entries) |
config:get / config:set |
Read or change a value in configuration.php |
site:down / site:up |
Put the site into, or out of, offline mode |
maintenance:database |
Run database fixes and structure checks |
3.2 Users
| Command | What it does |
|---|---|
user:add |
Create a user (asks for the details, or pass them as options) |
user:list |
List the existing users |
user:reset-password |
Set a new password for a user |
user:addtogroup / user:removefromgroup |
Manage group membership |
user:delete |
Delete a user |
The command user:reset-password alone is worth knowing: it is the standard way back in when you are locked out of the administrator login.
3.3 Extensions and Updates
| Command | What it does |
|---|---|
extension:install --path=... |
Install an extension from a zip or folder |
extension:list |
List installed extensions |
extension:enable / extension:disable |
Turn an extension on or off |
extension:remove |
Uninstall an extension |
extension:discover / extension:discover:install |
Find and install files dropped on the server by hand |
update:extensions:check |
Check for available extension updates |
core:update |
Update Joomla itself |
3.4 Scheduler, Search and Sessions
| Command | What it does |
|---|---|
scheduler:run |
Run due scheduled tasks (the cron entry point) |
scheduler:list / scheduler:state |
List tasks and enable or disable them |
finder:index |
Rebuild the Smart Search index |
session:gc / session:metadata:gc |
Clean up old sessions |
You do not have to memorise this list. Run php cli/joomla.php list on your own site and the current, exact set is printed for you.
4. Anatomy of the CLI Application
For developers, it helps to see how little stands between the terminal and the running CMS. The CLI is a small, clear stack.
4.1 The Entry Point
Everything starts in cli/joomla.php. That file does a few careful checks, then boots Joomla and runs the application:
Request from the terminal
│
v
Check PHP version (Joomla 6 needs PHP 8.3+)
│
v
Load defines + framework (includes/framework.php)
│
v
Boot the DI container (Factory::getContainer())
│
v
Get the Console Application and call execute()
The relevant lines are short and worth reading once:
$container = \Joomla\CMS\Factory::getContainer();
$app = $container->get(\Joomla\Console\Application::class);
\Joomla\CMS\Factory::$application = $app;
$app->execute();
From here on, Joomla is fully booted. Your command has the database, the user system, the language files, and the event dispatcher, exactly like a web request, minus the HTTP layer and the template.
4.2 Built on Symfony Console
Joomla does not reinvent the wheel here. The CLI is built on the well-known Symfony Console library, wrapped by the Joomla Console framework package. That is why the help screens, the option parsing, and the coloured output feel familiar if you have used other modern PHP tools.
The class you reach in cli/joomla.php is the framework's \Joomla\Console\Application. The CMS extends it with \Joomla\CMS\Application\ConsoleApplication, which adds the Joomla-specific wiring: the database, the language, and the plugin system.
4.3 The CLI Session
A browser request has a cookie-based session. The command line has none, so Joomla swaps in a dedicated CLI session backend. In cli/joomla.php you can see the application alias the normal session service to session.cli. This is why CLI code can still call user and session APIs without a browser ever being involved.
5. How Commands Are Registered
A command is a small class that Joomla knows how to find and run. There are two ways a command reaches the application, and understanding both explains how the whole system extends cleanly.
5.1 Default Commands
The CMS application lists its own built-in commands in a method called getDefaultCommands(). Each one is a class that gets created when the application starts:
protected function getDefaultCommands(): array
{
return array_merge(parent::getDefaultCommands(), [
new Console\CleanCacheCommand(),
new Console\AddUserCommand($this->getDatabase()),
// ... and more
]);
}
The framework's own list and help commands come from parent::getDefaultCommands(). The CMS adds the Joomla-specific ones on top.
5.2 Lazy Commands Through the DI Container
Creating dozens of command objects on every single CLI call would be wasteful, because you only ever run one of them. So most core commands are registered lazily: Joomla keeps a simple map of command name to class name, and only builds the object when you actually call that command.
// Simplified: a name -> class map handed to a command loader
$mapping = [
'cache:clean' => CleanCacheCommand::class,
'extension:install' => ExtensionInstallCommand::class,
'scheduler:run' => TasksRunCommand::class,
// ...
];
$app->setCommandLoader(new WritableContainerLoader($container, $mapping));
The command loader asks the DI container for the class only when its name is requested. This keeps the CLI fast: nothing heavy is built until it is needed.
5.3 The Two Paths in One Picture
Console Application
├─ getDefaultCommands() → created eagerly (list, help, a few core)
├─ command loader (DI map) → created lazily, only when called
└─ console plugins → add commands at runtime (see next section)
The third path, console plugins, is how your extension joins the list without touching any core file.
Back to top6. Console Plugins: Extending the CLI
This is the heart of the article for developers. A console plugin is a normal Joomla plugin whose job is to add one or more commands to the CLI. It belongs to the plugin group console.
6.1 The console Plugin Group
When the CLI application runs, it imports a few plugin groups so they can listen to events. One of them is console:
// Inside ConsoleApplication::execute()
PluginHelper::importPlugin('behaviour', null, true, $this->getDispatcher());
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
PluginHelper::importPlugin('console', null, true, $this->getDispatcher());
So just like content plugins react to articles and system plugins react to the request lifecycle, console plugins react to the CLI starting up. (Note that system plugins also load on the CLI, so a system plugin can register a command too. A dedicated console plugin is simply the clearest home for command code.)
6.2 The Event: Add Commands Before Execute
The application fires an event right before it runs the requested command: ApplicationEvents::BEFORE_EXECUTE. A console plugin subscribes to that event and uses it to add its commands to the application:
use Joomla\Application\ApplicationEvents;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
final class MyCli extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands'];
}
public function registerCommands(): void
{
$this->getApplication()->addCommand(new HelloCommand());
}
}
That is the whole trick. The plugin does not run the command itself; it simply hands the command object to the application. Joomla then lists it in list, shows it in help, and runs it when its name is typed.
6.3 Why a Plugin and Not Just a Script?
You could write a stand-alone PHP script, but a console plugin gives you everything the CMS already offers:
- A consistent name in
php cli/joomla.php list, next to the core commands. - Built-in help, argument parsing, and coloured output.
- Full access to the booted CMS: database, users, components, language.
- A clean install and uninstall through the normal extension installer.
The shipped scheduler:run command is a good example of this design in core: the command lives in the CMS, and Joomla's scheduled-task system runs through it, which is why a single cron line can trigger every scheduled job on the site.
7. Building Your Own Command
A command is a single class that extends AbstractCommand. It needs a name, an optional configuration of arguments and options, and the code to run.
7.1 The Minimum Command
use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
final class HelloCommand extends AbstractCommand
{
// The command name, as typed in the terminal:
protected static $defaultName = 'hello:world';
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Hello from the Joomla CLI');
$io->success('It works.');
return Command::SUCCESS; // exit code 0
}
}
Run it with php cli/joomla.php hello:world once the plugin that adds it is enabled.
7.2 Configuring Arguments and Options
Override configure() to declare what your command accepts and to write its help text:
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
protected function configure(): void
{
$this->setDescription('Greet someone from the command line');
$this->addArgument('name', InputArgument::OPTIONAL, 'Who to greet', 'world');
$this->addOption('shout', null, InputOption::VALUE_NONE, 'Use capital letters');
$this->setHelp('This command prints a friendly greeting.');
}
| Idea | Argument | Option |
|---|---|---|
| Written as | hello:world Peter |
hello:world --shout |
| Position | Fixed order | Any order |
| Good for | The main subject of the command | Flags and named settings |
7.3 Reading Input and Returning a Result
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
if ($input->getOption('shout')) {
$name = strtoupper($name);
}
$io->writeln('Hello, ' . $name . '!');
return Command::SUCCESS;
}
Always return one of the named exit codes - Command::SUCCESS, Command::FAILURE, or Command::INVALID - so scripts and cron jobs can react to the outcome.
7.4 Talking to the CMS
Because the CMS is fully booted, your command can use the same modern APIs as a controller or model. To work with a component, boot it and ask its factory for a model:
$model = $this->getApplication()
->bootComponent('com_content')
->getMVCFactory()
->createModel('Articles', 'Administrator', ['ignore_request' => true]);
$count = \count($model->getItems());
$io->info($count . ' articles found.');
This is exactly how core commands such as cache:clean work: they boot a component and call its model, then report the result to the terminal.
7.5 Injecting Services Through the Constructor
Reaching for global state inside a command works, but the cleaner way is to let the DI container hand you the services you need. Give your command a constructor and ask for them by type:
use Joomla\Database\DatabaseInterface;
final class CountUsersCommand extends AbstractCommand
{
protected static $defaultName = 'users:count';
public function __construct(private DatabaseInterface $db)
{
parent::__construct(); // always call the parent constructor
}
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$query = $this->db->getQuery(true)
->select('COUNT(*)')
->from($this->db->quoteName('#__users'));
$total = (int) $this->db->setQuery($query)->loadResult();
(new SymfonyStyle($input, $output))->info($total . ' users.');
return Command::SUCCESS;
}
}
Core commands do this too: user:add receives the database through its constructor rather than fetching it itself. Your console plugin then creates the command with its dependency when it registers it - $this->getApplication()->addCommand(new CountUsersCommand($db)). The result is code that is loosely coupled and easy to test, because you can pass a fake database in a unit test.
8. Packaging a Console Plugin
A console plugin uses the same modern, namespaced layout as any Joomla 6 plugin. Only two things mark it as a console plugin: the group in the manifest, and the event it subscribes to.
8.1 The Folder Layout
plugins/console/mycli/
├─ mycli.xml ─ manifest (group="console")
├─ services/
│ └─ provider.php ─ DI registration (entry point)
├─ src/
│ ├─ Extension/
│ │ └─ MyCli.php ─ the plugin class (subscribes to BEFORE_EXECUTE)
│ └─ Command/
│ └─ HelloCommand.php ─ your command class
└─ language/ ─ translation .ini files
8.2 The Manifest
The one line that makes this a console plugin is group="console":
<extension type="plugin" group="console" method="upgrade">
<name>plg_console_mycli</name>
<version>1.0.0</version>
<namespace path="src">Joomla\Plugin\Console\MyCli</namespace>
<files>
<folder plugin="mycli">services</folder>
<folder>src</folder>
</files>
</extension>
8.3 The Service Provider
The entry point is services/provider.php, the same pattern every modern plugin uses. It registers the plugin class with the DI container:
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->set(PluginInterface::class, function (Container $container) {
$plugin = new MyCli(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('console', 'mycli')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
});
}
};
Zip the folder, install it through System → Install → Extensions, enable it in the Plugins list, and your command appears in php cli/joomla.php list. No core file is touched.
8.4 Where the Plugin Is Recorded
A console plugin is an ordinary plugin, so it lives as one row in the #__extensions table, exactly like a content or system plugin. The CLI reads that table on start-up to decide which plugins to load:
| Column | For a console plugin |
|---|---|
type |
plugin |
folder |
console (the group) |
element |
mycli (the plugin name) |
enabled |
1 = loaded, 0 = ignored |
If a command is missing from the list, this is the first place to check. You can see every console plugin and its state with one query:
SELECT element, enabled
FROM #__extensions
WHERE type = 'plugin' AND folder = 'console';
A row with enabled = 0 never loads, so its commands never reach the CLI. That is the database-level reason behind the most common "my command does not appear" problem.
9. Input, Output, and Interaction
A good command is pleasant to use by hand and reliable inside a script. Joomla's CLI gives you the tools for both.
9.1 SymfonyStyle: Clean Output
The SymfonyStyle helper wraps the raw output in tidy, consistent formatting. Core commands use it everywhere:
$io = new SymfonyStyle($input, $output);
$io->title('Cleaning System Cache'); // a heading
$io->success('Cache cleaned'); // green success block
$io->error('Cache not cleaned'); // red error block
$io->warning('Nothing to do'); // yellow warning
$io->table(['Id', 'Name'], $rows); // a formatted table
9.2 Asking the User Questions
When run by a person, a command can prompt for missing details. The user:add command does exactly this when you do not pass every option:
$username = $io->ask('Username');
$password = $io->askHidden('Password'); // input is not shown on screen
$confirm = $io->confirm('Create this user?', true);
This is friendly for humans, but remember automation. In a cron job there is nobody to answer, so either pass every value as an option or add --no-interaction and make sure your command can run with its defaults.
9.3 Verbosity Levels
Let the user choose how much detail they want by checking the verbosity, instead of printing everything always:
if ($output->isVerbose()) { // shown with -v
$io->writeln('Extra detail for the curious.');
}
Normal output stays clean, and -v, -vv, or -vvv reveal progressively more for debugging.
9.4 Logging What a Command Does
Output on screen disappears the moment a cron job finishes. For anything that runs unattended, write a permanent record with Joomla's logging framework instead of, or alongside, the screen output:
use Joomla\CMS\Log\Log;
Log::add('Nightly import started', Log::INFO, 'cli');
// ... do the work ...
Log::add('Nightly import finished: ' . $count . ' items', Log::INFO, 'cli');
The third argument is a category. Giving your command its own category (here cli) lets you send those entries to their own log file and read them later under System → Maintenance → Manage Logs, separate from the rest of the site. By default the files live in the configured log path, usually administrator/logs/.
Log the start, the end, and any error. When a scheduled job fails at three in the morning, the log is the only witness you will have.
9.5 Debugging a Command That Will Not Run
When a command misbehaves or refuses to appear, work through this short checklist in order:
- Is the plugin enabled? Check the Plugins list, or run the
#__extensionsquery from section 8.4. - Does it appear at all? Run
php cli/joomla.php list. If it is missing, the plugin is not loading or not callingaddCommand(). - Is the namespace correct? The manifest
<namespace>and the folders undersrc/must match exactly, or autoloading fails silently. - Run it verbosely. Add
-vvvto see the full stack trace instead of a short message:php cli/joomla.php mycli:hello -vvv. - Read its help.
php cli/joomla.php help mycli:helloconfirms the name, arguments, and options Joomla actually registered.
Most "nothing happens" reports come down to one of these five: disabled plugin, missing addCommand(), or a namespace typo that breaks autoloading.
10. The CLI and Scheduled Tasks (Cron)
The most common reason site owners meet the CLI is automation. Joomla's Scheduled Tasks feature is designed to be triggered from the command line.
10.1 One Command, Every Task
The scheduler:run command runs any scheduled task that is due. A single cron entry on your server is enough to power them all:
# Run Joomla's due scheduled tasks every 15 minutes
*/15 * * * * /usr/bin/php /path/to/site/cli/joomla.php scheduler:run --no-interaction
Each "task" is itself a plugin (in the task group), so the design is consistent: the scheduler component lists the jobs, and the CLI provides the reliable, browser-free trigger to run them.
10.2 Why the CLI Is Better Than the Web Trigger
Joomla can also trigger scheduled tasks during ordinary page views ("lazy" scheduling), but the CLI trigger is more dependable:
| Concern | Web (lazy) trigger | CLI cron trigger |
|---|---|---|
| Needs a visitor | Yes - no traffic, no run | No - the server clock fires it |
| Timing | Approximate, depends on traffic | Exact, on the schedule |
| Time limit | Bound by the web request timeout | Long-running jobs are fine |
| Slows a visitor's page | Possibly | Never |
For any serious site, a real cron job calling scheduler:run is the recommended setup.
10.3 The Tables Behind the Scheduler
When scheduler:run works, it reads and writes two database tables. Knowing them helps when you need to see why a task did not fire:
| Table | What it holds |
|---|---|
#__scheduler_tasks |
The task definitions: type, parameters, state, next run time, and the last exit code |
#__scheduler_logs |
The execution history: when each task ran, how long it took, and whether it succeeded |
A quick look at the task table tells you what is scheduled and when each item is due next:
SELECT title, state, next_execution, last_exit_code
FROM #__scheduler_tasks
ORDER BY next_execution;
If a task never seems to run, check that its state is enabled (1) and that next_execution is in the past. The command only runs tasks that are both enabled and due.
11. CLI versus Web versus Web Services
Joomla can be reached in three ways. They share the same code and data, but they suit different jobs.
| Aspect | CLI | Web (browser) | Web Services API |
|---|---|---|---|
| Entry point | cli/joomla.php |
index.php |
api/index.php |
| Caller | You or a cron job | A visitor in a browser | An external app or script |
| Output | Plain text | A full HTML page | JSON |
| Auth | Server access (the shell user) | Login session + cookies | API token in a header |
| Best for | Maintenance, automation, bulk jobs | People browsing and editing | Integrations, mobile apps |
A REST call to the Web Services API carries its token in a header, like this:
curl -H "X-Joomla-Token: <token>" \
https://example.test/api/index.php/v1/content/articles
Rule of thumb: use the CLI for jobs you run on the server, the web for people, and the Web Services API for other programs talking to your site.
Back to top12. Security and Server Setup
The CLI is powerful precisely because it runs with full rights and skips the login screen. That power is safe only if the command line itself is protected.
12.1 Run as the Right User
Run commands as the same operating-system user that owns the web files (often something like www-data or your hosting account), not as root. Running as root can create files the web server cannot manage, which causes "permission denied" errors later.
12.2 Match the PHP Version
The PHP that runs your command can differ from the PHP your web server uses. Joomla 6 needs PHP 8.3 or newer, and cli/joomla.php checks this on start. If a command behaves oddly, confirm the version with php -v and use the same major version as the website.
12.3 Keep the CLI Off the Public Web
The cli/ folder is meant for the shell, not for browsers. There is no reason a visitor should ever load it over HTTP. The standard Joomla hosting layout keeps these scripts out of harm's way, and the small cli/index.html file prevents directory listing. Never expose a command runner as a public URL.
12.4 Treat Input Carefully
Even on the command line, validate what you receive. Use the typed getters on arguments and options, check the user's rights when a command acts on their behalf, and never build a database query by pasting raw input into a string.
Back to top13. CLI and SEO: Keep It Invisible to Search
The CLI has no place in search results, and getting that right is mostly about not doing the wrong thing.
- CLI commands produce no web pages, no URLs, and no metadata. There is nothing for a search engine to index, which is exactly right.
- Do not turn a command into a public web endpoint just to trigger it from a browser. That would expose powerful actions to anyone who finds the URL.
- Keep the
cli/directory out of the public document root where your host's layout allows it, so the scripts are never reachable over HTTP. - Use the CLI to improve SEO indirectly:
finder:indexkeeps Smart Search fresh, and scheduled tasks run viascheduler:runcan rebuild sitemaps or clean up redirects on time.
In short, the CLI works behind the scenes. It helps your public pages stay fast and current, while staying completely invisible to crawlers itself.
Back to top14. Common Mistakes and Pitfalls
14.1 Running From the Wrong Folder
Symptom: "Could not open input file: cli/joomla.php" or "Install Joomla to run cli commands".
Fix: run the command from the Joomla root (the folder that holds configuration.php), or give the full path: php /full/path/to/cli/joomla.php list.
14.2 The Wrong PHP Binary
Symptom: "your PHP version is not supported", or extensions behave differently than on the website.
Fix: the shell php may be older than the web server's PHP. Check with php -v and call the correct binary, for example /usr/bin/php8.3, in your cron line.
14.3 A Cron Job That Hangs
Symptom: a scheduled command never finishes or never runs.
Fix: an interactive prompt is waiting for an answer that will never come. Add --no-interaction and pass every required value as an option.
14.4 Your New Command Does Not Appear
Symptom: you wrote a command, but it is missing from list.
Fix: check three things - the plugin is enabled, it subscribes to ApplicationEvents::BEFORE_EXECUTE, and that handler actually calls $this->getApplication()->addCommand(...).
14.5 File Ownership Problems
Symptom: the website later cannot write files the CLI created, or vice versa.
Fix: run CLI commands as the web user, not as root, so ownership stays consistent.
14.6 Forgetting the Exit Code
Symptom: a deployment script treats a failed command as success.
Fix: always return Command::SUCCESS or Command::FAILURE from doExecute(), so the caller can tell what happened.
15. Best Practices
If you only remember a few things from this article, remember these:
- Start with
php cli/joomla.php listandhelp- they document any site for you. - Run commands from the Joomla root, as the web user, with the PHP version that matches the website.
- For automation, use full paths and add
--no-interactionso nothing waits for a human. - Trigger scheduled tasks with a real cron job calling
scheduler:run, not the web trigger. - Build new commands as a
consoleplugin that adds them onBEFORE_EXECUTE- never edit core. - Keep the handler thin, use
SymfonyStylefor output, and return a clear exit code. - Keep the
cli/scripts off the public web.
16. Quick Reference
WHAT IT IS Run Joomla from a terminal, no browser needed
ENTRY POINT php cli/joomla.php <command> [arguments] [--options]
DISCOVER php cli/joomla.php list (all commands)
php cli/joomla.php help <command> (one command)
NEEDS PHP 8.3+ ; run from the Joomla root, as the web user
CORE EXAMPLES cache:clean | config:get | site:down / site:up
user:add | user:reset-password | extension:install --path=...
core:update | finder:index | scheduler:run
GLOBAL OPTS --help -h | --quiet -q | --verbose -v/-vv/-vvv
--no-interaction -n | --version -V
EXIT CODES 0 = success | 1 = failure | 2 = invalid input
EXTEND IT console plugin, group="console"
subscribe ApplicationEvents::BEFORE_EXECUTE
then $this->getApplication()->addCommand(new MyCommand())
COMMAND extends AbstractCommand
$defaultName = 'group:action'
configure(): addArgument / addOption / setDescription
doExecute(InputInterface, OutputInterface): int
INJECT DEPS __construct(DatabaseInterface $db) + parent::__construct()
RECORDED IN #__extensions (type=plugin, folder=console, enabled=1)
DB TABLES #__scheduler_tasks + #__scheduler_logs (the scheduler)
LOGGING Log::add('msg', Log::INFO, 'cli') (writes to administrator/logs/)
DEBUG in list? plugin enabled? namespace ok? run with -vvv
CRON */15 * * * * php /path/cli/joomla.php scheduler:run --no-interaction
Back to top17. Summary
The Joomla CLI is the same site you know, reached through a different door. One entry point, cli/joomla.php, boots the full CMS and hands control to a command, which prints text and returns an exit code. There is no template, no visitor, and no HTTP - just Joomla doing one job, quickly.
The key ideas are short:
- Run
php cli/joomla.php listto see every command on a site, andhelpto learn one. - Core ships dozens of commands for cache, config, users, extensions, updates, search, and the scheduler.
- The application is built on Symfony Console and registers commands eagerly, lazily through the DI container, and at runtime through console plugins.
- A console plugin lives in the
consolegroup and adds commands on theBEFORE_EXECUTEevent, so you extend the CLI without touching core. - A command is a small class: a name, a
configure()for its arguments and options, and adoExecute()that returns an exit code.
Once you are comfortable here, a lot of Joomla maintenance becomes a single reliable line: clear the cache, reindex search, run the nightly tasks, or recover a locked-out site.
If you want help automating your Joomla maintenance, setting up a dependable cron-driven scheduler, or building custom CLI commands and console plugins for your own workflows, that is exactly the kind of advanced Joomla work I do every day.
Back to top

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.


