---
title: "Writing custom commands"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:writing-custom-commands@main"
source: "ApiOverview/CommandControllers/CustomCommands.rst"
rendered: "2026-09-24T12:36:55+00:00"
---

# 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@main).

**Table of contents**

-   [The custom command class](https://docs.typo3.org/permalink/t3coreapi:the-custom-command-class@main)
-   [Console command registration](https://docs.typo3.org/permalink/t3coreapi:console-command-registration@main)
-   [Making a command non-schedulable](https://docs.typo3.org/permalink/t3coreapi:making-a-command-non-schedulable@main)
-   [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@main)
-   [Create a command with arguments and interaction](https://docs.typo3.org/permalink/t3coreapi:create-a-command-with-arguments-and-interaction@main)
-   [Dependency injection in console commands](https://docs.typo3.org/permalink/t3coreapi:dependency-injection-in-console-commands@main)
-   [More about Symfony console commands](https://docs.typo3.org/permalink/t3coreapi:more-about-symfony-console-commands@main)

## 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}

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`.

**EXT:my_extension/Classes/Command/MyImportCommand.php (excerpt)**

```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@main)
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 (excerpt)**

```yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and always succeeds.'
        # To hide it from the scheduler:
        schedulable: false
      # 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) or
> use the PHP attribute `#[AsCommand]`.

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

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

The PHP attribute #[AsNonSchedulableCommand] has been introduced to
mark a command as non-schedulable.To provide support for both TYPO3 v14 and v13 continue to register your
command in Services.yaml (see
Tag console.command in the Services.yaml).

A command can be set as disabled for the scheduler by using the
`#[AsNonSchedulableCommand]` attribute:

**EXT:my_extension/Classes/Commands/MyImportCommand.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Commands;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand('myextension:import', 'Import data from external source')]
#[AsNonSchedulableCommand]
final class MyImportCommand extends Command
{
  // ...
}

```

## 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@main)
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-extension-framework@main) repositories in a
> CLI context.

Extbase relies on frontend [TypoScript](https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Index.html#start),  and features such as
[request-based TypoScript conditions](https://docs.typo3.org/m/typo3/reference-typoscript/main/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@main) or
[DataHandler](https://docs.typo3.org/permalink/t3coreapi:datahandler-basics@main) 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@main) 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@main).

### 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@main)
or other backend permission-handling-related tasks.

### 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@main).

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

**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;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand(
  name: 'myextension:createwizard',
)]
#[AsNonSchedulableCommand]
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@main) 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)
