dsh-cron
Unattended scheduled jobs for the DeepSeek Harness (dsh): agent/command tasks on cron schedules
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 26, 2026
- Updated
- Aug 26, 2026
Introduction
dsh-cron
English | 简体中文
Unattended scheduled-jobs plugin for DeepSeek Harness (dsh): run agent tasks (spawn a one-shot agent to execute a prompt through the full dsh toolchain) or command tasks (run a script directly) on a cron expression, a fixed interval, or a one-time instant. Complementary to @deepseek-ai/dsh-schedule — that one is persistent in-session reminders, this one is a host-side job scheduler: jobs belong to no interactive session and fire automatically while the process is up.

UI
- Sidebar section: status dot (last result) + next-trigger time, live elapsed timer while running; rows expand into run history, and clicking an agent run jumps straight to that run's full session.
- Create / edit modal: trigger kind, task kind, mode / permission / model knobs (blank = inherit defaults), working directory, timeout, and overlap / misfire policies — all on one screen.
| Create a job | Row actions |
|---|---|
![]() | ![]() |
Features
- Three trigger kinds:
cron(5-field expression + explicit IANAtimeZone; the process time zone is never consulted),everySeconds(anchor-aligned interval, 60s minimum),at(one-time RFC 3339 instant; a Z or numeric offset is required). - Two task kinds:
agent(create a one-shot agent viactx.agents.create, submit the prompt, wait for quiescence, take the last assistant message as the summary, dispose to finish — i.e. the dsh-headless one-shot recipe);command(spawn a child process, record exit code and output tail). - Persistent state: job dispatch state and run history live in the storage domain layer (
ctx.storage.domain, domain namecron), never in session event logs. - Reliability semantics: at-most-once per occurrence (
lastFiredMsis persisted before execution); missed occurrences are never replayed one by one (the misfire policy runs at most once, against the latest due occurrence); runs interrupted by a crash are repaired toabortedon the next startup. - Policies:
overlap: skip | queue | replace(when the previous run is still going: skip / queue the latest one occurrence / kill and restart);misfire: skip | runOnce(occurrences missed while the process was down: ignore / catch up once). - Delivery: optional delivery command; the run record is fed as JSON on stdin (fires only on failure by default).
- Clock discipline (inherited from dsh-schedule): long waits are chunked and the wall clock is re-read on every wake-up — a backwards clock jump never fires early, a forwards jump is handled as overdue.
Installation
From the Plugin Market
With dsh-market installed, open Settings → Plugin Market, search dsh-cron, and install with one click — the market adds both the dependency and the profile bundle entry for you, and most installs go live after a page refresh.
Or install the release tarball from the command line (this only runs the package install — add "dsh-cron" to dsh.profile.bundles yourself, as shown below):
dsh plugin --profile web add https://github.com/squirrel20/dsh-cron/releases/latest/download/dsh-cron.tgz
The npm package named
dsh-cronis an unrelated project — install from the market or the release tarball, not from the npm registry.
From source
In your profile's package.json:
{
"dependencies": { "dsh-cron": "link:/path/to/dsh-cron" }, // or a git checkout / release tarball
"dsh": { "profile": { "bundles": [ /* …existing bundles… */, "dsh-cron" ] } }
}
Config-declared jobs (optional)
Declare always-on jobs by overriding the config in the profile's cordis.patch.yml — or skip this entirely and create jobs from the UI or a session (see Usage):
- id: dsh-cron
config:
maxConcurrentRuns: 1
historyLimit: 50
jobs:
- name: daily-log-review
schedule: { cron: "0 7 * * *", timeZone: "Asia/Shanghai" }
task:
kind: agent
prompt: Read yesterday's logs under logs/, summarize anomalies and suggest remediations.
cwd: /path/to/project
timeoutSeconds: 1800
policy: { overlap: skip, misfire: skip }
delivery:
argv: ["/usr/local/bin/notify", "--stdin"]
onlyOnFailure: true
- name: heartbeat
schedule: { everySeconds: 3600 }
task: { kind: command, argv: ["./scripts/heartbeat.sh"], cwd: /path/to/project }
sessionGc: # optional; defaults: enabled: true, graceMinutes: 30, root: ~/.dsh/sessions
enabled: true
graceMinutes: 30
Job misconfiguration (duplicate names, invalid expressions, missing time zone, …) fails loud at mount time — it is never swallowed silently.
Usage
Adding a job by hand
Click + in the sidebar's Cron Jobs section header. The New job dialog configures everything on one screen:
- Name — lowercase, digits and
-. - Trigger —
cron(5-field expression + IANA time zone),interval, orone-shot. - Task —
agent(a prompt executed unattended through the full dsh toolchain) orcommand(an argv to spawn). - Preset / Access / Model — leave blank to inherit the host defaults.
- Working directory — type a path or browse via the folder icon.
- Timeout, On overlap, On misfire — see Features for the policy semantics.
Create & enable persists the job (a "manual" chip marks it apart from config-declared jobs). Afterwards, each row's ⋯ menu offers Run now / Pause schedule / Edit job / Delete job; clicking a row expands its run history, and clicking an agent run opens that run's full session replay.
Adding a job from a session
Just ask the agent in any session:
Every Monday at 07:00 review our outdated dependencies and save an upgrade checklist to reports/deps-audit.md.
The bundled cron-create skill (auto-registered when the host has a skill registry) walks the model through collect → confirm → create → verify, calling the cron_create tool under the hood; cron_delete removes a manual job the same way. Jobs created from a session are ordinary manual jobs — the exact same overlay the web dialog writes — so they show up in the sidebar immediately and can be edited there later. The read/steer tools (cron_list, cron_runs, cron_run_now, cron_enable, cron_disable) work on config-declared jobs too.
Run records
The runs table keeps the most recent historyLimit entries keyed by <job>#<seq>:
{
"job": "daily-log-review", "seq": 42,
"target": "2026-08-26T23:00:00.000Z", // the occurrence this run is for
"startedAt": "…", "finishedAt": "…",
"status": "ok", // ok|failed|timeout|skipped-overlap|replaced|aborted
"summary": "…", // agent's last reply / command output tail (truncated)
"sessionId": "cron-daily-log-review-…" // agent task's session, inspectable under ~/.dsh/sessions
}
Boundaries and known limitations
- Agent runs carry a fixed
[CRON RUN]framing that states the run is unattended and questions are forbidden. It is injected as a scoped system-prompt section, so the user message holds only the job's prompt; hosts without the system-prompt service fall back to prepending it to the message. - Config jobs come from plugin config (declarative); the conversational tools (
cron_list/cron_runs/cron_run_now/cron_enable/cron_disable) observe and steer them but never create or delete them. Runtime "manual" jobs are the exception:cron_create/cron_deletemanage those from a session, guided by the bundledcron-createskill (registered into the host's skill registry when one exists), through the samemanual-table overlay as the web dialog. queuedepth is 1: only the single latest squeezed-out occurrence is kept.
Web overlay
When the profile includes @deepseek-ai/dsh-web-app, the plugin also ships a
sidebar overlay: a clock badge at the sidebar foot opens a panel listing
every job (kind, schedule, next occurrence, latest outcome); a job row
drills into its recent run history, and failed runs expand their summary
tail, session id, and exit code. Rows carry hover actions — run an idle job
now (the cron_run_now semantics), or stop the run in flight (the record
settles as killed; later occurrences are untouched). The panel's +
opens a create form (name, cron/interval/one-shot trigger, agent/command
task, working directory with a browse dialog over the host's directory
capability, timeout, overlap/misfire policy); created jobs persist in the
storage domain's manual table, re-normalize on every boot, and show a
"manual" chip beside config-declared jobs — a config job with the same name
wins and evicts the manual copy. A manual job's drill-in view carries a
two-click delete (trash, then confirm) that drops the job and its whole run
ledger; config jobs and jobs with a run in flight are refused.
The browser half is lib/client.js (declared via exports["./client"] +
the dsh.client package field). The host half (lib/web.js) serves
GET /dsh-cron/api/state plus four writes — POST …/run-now, …/stop,
…/jobs, …/delete — which demand application/json bodies so cross-site simple
requests die before dispatch; routes register on ctx.webServer only while
a webserver is present, so headless profiles mount unchanged.
Tests
npm test # unit tests for the scheduling math (cron parsing, time zones, anchor alignment, misfire collapsing)

