Building Dubnium: Scheduling Agentic Work
This is the third post in the Dubnium series, about my NixOS-based local AI workstation.
The first post described the machine. The second, Building Dubnium: An Agentic Orchestrator, described the agentic control plane: the Supervisor Gateway, Supervisor Orchestrator, policy, memory, specialists, tools, and run evidence.
The next problem is time.
How should agentic work begin when no human is present?
The tempting answer is to put a model call in cron.
That is also how a small automation grows into an opaque workflow with ambient credentials, unclear ownership, and no reliable recovery path.
Dubnium takes a narrower approach:
scheduler: declares and triggers when bounded work starts
workflow layer: coordinates multi-step work
agentic control plane: reasons within policy
run ledger: records agentic lifecycle and effect evidence
executor: performs approved effects
The scheduler is intentionally not the orchestrator.
Cron is useful because it is small
Cron and systemd timers are good at answering one question:
Should this command start now?
They are auditable, observable, and already integrated into the host lifecycle. For local maintenance, that is often all the architecture required.
Examples include:
- cache cleanup
- model-seed reconciliation
- health snapshots
- local index maintenance
- periodic status generation
The mistake is not using timers. The mistake is asking them to own workflow policy, memory, approvals, semantic retries, and agentic reasoning.
A timer should trigger work. It should not become the work.
The Dubnium scheduler boundary
Dubnium’s current scheduler is a local host substrate built on declarative Nix configuration, systemd timers, and oneshot services.
Its default posture is deliberately conservative:
- disabled by default
- no network listener required for timer execution
- no implicit n8n enablement
- no implicit GitHub runner enablement
- absolute packaged commands rather than mutable shell fragments
- explicit executor boundaries for repository mutation
- runtime secrets outside the Nix store
A simple local task looks like this:
{
dubnium.scheduler = {
enable = true;
jobs.daily-status = {
command = "/run/current-system/sw/bin/dubnium-daily-status";
onCalendar = "daily";
persistent = true;
user = "root";
group = "root";
workingDirectory = "/var/lib/dubnium/scheduler/daily-status";
mutatesRepositories = false;
};
};
}
The durable schedule lives in Nix. The timer materializes at rebuild time. The service runs inside the host’s normal systemd and journald boundaries.
declarative schedule
-> Dubnium scheduler module
-> systemd timer
-> systemd oneshot service
This keeps the source of truth reviewable and reconstructable.
The scheduler API is operational, not canonical
Dubnium also has a localhost scheduler API and corresponding dubctl schedule
commands.
The runtime surface supports:
GET /schedules
GET /schedules/{id}
POST /schedules/{id}/trigger
POST /schedules/{id}/pause
POST /schedules/{id}/resume
GET /schedules/{id}/history
GET /healthz
The CLI mirrors those operations:
dubctl schedule list
dubctl schedule show <id>
dubctl schedule trigger <id>
dubctl schedule pause <id>
dubctl schedule resume <id>
dubctl schedule history <id>
This API is an operator control surface. It may inspect materialized schedules, start a declared service, mask or unmask a timer, and expose recent journald-backed history.
It must not create hidden durable schedules that compete with Nix declarations.
Schedules are declared durably.
Execution happens through a declared boundary.
Runtime control is operational, not canonical.
One clock, several ownership boundaries
Not every scheduled task should execute locally.
| Concern | Owner |
|---|---|
| Local timed execution | Dubnium scheduler and systemd |
| Multi-step workflow orchestration | n8n or another workflow executor |
| Repository-governed mutation | GitHub Actions and an approved runner |
| Agentic reasoning and context assembly | Supervisor Orchestrator |
| Policy and approval contracts | Anthesis or another governance tool |
| Agentic lifecycle and effect evidence | Run Ledger |
| Approved external effects | bounded executor or Tool Execution Gateway |
The timer may start the path, but ownership transfers at explicit boundaries.
The central design rule is:
Scheduled agentic work must enter the same orchestration and governance path as interactive agentic work.
The scheduler should not grow its own model client, memory retrieval, prompt library, credentials, or semantic retry engine.
Local task or workflow dispatch?
The shared scheduling contract has two execution classes.
Local task
Use a local task when the work is bounded, host-local, and naturally expressed as a systemd-managed oneshot command.
Examples include health checks, cache cleanup, model-file reconciliation, local index compaction, and snapshot generation.
Workflow dispatch
Use workflow dispatch when the schedule should start another orchestrator or executor.
Examples include:
- starting an n8n workflow
- dispatching a GitHub Actions workflow
- beginning an approval-bearing automation path
- submitting work to the Supervisor Orchestrator
A target declarative shape looks like this:
{
id = "weekly-docs-review";
owner = "docs-automation";
kind = "workflow-dispatch";
onCalendar = "Sun 18:00";
dispatch = {
backend = "github-actions";
target = "docs-review.yml";
payloadFile = "/run/secrets/docs-review-dispatch.json";
};
executorBoundary = "github-runner";
mutatesRepositories = true;
}
This workflow-dispatch object is a target contract rather than a claim that every field is already implemented. The timer remains small. The receiving system owns the richer workflow.
Repository mutation belongs to the repository
A scheduled task that commits, pushes, opens a pull request, or publishes an artifact crosses a stronger trust boundary than a local health check.
Dubnium requires repository-mutating jobs to declare that fact and name an executor boundary.
{
dubnium.scheduler.jobs.career-digest = {
command = "/run/current-system/sw/bin/career-digest";
onCalendar = "Mon..Fri 08:30";
mutatesRepositories = true;
executorBoundary = "github-runner";
environmentFile = "/run/secrets/career-digest.env";
};
}
The current boundary choices are:
| Boundary | Meaning |
|---|---|
local | bounded host-local execution |
github-runner | repository-governed execution through GitHub Actions |
n8n | workflow orchestration through n8n |
The declaration records intent. It does not silently enable n8n, register a runner, or prove that a command delegates correctly.
For repository mutation, GitHub Actions should normally own workflow policy and produce a reviewable pull request rather than allowing a timer script to push directly.
Scheduling agentic work
Agentic work adds uncertainty, memory access, tool proposals, and possible side effects. That belongs above the scheduler boundary.
The scheduler decides that a declared occurrence should begin. The Supervisor Orchestrator owns normalization, planning, policy invocation, specialist routing, evaluation, and synthesis.
The Run Ledger, not scheduler timer metadata or memory, is the future canonical record for the agentic lifecycle.
Richer state belongs above the timer substrate
A production workflow may need:
- logical run identity
- deduplication and event coalescing
- concurrency control
- retries with semantic boundaries
- deadlines and expiry
- approval waiting
- resumability
- step-level evidence
Those are real requirements, but assigning all of them to the local scheduler would turn it into the general workflow engine that the architecture explicitly avoids.
systemd / Dubnium scheduler
-> timer materialization, local triggering, pause/resume, recent history
n8n
-> integration workflows, forms, waiting states, human routing
GitHub Actions
-> repository workflow state, retries, artifacts, mutation policy
Supervisor Orchestrator + Run Ledger
-> agentic run identity, lifecycle, policy records, specialist handoffs,
tool effects, evaluation, future replay and resumption
The system may later share common run-envelope and evidence contracts across these layers. That does not require one scheduler to own them all.
Retries are not one thing
A timer retry, model retry, tool retry, workflow retry, and complete job replay have different safety properties.
A failed read-only model request may be safe to retry. A successful email send followed by a lost acknowledgement is not.
The relevant execution owner needs enough evidence to distinguish work that never began, inference that failed before an effect, a repeatable read-only operation, a completed effect with a missing acknowledgement, and a partially completed workflow.
retryable operation
-> stable idempotency key
-> bounded attempts
-> backoff and deadline
-> prior-result record
-> side-effect classification
The local scheduler should report service failure clearly. The workflow or agentic execution layer should decide whether semantic replay is safe.
Approval is not a timer feature
A scheduled analysis may discover a mutation that should not happen unattended. The correct result is often an approval request rather than success or failure.
The policy or governance layer defines the exact proposed action and approval envelope. The workflow layer persists the waiting state and expiry. The bounded executor performs only the approved effect. The Run Ledger records the lifecycle, disposition, approval, and resulting effect evidence.
scheduled trigger
-> analysis
-> proposed bounded action
-> policy disposition
-> approval if required
-> bounded execution
-> run-ledger evidence
An approval should authorize one specific action with fixed parameters, target, evidence, and expiry. It should not grant a general period of model authority.
Event-driven work uses the same boundaries
Not all work begins on a clock. Events may come from GitHub, n8n, filesystem watchers, memory ingestion, service health transitions, or a previous workflow.
The source may differ, but the boundary should remain stable:
event source
-> normalize and deduplicate in the owning workflow layer
-> submit bounded work
-> Supervisor Orchestrator
-> policy and approved execution
-> Run Ledger
A filesystem watcher does not become a privileged agent because it can notice a change. Observation and authority remain separate.
Memory maintenance is scheduled; memory authority is not
The scheduler may trigger operational memory jobs such as expiry scans, embedding refresh, index compaction, backup verification, or ingestion dispatch.
But memory access and promotion remain governed operations. A scheduled job should not silently promote active planning context into durable knowledge. Operational memory, curated knowledge memory, and canonical project documentation have different authority and lifecycle rules.
The timer starts maintenance. The memory service enforces mechanical filters. The control plane declares purpose and scope. Governance decides access and promotion.
Secrets and exposure
Dubnium keeps the defaults narrow:
- timer execution does not require a network listener
- the scheduler API binds to
127.0.0.1 - n8n is opt-in and local by default
- GitHub runner registration is explicit
- secrets live under runtime paths such as
/run/secrets/... - generated Nix-store files contain no tokens
- external exposure requires a separate documented policy
A local scheduler should not become a remote arbitrary-command API.
A practical example: CareerOps today
CareerOps spans several existing boundaries.
The timer owns time. GitHub owns the repository workflow. The runner owns local execution of trusted jobs. The raw local endpoint currently provides bounded inference. The result remains reviewable before a human applies.
The target evolution is to replace the direct model call with the Supervisor Gateway and Orchestrator so memory, policy, specialist routing, and Run Ledger evidence use the same control-plane contract as other agentic work.
No layer needs to become universally privileged.
What exists today
Dubnium already has:
- declarative scheduler jobs
- systemd timer and oneshot materialization
- per-job working directories
- explicit repository-mutation declarations
- executor-boundary metadata
- localhost scheduler runtime API
dubctl scheduleinspection and control- n8n as an opt-in workflow boundary
- self-hosted GitHub runner support
- documented automation ownership rules
The next useful integration work is:
- add first-class declarative workflow-dispatch objects
- route agentic dispatches into the Supervisor Orchestrator contract
- share actor, task, scope, and evidence envelopes across boundaries
- connect the future Run Ledger to agentic runs
- make approval and effect evidence visible through operator tooling
- retain Nix as the source of truth for durable host schedules
The larger lesson
Scheduling autonomy is not mainly a calendar problem.
It is an ownership problem.
A durable system needs to explain:
- who declared the schedule
- which layer triggered it
- which system owned the workflow
- which model or specialist proposed an action
- which policy authorized it
- which executor performed it
- where lifecycle and effect evidence were recorded
Dubnium’s answer is not a universal scheduler.
It is a layered system where timers remain small, workflow engines remain explicit, repository mutation stays repository-governed, and agentic work passes through the same policy and execution boundaries whether a human or a clock started it. The full sequence is collected on the Dubnium series page.
