Command Mode and CLI Applications

This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against.

Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change.

This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Not every Quarkus application is a service that stays up waiting for requests. Command mode is Quarkus' support for the opposite shape: a process that starts, does one piece of work, and exits — a batch job, a migration script, an admin utility, a one-shot CLI tool. This page covers the @QuarkusMain/QuarkusApplication lifecycle that command mode runs on, building richer command-line interfaces with Picocli, packaging a CLI as a fast-starting native executable, running one as a JBang script, and how to recognize when command mode is the right fit for a given piece of work instead of an always-on HTTP or messaging service.

@QuarkusMain and QuarkusApplication

A command-mode application is a class that implements io.quarkus.runtime.QuarkusApplication and is marked as the application’s entry point with io.quarkus.runtime.annotations.QuarkusMain. Its single method, run, receives the process' command-line arguments and returns an int that becomes the process exit code:

package org.acme.cli;

import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;

@QuarkusMain
public class MyApp implements QuarkusApplication {

    @Override
    public int run(String... args) {
        System.out.println("Hello from command mode, args=" + java.util.Arrays.toString(args));
        // do the one-shot piece of work here, then return an exit code
        return 0;
    }

    public static void main(String... args) {
        Quarkus.run(MyApp.class, args);
    }
}

The standard Java main method does not do the work itself — it hands control to Quarkus.run(MyApp.class, args), which boots the Quarkus runtime (including the ArC/CDI container, so @Inject works normally inside run), invokes MyApp#run, and then shuts the runtime down once run returns.

Command-mode lifecycle vs. a long-running service

