Skip to content
These pages describe Stewart 1.x. Read the docs for the latest release, and how to upgrade.

Status and control

A running Stewart answers questions about itself through a control socket: a private connection that stewart status and the other commands on this page use. make status asks it:

Terminal window
make status # Stewart, its workers and its apps, as tables

It needs the control token from .env, which make setup creates:

Terminal window
STEWART_CONTROL__TOKEN=… # openssl rand -hex 16 makes a new one

Stewart refuses to start without a token while the socket is on. To run without the socket, set STEWART_CONTROL__LISTEN=off (the word: an empty variable means “not set”).

In a project from the skeleton, make app-pause APP=…, make app-resume APP=…, make app-reset APP=…, make worker-restart WORKER=… and make reload reach the running Stewart the way make status does. Otherwise, the commands on this page run inside the Stewart container:

Where Stewart runs Prefix
make up on your machine or a server checkout docker compose exec stewart stewart …
A Git or image server docker compose -f compose.git.yaml exec stewart stewart … (or compose.image.yaml)
Kubernetes kubectl exec deployment/<release>-stewart -- stewart …
Table Shows
Daemon Stewart’s version, uptime and resident memory; the Home Assistant connection and its last outage; whether the store is up; the mqtt row (the MQTT connection, its last outage, and messages queued or dropped); the component row (the Stewart integration in Home Assistant); the deploy row (the running commit and the last rejected one, in Git mode); the reload row (the last reload and how it went); the generated row (whether the generated classes still match Home Assistant); service calls waiting and refused.
Workers Whether each worker is running, how much resident memory it uses, how often it restarted, and whether it is falling behind (events waiting or dropped).
Apps Whether each app is running, paused (and by whom: config, control or http) or failed; when its handler last ran for an event (last handled) and when its next schedule or timer is due (next run); how many events and schedule runs it handled or skipped while paused; its exposed entities; how its service calls went.
Failures Each app’s last failure: when, where, the reason and the message.

The worker and app figures are refreshed every supervision.ping_interval (10 seconds by default), and keep adding up across worker restarts until Stewart itself restarts. stewart status --json holds every field.

Terminal window
make status APP=porch-light # APP="porch-light hall" for two

Name one or more apps (stewart status porch-light), and the Apps and Failures tables show only those, followed by two more tables:

Table Shows
Subscriptions Each entity, event, topic, MQTT filter and trigger the app listens to: how many events reached it (received), how many got through its operators to the handler (handled), how many a full queue dropped, and when the handler last ran
Schedules Each schedule and timer: what it is, when it runs next (due if the last report expected it already) and when it last ran

A subscription that receives but never handles has an operator that stops its events; trace the app to see which. An ID no app has fails, and names the closest one. --json narrows the same way.

Terminal window
make status WATCH=1 # WATCH=5s for another interval; also with APP=

stewart status --watch redraws every 2 seconds (--watch=5s for another interval) until Ctrl-C. While Stewart is down or restarting, it says so and keeps trying. With --json, it prints one snapshot per line. The worker and app figures still change only once per ping_interval.

A paused app stays loaded, but receives nothing and skips its schedule runs until it is resumed. A handler already running finishes. Nothing is replayed on resume.

Terminal window
docker compose exec stewart stewart app:pause porch-light # App porch-light paused.
docker compose exec stewart stewart app:resume porch-light # App porch-light resumed.
docker compose exec stewart stewart app:reset porch-light # Forgets the pause; stewart.yaml decides again.

Repeating a command is harmless and still succeeds. An unknown app, or one switched off with apps.<id>.enabled: false, fails. To start an app paused, set apps.<id>.paused: true.

With persistence set up, a pause or resume is stored and survives restarts, and it wins over apps.<id>.paused. app:reset forgets the stored choice, also for an app that no longer exists. Without a store, the change still applies, and a Not saved: warning says it lasts until Stewart restarts.

All three accept the options described under Another host.

Terminal window
docker compose exec stewart stewart worker:restart 0 # Worker 0 is restarting. Workers are numbered from 0.

The number comes from the Workers table. A running worker gets shutdown_grace to stop its apps, then a new one starts. A worker that Stewart gave up on after repeated crashes (quarantined), or one waiting to retry, starts at once, with its crash count reset. The command returns once the restart has begun; stewart status shows the worker stopping, then spawning.

Terminal window
docker compose exec stewart stewart reload # Reload started; stewart status shows how it went.

A reload restarts every worker on the code as it is now, so edited apps, services.php and regenerated classes take effect. It also applies stewart.yaml: apps added to or removed from apps/, and apps.<id> options, enabled, paused and worker, as well as workers. The connection to Home Assistant stays open, and no states are fetched again. Exposed entities are briefly unavailable while their worker restarts, as on worker:restart; those of a removed app are deleted after the usual grace when expose.prune is on.

It first checks the project in a separate process, as stewart check does. If that fails, nothing changes, and the reload row in stewart status shows the first error. Restarts a reload makes do not count toward a worker’s restarts, and a quarantined worker comes back. A second reload while one runs is refused.

Every other setting, such as home_assistant, persistence, mqtt or log_level, keeps the value Stewart started with: the reload still applies the rest, and the reload row says partly applied and names what needs a restart. .env, a changed composer.lock and a new Stewart version need a restart too. docker compose kill -s HUP stewart (or kill -HUP on the Stewart process) reloads as well, and so does POST /api/reload on the admin API.

During development, make watch (stewart run --watch) reloads on its own. Every second it looks at the directories the project’s composer.json autoloads (not autoload-dev, so editing a test changes nothing), services.php and stewart.yaml, and reloads once a change has stopped changing. A changed .env or composer.lock is only logged, since it needs a restart.

