---
title: "Writing custom commands"
manual: "TYPO3 Explained"
version: "12.4"
permalink: "https://docs.typo3.org/permalink/t3coreapi:writing-custom-commands@12.4"
source: "ApiOverview/CommandControllers/CustomCommands.rst"
rendered: "2026-10-21T15:29:37+00:00"
---

> [!WARNING]
> **Attention**
>
> TYPO3 v12 has reached end-of-life as of April 30th 2026 and
> is no longer being maintained. Use the version switcher on the top
> left of this page to select documentation for a supported version of TYPO3.
>
> Need more time before upgrading?
> You can purchase Extended Long Term Support (ELTS) for TYPO3 v12 here:
> [TYPO3 ELTS](https://typo3.com/services/extended-support-elts).

# Writing custom commands {#writing-custom-commands}

TYPO3 uses the Symfony Console component to define and execute command-line
interface (CLI) commands. Custom commands allow extension developers to
provide their own functionality for use on the command line or in the TYPO3
scheduler.

> [!NOTE]
> **See also**
>
> -   For a step-by-step guide, see:
>     [Tutorial: Create a console command](https://docs.typo3.org/permalink/t3coreapi:console-command-tutorial@12.4).

**Table of contents**

-   [The custom command class](https://docs.typo3.org/permalink/t3coreapi:the-custom-command-class@12.4)
-   [Console command registration](https://docs.typo3.org/permalink/t3coreapi:console-command-registration@12.4)
-   [Making a command non-schedulable](https://docs.typo3.org/permalink/t3coreapi:making-a-command-non-schedulable@12.4)
-   [Context of a command: No request, no site, no user](https://docs.typo3.org/permalink/t3coreapi:context-of-a-command-no-request-no-site-no-user@12.4)
-   [Create a command with arguments and interaction](https://docs.typo3.org/permalink/t3coreapi:create-a-command-with-arguments-and-interaction@12.4)
-   [Dependency injection in console commands](https://docs.typo3.org/permalink/t3coreapi:dependency-injection-in-console-commands@12.4)
-   [More about Symfony console commands](https://docs.typo3.org/permalink/t3coreapi:more-about-symfony-console-commands@12.4)

## The custom command class {#writing-custom-symfony-console-command}

To implement a console command in TYPO3 extend the
`\Symfony\Component\Console\Command\Command` class.

> [!NOTE]
> **See also**
>
> Console commands in TYPO3 are based on the same technology as
> commands in Symfony. Find more information about
> [Commands in the Symfony documentation](https://symfony.com/doc/current/console.html).

## Console command registration {#console-command-tutorial-registration-services}

<!-- TODO: no Markdown rendering for "versionadded" -->

The Symfony PHP attribute \Symfony\Component\Console\Attribute\AsCommand
is now accepted to register console commands.
See the section PHP attribute AsCommand
for more details.

There are two ways that a console command can be registered: you can use the
PHP Attribute AsCommand or register the command in your `Services.yaml`:

### PHP attribute `AsCommand` {#console-command-tutorial-registration-attribute}

CLI commands can be registered by setting the attribute
`\Symfony\Component\Console\Attribute\AsCommand` in the command class.
When using this attribute there is no need to register the command in
`Services.yaml`.

```php
#[AsCommand(
    name: 'examples:dosomething',
    description: 'A command that does nothing and always succeeds.',
    aliases: ['examples:dosomethingalias'],
)]
```

The following parameters are available:

-   **`name`**

    The name under which the command is available.

-   **`description`**

    Gives a short description. It will be displayed in the list of commands and
    the help information for the command.

-   **`hidden`**

    Hide the command from the command list by setting `hidden` to
    `true`.

-   **`alias`**

    A command can be made available under a different name. Set to `true`
    if your command name is an alias.

If you want to set a command as [non-schedulable](https://docs.typo3.org/permalink/t3coreapi:console-command-tutorial-registration-tag@12.4)
it has to be registered via tag not attribute.

### Tag `console.command` in the `Services.yaml` {#console-command-tutorial-registration-tag}

You can register the command in [`Configuration/Services.yaml`](../../ExtensionArchitecture/FileStructure/Configuration/ServicesYaml.md#file-extension-configuration-services-yaml) by adding the service
definition of your class as a tag `console.command`:

**packages/my_extension/Configuration/Services.yaml**

```yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and always succeeds.'
      # Also an alias for the command can be configured
      - name: console.command
        command: 'examples:dosomethingalias'
        alias: true
```

> [!NOTE]
> Despite using `autoconfigure: true` the commands
> have to be explicitly defined in [`Configuration/Services.yaml`](../../ExtensionArchitecture/FileStructure/Configuration/ServicesYaml.md#file-extension-configuration-services-yaml). It
> is recommended to always supply a description, otherwise there will be
> an empty space in the list of commands.

<a id="deactivating-the-command-in-scheduler"></a>

## Making a command non-schedulable {#schedulable}

A command can be set as disabled for the scheduler by setting `schedulable`
to `false`. This can only be done when registering the command via
[tag](https://docs.typo3.org/permalink/t3coreapi:console-command-tutorial-registration-tag@12.4) and not via attribute:

**packages/my_extension/Configuration/Services.yaml**

```yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and cannot be scheduled.'
        schedulable: false
```

## Context of a command: No request, no site, no user {#writing-custom-symfony-console-command-context}

Commands are called from the console / command line and not through a web
request. Therefore, when the code of your custom command is run by default there
is no [ServerRequest](https://docs.typo3.org/permalink/t3coreapi:typo3-request@12.4)
available, no backend or frontend user logged in and a request is called without
context of a site or page.

For that reason Site Settings, TypoScript and TSconfig are not loaded by default,
Extbase repositories cannot be used without taking precautions and there are many
more limitations.

### Extbase limitations in CLI context {#writing-custom-commands-extbase}

> [!WARNING]
> **Attention**
>
> It is not recommended to use [Extbase](https://docs.typo3.org/permalink/t3coreapi:extbase@12.4) repositories in a
> CLI context.

Extbase relies on frontend [TypoScript](https://docs.typo3.org/m/typo3/reference-typoscript/12.4/en-us/Index.html#start),  and features such as
[request-based TypoScript conditions](https://docs.typo3.org/m/typo3/reference-typoscript/12.4/en-us/Conditions/Index.html#condition-function-request)
may not behave as expected.

Instead, use the [Query Builder](https://docs.typo3.org/permalink/t3coreapi:database-query-builder@12.4) or
[DataHandler](https://docs.typo3.org/permalink/t3coreapi:datahandler-basics@12.4) when implementing custom commands.

### Using the DataHandler in CLI commands {#writing-custom-commands-backend-authentication}

When using the [DataHandler](https://docs.typo3.org/permalink/t3coreapi:datahandler-basics@12.4) in a CLI command,
backend user authentication is required. For more information see:
[Using the DataHandler in a Symfony command](https://docs.typo3.org/permalink/t3coreapi:datahandler-cli-command@12.4).

### Initialize backend user {#writing-custom-commands-backend-user}

A backend user can be initialized inside the `execute()` method as follows:

**packages/my_extension/Classes/Command/DoBackendRelatedThingsCommand.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;

#[AsCommand(
    name: 'myextension:dosomething',
)]
final class DoBackendRelatedThingsCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();
        // Do backend related stuff

        return Command::SUCCESS;
    }
}

```

This is necessary when using the [DataHandler](https://docs.typo3.org/permalink/t3coreapi:datahandler-basics@12.4)
or other backend permission-handling-related tasks.

<a id="console-command-tutorial-fe-request"></a>

### Simulating a frontend request in TYPO3 Commands {#console-command-tutorial-fe-request-example}

Executing a TYPO3 command in the CLI does not trigger a frontend (web)
request. This means that several request attributes required for link generation
via Fluid or TypoScript are missing by default. While setting the `site`
attribute in the request is a first step, it does not fully replicate the
frontend behavior.

> [!NOTE]
> **See also**
>
> See chapter [Simulating a frontend request](https://docs.typo3.org/permalink/t3coreapi:frontend-requests-simulation@12.4).

A minimal request configuration may be sufficient for
generating simple links or using [FluidEmail](https://docs.typo3.org/permalink/t3coreapi:mail-fluid-email@12.4):

**packages/my_extension/Classes/Command/DoBackendRelatedThingsCommand.php**

```php
<?php

declare(strict_types=1);

namespace T3docs\Examples\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Mail\FluidEmail;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Site\SiteFinder;

#[AsCommand(
    name: 'myextension:sendmail',
)]
class SendFluidMailCommand extends Command
{
    public function __construct(
        private readonly SiteFinder $siteFinder,
        private readonly MailerInterface $mailer,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();

        // The site has to have a fully qualified domain name
        $site = $this->siteFinder->getSiteByPageId(1);
        $request = (new ServerRequest())
            ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
            ->withAttribute('site', $site);
        $GLOBALS['TYPO3_REQUEST'] = $request;
        // Send some mails with FluidEmail
        $email = new FluidEmail();
        $email->setRequest($request);
        // Set receiver etc
        $this->mailer->send($email);
        return Command::SUCCESS;
    }
}

```

> [!NOTE]
> Simulating a frontend request in CLI is possible but requires
> all bootstrapping steps to be manually carried out. While basic functionality,
> such as link generation, can work with a minimal setup, complex
> TypoScript-based link modifications, access restrictions, and
> context-aware rendering require additional configuration and may still
> behave differently from a real web request.

## Create a command with arguments and interaction {#writing-custom-commands-interaction}

### Passing arguments {#writing-custom-commands-arguments}

Since a command extends `\Symfony\Component\Console\Command\Command`,
it is possible to define arguments (ordered) and options (unordered) using the Symfony
command API. This is explained in depth on the following Symfony Documentation page:

> [!NOTE]
> **See also**
>
> -   [Symfony: Console Input (Arguments & Options)](https://symfony.com/doc/current/console/input.html)

Both arguments and properties can be registered in a command implementation by
overriding the `configure()` method. You can call methods `addArgument()` and
`addOption()` to register them.

This argument can be retrieved with `$input->getArgument()`, the options with
`$input->getOption()`, for example:

**packages/my_extension/Classes/Command/SendFluidMailCommand.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use T3docs\Examples\Exception\InvalidWizardException;

#[AsCommand(
    name: 'myextension:createwizard',
)]
final class CreateWizardCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->setHelp('This command accepts arguments')
            ->addArgument(
                'wizardName',
                InputArgument::OPTIONAL,
                'The wizard\'s name',
            )
            ->addOption(
                'brute-force',
                'b',
                InputOption::VALUE_NONE,
                'Allow the "Wizard of Oz". You can use --brute-force or -b when running command',
            );
    }
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $wizardName = $input->getArgument('wizardName');
        $bruteForce = (bool)$input->getOption('brute-force');
        try {
            $this->doMagic($io, $wizardName, $bruteForce);
        } catch (InvalidWizardException) {
            return Command::FAILURE;
        }
        return Command::SUCCESS;
    }

    private function doMagic(SymfonyStyle $io, mixed $wizardName, bool $bruteForce): void
    {
        // do your magic here
    }
}

```

### User interaction on the console {#writing-custom-commands-user-interaction}

You can create a `SymfonyStyle` console user interface using the
`$input` and `$output` parameters of the `execute()` function:

**packages/my_extension/Classes/Command/CrazyCalculatorCommand.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'myextension:crazycalculator',
)]
final class CrazyCalculatorCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $io->title('Welcome to our awesome extension');

        $io->text([
            'We will ask some questions.',
            'Please take your time to answer them.',
        ]);
        do {
            $number = (int)$io->ask(
                'Please enter a number greater 0',
                '42',
            );
        } while ($number <= 0);
        $operation = (string)$io->choice(
            'Chose the desired operation',
            ['squared', 'divided by 0'],
            'squared',
        );
        switch ($operation) {
            case 'squared':
                $io->success(sprintf('%d squared is %d', $number, $number * $number));
                return Command::SUCCESS;
            default:
                $io->error('Operation ' . $operation . 'is not supported. ');
                return Command::FAILURE;
        }
    }
}

```

The `$io` variable can be used to generate output and prompt for
input.

## Dependency injection in console commands {#writing-custom-commands-dependencyinjection}

You can use [dependency injection (DI)](https://docs.typo3.org/permalink/t3coreapi:dependency-injection@12.4) in console
commands via constructor injection or method injection.

**packages/my_extension/Classes/Command/MeowInformationCommand.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use T3docs\Examples\Http\MeowInformationRequester;

#[AsCommand(
    name: 'myextension:dosomething',
)]
final class MeowInformationCommand extends Command
{
    public function __construct(
        private readonly MeowInformationRequester $requester,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        if (!$this->requester->isReady()) {
            $this->logger->error('MeowInformationRequester was not ready! ');
            return Command::SUCCESS;
        }
        // Do awesome stuff
        return Command::SUCCESS;
    }
}

```

## More about Symfony console commands {#writing-custom-commands-more}

-   See implementation of existing command controllers in the Core:
    `typo3/sysext/*/Classes/Command`
-   [Symfony Command Documentation](https://symfony.com/doc/current/console.html)
-   [Symfony Commands: Console Input (Arguments & Options)](https://symfony.com/doc/current/console/input.html)
