maestro-cli cue engine, with the same triggers, agents and chains as the desktop, with no window and no one logged in. An exported agent can also be run on demand with maestro-cli send, playbook, run-doc and goal-run.
This page goes in order: install, sign agents in, import, check, start, run an agent on demand, put webhooks behind a proxy, and operate the server.
Before you start
- Agents run unattended with full permissions. Cue always runs agents in YOLO mode, and on a server nobody watches the run. Give the engine its own machine or container, the dedicated
maestrouser both installs create, and workspaces that hold nothing else. - Secrets never travel in a bundle. A bundle carries the names of the secrets its agents and webhooks need, never the values. You supply each value on the server.
- Sign agents in on the server. Each provider is signed in on the server, either with an API key supplied as a secret or by running that provider’s own login there, as the
maestrouser. Do not copy a login from your desktop. - Claude agents run in API mode. The server bundle does not include the driver for Claude’s TUI mode, so a Claude agent exported in TUI or Dynamic mode runs
claude --printon the server. - Webhooks stay on loopback. The webhook listener is never exposed directly. Public delivery goes through a reverse proxy or a tunnel (see Webhooks behind a proxy).
- One engine per data directory. Do not point two servers or containers at the same data directory or volume.
bundle validate would reject for how it is written, so a broken config never reaches the server only to fail there. The check covers each exported cue.yaml as written into the bundle: the same config rules, plus subscription names with :, heartbeats under a minute and command.mode: cli subscriptions. What depends on the rest of the bundle, such as an owner it leaves out (see Agents that share a workspace), is still exported and reported by bundle validate and the import. It looks only at the subscriptions being exported, so a broken subscription of another pipeline in the same cue.yaml does not block this one. A refusal writes no file, exits 1 (BUNDLE_INVALID with --json, every problem in details.errors) and lists every problem with its workspace and subscription, for example:
maestro-cli bundle inspect pipeline.zip lists the bundle’s workspaces, agents and the secrets it needs. See Sharing Pipelines and Agents for what a bundle holds.
Build the server bundle
From a Maestro checkout:dist/maestro-server-<version>.tgz: the CLI, the prompt files it loads, the systemd unit, and the installer. The SQLite driver the engine needs is installed on the target, so the bundle works on both x86_64 and arm64.
The container build runs this step itself.
Install
Option 1: Linux VM with systemd
Debian 12, Ubuntu 22.04, or newer.--agent-cli replaces the default, so list Claude Code too when you add another agent CLI. For Claude Code and OpenCode:
maestro user, and picks one Node.js for everything: the one on PATH when it is 22 or newer and the maestro user can run it, otherwise Node.js 24 from nodejs.org. With that Node.js it installs Claude Code, the SQLite driver and the CLI, then installs the maestro-cue service without starting it.
Before its first npm install, the installer checks that npm can fetch from its registry, with npm’s own registry, proxy and CA settings (npm config, NPM_CONFIG_*, HTTPS_PROXY). A registry that cannot be reached stops the install within 30 seconds with exit status 1 and a message naming the registry, the proxy if there is one, and npm’s error code, for example:
The installer exits 0 when done, 1 on an error, and 3 when everything was installed but
--enable was refused because nothing is imported yet. It never exits with anything else: a failed step (an apt-get or npm error, say) or an interrupt also exits 1, after the step’s own output and a line with its original status.
Run every
maestro-cli command as the maestro user (sudo -H -u maestro maestro-cli ...). The wrapper refuses to run as root, because files root writes into the data directory are unreadable to the engine.
Option 2: Container
Build the image from the repository root:
The image runs as the
maestro user under tini, with git and the GitHub CLI. Its entrypoint is maestro-cli, so docker run maestro-cue <verb> runs one CLI command. It has two volumes:
Use named volumes (
maestro-home and maestro-work below). They outlive the container, so logins and imported agents survive a rebuild of the image.
packaging/server/compose.yaml has the same setup for Docker Compose. Put maestro.env beside it and run docker compose up -d --build from that folder once the steps below are done. It gives the container and the volumes the names this page uses (maestro-cue, maestro-home and maestro-work), so the docker run, docker exec and docker stop commands here work on a Compose stack as written.
Compose reads AGENT_CLIS from your shell or from a .env file beside compose.yaml (not from maestro.env, which goes to the container), and builds Claude Code alone when it is unset:
.env.
The fixed names mean one stack per host from this file. For a second engine on the same host, copy compose.yaml and change container_name, both volume name: values and the published webhook port: each engine needs its own data volume, and a second engine on the same one exits at once (see “One engine per volume” under Start in a container). If the host already has a maestro-home or maestro-work volume from a docker run setup, Compose uses it, with a warning that it did not create it.
A stack created before these names were fixed has server_maestro-home, server_maestro-work and server-maestro-cue-1. The next docker compose up replaces the container with maestro-cue and mounts new, empty maestro-home and maestro-work volumes; the old volumes are kept but no longer used. Copy them over once, before that up:
Sign agents in
Each agent needs its provider’s credential on the server. There are three ways to supply one; the table under Per provider says which apply to each provider.Declared secrets
When an agent sets a secret-looking variable on the desktop (a name containingKEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, AUTH or SESSION, or a value shaped like a known credential), the export keeps the name and drops the value. A Claude Code agent also declares every secret ${VAR} its workspace’s .mcp.json uses. bundle inspect lists them, and bundle validate --check-env on the server says which are set.
The engine looks each declared secret up in this order and gives the value to that agent alone:
$CREDENTIALS_DIRECTORY/<NAME>, a systemd credential (LoadCredential=)/run/secrets/<NAME>, a Docker or Compose secret- the environment variable
<NAME>
/run/secrets/<NAME> (see the commented secrets: block in compose.yaml), or pass it in the environment. Compose mounts a file secret with the host file’s owner and mode, and the engine runs as maestro (uid 999), so a file only you can read (mode 0600) is reported as unreadable. Keep the file in a folder only you can open, and make the file itself readable:
/etc/maestro/credentials/ANTHROPIC_API_KEY does nothing for an agent that did not declare ANTHROPIC_API_KEY; use a variable for that agent instead.
Variables server mode lets through
The unit and the image setMAESTRO_SERVER_MODE=1, so a Cue agent does not inherit the service’s whole environment, only:
- system variables (
PATH,HOME,USER,LANG,TMPDIRand the like), proxy and TLS trust variables (HTTPS_PROXY,NODE_EXTRA_CA_CERTSand the like), and common toolchain homes; - every name starting with
LC_,XDG_,SSH_orMAESTRO_; - the provider names Maestro knows:
ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URL,ANTHROPIC_MODEL,CLAUDE_CONFIG_DIR,CLAUDE_CODE_MAX_OUTPUT_TOKENS,MAX_THINKING_TOKENS,CLAUDE_CODE_USE_BEDROCK,CLAUDE_CODE_USE_VERTEX,CODEX_HOME,OPENAI_API_KEY,OPENAI_BASE_URL,OPENCODE_CONFIG,OPENCODE_CONFIG_CONTENT,FACTORY_API_KEY,FACTORY_PROJECT_DIR,FACTORY_DISABLE_KEYRING,FACTORY_LOG_FILE,COPILOT_HOME,COPILOT_GITHUB_TOKEN,GH_TOKEN,GITHUB_TOKEN; - the names you list, comma separated, in
MAESTRO_SERVER_ENV_ALLOW; - the agent’s declared secrets.
GEMINI_API_KEY, or the AWS or Google credentials behind CLAUDE_CODE_USE_BEDROCK and CLAUDE_CODE_USE_VERTEX) needs a line such as:
/etc/maestro/maestro.env (sudo nano /etc/maestro/maestro.env, one KEY=value per line). In a container they go in an env file passed with --env-file maestro.env, or env_file in Compose.
Cue action: command shell steps receive the same allowlist and no declared secrets. Name a variable in MAESTRO_SERVER_ENV_ALLOW if a step needs it.
Provider logins
A login runs as themaestro user and is stored in its home, /var/lib/maestro. On a VM that directory survives restarts and upgrades; in a container it is the maestro-home volume, which survives rebuilds of the image.
On a VM, open a shell as the maestro user and run the login there:
localhost cannot finish on a server you reach over SSH.
Per provider
Cue can run every provider that has an output parser. Gemini CLI and Hermes have none:cue engine check reports an unsupported-provider gap for their agents, and the engine does not run them.
Readiness looks for each provider’s binary on PATH; bundle import --agent-path <agent id>=<path> points it at another location.
Notes:
- Claude Code. An API key takes precedence over a login. For Bedrock or Vertex, set
CLAUDE_CODE_USE_BEDROCK=1orCLAUDE_CODE_USE_VERTEX=1and add the cloud credentials’ names toMAESTRO_SERVER_ENV_ALLOW. The installer and the image install Claude Code by default. - Codex. Whether a given Codex version reads
OPENAI_API_KEYfrom the environment on its own is not verified here. Codex documents storing a key with its login instead,printenv OPENAI_API_KEY | codex login --with-api-key, run as themaestrouser (not verified here). Either way the result is kept in~/.codex/auth.json. - Copilot-CLI.
GH_TOKENandGITHUB_TOKENare also whatghand Cue’s GitHub triggers use, and inmaestro.envthey reach every agent. Give Copilot its ownCOPILOT_GITHUB_TOKEN, and keep Cue’s GitHub token in a credential file (next section). - Rows marked “Not verified”. Maestro knows the login command, but not which variable the CLI reads or where it stores the login. Check that provider’s documentation, then prove the sign-in with one turn (see Run an exported agent on demand).
GitHub for Cue triggers
github.* triggers call gh. Cue hands gh a token from GH_TOKEN or GITHUB_TOKEN, looked up the same way as a declared secret (credential file, /run/secrets, environment); without one, gh uses its own login.
- A token file keeps the token away from agents:
/etc/maestro/credentials/GH_TOKENwithLoadCredential=GH_TOKEN:/etc/maestro/credentials/GH_TOKEN, or aGH_TOKENDocker secret. It is read again on every poll, so a rotated file takes effect without a restart, and it takes precedence overgh auth login. - A login:
sudo -iu maestrothengh auth login, ordocker exec -it maestro-cue gh auth login. It is kept in themaestrouser’s home. GH_TOKENinmaestro.envworks too, but every agent receives it.
gh pr list and gh pr view --json comments for github.pull_request, gh issue list and gh issue view --json comments for github.issue, and gh api repos/<owner>/<repo>/issues/events for github.label. The comment reads run only with retrigger_on_comments. Read access to the watched repositories is enough:
No write permission is needed, and neither is Contents. With a fine-grained token,
github.pull_request needs Pull requests and github.issue and github.label need Issues (Metadata is always granted); granting both covers every trigger. The classic public_repo scope grants write access and adds nothing for these reads.
A GitHub trigger with a webhook block still needs the token: the engine keeps polling as a reconcile, and a comment delivery on a pull request makes it poll at once to fetch the whole pull request. The webhook secret only verifies deliveries.
The permissions are derived from GitHub’s table of the endpoints each fine-grained permission covers, matched to the calls above. What was tested: every call ran with gh 2.101 against a public repository, using a token with the broader repo scope. A fine-grained token and a classic token with no scope have not been tried.
Import
Each workspace in a bundle is bound to a folder that must already exist, usually a clone of the project. Each workspace needs its own folder: mapping two workspaces to the same folder (directly or through a symlink) is refused, since both would write the samecue.yaml. A workspace folder may sit inside another one’s, as long as no two bundle files land on the same path. bundle inspect lists the workspace keys. bundle import --dry-run prints the plan, including every shell command the bundle runs, and writes nothing.
Import on a VM
maestro user, so copy it somewhere like /tmp first.
Import in a container
Check
cue engine check checks everything the engine needs to run the imported subscriptions, without starting it: each agent’s binary, declared secrets and workspace, cue.yaml, webhook secrets, gh and a repo: on every GitHub trigger, and that there is something to run. It lists every gap and exits 0 when ready, 1 when not.
-
Secrets in credential files. On a VM,
LoadCredential=files are visible to the service alone, so a check from a shell reports them as missing. The service runs the same check when it starts (--require-ready) and logs every gap it still finds. To run the check with the service’s credentials before you start it, usesystemd-runwith the unit’s user, env file and oneLoadCredential=per drop-in line: -
Disabled subscriptions are ignored. A subscription with
enabled: falseadds no gap: it needs nogh, webhook secret, provider binary or target agent until it is enabled. Acue.yamlthat does not parse is still reported. -
One owner per workspace. When several agents share a workspace, only the owner runs its unpinned subscriptions, so only the owner is checked for them: the agent named by
settings.owner_agent_id(id, or display name), else the first agent listed. Ifowner_agent_idmatches no agent there, or more than one by name, the engine runs none of them and the check reports it as acue-configgap naming the setting and the workspace. A subscription withagent_idruns only on that agent, and only when the agent’s own workspace holds it; one pinned to an agent in another workspace is reported asunknown-agent. An exported agent that does not own its workspace brings only the subscriptions pinned to it and noowner_agent_id, so it runs on the server what it ran on the desktop (see Agents that share a workspace). -
Nothing to run. A data directory with no agents, or with agents but no enabled subscription on any of them, has a
nothing-to-rungap: the check exits 1,--require-readyrefuses to start, and/readyzanswers 503. That is almost always a bundle that was never imported. An exported agent brings its own subscriptions, if it had any. A server that only runs agents on demand needs no engine: leave the service off. -
install.sh --enableruns the check before it enables the service. Onnothing-to-runit still installs everything, leaves the service disabled, says why, and exits 3. Any other gap is left to the service, which judges secrets in its own environment. -
Not checked: whether a provider or
ghis signed in. Run one turn to prove a sign-in (see Run an exported agent on demand).
Start
Start on a VM
maestro-cli cue engine start --data-dir /var/lib/maestro/data --status-port 7433 --require-ready --log-format json --drain-timeout 90 as the maestro user, with /etc/maestro/maestro.env as its environment.
- It is ready (
systemctl statusshowsactive) once agents are loaded, the lock is held and triggers are armed. A missing agent binary, secret orghstops the start, and the log lists every gap.Restart=on-failurethen tries again every 5 seconds, logging the gaps each time, until you fix them or runsudo systemctl stop maestro-cue. Start-up may take up to 60 seconds. - A watchdog restarts it if the engine stops answering for 30 seconds or loses its lock to another engine (the journal shows
Watchdog timeoutand the engine is killed withSIGABRT), and it restarts after a crash, 5 seconds later. When the engine dies either way, systemd kills the rest of the service with it, including agent turns and shell steps still running; the restarted engine marks those runs failed and runs what was queued. - It never writes a core dump (
LimitCORE=0), and neither do its agents, which inherit the limit. The engine holds webhook secrets, tokens and provider keys in memory, and a core file would put them on disk; the watchdog’sSIGABRTabove would otherwise leave one. - It runs with a read-only system. Only
/var/lib/maestroand/srv/maestroare writable. For workspaces elsewhere, add a drop-in withsudo systemctl edit maestro-cue:
Start in a container
- Stop timeout. The engine drains active runs for up to 90 seconds on stop, but
docker stopwaits only 10 seconds unless the container was created with a longer stop timeout. The--stop-timeout 120above and Compose’sstop_grace_period: 120sset one, so a plaindocker stopof either container drains in full; for a container created without it, usedocker stop -t 120. After 10 seconds Docker kills the engine in the middle of the drain (exit code 137): the run in flight is marked failed at the next start, and the lock is left behind (see “Restart after a hard kill”). The image runstiniwithout-g, so the stop signal reaches the engine alone and runs in flight can finish. - No core dumps.
--ulimit core=0(ulimits: core: 0in Compose) keeps a crash from writing the engine’s memory, with its secrets and keys, to disk. The image cannot set it; Docker’s default comes from the daemon and often allows core files. Add it to any otherdocker runthat is given secrets, such as an on-demand run. - One engine per volume. A second engine started on the same data volume, in another container, sees the first one’s lock while its heartbeat is fresh, logs that a Cue engine in another PID namespace (another container) holds the lock, and exits 1.
- Restart after a hard kill. If the engine is killed without stopping (
docker kill, out of memory, a host crash), its lock stays behind. When the new engine can tell the lock was left by an earlier process with its own process id in its own PID namespace, it takes the lock at once. A container restarted by its restart policy normally gets both back (same namespace number, same process id), so that is the usual case. When it cannot tell, the lock may belong to a live engine in another container, so it waits for that lock to go quiet: it refuses to start for up to 3 minutes, says when the lock frees, and the restart policy brings it back after that. - Health. The image’s health check calls
/healthzon the engine’s status port, 7433, inside the container.docker psshowshealthyonce it answers. If the engine stops answering, the container turnsunhealthyafter three failed checks (about 90 seconds), but Docker does not restart it: a restart policy acts only when the container exits. Restart it yourself (docker restart -t 120 maestro-cue), or run it under something that acts on health. - Webhook port. Inside the container the webhook listener listens on all interfaces so the port can be published. Publish it to the host’s loopback only (
127.0.0.1:17997:17997), as above.
Run an exported agent on demand
An imported agent can be run from the command line, with or without the engine running. These commands run one agent once and return; the engine runs subscriptions whenever their triggers fire.<agent> is the agent’s id or a unique id prefix (list agents shows them). send --read-only runs the turn in plan mode. playbook, run-doc and goal-run take --json for one JSON event per line, --model and --effort for this run only, and --no-history to write no history entry.
On a VM, a command you type gets your shell’s environment, not the service’s. This shell function runs maestro-cli as the maestro user with the env file loaded:
mrun lives in your shell session only; nothing installs it. The env file is read as shell code here, so quote any value in it that has spaces or $. An agent signed in with a provider login needs none of this.
In a container, docker exec runs with the container’s environment and secrets:
docker run --rm instead:
- Environment. An on-demand run passes the command’s whole environment to the agent; server mode filters only what the engine passes. Declared secrets are looked up the same way, but on a VM the service’s
LoadCredential=files are not visible to the command, so supply those as variables or use a provider login. - Records. Each turn is recorded in the data directory’s run ledger with its token usage.
playbook,run-docandgoal-runalso write history entries;sendwrites none. - No triggers. Nothing fires on its own. To fire a subscription by hand while the engine runs, use
maestro-cli cue trigger <subscription-name>(in a container,docker exec maestro-cue maestro-cli cue trigger <subscription-name>). Like a real tick, it also fires every subscription in the same pipeline with identical trigger settings; give a subscription a pipeline of its own, or different trigger settings, to fire it alone (see Triggering a Subscription).
Webhooks behind a proxy
webhook.received subscriptions, and GitHub triggers that take webhooks, are served by one listener on 127.0.0.1:17997 at /cue/<path>. Do not expose that port. Put a reverse proxy or a tunnel in front of it that:
- forwards
POST /cue/...tohttp://127.0.0.1:17997with the path unchanged; - passes the body byte for byte, since signatures are computed over the raw body;
- passes the signature header (
X-Hub-Signature-256for GitHub, or thesignature_headerthe subscription names),X-Maestro-Cue-SecretorAuthorizationfor senders that present the secret, and the delivery id headersX-GitHub-Delivery,X-Request-IdandX-Maestro-Delivery; - does not set
X-Request-Iditself. The listener uses it as the delivery id when the sender sends noX-GitHub-Delivery, and a new id on every request defeats the protection against repeats; - allows a body of 1 MiB, the listener’s cap (it answers
413above that); - waits long enough for the answer. The listener answers only after it has handled the delivery (see What a 2xx means). 60 seconds is ample; GitHub itself gives up after 10.
webhook.received and GitHub webhooks in Cue Event Types for the subscription side.
nginx
proxy_pass has no path after the port, so nginx passes the request path as the client sent it. nginx passes the request headers and the body unchanged by default.
Caddy
A tunnel
With no public address, a tunnel carries deliveries to the loopback port. For a quick test with cloudflared or ngrok:/cue/<path>. For a permanent setup, use a named cloudflared tunnel with an ingress rule from your hostname to http://127.0.0.1:17997. Their body limits are above 1 MiB, so the listener’s 413 is the limit that applies.
The container case
The container publishes the listener to the host’s loopback (127.0.0.1:17997:17997), so a proxy or tunnel on the host uses the configurations above unchanged. A proxy running in another container on the same Compose network can reach http://maestro-cue:17997 instead, and then the port need not be published at all.
Test it
Sign a body with the subscription’s secret and send it through the proxy:signature_header, send the secret in X-Maestro-Cue-Secret (or Authorization: Bearer <secret>) instead of the signature.
What a 2xx means
Maestro acknowledges a delivery (answers2xx) only after it has handled it: its run has started or is queued, or it was dropped on purpose (a filter that does not match, or a SusFactor block, which is recorded). Once a sender has a 2xx:
- A crash (
kill -9, out of memory, a host reboot) followed by a restart loses nothing. A queued delivery runs after the restart; a run that was in progress is marked failed at the next start. systemctl stopanddocker stopdrain: a delivery still being checked when the stop begins is finished and queued for the next start, within the drain timeout.- A redelivery of the same id fires nothing for 24 hours, and a GitHub change seen by both a webhook and a poll fires once.
- A
cue.yamlchange while the delivery waits (a hand edit,maestro-cli cue model) keeps it queued while its subscription is still in the file and switched on. If the change removes, renames or switches off that subscription, the delivery is dropped, logged (Dropped N queued event(s) ... on a cue.yaml reload) and recorded as never run. A change that fails to parse keeps the queue until the next good save.
2xx, nothing is recorded. A crash while Maestro is still checking a delivery leaves the sender without an answer, and its retry fires once. GitHub does not retry on its own; the GitHub trigger’s poll finds the change instead, or use Redeliver.
Limits
- From the stop signal until the engine exits, including after a second stop signal cuts the drain short, the listener stays open: a delivery to a path it listens on (with the right secret or signature) is answered
503withRetry-After: 30and nothing is recorded, so the sender should retry. The listener closes as the engine exits. A stopped engine has no listener at all, and the connection is refused (a proxy answers502). - The SusFactor check runs before the answer, so a slow 0DIN call delays it (each call gives up after 3 to 5 seconds). A sender that stops waiting first (GitHub waits 10 seconds) records a failure although the delivery runs; its retry is then a duplicate and fires nothing.
- A full queue drops its oldest event, and
queue_size: 0drops a delivery that arrives while the agent is busy. A subscription with no prompt, or a fan-out target that does not exist, does not run. Each of these is logged, and the delivery still counts as handled. - After a crash (not a drain), a queued delivery that has waited longer than its subscription’s
timeout_minutesby the time Maestro is back is dropped as stale and shown as timed out in the activity log. A drain stamps the queue so that rule does not apply. - The delivery id is recorded in the same step that hands the delivery on. A crash at exactly that instant can let one redelivery fire again.
- A sender that sends no delivery id header (
X-GitHub-Delivery,X-Request-IdorX-Maestro-Delivery) gets no protection against repeats.
Operate
Health and status
The engine answers on127.0.0.1:7433, reachable only from the same host (or, for the container, from inside it). Never proxy this port: its endpoints have no authentication.
maestro-cli cue engine status and maestro-cli cue engine inspect report the engine’s lock, heartbeat and event count from the data directory. They open the database read-only and change nothing on disk, so they are safe to run next to a live engine, as another user too. If the database cannot be read, they still show the lock and say why the figures are missing.
Logs
The engine logs one JSON object per line. On a VM they go to the journal (journalctl -u maestro-cue), in a container to docker logs maestro-cue. Every line the engine prints is JSON, so a log shipper can parse all of them. On a VM, journalctl -u maestro-cue also shows systemd’s own lines about the unit (Starting, Stopped and the like), which are plain text; journalctl _SYSTEMD_UNIT=maestro-cue.service -o cat shows the engine’s lines alone. Each line has timestamp, level (debug, info, warn or error) and message, and a line about a run also carries its runId, subscriptionName, pipelineId and sessionId. Prompt text and trigger payloads are never logged.
Debug output (the [CueDebug] lines printed when a cue.yaml reloads) is left out of the JSON logs. To see it, run the engine by hand with --log-format text instead of --log-format json, which prints the debug lines as plain text. MAESTRO_CUE_DEBUG=0 turns them off in text mode too.
Stop
sudo systemctl stop maestro-cue and docker stop -t 120 maestro-cue drain: new events are queued, active runs get up to 90 seconds to finish, and whatever is queued runs after the next start. A run still starting when it is stopped (loading, or waiting on its SSH connection) is cancelled and never starts its agent, and the engine waits for it before it exits. A second stop signal cuts the drain short. A script that stops the engine with maestro-cli cue engine stop should pass --wait-ms 120000, since that command waits only 5 seconds by default.
Sleep and pause
If the host sleeps or the engine is paused (a VM suspend, a closed laptop lid,kill -STOP) for 2 minutes or more, the engine notices within 30 seconds of waking and catches up: each interval (time.heartbeat) and scheduled (time.scheduled) trigger that came due during the gap runs once, however many times it came due, and GitHub triggers poll straight away. A trigger whose own timer already ran after the wake is not run a second time, and an interval trigger’s next run comes one interval after its catch-up. The log shows one Sleep detected line with the length of the gap. A clock set backward catches up nothing, and neither does an engine that is stopping.
Add or replace agents and pipelines
Import needs the engine stopped: it refuses while one is running against the data directory. The engine reads its agents and theircue.yaml files when it starts, so the next start picks up every agent and subscription the import added, changed or removed. Stop the engine, import, check, and start it again:
--force to replace it with the bundle’s version. Sign in any new provider before the start.
An import never deletes an agent or a subscription, but a --force import that maps an agent to a different workspace leaves the old workspace’s subscriptions behind. Work still queued from the last run for an agent or subscription that is gone is dropped at start with a warning in the log and an entry in the run history, never run, and so is any half-finished fan-in that waits on it.
Do not open the desktop app on the server’s data directory
The engine reads its agents when it starts and does not watch for changes. A Maestro desktop app opened on the same data directory while the engine holds it cannot start its own Cue engine (it logs that another engine holds the lock), but it still saves the changes you make to agents. The server engine picks those up only after it restarts. Change agents on a server by stopping the engine, importing and starting it again, as above, and do not run the desktop app against the server’s data directory.Upgrade
VM. Unpack a newer bundle and run itsinstall.sh again. The data directory, workspaces, env file, credentials and logins are kept, and a running service is restarted on the new version. The installer replaces the unit file, so keep your changes in drop-ins.
Container. Rebuild the image and replace the container; the volumes keep the data directory and the logins.
docker run -d command as in Start. With Compose, docker compose up -d --build does both (with the same AGENT_CLIS, if you set one).
Known limits
What an operator can run into today. The full, current list, including what has not been verified, is kept in the verification record.- Run history without output.
maestro-cli cue activityworks on a server: with the desktop app not running it readscue.dbread-only and lists the most recent runs (up to the last 1,000), newest first. The database keeps no run output, so for that read the engine’s log (each run logsRun startedandRun finishedwith itsrunId). A run that was still in flight when its engine stopped is listed asfailed. While an engine is running on that directory, a run that has not finished yet is listed asrunning, with how long it has run so far (nodurationin--json). After a hard kill, a read from outside the stopped container (adocker runon its volumes) can still list that run asrunningfor up to 3 minutes, ascue engine statusstill saysRunning, until the dead engine’s heartbeat goes stale; once the engine restarts the run readsfailed, with the downtime counted in its duration. On a VM, run it as themaestrouser with--data-dir /var/lib/maestro/data(the container sets the directory already). It refuses to read acue.db-walleft without itscue.db-shmby a process that was killed, until an engine has started on that directory again. With no engine running, the read copiescue.dbinto memory; one that overlaps an engine starting on the same directory could fail or show wrong rows (not seen so far), so run it again. - No log level in JSON mode. Debug output is left out of the JSON logs and cannot be turned on there (see Logs).
- After a hard kill. The restarted engine normally takes the lock at once, on a VM and in a container restarted by Docker. It refuses to start for up to 3 minutes, while the old lock goes stale, only when it cannot tell the lock is its predecessor’s (see Start in a container). Its refusal says whether the lock belongs to a different running process, to an engine that had this process’s own id and is gone, or to another container, and how long until the lock frees. Once it starts it logs one
Engine was not running for Nmline for the gap. - GitHub triggers quiet for 30 days. What a GitHub trigger has already seen is kept for 30 days, so after 30 days with no new items a subscription treats its next poll as its first: it records the items it finds and runs none of them.
- Stop over SSH for shell steps. Stopping a Cue
action: commandstep that runs on an SSH remote stops only the localsshclient, not the command on the remote host. Agent runs over SSH are stopped on the remote. - GitHub triggers need
repo:. Agithub.pull_request,github.issueorgithub.labelsubscription withoutrepo: owner/namenever polls or receives webhooks: the engine does not detect the repository from the checkout.cue engine check(and--require-ready) reports each such enabled subscription as agithub-repo-missinggap. A manual run (cue trigger, or Play in the desktop dashboard) still dispatches it, with no GitHub item. Setrepo:on every GitHub trigger. - One runner per data directory. Do not run
maestro-cli host startand themaestro-cueservice (orcue engine start) on the same data directory. They do not keep each other out in every case, andcue engine stopthere stops the whole host, not just Cue. Use the service for Cue on a server.