Terminal window
docker compose exec stewart stewart status --json | jq '.snapshot.apps[] | {id, state, counters}'

The JSON holds everything the tables show, and more. The command exits with 1 when Stewart is down or the token is wrong, so it also works as a health check.

Key Holds
apps[] Each app, with last_handled_at (when its own handler last ran for an event) and next_run_at (its earliest schedule or timer); counters.handled counts events that got through the operators
subscriptions[] Every entity, event, topic, MQTT filter and trigger each app listens to, with received (reached the subscription), handled (reached the handler), dropped, last_received_at and last_handled_at
schedules[] Every schedule and timer, with its description, next_run_at and last_run_at

received without handled means an operator such as filter() stopped the events; tracing shows which ones. The figures come from each worker’s last answer to its liveness probe, so they trail by up to supervision.ping_interval, and with ping_interval: off they never arrive. A subscription’s figures start again when its worker restarts, because it subscribes again under a new ID; the app’s own figures keep adding up. A runAtEntityTime() shows up as one subscription, which follows the entity, and one schedule.

For Docker, Kubernetes or a monitoring tool, Stewart can answer health checks over plain HTTP, without the control token. The port is off by default:

http:
listen: tcp://0.0.0.0:8080 # an IP address, not a host name; tcp://[::]:8080 for IPv6

Publish the port in your compose file (ports: ["8080:8080"]) to reach it from outside the container.

Path 200 503
/healthz Stewart answers never
/readyz Connected to Home Assistant, at least one worker running its apps, no worker quarantined Still connecting or reconnecting, no worker running yet, or a worker quarantined
Terminal window
curl -s localhost:8080/readyz # {"status":"not_ready","reasons":["Home Assistant is connecting"]}

The body is {"status":"alive|ready|not_ready","reasons":[…]}. The port opens only after Stewart has reached Valkey at start. The Helm chart turns it on and uses it by default; stewart status --probe=liveness or --probe=readiness gives the same answers through the control socket.

A second HTTP port lists, pauses and resumes apps, and restarts workers, for scripts and Home Assistant. It is off by default and always needs a bearer token:

http:
admin:
listen: tcp://127.0.0.1:8081 # an IP; 0.0.0.0 to reach it from other hosts

Set the token from the environment, STEWART_HTTP__ADMIN__TOKEN; the daemon refuses to start without one, and stewart check and stewart doctor say so first. Stewart logs a warning when the API listens off loopback, because anyone with the token can pause apps.

Request Answer
GET /api/apps {"apps":[{"id","state","paused","pause","config_pause_override","last_handled_at","next_run_at"}, …]}
GET /api/apps/{id} One app, same shape
POST /api/apps/{id}/pause, /resume, /reset {"changed","message","warning"}, as app:pause, app:resume, app:reset
POST /api/workers/{id}/restart The same shape, as worker:restart
POST /api/reload The same shape, as reload
Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" localhost:8081/api/apps/porch/pause
# {"changed":true,"message":"App porch paused.","warning":null}

Repeating a change answers 200 with changed: false. A missing or wrong token is 401 and logs a warning (the first, then every 100th), an unknown app or worker 404, a disabled app or a daemon shutting down 409, the wrong method 405; failures carry {"reason","message"}. Changes are recorded with source http, so stewart status shows paused by http. There is no CORS: a browser page on another origin cannot send the token. From Home Assistant, a rest_command calls it:

rest_command:
stewart_pause:
url: "http://home-stewart.default.svc:8081/api/apps/{{ app }}/pause" # the Helm chart's Service; on Docker, the host
method: post
headers:
authorization: !secret stewart_admin_bearer # "Bearer <token>"

A third HTTP port serves only GET /metrics, so Prometheus needs neither the admin API nor its token. It is off by default:

http:
metrics:
listen: tcp://0.0.0.0:9464

Without a token, anyone who reaches the port reads the metrics, as with most exporters; Stewart logs that at start. Set STEWART_HTTP__METRICS__TOKEN to require a bearer token, which can only read metrics. A missing or wrong token is 401, any other path 404, and anything but GET or HEAD 405. GET /metrics on the admin port still answers too, under the admin token. Each HTTP port needs a port of its own; the daemon refuses to start when two share one.

The answer is the Prometheus text format (every series is in the metrics reference), built from the same snapshot as stewart status. Series are named stewart_*. Per-app series have app and worker labels, per-worker series have worker, and service-call latency is the histogram stewart_app_service_call_duration_seconds{outcome}. Values that are not known yet, such as a worker’s memory before its first probe, are left out instead of reported as 0. While deploy.git.poll is on, stewart_deploy_info{commit} names the running commit and stewart_deploy_failures_total counts commits that failed stewart check. Worker counters arrive with each liveness probe, so they trail by up to supervision.ping_interval.

scrape_configs:
- job_name: stewart
static_configs:
- targets: ["stewart.home.lan:9464"]

With a token, Prometheus reads it from a file:

scrape_configs:
- job_name: stewart
authorization:
credentials_file: /etc/prometheus/stewart-metrics-token
static_configs:
- targets: ["stewart.home.lan:9464"]

Listen on TCP, and give both sides the same token:

control:
listen: tcp://0.0.0.0:7333
Terminal window
stewart status --address tcp://homeassistant.local:7333 --token …

With both --address and --token given, no stewart.yaml or .env is read, so the command runs on a machine that has neither. Stewart logs a warning when it listens off loopback: anyone with the token reads the runtime snapshot and can pause apps.

Docker Desktop on macOS or Windows cannot share a unix socket through a bind mount, so make status from the host needs tcp:// there too.