Non-interactive agent job runner. Runs commands as background jobs and returns structured JSON on stdout.
- stdout: JSON only — every command prints exactly one JSON object
- stderr: Diagnostic logs (controlled by
RUST_LOGor-v/-vvflags)
This separation lets agents parse stdout reliably without filtering log noise.
cargo install --path .Run a command, wait for it to finish, then read its output:
# 1. Start the job (returns immediately with a job_id)
JOB=$(agent-exec run echo "hello world" | jq -r .job_id)
# 2. Wait for completion
agent-exec wait "$JOB"
# 3. Read output
agent-exec tail "$JOB"Example output of tail:
{
"schema_version": "0.1",
"ok": true,
"type": "tail",
"job_id": "01J...",
"stdout_tail": "hello world",
"stderr_tail": "",
"truncated": false
}Start a background job, poll its status, then read its output:
# 1. Start the job (returns immediately with a job_id)
JOB=$(agent-exec run sleep 30 | jq -r .job_id)
# 2. Check status
agent-exec status "$JOB"
# 3. Stream output tail
agent-exec tail "$JOB"
# 4. Wait for completion
agent-exec wait "$JOB"Run a job with a timeout; SIGTERM after 5 s, SIGKILL after 2 s more:
agent-exec run \
--timeout 5000 \
--kill-after 2000 \
sleep 60agent-exec run [OPTIONS] <COMMAND>...Key options:
| Flag | Default | Description |
|---|---|---|
--snapshot-after <ms> |
10000 | Wait N ms before returning (0 = return immediately) |
--timeout <ms> |
0 (none) | Kill job after N ms |
--kill-after <ms> |
0 | ms after SIGTERM to send SIGKILL |
--tail-lines <N> |
50 | Lines of output captured in the snapshot |
--cwd <dir> |
inherited | Working directory |
--env KEY=VALUE |
— | Set environment variable (repeatable) |
--mask KEY |
— | Redact secret values from JSON output (repeatable) |
--wait |
false | Block until the job reaches a terminal state |
--wait-poll-ms <ms> |
200 | Poll interval used with --wait |
--notify-command <COMMAND> |
— | Run a shell command when the job finishes; event JSON is sent on stdin |
--notify-file <PATH> |
— | Append a job.finished event as NDJSON |
--config <PATH> |
XDG default | Load shell wrapper config from a specific config.toml |
--shell-wrapper <PROG FLAGS> |
platform default | Override shell wrapper for this invocation (e.g. "bash -lc") |
agent-exec status <JOB_ID>Returns running, exited, killed, or failed, plus exit_code when finished.
agent-exec tail [--tail-lines N] <JOB_ID>Returns the last N lines of stdout and stderr.
agent-exec wait [--timeout-ms N] [--poll-ms N] <JOB_ID>Polls until the job finishes or the timeout elapses.
agent-exec kill [--signal TERM|INT|KILL] <JOB_ID>agent-exec list [--state running|exited|killed|failed] [--limit N]agent-exec gc [--older-than <DURATION>] [--dry-run] [--root <PATH>]Deletes job directories under the root whose terminal state (exited, killed, or failed) is older than the retention window. Running jobs are never touched.
| Flag | Default | Description |
|---|---|---|
--older-than <DURATION> |
30d |
Retention window: jobs older than this are eligible for deletion. Supports 30d, 24h, 60m, 3600s. |
--dry-run |
false | Report candidates without deleting anything. |
--root <PATH> |
XDG default | Override the jobs root directory. |
Retention semantics
- The GC timestamp used for age evaluation is
finished_atwhen present, falling back toupdated_at. - Jobs where both timestamps are absent are skipped safely.
runningjobs are never deleted regardless of age.
Examples
# Preview what would be deleted (30-day default window).
agent-exec gc --dry-run
# Preview with a custom 7-day window.
agent-exec gc --older-than 7d --dry-run
# Delete jobs older than 7 days.
agent-exec gc --older-than 7dJSON response fields
| Field | Type | Description |
|---|---|---|
root |
string | Resolved jobs root path |
dry_run |
bool | Whether this was a preview-only run |
older_than |
string | Effective retention window (e.g. "30d") |
older_than_source |
string | "default" or "flag" |
deleted |
number | Count of directories actually deleted |
skipped |
number | Count of directories skipped |
freed_bytes |
number | Bytes freed (or would be freed in dry-run) |
jobs |
array | Per-job details: job_id, state, action, reason, bytes |
The action field in each jobs entry is one of:
"deleted"— directory was removed"would_delete"— would be removed in a real run (dry-run only)"skipped"— preserved with an explanation inreason
agent-exec reads an optional config.toml to configure the shell wrapper used for command-string execution.
$XDG_CONFIG_HOME/agent-exec/config.toml(defaults to~/.config/agent-exec/config.toml)
[shell]
unix = ["sh", "-lc"] # used on Unix-like platforms
windows = ["cmd", "/C"] # used on WindowsBoth keys are optional. Absent values fall back to the built-in platform default (sh -lc / cmd /C).
--shell-wrapper <PROG FLAGS>CLI flag (highest priority)--config <PATH>explicit config file- Default XDG config file (
~/.config/agent-exec/config.toml) - Built-in platform default (lowest priority)
The configured wrapper applies to both run command-string execution and --notify-command delivery so the two execution paths stay consistent.
agent-exec run --shell-wrapper "bash -lc" -- my_script.shagent-exec run --config /path/to/config.toml -- my_script.shWhen run is called with --notify-command or --notify-file, agent-exec emits a job.finished event after the job reaches a terminal state.
--notify-commandaccepts a shell command string, executes it via the configured shell wrapper (default:sh -lcon Unix,cmd /Con Windows), and writes the event JSON to stdin.--notify-fileappends the event as a single NDJSON line.completion_event.jsonis also written in the job directory with the event plus sink delivery results.- Notification delivery is best effort; sink failures do not change the main job state.
- When delivery success matters, inspect
completion_event.json.delivery_results.
Choose the sink based on the next consumer:
- Use
--notify-commandfor small, direct reactions such as posting to chat or forwarding the event back to the launching OpenClaw session with eitheropenclaw message sendoropenclaw agent --session-id ... --deliver. - Use
--notify-filewhen you want a durable queue-like handoff to a separate worker that can retry or fan out. - Prefer checked-in helper scripts over large inline shell or Python snippets.
Example:
agent-exec run \
--wait \
--notify-file /tmp/agent-exec-events.ndjson \
-- echo helloCommand sink example:
agent-exec run \
--wait \
--notify-command 'cat > /tmp/agent-exec-event.json' \
-- echo helloPass a plain shell command string to --notify-command. The command runs via the configured shell wrapper (default: sh -lc) and has access to the event JSON on stdin and the AGENT_EXEC_EVENT_PATH environment variable.
agent-exec run \
--notify-command 'openclaw message send --chat telegram:deployments --text "job $(jq -r .job_id "$AGENT_EXEC_EVENT_PATH") finished: state=$(jq -r .state "$AGENT_EXEC_EVENT_PATH")"' \
-- long-running-command --flag valueFor repeated use, a checked-in helper script is easier to review and maintain than a long inline command.
This pattern is often more flexible than sending a final user message directly from the notify command. The launching session can inspect logs, decide whether the result is meaningful, and summarize it in context. Depending on the workflow, either openclaw message send or openclaw agent --session-id ... --deliver may be the better fit.
SESSION_ID="oc_session_123"
agent-exec run \
--notify-command "openclaw message send --session $SESSION_ID --text \"\$(jq -c . \"\$AGENT_EXEC_EVENT_PATH\")\"" \
-- ./scripts/run-heavy-task.shWith this pattern, the receiving OpenClaw session can read the event payload, inspect stdout_log_path or stderr_log_path, and decide whether to reply, retry, or trigger follow-up work.
If you want explicit agent re-entry instead of lightweight message delivery, call openclaw agent --deliver directly:
SESSION_ID="oc_session_123"
agent-exec run \
--notify-command "openclaw agent --session-id $SESSION_ID --deliver \"\$(jq -c . \"\$AGENT_EXEC_EVENT_PATH\")\"" \
-- ./scripts/run-heavy-task.shIn practice, both message send and agent --deliver can target either a user-facing or agent-facing flow; pick the one that matches the downstream behavior you want.
Use --notify-file when you want retries or fanout outside the main job lifecycle:
agent-exec run \
--notify-file /var/lib/agent-exec/events.ndjson \
-- ./scripts/run-heavy-task.shA separate worker can tail or batch-process the NDJSON file, retry failed downstream sends, and route events to chat, webhooks, or OpenClaw sessions without coupling that logic to the main job completion path.
--notify-commandaccepts a plain shell command string; no JSON encoding is needed.- Keep notify commands small, fast, and idempotent.
- Common sink failures include quoting mistakes, PATH or env mismatches, downstream non-zero exits, and wrong chat, session, or delivery-mode targets.
- If you need heavier orchestration, let the notify sink hand off to a checked-in helper or durable worker.
For command sinks, the event JSON is written to stdin and these environment variables are set:
AGENT_EXEC_EVENT_PATH: path to the persistedcompletion_event.jsonAGENT_EXEC_JOB_ID: finished job idAGENT_EXEC_EVENT_TYPE: currentlyjob.finished
Example job.finished payload:
{
"schema_version": "0.1",
"event_type": "job.finished",
"job_id": "01J...",
"state": "exited",
"command": ["echo", "hello"],
"cwd": "/path/to/cwd",
"started_at": "2026-03-15T12:00:00Z",
"finished_at": "2026-03-15T12:00:00Z",
"duration_ms": 12,
"exit_code": 0,
"stdout_log_path": "/jobs/01J.../stdout.log",
"stderr_log_path": "/jobs/01J.../stderr.log"
}If the job is killed by a signal, state becomes killed, exit_code may be absent, and signal is populated when available.
Logs go to stderr only. Use -v / -vv or RUST_LOG:
RUST_LOG=debug agent-exec run echo hello
agent-exec -v run echo hellocargo build
cargo test --all
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings