Introduction
This post builds a self-hosted uptime monitor and bottles it into a single command: aux4 monitor <url>. You'll make an HTTP request with curl, retry it with poller, run the check as a background job with jobs that emails an alert through email on failure, package that into your own monitor command, and schedule it with cron. The result probes a site every few minutes and emails you the moment it stops responding — no monitoring SaaS required.
Install the packages
aux4 aux4 pkger install aux4/curl aux4/poller aux4/jobs aux4/cron aux4/email
Step 1: make the request
aux4 curl request is a pipe-friendly HTTP client. By default it prints only the response body:
aux4 curl request https://example.com
<!doctype html><html lang="en"><head><title>Example Domain</title>...</body></html>
Add --showHeaders true to see the status line and headers:
aux4 curl request --showHeaders true https://example.com | head -1
HTTP/2.0 200 OK
That first line is your signal: 200 means healthy, anything else (or a connection error) means trouble.
Step 2: poll until healthy (or give up)
A single request tells you the state right now, but a blip isn't an outage. aux4 poll runs a command on an interval until the output matches a success pattern, hits a failure pattern, or times out:
aux4 poll \
--command="aux4 curl request --showHeaders true https://example.com | head -1" \
--expectation="*200*" \
--maxWait=20 --interval=5
[0 s] HTTP/2.0 200 OK
Complete
The flags:
--command— the command to run each cycle.--expectation— the success pattern, matched with shell glob rules:*200*matches output containing200. Use a pattern with no spaces.--maxWait— total seconds to keep trying (default900).--interval— seconds between attempts (default30).
Each cycle prints [<elapsed> s] <output>. On success it prints Complete and exits 0; on timeout it prints Timeout and exits 1:
[0 s] HTTP/2.0 503 Service Unavailable
[5 s] HTTP/2.0 503 Service Unavailable
Timeout
Because poll keeps retrying until maxWait, a single blip never alerts — only a sustained outage does. --interval and --maxWait set how many attempts to allow first: --interval=20 --maxWait=60 checks at 0s, 20s, and 40s — three strikes before the check fails.
That non-zero exit is what makes it composable. You can also fail fast on a known-bad response with --failValue:
aux4 poll \
--command="aux4 curl request --showHeaders true https://example.com | head -1" \
--expectation="*200*" \
--failValue="*503*" \
--maxWait=120 --interval=15
Step 3: alert by email
aux4 email send sends mail over SMTP. Credentials live in a YAML config file — by default ~/.config/aux4/email.yaml — so no password ends up on the command line:
imap:
host: imap.gmail.com
port: "993"
smtp:
host: smtp.gmail.com
port: "465"
user: alerts@yourdomain.com
password: xxxx xxxx xxxx xxxx
For Gmail, Outlook, or Yahoo, use an app-specific password. The sender address is the
user:field — there is no--fromflag.
Sending is a one-liner:
aux4 email send \
--to ops@yourdomain.com \
--subject "ALERT: example.com health check failed" \
--body "The health endpoint did not return 200 in time."
On success it prints the message ID:
Email sent successfully. Message ID: <abc123@smtp.gmail.com>
Step 4: alert on failure with a job hook
poll exits non-zero when the check fails. Hand the whole check to aux4 jobs run: it runs the command in the background and fires a hook based on the exit code. --onFailure sends the email only when the poll fails — no shell || glue, and every run is recorded:
aux4 jobs run \
--command "aux4 poll --command='aux4 curl request --showHeaders true https://example.com | head -1' --expectation='*200*' --maxWait=20 --interval=5" \
--onFailure "aux4 email send --to ops@yourdomain.com --subject 'ALERT: example.com is down' --body 'Health check failed.'"
It returns the job immediately:
{
"id": "1",
"state": "RUNNING",
"exitCode": 0,
"onFailure": "aux4 email send ..."
}
jobs list shows how it ended — COMPLETED when the site is up (no email), FAILED when it isn't (the hook fired the alert):
aux4 jobs list
[
{
"id": "1",
"state": "COMPLETED",
"exitCode": 0,
"command": "aux4 poll ..."
}
]
Step 5: bottle it into one command
That one-liner does the job, but you don't want to retype it. Wrap it in an .aux4 file so it becomes a command with the URL as an argument. Create .aux4:
{
"profiles": [
{
"name": "main",
"commands": [
{
"name": "monitor",
"execute": [
"aux4 jobs run --command \"aux4 poll --command='aux4 curl request --showHeaders true ${url} | head -1' --expectation='*200*' --maxWait=${maxWait} --interval=${interval}\" --onFailure \"aux4 email send --to ${email} --subject 'ALERT: ${url} is down' --body 'Health check failed.'\""
],
"help": {
"text": "Monitor a URL and email an alert if it stops responding",
"variables": [
{ "name": "url", "text": "URL to monitor", "arg": true },
{ "name": "email", "text": "Alert recipient", "default": "ops@yourdomain.com" },
{ "name": "interval", "text": "Seconds between retries", "default": "10" },
{ "name": "maxWait", "text": "Max seconds to wait for a healthy response", "default": "60" }
]
}
}
]
}
]
}
aux4 picks up the .aux4 in your working directory, so the command is ready:
aux4 monitor https://example.com
{
"id": "1",
"state": "RUNNING",
"command": "aux4 poll ...",
"onFailure": "aux4 email send ..."
}
The URL is a positional argument; --email, --interval, and --maxWait have defaults you can override. Install it as a package to run aux4 monitor from any directory.
Step 6: run it on a schedule
With the whole check behind one command, scheduling it is trivial — point cron at aux4 monitor:
aux4 cron start
aux4 cron add --name uptime-check --every "5 min" --run "aux4 monitor https://example.com"
{
"name": "uptime-check",
"every": "5 min",
"run": "aux4 monitor https://example.com",
"state": "active"
}
Each firing is recorded in the history with its job id:
aux4 cron history --name uptime-check
[
{
"name": "uptime-check",
"jobId": "1",
"timestamp": "2025-11-19T14:00:00Z",
"status": "TRIGGERED"
}
]
Pause, resume, or remove it without restarting the daemon:
aux4 cron pause --name uptime-check
aux4 cron resume --name uptime-check
aux4 cron remove --name uptime-check
Conclusion
You built aux4 monitor <url> out of five single-purpose packages:
- curl makes the request,
- poller turns a one-shot check into a patient retry loop with a clean exit code,
- jobs runs that check in the background and fires the alert on failure,
- email delivers the alert,
- cron runs the command on a schedule and keeps a history.
The composition lives in one small .aux4 file, so aux4 monitor https://example.com is all anyone has to remember. Swap the URL for an internal service, point the expectation at a JSON field, or schedule a dozen of them — the command stays the same.