diff --git a/_config/dev.yml b/_config/dev.yml
index c19ab4e1..f53cbd2d 100644
--- a/_config/dev.yml
+++ b/_config/dev.yml
@@ -1,19 +1,10 @@
 ---
 Name: graphql-dev
 ---
-SilverStripe\ORM\DatabaseAdmin:
+SilverStripe\Dev\Command\DbBuild:
   extensions:
-    - SilverStripe\GraphQL\Extensions\DevBuildExtension
+    - SilverStripe\GraphQL\Extensions\DbBuildExtension
 
 SilverStripe\Dev\DevelopmentAdmin:
-  registered_controllers:
-    graphql:
-      controller: SilverStripe\GraphQL\Dev\DevelopmentAdmin
-      links:
-        graphql: 'List GraphQL development tools'
-SilverStripe\GraphQL\Dev\DevelopmentAdmin:
-  registered_controllers:
-    build:
-      controller: SilverStripe\GraphQL\Dev\Build
-      links:
-        build: Build the GraphQL schema
+  commands:
+    'graphql/build': 'SilverStripe\GraphQL\Dev\SchemaBuild'
diff --git a/_config/logging.yml b/_config/logging.yml
index e19a72a8..0de75516 100644
--- a/_config/logging.yml
+++ b/_config/logging.yml
@@ -4,7 +4,7 @@ after: '#logging'
 ---
 
 SilverStripe\Core\Injector\Injector:
-  # Omits the HTTPOutputHandler from the logger so errors won't appear in output
+  # Omits the ErrorOutputHandler from the logger so errors won't appear in output
   Psr\Log\LoggerInterface.graphql-quiet:
     type: singleton
     class: Monolog\Logger
diff --git a/docs/en/01_getting_started/04_building_the_schema.md b/docs/en/01_getting_started/04_building_the_schema.md
index 9e818076..ef0bcbbe 100644
--- a/docs/en/01_getting_started/04_building_the_schema.md
+++ b/docs/en/01_getting_started/04_building_the_schema.md
@@ -22,19 +22,19 @@ whenever the schema definition changes, or a new schema definition is added.
 
 ### What triggers a GraphQL code build?
 