The default Quarkus application shape — an HTTP service, a messaging consumer — starts the runtime and then stays running, waiting for inbound requests or messages indefinitely; nothing about a normal request-response cycle tells the process to exit. Command mode inverts that: once QuarkusApplication#run returns, Quarkus proceeds straight to shutdown and the process ends. The three moments in a command-mode run are:

  1. The runtime starts up — static init and runtime init replay as usual, and the CDI container is fully up before run is invoked.

  2. run(String…​ args) executes — this is the entire useful lifetime of the application; whatever the tool is meant to do happens here (or in objects it invokes).

  3. The runtime shuts down and the process exits, using `run’s return value as the exit code.

There is no step 4 — nothing keeps the process alive after run returns unless run (or something it started) explicitly asks Quarkus to wait, which is exactly what Quarkus.waitForExit() is for.

Exit codes

The int returned from run becomes the operating system’s process exit code, which is what lets a command-mode Quarkus application integrate with anything that inspects exit status — shell scripts, CI pipelines, cron jobs, container orchestrators treating a non-zero exit as a failed job. Returning 0 signals success; any other value signals failure, and the caller decides what to do with it (retry, alert, fail the pipeline).

Quarkus.waitForExit()

Some command-mode applications kick off asynchronous work in run — a Vert.x event loop consuming messages, a scheduled job, a reactive pipeline — and need the process to stay alive after run itself has finished launching that work, rather than exiting immediately. Quarkus.waitForExit() blocks the calling thread until something else (a signal, a call to Quarkus.asyncExit(), or an unhandled failure) actually requests shutdown:

@QuarkusMain
public class MyLongRunningCliApp implements QuarkusApplication {

    @Override
    public int run(String... args) {
        // start a scheduler, a Vert.x verticle, a message consumer, etc.
        startBackgroundWork();

        // keep the process alive until something calls Quarkus.asyncExit()
        // or the process receives a shutdown signal
        Quarkus.waitForExit();
        return 0;
    }
}

This is the mechanism that lets a command-mode application behave like a long-running daemon when that is what the tool actually needs, without abandoning the @QuarkusMain/QuarkusApplication model that gives it a clean exit code on the way out.

Building CLIs with Picocli

For anything beyond a couple of positional arguments, the quarkus-picocli extension wires the Picocli command-line parsing library (see the Quarkus Picocli guide) into command mode, giving CLI applications subcommands, typed options, validation, and generated --help/--version output for free. Add it with:

./mvnw quarkus:add-extension -Dextensions=picocli

A Picocli command is a plain class annotated with @CommandLine.Command, declaring its options and parameters as annotated fields, and implementing either Runnable or Callable<Integer> (the latter lets the command return its own exit code):

package org.acme.cli;

import java.util.concurrent.Callable;

import picocli.CommandLine;

@CommandLine.Command(
        name = "greet",
        mixinStandardHelpOptions = true,
        version = "greet 1.0",
        description = "Greets the given name, optionally shouting it.")
public class GreetCommand implements Callable<Integer> {

    @CommandLine.Option(names = {"-u", "--uppercase"}, description = "Shout the greeting")
    boolean uppercase;

    @CommandLine.Parameters(index = "0", description = "The name to greet")
    String name;

    @Override
    public Integer call() {
        String greeting = "Hello, " + name + "!";
        System.out.println(uppercase ? greeting.toUpperCase() : greeting);
        return 0;
    }
}

quarkus-picocli supplies its own @QuarkusMain-annotated entry point that drives Picocli’s parser, so the application only needs to tell it which annotated class is the top-level command:

quarkus.picocli.top-command=org.acme.cli.GreetCommand

With that in place, java -jar target/quarkus-app/quarkus-run.jar World --uppercase parses --uppercase and the World parameter through Picocli and invokes GreetCommand#call, with the returned integer flowing through as the process exit code exactly as it would for a plain QuarkusApplication#run. Picocli subcommands are registered the same way Picocli itself supports — via @CommandLine.Command(subcommands = …​) on the top-level command — and CDI injection into a command works normally since it runs inside a fully-started ArC container.

Interactive shells with Aesh

Picocli covers one-shot commands: parse arguments, run, exit. When a tool instead needs an interactive, readline-style shell — a prompt that stays open, accepts commands one at a time, supports history and tab-completion — Quarkus' console support (quarkus-console, built on the Aesh library) is the piece to reach for instead. Aesh is Quarkus' interactive-console/readline layer, and it also powers the dev-mode terminal (the one that reacts to keystrokes like r to resume or q to quit while quarkus:dev is running); building a custom interactive shell on top of it is a more involved setup than a Picocli command and is out of scope for the minimal example above, but worth knowing about when a one-shot command is not the right interaction model.

Native executables for CLI tools

Startup time matters more for a CLI tool than almost any other Quarkus application shape: an HTTP service pays its startup cost once and then serves requests for a long time, but a command-mode tool pays that same cost on every single invocation — a migration script run from CI, a greet command invoked from a shell loop, an admin task run dozens of times a day all restart the JVM (or the native binary) from scratch each time. This is exactly the case native-image compilation is built for: a native executable’s near-instant startup turns a CLI tool that takes a perceptible fraction of a second to boot on the JVM into one that returns control to the shell essentially immediately. See Native Executables with GraalVM/Mandrel for how to build and package one.

Scripting with JBang

For quick, dependency-free scripting, Quarkus' JBang integration lets a single .java file declare its own Quarkus dependencies via //DEPS directives and run directly with jbang, with no pom.xml, no separate build step, and no manually managed classpath:

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS io.quarkus.platform:quarkus-bom:3.39.0@pom
//DEPS io.quarkus:quarkus-picocli

import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;

@QuarkusMain
public class greet implements QuarkusApplication {

    @Override
    public int run(String... args) {
        System.out.println("Hello, " + (args.length > 0 ? args[0] : "world") + "!");
        return 0;
    }

    public static void main(String... args) {
        Quarkus.run(greet.class, args);
    }
}

Running jbang greet.java World downloads the declared Quarkus artifacts (cached after the first run), boots the same command-mode lifecycle described above, and exits — useful for throwaway tooling and quick reproductions that do not warrant a full Maven/Gradle project.

When command mode is the right shape

Command mode fits work that has a natural beginning and end, rather than work that is naturally always-on:

  • Batch jobs — process a fixed input (a file, a table, a queue drained once), then stop.

  • Database migrations — run a fixed set of schema changes against a target database and exit with a status code CI or a deployment pipeline can check.

  • Admin and maintenance tools — one-off operational tasks (reindexing, cache warm-up, data backfills, ad hoc diagnostics) that an operator or a scheduler invokes on demand.

The contrast is with HTTP endpoints and messaging consumers, which are built to stay up indefinitely, serving an unbounded stream of requests or messages — there, Quarkus.waitForExit()-style blocking is not an exception, it is simply what the default application shape already does. Reach for command mode when the unit of work is a single invocation with a clear success/failure outcome; reach for the default service shape when the unit of work is "keep responding to whatever arrives next."