# How to Run wacli as a Non-Interactive Daemon or in Scripts

> Learn how to run wacli as a non-interactive daemon or in scripts. Authenticate once then use sync flags for automation with machine-readable JSON output.

- Repository: [Peter Steinberger/wacli](https://github.com/steipete/wacli)
- Tags: how-to-guide
- Published: 2026-04-17

---

**You can run wacli in non-interactive mode by authenticating once interactively to capture the QR code, then launching the `sync` command with `--follow` or `--once` flags while using `--json` for machine-readable output suitable for automation.**

`wacli` is a lightweight WhatsApp CLI built for automation. Unlike interactive chat clients, its architecture assumes you will authenticate once and then run headless operations indefinitely. This design makes it ideal for systemd services, cron jobs, and CI/CD pipelines that need to process WhatsApp messages without human intervention.

## Understanding the Authentication Model

The `wacli` authentication flow separates the one-time setup from ongoing operations. In [`internal/app/app.go`](https://github.com/steipete/wacli/blob/main/internal/app/app.go), the `EnsureAuthed()` function validates that a valid session exists in the store before allowing any non-interactive command to proceed.

The CLI entry point in [`cmd/wacli/root.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/root.go) defines the global `--store` flag, which determines where the SQLite database and session files persist. Once you run `wacli auth` and scan the QR code, subsequent invocations read from this store without displaying any UI.

## Running the Sync Daemon

The `sync` command in [`cmd/wacli/sync.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/sync.go) provides the primary daemon functionality. It supports two operational modes controlled by the `Mode` field in [`internal/app/sync.go`](https://github.com/steipete/wacli/blob/main/internal/app/sync.go): `SyncModeFollow` for continuous operation and `SyncModeOnce` for bounded execution.

### Continuous Sync with `--follow`

The `--follow` flag (default `true`) starts a long-running process that maintains the WhatsApp connection indefinitely. In [`internal/app/sync.go`](https://github.com/steipete/wacli/blob/main/internal/app/sync.go), this sets `AllowQR: false`, preventing the client from attempting to render a QR code in the terminal.

```bash

# Start the daemon in the background

wacli sync --follow --download-media --refresh-contacts

```

This command:

- Runs until terminated by `SIGINT` or `SIGTERM`
- Downloads media automatically as messages arrive
- Refreshes contact and group metadata periodically
- Outputs logs to stderr (or stdout if `--json` is used)

### One-Off Sync with `--once`

For cron jobs or scheduled tasks, use `--once` combined with `--idle-exit`. This runs the sync loop until the client has been idle for a specified duration, then exits cleanly.

```bash

# Sync for up to 5 minutes, exit when idle for 60 seconds

wacli sync --once --idle-exit 60s --max-reconnect 3

```

In [`internal/app/sync.go`](https://github.com/steipete/wacli/blob/main/internal/app/sync.go), the `SyncModeOnce` implementation monitors the connection state and breaks the loop when no events have occurred for the `IdleExit` duration.

## Scripting with JSON Output

All commands respect the global `--json` flag defined in [`cmd/wacli/root.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/root.go). When enabled, the application uses the `WriteJSON` helper from [`internal/out/out.go`](https://github.com/steipete/wacli/blob/main/internal/out/out.go) to emit machine-readable output, bypassing any human formatting.

```bash

# List messages in JSON format for piping to jq

wacli messages list --chat 1234567890@s.whatsapp.net --limit 10 --json | jq -r '.[].display_text'

# Search messages and extract specific fields

wacli search --query "meeting" --json | jq '.[] | {id: .info.id, text: .display_text}'

```

This approach integrates naturally with shell scripts, Python automation, or monitoring systems that expect structured data.

## Systemd Integration

In [`cmd/wacli/signal.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/signal.go), the `signalContext()` function creates a cancellable context that reacts to `SIGINT` and `SIGTERM`. This makes `wacli` suitable for running as a systemd service without risk of zombie processes.

Create a systemd unit file at `/etc/systemd/system/wacli-sync.service`:

```ini
[Unit]
Description=Wacli background sync daemon
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/wacli sync --follow --download-media
Restart=on-failure
Environment=WACLI_STORE_DIR=/var/lib/wacli
StandardOutput=append:/var/log/wacli-sync.log
StandardError=append:/var/log/wacli-sync.log

[Install]
WantedBy=multi-user.target

```

Then enable and start the service:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now wacli-sync.service

```

## Backfilling Messages in Scripts

For initial data population or periodic archival, the `history backfill` command operates non-interactively once authentication is established. The README provides a reference pattern for iterating over all chats:

```bash
#!/usr/bin/env bash

# Backfill every known chat (safe for cron jobs)

pnpm -s wacli -- --json chats list --limit 100000 |
  jq -r '.[].JID' |
  while read -r jid; do
    pnpm -s wacli -- history backfill --chat "$jid" --requests 3 --count 50
  done

```

Because each invocation uses `--json` and relies on the pre-existing store, this script runs unattended on servers without DISPLAY or TTY access.

## Summary

- **Authenticate once**: Run `wacli auth` interactively to capture the QR code; the session persists in the store directory.
- **Daemon mode**: Use `wacli sync --follow` for continuous background synchronization that never prompts for authentication.
- **Scripting**: Add `--json` to any command for machine-readable output suitable for piping to `jq` or other tools.
- **Systemd ready**: The application handles `SIGTERM` gracefully via `signalContext()` in [`cmd/wacli/signal.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/signal.go), making it safe for service management.
- **Bounded execution**: Use `--once` with `--idle-exit` for cron jobs that need to synchronize and exit cleanly.

## Frequently Asked Questions

### Does wacli require a terminal for authentication?

Yes, but only for the initial setup. The `wacli auth` command displays a QR code in the terminal that you must scan with your phone. Once authenticated, `wacli` stores the session in the directory specified by `--store` (defaulting to `~/.wacli`), and all subsequent commands run without requiring a TTY or interactive input.

### How do I prevent wacli from showing a QR code when running as a service?

When running the sync daemon or any command programmatically, the code explicitly sets `AllowQR: false` in the `SyncOptions` struct passed to `App.Sync` in [`internal/app/sync.go`](https://github.com/steipete/wacli/blob/main/internal/app/sync.go). If the session is invalid and `AllowQR` is false, the application returns an error rather than attempting to display a QR code, ensuring that background services fail safely instead of hanging indefinitely.

### Can I run wacli on a server without a display or X11?

Absolutely. Because `wacli` does not use a graphical toolkit or browser for its core functionality, it runs on headless servers without `DISPLAY` or X11 forwarding. The only requirement is that you first authenticate on a machine with a terminal (or use `ssh -X` temporarily for the QR scan), then copy the store directory to the target server. After that, all daemon and scripting operations work in pure console mode.

### What is the difference between `--follow` and `--once` modes?

The `--follow` flag (default) starts an infinite sync loop that maintains the WebSocket connection to WhatsApp indefinitely, reconnecting automatically if the network drops. This is designed for long-running services. Conversely, `--once` runs the sync loop until the client has been idle (no new messages) for the duration specified by `--idle-exit`, then exits cleanly. Use `--follow` for systemd services and `--once` for cron jobs or batch processing tasks.