-- Any time you run the `dev/graphql/build` command to explicitly build your GraphQL schemas.
-- Any time you run the `dev/build` command on your project.
+- Any time you run the `sake graphql:build` command to explicitly build your GraphQL schemas.
+- Any time you run the `sake db:build` command on your project.
 - `silverstripe/graphql` will attempt to generate your schema "on-demand" on the first GraphQL request *only* if it wasn’t already generated.
 
 > [!WARNING]
 > Relying on the "on-demand" schema generation on the first GraphQL request requires some additional consideration.
 > See [deploying the schema](06_deploying_the_schema.md#on-demand).
 
-#### Running `dev/graphql/build`
+#### Running `sake graphql:build`
 
-The main command for generating the schema code is `dev/graphql/build`.
+The main command for generating the schema code is `sake graphql:build`.
 
-`vendor/bin/sake dev/graphql/build`
+`vendor/bin/sake graphql:build`
 
 This command takes an optional `schema` parameter. If you only want to generate a specific schema
 (e.g. generate your custom schema, but not the CMS schema), you should pass in the name of the
@@ -43,7 +43,7 @@ schema you want to build.
 > [!NOTE]
 > If you do not provide a `schema` parameter, the command will build all schemas.
 
-`vendor/bin/sake dev/graphql/build schema=default`
+`vendor/bin/sake graphql:build --schema=default`
 
 > [!NOTE]
 > Most of the time, the name of your custom schema is `default`.
@@ -51,11 +51,11 @@ schema you want to build.
 Keep in mind that some of your changes will be in YAML in the `_config/` directory, which also
 requires a flush.
 
-`vendor/bin/sake dev/graphql/build schema=default flush=1`
+`vendor/bin/sake graphql:build --schema=default --flush`
 
-#### Building on dev/build
+#### Building with `sake db:build`
 
-By default, all schemas will be built during `dev/build`. To disable this, change the config:
+By default, all schemas will be built during `sake db:build`. To disable this, change the config:
 
 ```yml
 SilverStripe\GraphQL\Extensions\DevBuildExtension:
@@ -74,11 +74,11 @@ If the type hasn't changed, it doesn't get re-built. This reduces build times to
 
 #### Clearing the schema cache
 
-If you want to completely re-generate your schema from scratch, you can add `clear=1` to the `dev/graphql/build` command.
+If you want to completely re-generate your schema from scratch, you can add `--clear` to the `sake graphql:build` command.
 
-`vendor/bin/sake dev/graphql/build schema=default clear=1`
+`vendor/bin/sake graphql:build --schema=default --clear`
 
-If your schema is producing unexpected results, try using `clear=1` to eliminate the possibility
+If your schema is producing unexpected results, try using `--clear` to eliminate the possibility
 of a caching issue. If the issue is resolved, record exactly what you changed and [create an issue](https://github.com/silverstripe/silverstripe-graphql/issues/new).
 
 ### Build gotchas
diff --git a/docs/en/01_getting_started/06_deploying_the_schema.md b/docs/en/01_getting_started/06_deploying_the_schema.md
index e25b11f4..b22e45fb 100644
--- a/docs/en/01_getting_started/06_deploying_the_schema.md
+++ b/docs/en/01_getting_started/06_deploying_the_schema.md
@@ -14,13 +14,13 @@ One way or another, you must get the `.graphql-generated/` and `public/_graphql/
 
 ### Single-server hosting solutions with simple deployments {#simple-single-server}
 
-If you host your site on a single server and you always run `dev/build` during the deployment, then assuming you have set up permissions to allow the webserver to write to the `.graphql-generated/` and `public/_graphql/` folders, your GraphQL schema will be built for you as a side-effect of running `dev/build`. You don't need to do anything further. Note that if your schema is exceptionally large you may still want to read through the rest of the options below.
+If you host your site on a single server and you always run `sake db:build` during the deployment, then assuming you have set up permissions to allow the webserver to write to the `.graphql-generated/` and `public/_graphql/` folders, your GraphQL schema will be built for you as a side-effect of running `sake db:build`. You don't need to do anything further. Note that if your schema is exceptionally large you may still want to read through the rest of the options below.
 
 ### Options for any hosting solution
 
 #### Commit the schema to version control {#commit-to-vcs}
 
-A simplistic approach is to build the schema in your local development environment and add the `.graphql-generated/` and `public/_graphql/` folders to your version control system. With this approach you would most likely want to disable schema generation at `dev/build`.
+A simplistic approach is to build the schema in your local development environment and add the `.graphql-generated/` and `public/_graphql/` folders to your version control system. With this approach you would most likely want to disable schema generation with `sake db:build`.
 
 This approach has the advantage of being very simple, but it will pollute your commits with massive diffs for the generated code.
 
@@ -29,19 +29,19 @@ This approach has the advantage of being very simple, but it will pollute your c
 
 #### Explicitly build the schema during each deployment {#build-during-deployment}
 
-Many projects will automatically run a `dev/build` whenever they deploy a site to their production environment. If that’s your case, then you can just let this process run normally and generate the `.graphql-generated/` and `public/_graphql/` folders for you. This will allow you to add these folders to your `.gitignore` file and avoid tracking the folder in your version control system.
+Many projects will automatically run `sake db:build` whenever they deploy a site to their production environment. If that’s your case, then you can just let this process run normally and generate the `.graphql-generated/` and `public/_graphql/` folders for you. This will allow you to add these folders to your `.gitignore` file and avoid tracking the folder in your version control system.
 
-Be aware that for this approach to work, the process executing the `dev/build` must have write access to create the folders (or you must create those folders yourself, and give write access for those folders specifically), and for multi-server environments a `dev/build` or `dev/graphql/build` must be executed on each server hosting your site after each deployment.
+Be aware that for this approach to work, the process executing `sake db:build` must have write access to create the folders (or you must create those folders yourself, and give write access for those folders specifically), and for multi-server environments `sake db:build` or `sake graphql:build` must be executed on each server hosting your site after each deployment.
 
 #### Use a CI/CD pipeline to build your schema {#using-ci-cd}
 
 Projects with more sophisticated requirements or bigger schemas exposing more than 100 `DataObject` classes may want to consider using a continuous-integration/continuous-deployment (CI/CD) pipeline to build their GraphQL schema.
 
-In this kind of setup, you would need to update your deployment script to run the `dev/graphql/build` command which builds the `.graphql-generated/` and `public/_graphql/` folders. In multi-server environments this must be executed on each server hosting your site.
+In this kind of setup, you would need to update your deployment script to run the `sake graphql:build` command which builds the `.graphql-generated/` and `public/_graphql/` folders. In multi-server environments this must be executed on each server hosting your site.
 
 ### Multi-server hosting solutions {#multi-server}
 
-If your site is hosted in an environment with multiple servers or configured to auto-scale with demand, there are some additional considerations. For example if you only generate the schema on one single server (i.e. via `dev/build` or `dev/graphql/build`), then the other servers won’t have a `.graphql-generated/` or `public/_graphql/` folder (or those folders will be empty if you manually created them).
+If your site is hosted in an environment with multiple servers or configured to auto-scale with demand, there are some additional considerations. For example if you only generate the schema on one single server (i.e. via `sake db:build` or `sake graphql:build`), then the other servers won’t have a `.graphql-generated/` or `public/_graphql/` folder (or those folders will be empty if you manually created them).
 
 #### Rely on "on-demand" schema generation on the first GraphQL request {#on-demand}
 
@@ -58,7 +58,7 @@ Our expectation is that on-demand schema generation will be performant for most
 
 #### Build the schema during/before deployment and share it across your servers {#multi-server-shared-dirs}
 
-If you have a particularly large schema, you may want to ensure it is always built before the first GraphQL request. It might make sense for you to sync your `.graphql-generated/` and `public/_graphql/` folders across all your servers using an EFS or similar mechanism. In that case you only need to run `dev/build` or `dev/graphql/build` on the server with the original folder - but bear in mind that this may have a performance impact.
+If you have a particularly large schema, you may want to ensure it is always built before the first GraphQL request. It might make sense for you to sync your `.graphql-generated/` and `public/_graphql/` folders across all your servers using an EFS or similar mechanism. In that case you only need to run `sake db:build` or `sake graphql:build` on the server with the original folder - but bear in mind that this may have a performance impact.
 
 ### Performance considerations when building the GraphQL schema {#performance-considerations}
 
@@ -81,7 +81,7 @@ DataObjects in schema | Build time (ms) | Memory use (MB)
 
 The process that is generating these folders must have write permissions to create the folder and to update existing files. If different users are used to generate the folders, then you must make sure that each user retains write access on them.
 
-For example, if you manually run a `dev/build` under a foobar user, the folders will be owned by foobar. If your web server is running under the www-data user and you try to call `dev/graphql/build` in your browser, you might get an error if www-data doesn’t have write access.
+For example, if you manually run `sake db:build` under a foobar user, the folders will be owned by foobar. If your web server is running under the www-data user and you try to visit `dev/graphql/build` in your browser, you might get an error if www-data doesn’t have write access.
 
 ### Further reading
 
diff --git a/docs/en/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md b/docs/en/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md
index 612da145..715fa63a 100644
--- a/docs/en/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md
+++ b/docs/en/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md
@@ -56,9 +56,9 @@ The `*` value on `operations` tells the schema to create all available queries a
 - `update`
 - `delete`
 
-Now that we've changed our schema, we need to build it using the `dev/graphql/build` command:
+Now that we've changed our schema, we need to build it using the `graphql:build` command:
 
-`vendor/bin/sake dev/graphql/build schema=default`
+`vendor/bin/sake graphql:build --schema=default`
 
 Now we can access our schema on the default GraphQL endpoint, `/graphql`.
 
diff --git a/docs/en/03_working_with_generic_types/02_building_a_custom_query.md b/docs/en/03_working_with_generic_types/02_building_a_custom_query.md
index 2cda2461..67474218 100644
--- a/docs/en/03_working_with_generic_types/02_building_a_custom_query.md
+++ b/docs/en/03_working_with_generic_types/02_building_a_custom_query.md
@@ -67,7 +67,7 @@ queries:
 
 Now, we just have to build the schema:
 
-`vendor/bin/sake dev/graphql/build schema=default`
+`vendor/bin/sake graphql:build --schema=default`
 
 ### Testing the query
 
diff --git a/docs/en/03_working_with_generic_types/03_resolver_discovery.md b/docs/en/03_working_with_generic_types/03_resolver_discovery.md
index c938a1dd..6e3c6b95 100644
--- a/docs/en/03_working_with_generic_types/03_resolver_discovery.md
+++ b/docs/en/03_working_with_generic_types/03_resolver_discovery.md
@@ -145,7 +145,7 @@ resolvers:
 
 Re-run the schema build, with a flush (because we created a new PHP class), and let's go!
 
-`vendor/bin/sake dev/graphql/build schema=default flush=1`
+`vendor/bin/sake graphql:build --schema=default flush=1`
 
 ### Field resolvers
 
diff --git a/docs/en/03_working_with_generic_types/05_adding_pagination.md b/docs/en/03_working_with_generic_types/05_adding_pagination.md
index d207e783..aeb07897 100644
--- a/docs/en/03_working_with_generic_types/05_adding_pagination.md
+++ b/docs/en/03_working_with_generic_types/05_adding_pagination.md
@@ -96,7 +96,7 @@ can handle the rest. It will return an array including `edges`, `nodes`, and `pa
 
 Rebuild the schema and test it out:
 
-`vendor/bin/sake dev/graphql/build schema=default`
+`vendor/bin/sake graphql:build --schema=default`
 
 ```graphql
 query {
diff --git a/docs/en/03_working_with_generic_types/06_adding_descriptions.md b/docs/en/03_working_with_generic_types/06_adding_descriptions.md
index ebc87012..551fc0fe 100644
--- a/docs/en/03_working_with_generic_types/06_adding_descriptions.md
+++ b/docs/en/03_working_with_generic_types/06_adding_descriptions.md
@@ -10,7 +10,7 @@ summary: Add descriptions to just about anything in your schema to improve your
 
 One of the great features of a schema-backed API is that it is self-documenting. If you use
 the [`silverstripe/graphql-devtools`](https://github.com/silverstripe/silverstripe-graphql-devtools)
-module you can see the documentation by navigating to /dev/graphql/ide in your browser anc clicking
+module you can see the documentation by navigating to `/dev/graphql/ide` in your browser anc clicking
 on "DOCS" on the right.
 
 Many API developers choose to maximise the benefit of this by adding descriptions to some or
diff --git a/docs/en/07_tips_and_tricks.md b/docs/en/07_tips_and_tricks.md
index 0b528683..a02f4b19 100644
--- a/docs/en/07_tips_and_tricks.md
+++ b/docs/en/07_tips_and_tricks.md
@@ -17,7 +17,7 @@ to your environment file. The classnames and filenames in the generated code dir
 
 > [!WARNING]
 > Take care not to use `DEBUG_SCHEMA=1` as an inline environment variable to your build command, e.g.
-> `DEBUG_SCHEMA=1 vendor/bin/sake dev/graphql/build` because any activity that happens at run time, e.g. querying the schema
+> `DEBUG_SCHEMA=1 vendor/bin/sake graphql:build` because any activity that happens at run time, e.g. querying the schema
 > will fail, since the environment variable is no longer set.
 
 In live mode, full obfuscation kicks in and the filenames become unreadable. You can only determine the type they map
diff --git a/docs/en/08_architecture_diagrams.md b/docs/en/08_architecture_diagrams.md
index e4138273..95f6c7e9 100644
--- a/docs/en/08_architecture_diagrams.md
+++ b/docs/en/08_architecture_diagrams.md
@@ -16,7 +16,7 @@ The schema is generated during a build step, which generates code generation art
 
 ![A high-level view of the GraphQL build process](_images/build_process.png)
 
-- **`/dev/graphql/build`**: This is the command that builds the schema. It also runs as a side effect of `dev/build` as a fallback. It accepts a `schema` parameter if you only want to build one schema.
+- **`sake graphql:build`**: This is the command that builds the schema. It also runs as a side effect of `sake db:build` as a fallback. It accepts a `schema` parameter if you only want to build one schema.
 
 - **Schema Factory**: This class is responsible for rebuilding a schema or fetching an existing one (i.e. as cached generated code)
 
diff --git a/src/Dev/DevelopmentAdmin.php b/src/Dev/DevelopmentAdmin.php
deleted file mode 100644
index dc9db04b..00000000
--- a/src/Dev/DevelopmentAdmin.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-
-
-namespace SilverStripe\GraphQL\Dev;
-
-use SilverStripe\Control\Controller;
-use SilverStripe\Control\Director;
-use SilverStripe\Control\HTTPRequest;
-use SilverStripe\Core\Config\Config;
-use SilverStripe\Dev\DebugView;
-use SilverStripe\Dev\DevelopmentAdmin as RootDevelopmentAdmin;
-use SilverStripe\Security\Permission;
-use SilverStripe\Security\PermissionProvider;
-use SilverStripe\Security\Security;
-use Exception;
-use Psr\Log\LoggerInterface;
-use SilverStripe\Core\Injector\Injector;
-use SilverStripe\Dev\Deprecation;
-use SilverStripe\GraphQL\Schema\Logger;
-
-/**
- * @deprecated 5.3.0 Will be removed without equivalent functionality to replace it
- */
-class DevelopmentAdmin extends Controller implements PermissionProvider
-{
-    private static $allowed_actions = [
-        'runRegisteredController'
-    ];
-
-    private static $url_handlers = [
-        '' => 'index',
-        '$Action' => 'runRegisteredController',
-    ];
-
-    private static $init_permissions = [
-        'ADMIN',
-        'ALL_DEV_ADMIN',
-        'CAN_DEV_GRAPHQL',
-    ];
-
-    public function __construct()
-    {
-        parent::__construct();
-        Deprecation::withSuppressedNotice(function () {
-            Deprecation::notice(
-                '5.4.0',
-                'Will be removed without equivalent functionality to replace it',
-                Deprecation::SCOPE_CLASS
-            );
-        });
-    }
-
-    protected function init()
-    {
-        parent::init();
-
-        if (RootDevelopmentAdmin::config()->get('deny_non_cli') && !Director::is_cli()) {
-            return $this->httpError(404);
-        }
-
-        if (!$this->canInit()) {
-            Security::permissionFailure($this);
-        }
-
-        // Define custom logger
-        $logger = Logger::singleton();
-        Injector::inst()->registerService($logger, LoggerInterface::class . '.graphql-build');
-    }
-
-    public function index(HTTPRequest $request)
-    {
-        // Web mode
-        if (!Director::is_cli()) {
-            $renderer = DebugView::create();
-            echo $renderer->renderHeader();
-            echo $renderer->renderInfo("Silverstripe CMS GraphQL Tools", Director::absoluteBaseURL());
-            $base = Director::baseURL();
-
-            echo '<div class="options"><ul>';
-            $evenOdd = "odd";
-            foreach (DevelopmentAdmin::get_links() as $action => $description) {
-                echo "<li class=\"$evenOdd\"><a href=\"{$base}dev/graphql/$action\"><b>/dev/graphql/$action:</b>"
-                    . " $description</a></li>\n";
-                $evenOdd = ($evenOdd == "odd") ? "even" : "odd";
-            }
-
-            echo $renderer->renderFooter();
-
-            // CLI mode
-        } else {
-            echo "SILVERSTRIPE CMS GRAPHQL TOOLS\n--------------------------\n\n";
-            echo "You can execute any of the following commands:\n\n";
-            foreach (DevelopmentAdmin::get_links() as $action => $description) {
-                echo "  sake dev/graphql/$action: $description\n";
-            }
-            echo "\n\n";
-        }
-    }
-
-    public function runRegisteredController(HTTPRequest $request)
-    {
-        $controllerClass = null;
-
-        $baseUrlPart = $request->param('Action');
-        $reg = Config::inst()->get(static::class, 'registered_controllers');
-        if (isset($reg[$baseUrlPart])) {
-            $controllerClass = $reg[$baseUrlPart]['controller'];
-        }
-
-        if ($controllerClass && class_exists($controllerClass ?? '')) {
-            return $controllerClass::create();
-        }
-
-        $msg = 'Error: no controller registered in ' . __CLASS__ . ' for: ' . $request->param('Action');
-        if (Director::is_cli()) {
-            throw new Exception($msg);
-        } else {
-            $this->httpError(404, $msg);
-        }
-    }
-
-    public function canInit(): bool
-    {
-        return (
-            Director::isDev()
-            // We need to ensure that DevelopmentAdminTest can simulate permission failures when running
-            // "dev/tasks" from CLI.
-            || (Director::is_cli() && RootDevelopmentAdmin::config()->get('allow_all_cli'))
-            || Permission::check(static::config()->get('init_permissions'))
-        );
-    }
-
-    public function providePermissions(): array
-    {
-        return [
-            'CAN_DEV_GRAPHQL' => [
-                'name' => _t(__CLASS__ . '.CAN_DEV_GRAPHQL_DESCRIPTION', 'Can view and execute /dev/graphql'),
-                'help' => _t(__CLASS__ . '.CAN_DEV_GRAPHQL_HELP', 'Can view and execute GraphQL development tools (/dev/graphql).'),
-                'category' => RootDevelopmentAdmin::permissionsCategory(),
-                'sort' => 80
-            ],
-        ];
-    }
-
-    /**
-     * @return array of url => description
-     */
-    protected static function get_links(): array
-    {
-        $links = [];
-
-        $reg = Config::inst()->get(static::class, 'registered_controllers');
-        foreach ($reg as $registeredController) {
-            if (isset($registeredController['links'])) {
-                foreach ($registeredController['links'] as $url => $desc) {
-                    $links[$url] = $desc;
-                }
-            }
-        }
-        return $links;
-    }
-}
diff --git a/src/Dev/Build.php b/src/Dev/SchemaBuild.php
similarity index 54%
rename from src/Dev/Build.php
rename to src/Dev/SchemaBuild.php
index 1c31a38c..e12cee87 100644
--- a/src/Dev/Build.php
+++ b/src/Dev/SchemaBuild.php
@@ -4,12 +4,10 @@
 namespace SilverStripe\GraphQL\Dev;
 
 use Psr\Log\LoggerInterface;
-use SilverStripe\Control\Controller;
-use SilverStripe\Control\Director;
-use SilverStripe\Control\HTTPRequest;
 use SilverStripe\Core\Injector\Injector;
-use SilverStripe\Dev\DebugView;
-use SilverStripe\Dev\Deprecation;
+use SilverStripe\Dev\Command\DevCommand;
+use SilverStripe\Dev\DevelopmentAdmin;
+use SilverStripe\PolyExecution\PolyOutput;
 use SilverStripe\GraphQL\Schema\DataObject\FieldAccessor;
 use SilverStripe\GraphQL\Schema\Exception\EmptySchemaException;
 use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException;
@@ -19,53 +17,44 @@
 use SilverStripe\GraphQL\Schema\SchemaBuilder;
 use SilverStripe\GraphQL\Schema\Storage\CodeGenerationStore;
 use SilverStripe\ORM\Connect\NullDatabaseException;
+use SilverStripe\Security\PermissionProvider;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
 
-/**
- * @deprecated 5.3.0 Will be replaced with SilverStripe\GraphQL\Dev\SchemaBuild
- */
-class Build extends Controller
+class SchemaBuild extends DevCommand implements PermissionProvider
 {
-    private static $url_handlers = [
-        '' => 'build'
-    ];
+    protected static string $commandName = 'graphql:build';
+
+    protected static string $description = 'Build the GraphQL schema(s)';
 
-    private static $allowed_actions = [
-        'build'
+    private static array $permissions_for_browser_execution = [
+        'CAN_DEV_GRAPHQL',
     ];
 
-    public function __construct()
+    public function getTitle(): string
     {
-        parent::__construct();
-        Deprecation::withSuppressedNotice(function () {
-            Deprecation::notice(
-                '5.4.0',
-                'Will be replaced with SilverStripe\GraphQL\Dev\SchemaBuild',
-                Deprecation::SCOPE_CLASS
-            );
-        });
+        return 'GraphQL Schema Builder';
     }
 
-    /**
-     * @throws SchemaBuilderException
-     * @throws SchemaNotFoundException
-     */
-    public function build(HTTPRequest $request): void
+    protected function execute(InputInterface $input, PolyOutput $output): int
     {
-        $isBrowser = !Director::is_cli();
-        if ($isBrowser) {
-            $renderer = DebugView::create();
-            echo $renderer->renderHeader();
-            echo $renderer->renderInfo("GraphQL Schema Builder", Director::absoluteBaseURL());
-            echo "<div class=\"build\">";
+        $originalLogger = Injector::inst()->get(LoggerInterface::class . '.graphql-build');
+        try {
+            $logger = Logger::singleton();
+            $logger->setOutput($output);
+            Injector::inst()->registerService($logger, LoggerInterface::class . '.graphql-build');
+            $this->buildSchema($input->getOption('schema'), true, $output);
+        } finally {
+            // Restore default logger back to its starting state
+            Injector::inst()->registerService($originalLogger, LoggerInterface::class . '.graphql-build');
         }
-        $clear = true;
-
-        $this->buildSchema($request->getVar('schema'), $clear);
+        return Command::SUCCESS;
+    }
 
-        if ($isBrowser) {
-            echo "</div>";
-            echo $renderer->renderFooter();
-        }
+    protected function getHeading(): string
+    {
+        return '';
     }
 
     /**
@@ -113,15 +102,15 @@ public function buildSchema(string $key = null, bool $clear = true): void
                         break;
                     }
                 }
-                $logger->warning("
-                    Your schema configuration requires access to the database. This can happen
+                $logger->warning(
+                    "Your schema configuration requires access to the database. This can happen
                     when you add fields that require type introspection (i.e. custom getters).
                     It is recommended that you specify an explicit type when adding custom getters
-                    to your schema.");
+                    to your schema."
+                );
                 if ($candidate) {
                     $logger->warning(sprintf(
-                        "
-                    This most likely happened when you tried to add the field '%s' to '%s'",
+                        "This most likely happened when you tried to add the field '%s' to '%s'",
                         $candidate['args'][1],
                         get_class($candidate['args'][0])
                     ));
@@ -130,9 +119,32 @@ public function buildSchema(string $key = null, bool $clear = true): void
                 throw $e;
             }
 
-            $logger->info(
-                Benchmark::end('build-schema-' . $key, 'Built schema in %sms.')
-            );
+            $logger->info(Benchmark::end('build-schema-' . $key, 'Built schema in %sms.'));
         }
     }
+
+    public function getOptions(): array
+    {
+        return [
+            new InputOption(
+                'schema',
+                null,
+                InputOption::VALUE_REQUIRED,
+                'The name of the schema to be built. If not passed, all schemas will be built',
+                suggestedValues: array_keys(Schema::config()->get('schemas') ?? [])
+            )
+        ];
+    }
+
+    public function providePermissions(): array
+    {
+        return [
+            'CAN_DEV_GRAPHQL' => [
+                'name' => _t(__CLASS__ . '.CAN_DEV_GRAPHQL_DESCRIPTION', 'Can view and execute /dev/graphql'),
+                'help' => _t(__CLASS__ . '.CAN_DEV_GRAPHQL_HELP', 'Can view and execute GraphQL development tools (/dev/graphql).'),
+                'category' => DevelopmentAdmin::permissionsCategory(),
+                'sort' => 80
+            ],
+        ];
+    }
 }
diff --git a/src/Extensions/DevBuildExtension.php b/src/Extensions/DbBuildExtension.php
similarity index 58%
rename from src/Extensions/DevBuildExtension.php
rename to src/Extensions/DbBuildExtension.php
index 0146ef82..dec7e8e1 100644
--- a/src/Extensions/DevBuildExtension.php
+++ b/src/Extensions/DbBuildExtension.php
@@ -5,17 +5,17 @@
 
 use Psr\Log\LoggerInterface;
 use SilverStripe\Core\Config\Configurable;
+use SilverStripe\Core\Extension;
 use SilverStripe\Core\Injector\Injector;
-use SilverStripe\Dev\Deprecation;
-use SilverStripe\GraphQL\Dev\Build;
+use SilverStripe\Dev\Command\DbBuild;
+use SilverStripe\PolyExecution\PolyOutput;
+use SilverStripe\GraphQL\Dev\SchemaBuild;
 use SilverStripe\GraphQL\Schema\Logger;
-use SilverStripe\ORM\DatabaseAdmin;
-use SilverStripe\Core\Extension;
 
 /**
- * @extends Extension<DatabaseAdmin>
+ * @extends Extension<DbBuild>
  */
-class DevBuildExtension extends Extension
+class DbBuildExtension extends Extension
 {
     use Configurable;
 
@@ -24,19 +24,7 @@ class DevBuildExtension extends Extension
      */
     private static bool $enabled = true;
 
-    public function __construct()
-    {
-        parent::__construct();
-        Deprecation::withSuppressedNotice(function () {
-            Deprecation::notice(
-                '5.4.0',
-                'Will be replaced with SilverStripe\GraphQL\Extensions\DbBuildExtension',
-                Deprecation::SCOPE_CLASS
-            );
-        });
-    }
-
-    protected function onAfterBuild(): void
+    protected function onAfterBuild(PolyOutput $output): void
     {
         if (!static::config()->get('enabled')) {
             return;
@@ -48,10 +36,10 @@ protected function onAfterBuild(): void
         try {
             // Define custom logger
             $logger = Logger::singleton();
-            $logger->setVerbosity(Logger::INFO);
+            $logger->setOutput($output);
             Injector::inst()->registerService($logger, LoggerInterface::class . '.graphql-build');
 
-            Build::singleton()->buildSchema();
+            SchemaBuild::singleton()->buildSchema();
         } finally {
             // Restore default logger back to its starting state
             Injector::inst()->registerService($defaultLogger, LoggerInterface::class . '.graphql-build');
diff --git a/src/Extensions/TestSessionEnvironmentExtension.php b/src/Extensions/TestSessionEnvironmentExtension.php
index 47081895..159793fc 100644
--- a/src/Extensions/TestSessionEnvironmentExtension.php
+++ b/src/Extensions/TestSessionEnvironmentExtension.php
@@ -2,12 +2,14 @@
 
 namespace SilverStripe\GraphQL\Extensions;
 
+use SilverStripe\Control\Director;
 use SilverStripe\Core\Extension;
 use SilverStripe\GraphQL\Schema\Logger;
 use SilverStripe\GraphQL\Schema\Schema;
 use SilverStripe\GraphQL\Schema\SchemaBuilder;
 use SilverStripe\GraphQL\Schema\Exception\EmptySchemaException;
 use SilverStripe\GraphQL\Dev\Benchmark;
+use SilverStripe\PolyExecution\PolyOutput;
 use SilverStripe\TestSession\TestSessionEnvironment;
 
 /**
@@ -18,13 +20,18 @@ class TestSessionEnvironmentExtension extends Extension
     /**
      * Build the graphql schema after a new testsession is started
      * This is to ensure that the schema is available when a behat test is run, particularly on CI
-     * This does laregely the same thing as SilverStripe\GraphQL\Dev\Build::buildSchema(), though
+     * This does laregely the same thing as SilverStripe\GraphQL\Dev\SchemaBuild::buildSchema(), though
      * it also checks for the existance of persisted schemas first do that the schema is not rebuilt
      * after each behat scenario
      */
     protected function onAfterStartTestSession(): void
     {
+        $output = PolyOutput::create(
+            Director::is_cli() ? PolyOutput::FORMAT_ANSI : PolyOutput::FORMAT_HTML,
+            PolyOutput::VERBOSITY_QUIET
+        );
         $logger = Logger::singleton();
+        $logger->setOutput($output);
         $keys = array_keys(Schema::config()->get('schemas') ?? []);
         $keys = array_filter($keys ?? [], function ($key) {
             return $key !== Schema::ALL;
diff --git a/src/Schema/Logger.php b/src/Schema/Logger.php
index cfb49a6d..6dc054cb 100644
--- a/src/Schema/Logger.php
+++ b/src/Schema/Logger.php
@@ -1,11 +1,11 @@
 <?php
 
-
 namespace SilverStripe\GraphQL\Schema;
 
 use Psr\Log\LoggerInterface;
 use SilverStripe\Control\Director;
 use SilverStripe\Core\Injector\Injectable;
+use SilverStripe\PolyExecution\PolyOutput;
 use Stringable;
 
 class Logger implements LoggerInterface
@@ -33,6 +33,13 @@ class Logger implements LoggerInterface
 
     private int $level = Logger::INFO;
 
+    private ?PolyOutput $output = null;
+
+    public function setOutput(?PolyOutput $output): void
+    {
+        $this->output = $output;
+    }
+
     public function setVerbosity(int $level): Logger
     {
         $this->level = $level;
@@ -118,18 +125,45 @@ public function warning(Stringable|string $message, array $context = []): void
 
     public function output(string $msg, ?string $prefix = null, ?string $colour = null): void
     {
-        $cli = Director::is_cli();
-        $formatted = sprintf(
-            '%s%s%s%s',
-            $colour && $cli ? $colour :'',
-            $prefix ? '[' . $prefix . ']: ' : '',
-            $colour && $cli ? Logger::RESET : '',
-            $msg
-        );
-        if ($cli) {
-            fwrite(STDOUT, $formatted . PHP_EOL);
-        } else {
-            echo $formatted . "<br>";
+        $prefix = $prefix ? '[' . $prefix . ']: ' : '';
+        $this->output?->writeln($this->colouriseText($prefix, $colour) . $msg);
+        if (!$this->output) {
+            $cli = Director::is_cli();
+            $formatted = sprintf(
+                '%s%s%s%s',
+                $colour && $cli ? $colour :'',
+                $prefix,
+                $colour && $cli ? Logger::RESET : '',
+                $msg
+            );
+            if ($cli) {
+                fwrite(STDOUT, $formatted . PHP_EOL);
+            } else {
+                echo $formatted . "<br>";
+            }
+        }
+    }
+
+    private function colouriseText(string $msg, ?string $colour): string
+    {
+        switch ($colour) {
+            case Logger::BLACK:
+                return "<fg=black>$msg</>";
+            case Logger::RED:
+                return "<fg=red>$msg</>";
+            case Logger::GREEN:
+                return "<fg=green>$msg</>";
+            case Logger::YELLOW:
+                return "<fg=yellow>$msg</>";
+            case Logger::BLUE:
+                return "<fg=blue>$msg</>";
+            case Logger::MAGENTA:
+                return "<fg=magenta>$msg</>";
+            case Logger::CYAN:
+                return "<fg=cyan>$msg</>";
+            case Logger::WHITE:
+                return "<fg=white;bg=black>$msg</>";
         }
+        return $msg;
     }
 }