Running the Daemon
This guide walks you through starting and configuring the Cloacina daemon, a lightweight local scheduler that watches directories for .cloacina packages and runs their cron schedules and triggers using a SQLite backend.
cloacinactlbinary installed and on your PATH- One or more
.cloacinaworkflow packages built (see Tutorial 07 - Packaged Workflows for building packages)
The simplest way to start the daemon is with no arguments:
cloacinactl daemon
This creates the ~/.cloacina/ home directory (if it does not exist), opens a SQLite database at ~/.cloacina/cloacina.db, and watches ~/.cloacina/packages/ for .cloacina files.
To watch directories beyond the default ~/.cloacina/packages/, use the --watch-dir flag (repeatable):
cloacinactl daemon \
--watch-dir /opt/cloacina/workflows \
--watch-dir ~/my-project/packages
The default packages directory is always watched regardless of what you pass.
Copy .cloacina package files into any watched directory:
cp my-workflow.cloacina ~/.cloacina/packages/
The daemon detects the new file via filesystem events, loads the package through the registry reconciler, and registers any cron or trigger schedules defined in the package manifest. You will see log output confirming the load:
Reconciliation: 1 loaded, 0 unloaded
Registered cron schedule: 'nightly-cleanup' -> workflow 'cleanup' (cron: 0 0 * * *, id: ...)
To remove a workflow, delete its .cloacina file from the watched directory. The daemon will unload it on the next reconciliation cycle.
The daemon reads ~/.cloacina/config.toml on startup. You can edit it directly or use the config subcommand.
[daemon]
# Reconciler poll interval in milliseconds (fallback if filesystem events are missed)
poll_interval_ms = 500
# Log level: trace, debug, info, warn, error
log_level = "info"
# Graceful shutdown timeout in seconds
shutdown_timeout_s = 30
# Filesystem watcher debounce interval in milliseconds
watcher_debounce_ms = 500
# Trigger scheduler base poll interval in milliseconds
trigger_poll_interval_ms = 1000
# Maximum cron catchup executions after downtime (omit for unlimited)
# cron_max_catchup = 10
# Cron recovery check interval in seconds
cron_recovery_interval_s = 300
# Cron lost task threshold in minutes
cron_lost_threshold_min = 10
[watch]
directories = [
"/opt/cloacina/workflows",
"~/my-project/packages",
]
Paths starting with ~/ are expanded to the user’s home directory. These directories are merged with any --watch-dir flags passed on the command line.
# View a single value
cloacinactl config get daemon.poll_interval_ms
# Change a value
cloacinactl config set daemon.poll_interval_ms 1000
# List all values
cloacinactl config list
The --poll-interval flag overrides the config file value for the cron reconciler:
cloacinactl daemon --poll-interval 2000
The daemon writes logs to two destinations simultaneously:
- stderr: human-readable format for interactive use
- File: JSON-structured logs at
~/.cloacina/logs/cloacina.log(daily rotation)
Use the RUST_LOG environment variable for fine-grained control:
# Debug logging for Cloacina, info for everything else
RUST_LOG=cloacina=debug,info cloacinactl daemon
# Or use the --verbose flag for global debug output
cloacinactl daemon --verbose
You can also set the default level in config.toml:
[daemon]
log_level = "debug"
# Tail the current log file
tail -f ~/.cloacina/logs/cloacina.log
# Parse JSON logs with jq
tail -f ~/.cloacina/logs/cloacina.log | jq '.fields.message'
The daemon responds to the following Unix signals:
| Signal | Behavior |
|---|---|
| SIGINT (Ctrl+C) | Initiates graceful shutdown. In-flight pipelines are drained with a configurable timeout (default 30s). A second SIGINT forces immediate exit. |
| SIGTERM | Same as SIGINT – graceful shutdown with drain. |
| SIGHUP | Reloads ~/.cloacina/config.toml without restarting. New watch directories are added, removed directories are unwatched, and a reconciliation runs to pick up any packages in newly watched paths. |
After editing config.toml to add a new watch directory:
kill -HUP $(pgrep -f 'cloacinactl daemon')
The daemon logs will confirm the reload:
Received SIGHUP -- reloading configuration...
Added watch directory: /opt/cloacina/new-workflows
Triggering reconciliation after config reload...
Configuration reload complete.
- Verify the file has a
.cloacinaextension and is in a watched directory. - Check the daemon logs for reconciliation errors:
grep -i "failed" ~/.cloacina/logs/cloacina.log | tail -20 - If the package was built against a different platform or has a corrupted archive, the reconciler will report a failure with the package ID and error message.
- Ensure the daemon has read permissions on the package file and the watched directory.
- Confirm the schedule was registered by looking for
Registered cron schedulein the logs. - Check that
cron_max_catchupis not set too low if the daemon was down for a period. When omitted, the daemon runs all missed executions. - Verify the cron expression is valid (standard 5-field format: minute, hour, day-of-month, month, day-of-week).
- If the daemon was recently restarted, recovery runs after
cron_recovery_interval_sseconds (default 300).
- The trigger must have a registered
Triggerimplementation in the package. If only acron_expressionis defined inpackage.toml, it is treated as a cron schedule, not a poll trigger. - Look for the warning
Trigger '...' declared in package.toml but no Trigger impl found in registryin the logs. - Adjust
trigger_poll_interval_msif the default 1000ms polling is too frequent or too slow.
- Increase
watcher_debounce_msto reduce filesystem event processing (default 500ms). - Increase
poll_interval_msto reduce periodic reconciliation frequency. - If you have many packages, the periodic reconciliation can be costly. The filesystem watcher handles immediate detection; the periodic tick is a fallback.