Daemon Scheduler Internals
The daemon scheduler lives in the orchestrator-daemon-runtime crate. It implements the autonomous tick loop that drives workflow dispatch for a project.
Wake Model
The daemon loop (run_daemon in crates/orchestrator-daemon-runtime/src/daemon/run_daemon.rs) is event-driven, not a fixed-interval poller. It parks on a tokio::select! whose arms are:
- shutdown signals (SIGTERM / SIGINT),
- a process-global
Notifywake handle (scheduler_nudge) fired by control-socketdaemon/nudgemessages, workflow-config hot-reloads, and the completion forwarder, - the next cron deadline computed from compiled
schedules:, - a fallback heartbeat bounded by
interval_secs.
daemon/nudge is sent fire-and-forget by animus subject create/update/status and animus queue enqueue/release. The completion forwarder subscribes to the WorkflowEventBroadcaster for phase_completed / workflow_completed / workflow_failed events and nudges the loop so follow-on work dispatches immediately instead of waiting for the heartbeat. Notify stores at most one permit, so nudge bursts coalesce into a single extra pass. interval_secs is only a fallback that bounds out-of-band pickup and paces housekeeping — heavy reconciliation legs run at most once per heartbeat period. See Configuration.
The tokio::select! in run_daemon parks on exactly five arms; the first to fire wins the race, and every non-shutdown arm falls through to a fresh pass that re-arms a new heartbeat sleep and a new notified() future.
The cron arm folds in both the next compiled schedules: deadline and the queue plugin's next deferred-entry run_at, sleeping until the earlier of the two; when neither exists it parks forever (std::future::pending). The heartbeat is clamped to SCHEDULE_RETRY_SWEEP_MAX when any schedule is enabled so a cron occurrence that could not dispatch still gets a retry pass inside its catch-up horizon.
Tick Loop
The entry point is run_project_tick() in crates/orchestrator-daemon-runtime/src/tick/run_project_tick.rs. Each tick follows this sequence:
- Load context -- Build a
ProjectTickContextfrom the project root, daemon options, and current time. This includes schedule evaluation and active-hours gating. - Process due schedules -- If any cron-based schedules are due, fire their pipeline spawners.
- Capture snapshot -- Load the current project state (tasks, workflows, dispatch queue) into a
ProjectTickSnapshot. - Build preparation -- Compute
ProjectTickPreparationfrom the snapshot: how many ready dispatch slots are available, what the dispatch limit is. - Reconcile completed processes -- Poll the
ProcessManagerfor finished workflow-runner subprocesses. Record execution facts (success/failure) and update workflow/task state via projectors. - Dispatch ready tasks -- If dispatch slots are available (
ready_dispatch_limit > 0), select subjects from the dispatch queue and spawn workflow-runner processes. - Collect health -- Gather daemon health metrics.
- Build summary -- Assemble a
ProjectTickSummarywith all execution outcomes for logging and event emission.
Dispatch Queue
The dispatch queue (crates/orchestrator-daemon-runtime/src/queue/) maintains an ordered list of DispatchQueueEntry items, each representing a subject awaiting execution.
Entry statuses:
- Pending -- Enqueued but not yet assigned to a workflow-runner process
- Assigned -- A workflow-runner process has been spawned for this entry
- Completed -- Terminal state (succeeded or failed)
- Held -- Temporarily held back from dispatch (e.g., dependency gate)
Subjects are ordered by priority. The plan_ready_dispatch() function in crates/orchestrator-daemon-runtime/src/dispatch/ready_dispatch_plan.rs selects which pending entries to start, respecting capacity limits.
Process Manager
ProcessManager in crates/orchestrator-daemon-runtime/src/dispatch/process_manager.rs tracks spawned workflow-runner child processes. The runner binary is resolved by build_runner_command_from_dispatch() via kind-based plugin discovery (orchestrator_plugin_host::discover_by_kind("workflow_runner")), which finds the installed workflow_runner plugin (the reference impl is launchapp-dev/animus-workflow-runner-default). For back-compat it then falls back to binary-name resolution on animus-workflow-runner-default, animus-workflow-runner, and ao-workflow-runner for older installs. The in-tree workflow-runner binary was deleted in v0.5.1 — there is no in-process fallback.
Responsibilities:
- Spawn -- Build the runner command from a
SubjectDispatch, spawn it as a tokioChildprocess with piped stderr - Poll -- Check which child processes have exited, collecting their exit status and stderr output as
CompletedProcessentries - Track -- Maintain a list of active
WorkflowProcessentries with subject ID, task ID, workflow ref, and child handle
The runner command is constructed by build_runner_command_from_dispatch() which translates dispatch parameters into CLI arguments for the resolved workflow-runner executable.
The runner binary is resolved by kind first, then by name for older installs.
The spawn / poll / reconcile cycle ties the dispatch queue, the ProcessManager, and completion reconciliation together across ticks.
Capacity Rules
Dispatch capacity is computed by ready_dispatch_limit() and ready_dispatch_limit_for_options():
- max_workflows -- Upper bound on total concurrent workflow-runner processes
- slot_headroom -- Number of slots reserved to avoid saturating the system
- fallback_headroom -- Additional headroom when using fallback tool configurations
- pool_draining -- When the daemon is shutting down, no new dispatches are started
The effective limit is: max_workflows - active_count - headroom.
Completion Reconciliation
When a workflow-runner process exits, the CompletionReconciliationPlan (from crates/orchestrator-daemon-runtime/src/dispatch/completion_reconciliation_plan.rs) determines what state updates to apply:
- Record execution facts via projector functions (
project_task_execution_fact,project_schedule_execution_fact, etc.) - Update task status based on workflow outcome (success sets task to done, failure sets task to blocked)
- Remove terminal dispatch queue entries
- Emit runner events for monitoring
Schedule Evaluation
ScheduleDispatch in crates/orchestrator-daemon-runtime/src/schedule/schedule_dispatch.rs handles cron-based workflow scheduling:
- Cron parsing -- Uses the
cronercrate for cron expression evaluation - Active hours gate --
allows_proactive_dispatch()checks whether the current time falls within configured active hours before allowing proactive dispatches - Due evaluation -- Compares schedule cron expressions against the last run time stored in
ScheduleStateto determine which schedules are due - Dispatch -- Due schedules are converted into
SubjectDispatchentries and spawned via the pipeline spawner callback