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 03 - 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 the package declares in code — #[trigger(cron = "...", on = "...")] / #[trigger(poll_interval = "...", on = "...")] in Rust, @cloaca.trigger(...) in Python. (Schedules do not live in package.toml: the manifest is a closed schema, and a [[triggers]] section is rejected at load — trigger declarations were moved to code in CLOACI-I-0102.) You will see log output confirming the load:
Reconciliation: 1 loaded, 0 unloaded
Package my-workflow v1.0.0: registered cron schedule for workflow 'cleanup' (trigger 'nightly_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 to use the runner default of 100)
# 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 in
~/.cloacina/logs/(daily rotation; files are namedcloacina.YYYY-MM-DD.log)
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 today's log file
tail -f ~/.cloacina/logs/cloacina.$(date +%F).log
# Parse JSON logs with jq
tail -f ~/.cloacina/logs/cloacina.$(date +%F).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.$(date +%F).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. Cron schedules come from#[trigger(cron = "...")]/@cloaca.trigger(cron=..., on=...)declarations compiled into the package — if the package has no cron trigger in code, nothing is registered. (cloacinactl package new --kind cronscaffolds this shape for both languages — see thepython-cronexample for a worked Python package.) - Check that
cron_max_catchupis not set too low if the daemon was down for a period. When omitted, the runner default of 100 catchup executions applies. - Verify the cron expression is valid: 5-field format (minute, hour, day-of-month, month, day-of-week), with an optional leading seconds field for 6-field expressions.
- If the daemon was recently restarted, recovery runs after
cron_recovery_interval_sseconds (default 300).
- Poll triggers are declared in package code with
#[trigger(poll_interval = "...", on = "...")](or@cloaca.trigger(poll_interval=..., on=...)). Confirm the load loggedregistered poll-trigger schedule for workflow '...'— a trigger with acronattribute is registered as a cron schedule instead, not polled. - Check the logs for reconciler warnings mentioning the trigger name — a failed schedule registration is logged as a warning with the package name and error.
- 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.