Building Dubnium: Scheduling Agentic Work

Building Dubnium: Scheduling Agentic Work

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.

flowchart LR T[systemd timer] S[oneshot service] J[journald] T --> S --> J

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.

ConcernOwner
Local timed executionDubnium scheduler and systemd
Multi-step workflow orchestrationn8n or another workflow executor
Repository-governed mutationGitHub Actions and an approved runner
Agentic reasoning and context assemblySupervisor Orchestrator
Policy and approval contractsAnthesis or another governance tool
Agentic lifecycle and effect evidenceRun Ledger
Approved external effectsbounded executor or Tool Execution Gateway

The timer may start the path, but ownership transfers at explicit boundaries.

flowchart TD T[systemd timer] S[Dubnium scheduler] L[bounded local task] D[workflow dispatch] N[n8n] G[GitHub Actions] R[self-hosted runner] O[Supervisor Orchestrator] P[Policy and approval] E[Tool Execution Gateway] RL[Run Ledger] T --> S S --> L S --> D D --> N D --> G --> R N --> O O --> P --> E O --> RL E --> RL

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:

BoundaryMeaning
localbounded host-local execution
github-runnerrepository-governed execution through GitHub Actions
n8nworkflow 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.

sequenceDiagram participant T as systemd timer participant S as Dubnium scheduler participant O as Supervisor Orchestrator participant M as Memory Context Service participant P as Policy Engine participant X as Tool Execution Gateway participant L as Run Ledger T->>S: trigger declared dispatch S->>O: submit bounded task envelope O->>M: request scoped context pack M-->>O: source-attributed context O->>P: submit plan and tool intents P-->>O: allow, constrain, deny, or require approval O->>X: perform approved effect X-->>L: record effect result and evidence O-->>L: record lifecycle and policy dispositions

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.

sequenceDiagram participant T as scheduled trigger participant S as Dubnium scheduler participant G as GitHub Actions participant R as self-hosted runner participant A as raw local model endpoint participant P as reviewable artifacts T->>S: start declared workflow dispatch S->>G: workflow_dispatch G->>R: run repository-defined workflow R->>A: request local model analysis A-->>R: proposed resume overlay R->>P: render and publish DOCX and PDF artifacts

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 schedule inspection and control
  • n8n as an opt-in workflow boundary
  • self-hosted GitHub runner support
  • documented automation ownership rules

The next useful integration work is:

  1. add first-class declarative workflow-dispatch objects
  2. route agentic dispatches into the Supervisor Orchestrator contract
  3. share actor, task, scope, and evidence envelopes across boundaries
  4. connect the future Run Ledger to agentic runs
  5. make approval and effect evidence visible through operator tooling
  6. 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.