Skip to main content

CLI & Control Plane

The stem CLI complements the programmatic APIs—use it to inspect state, manage workers, and operate schedules and routing.

Inspect vs control​

  • Inspect commands read state (stem observe ..., stem worker stats) and are safe to run frequently.
  • Control commands mutate worker state (stem worker revoke, stem worker shutdown) and should be restricted to operators.
  • Broadcast commands (like stem worker ping) fan out to all workers unless you target a specific worker ID.
  • Use --namespace to ensure the CLI targets the same namespace as your workers.

Remote control primer​

The worker control commands (ping, stats, inspect, revoke, shutdown, pause, resume) publish control messages into the broker. Each command uses a request id and waits for replies on a per-request reply queue.

Targeting rules:

  • Broadcast (default): omit --worker to broadcast to every worker in the namespace.
  • Targeted: pass one or more --worker values to address specific worker ids only.

Inspect vs control semantics:

  • Inspect (ping, stats, inspect) returns snapshots and does not mutate worker state.
  • Control (revoke, shutdown, pause, resume) persists intent and asks workers to change behavior (terminate tasks, shut down, or pause queue consumption).

Payload highlights (as sent by the CLI):

  • ping/stats: command type with a targets list (defaults to *).
  • inspect: includes includeRevoked and the control namespace.
  • revoke: includes revocations plus requester and namespace.
  • shutdown: includes mode (warm, soft, hard).

Example usage:

# Broadcast to all workers
stem worker ping

# Target a single worker by id
stem worker stats --worker worker-a
Observe queues
const String observeQueuesCommand = 'stem observe queues';
Observe workers
const String observeWorkersCommand = 'stem observe workers';
Observe DLQ
const String observeDlqCommand = 'stem observe dlq';
Observe schedules
const String observeSchedulesCommand = 'stem observe schedules';
Ping workers
const String workerPingCommand = 'stem worker ping';
Worker stats
const String workerStatsCommand = 'stem worker stats';
Revoke a task
const String workerRevokeCommand = 'stem worker revoke --task <id>';
Shutdown workers
const String workerShutdownCommand = 'stem worker shutdown --mode warm';
Pause queues
const String workerPauseCommand = 'stem worker pause --queue default';
Resume queues
const String workerResumeCommand = 'stem worker resume --queue default';
Apply schedules
const String scheduleApplyCommand =
'stem schedule apply --file config/schedules.yaml --yes';
List schedules
const String scheduleListCommand = 'stem schedule list';
Dry-run a schedule
const String scheduleDryRunCommand = 'stem schedule dry-run --spec "every:5m"';

Task registry resolution for CLI commands​

Many CLI commands that reference task names need task metadata. That is a CLI concern, not the default application bootstrap path. The default CLI context does not load a task registry automatically, so wire it via runStemCli with a contextBuilder that sets CliContext.registry. For multi-binary deployments, ensure the CLI and workers share the same task-definition entrypoint so task names, encoders, and routing rules stay consistent.

A common pattern is to build that CLI registry from the same shared task list or generated stemModule your app uses, so task metadata stays consistent without teaching registry-first bootstrap for normal services.

If a command needs a registry and none is available, it will exit with an error or fall back to raw task metadata (depending on the subcommand).

For normal app bootstrap, prefer tasks: [...] or a generated stemModule. See Tasks and stem_builder.

List registered tasks​

stem tasks ls

Expected output:

NAME            DESCRIPTION        IDEMPOTENT  TAGS
email.send Sends an email yes notifications

Observe queues, workers, and schedules​

stem observe queues
stem observe workers
stem observe dlq
stem observe schedules

Expected output:

Queue     | Pending | Inflight
Worker | Active | Last Heartbeat
Queue | Task ID | Reason
Summary: due=... overdue=...

Requirements:

  • stem observe queues and stem observe dlq need a broker.
  • stem observe workers requires a result backend.
  • stem observe schedules reads from STEM_SCHEDULE_STORE_URL or a schedule file.

Worker control commands​

stem worker ping
stem worker stats
stem worker revoke --task <id>
stem worker shutdown --mode warm
stem worker pause --queue default
stem worker resume --queue default

Expected output:

ping: ok
stats: workers=... queues=...

Combine with programmatic signals for richer telemetry (see Worker Control CLI).

Scheduler​

stem schedule apply \
--file config/schedules.yaml \
--yes

stem schedule list
stem schedule trigger cleanup
stem schedule dry-run --spec "every:5m"

Run Beat from a Dart entrypoint wired to your schedule store:

lib/scheduler.dart
Future<void> startRedisBeat() async {
final brokerUrl =
Platform.environment['STEM_BROKER_URL'] ?? 'redis://localhost:6379';
final store = await RedisScheduleStore.connect('$brokerUrl/2');
final beat = Beat(
broker: await RedisStreamsBroker.connect(brokerUrl),
store: store,
lockStore: await RedisLockStore.connect('$brokerUrl/3'),
);

await applyScheduleFile(store, 'config/schedules.yaml');
await beat.start();
}

class EmailTask extends TaskHandler<void> {

String get name => 'email.send';


TaskOptions get options => const TaskOptions(queue: 'default');


Future<void> call(TaskContext context, Map<String, Object?> args) async {}
}

Requirements:

  • stem schedule apply/list/trigger/dry-run use STEM_SCHEDULE_STORE_URL when set, otherwise they operate on local schedule files.
  • schedule trigger also requires STEM_BROKER_URL and publishes one task without changing the recurring entry's next-run or execution counters.
  • Beat needs a broker, schedule store, and (for HA) a lock store.

Expected output (schedule list):

ID        | Task           | Queue    | Spec             | Next Run                | Last Run                | Jitter  | Enabled
----------+----------------+----------+------------------+------------------------+------------------------+---------+---------
cleanup | maintenance... | default | every:5m | 2025-01-01T00:05:00Z | 2025-01-01T00:00:00Z | 0ms | yes

Health checks​

stem health \
--broker "$STEM_BROKER_URL" \
--backend "$STEM_RESULT_BACKEND_URL"

The command exits non-zero when connectivity, TLS, or signing checks fail—ideal for CI/CD gates.

Backend requirements by command​

Use this table to sanity-check which connection strings are required:

CommandBrokerResult backendSchedule storeRevoke storeRegistry
stem tasks ls❌❌❌❌✅
stem observe queues✅❌❌❌❌
stem observe workers❌✅❌❌❌
stem observe dlq✅❌❌❌❌
stem observe schedules❌❌✅❌❌
stem worker ping/stats/inspect✅❌❌❌❌
stem worker statusoptional (follow)optional (snapshot)❌❌❌
stem worker revoke✅optional❌optional❌
stem worker shutdown✅❌❌❌❌
stem worker pause/resume✅❌❌optional❌
stem schedule apply/list/dry-run❌❌✅❌❌
stem schedule trigger✅optional✅❌❌
stem health✅optional❌❌❌

Notes:

  • The CLI resolves URLs from STEM_BROKER_URL, STEM_RESULT_BACKEND_URL, STEM_SCHEDULE_STORE_URL, and STEM_REVOKE_STORE_URL.
  • STEM_REVOKE_STORE_URL supports redis://, postgres://, sqlite:///, file:///, and memory:// targets.
  • When a backend is “optional”, the command still runs but will skip that slice of data (for example, worker heartbeats without a result backend).
  • Schedule commands fall back to local schedule files when no schedule store is configured.

Keep the CLI handy when iterating on the code examples in the feature guides.