how netsky loop works
netsky loop is the durable scheduler for delayed work. It is the current answer for one-shot and self-paced follow-up, and it replaces harness-local wakeups with state the system owns on disk (src/crates/netsky-cli/src/cli.rs:656-696, src/crates/netsky-core/src/consts.rs:62-66).
shape #
Each loop entry lives as one TOML file under ~/.netsky/state/loops/. The path resolver is a straight join from home() to .netsky/state/loops, then to <id>.toml for each entry (src/crates/netsky-core/src/paths.rs:317-322).
There are two loop modes. fixed means “fire every N seconds.” dynamic means “fire once, then wait for the next explicit reschedule.” The stored shape is the same either way: id, target agent, creation time, mode, prompt, next fire time, and optional last fire time. Only fixed loops keep interval_secs (src/crates/netsky-core/src/loops.rs:14-33, src/crates/netsky-core/src/loops.rs:77-108).
~/.netsky/state/loops/ loop-7f3c2a10.toml id = "loop-7f3c2a10" agent = "agent0" created_utc = "2026-04-19T19:00:00Z" mode = "dynamic" # or "fixed" prompt = "check the merge queue" next_fire_utc = "2026-04-19T19:25:00Z" last_fire_utc = "2026-04-19T18:35:00Z" # optional interval_secs = 900 # fixed only
Dynamic loops get a default delay of 1500 seconds, which is 25 minutes, unless the agent reschedules them sooner or later (src/crates/netsky-core/src/consts.rs:65-66, src/crates/netsky-core/src/loops.rs:57-75, src/crates/netsky-core/src/loops.rs:136-138). That makes them useful for self-paced work: “check again after the next chunk lands” without pretending the cadence is truly periodic.
The loop entry does not know who will be alive when it fires. It only stores a target name such as agent0 or agentinfinity. Delivery is just an envelope write to that agent’s inbox when the entry becomes due (src/crates/netsky-core/src/loops.rs:23-32, src/crates/netsky-cli/src/cmd/loop_cmd.rs:152-169).
concrete interface #
The CLI surface is small on purpose: create, list, delete, tick, and reschedule (src/crates/netsky-cli/src/cli.rs:656-696).
create has two forms. With two positional arguments, it makes a fixed loop. The first argument is the interval. The second is the prompt. With one positional argument, it makes a dynamic loop, and that argument is the prompt itself (src/crates/netsky-cli/src/cmd/loop_cmd.rs:47-70).
netsky loop create 15m "check CI and report delta only" netsky loop create "wait for the next review wave, then summarize blockers"
Interval parsing accepts s, m, h, and d. every 15m also works because the parser strips a leading every before reading the number and unit (src/crates/netsky-cli/src/cmd/loop_cmd.rs:226-246).
list loads every .toml file in the loops directory, sorts by next_fire_utc, and prints id, mode, target agent, interval, next fire, last fire, and prompt (src/crates/netsky-core/src/loops.rs:146-161, src/crates/netsky-cli/src/cmd/loop_cmd.rs:91-123).
netsky loop list
delete removes one entry by id. The delete path is exactly one file removal. If the file is missing, the command errors instead of silently passing (src/crates/netsky-cli/src/cmd/loop_cmd.rs:126-137, src/crates/netsky-core/src/loops.rs:194-199).
netsky loop delete loop-7f3c2a10
reschedule is the one special verb. It only works on dynamic loops. Fixed loops reject it because their cadence comes from interval_secs, not ad hoc delay overrides (src/crates/netsky-cli/src/cmd/loop_cmd.rs:188-210).
netsky loop reschedule loop-7f3c2a10 300
That 300 is seconds from now. The command loads the entry, rewrites next_fire_utc, and saves the file back to disk (src/crates/netsky-core/src/loops.rs:164-191, src/crates/netsky-core/src/loops.rs:214-218).
tick is the delivery verb. Most operators will never call it by hand because the ticker does it every minute, but the command is still exposed for testing and recovery (src/crates/netsky-cli/src/cmd/loop_cmd.rs:140-186).
netsky loop tick
the tick loop #
netsky-ticker is a dedicated loop that runs watchdog tick, cron tick, and loop tick once per interval. The default interval is 60 seconds (src/crates/netsky-cli/src/cmd/tick.rs:133-163).
When netsky loop tick runs, it loads every entry, checks entry.is_due(now), writes an envelope into the target inbox for each due prompt, then updates last_fire_utc and computes a new next_fire_utc before saving the entry back to disk (src/crates/netsky-cli/src/cmd/loop_cmd.rs:149-170, src/crates/netsky-core/src/loops.rs:122-138, src/crates/netsky-core/src/loops.rs:176-191).
The envelope landing in ~/.netsky/channels/agent/<target>/inbox/ is one JSON file:
{ "from": "agentloop", "to": "agent0", "text": "<the loop's prompt>", "ts": "2026-04-19T19:25:00Z", "kind": "loop-tick", "thread": "loop-7f3c2a10" }
The target agent’s channel source picks it up on the next drain and surfaces it as <channel source="agent" from="agentloop" kind="loop-tick" thread="loop-7f3c2a10">. At that point it reads like any other bus message - the agent decides whether to act, reply, or both. agentloop is the envelope - a JSON file carrying {from, to, text, ts, kind, thread} - the scheduler writes; the runtime treats it exactly the way it treats owner-sent or clone-sent traffic (src/crates/netsky-core/src/consts.rs:60-66).
sequenceDiagram
participant T as netsky-ticker
participant L as netsky loop tick
participant E as loop entry TOML
participant I as target inbox
participant A as agent drain
T->>L: run every 60s
L->>E: load entry and compare next_fire_utc
alt entry due
L->>I: write loop-tick envelope
L->>E: set last_fire_utc and next_fire_utc
A->>I: drain inbox
else not due
L->>E: no change
end
The envelope gets from=agentloop, kind=loop-tick, and thread=<loop id>. That means the receiving agent can tell that the prompt came from the scheduler, not from a person or another clone (src/crates/netsky-core/src/consts.rs:61-64, src/crates/netsky-cli/src/cmd/loop_cmd.rs:156-167).
one-shot vs recurring #
netsky loop and netsky cron are siblings, not rivals. Pick loop when the unit is “wake me up later with this prompt” or “keep this self-paced follow-up alive.” Pick cron when the unit is a calendar schedule such as 0 11 * * * (src/crates/netsky-cli/src/cli.rs:618-696, src/crates/netsky-core/src/cron.rs:42-63).
That difference shows up on disk. Loop state is one TOML file per entry under ~/.netsky/state/loops/. Cron state is one TOML file with an entries array under ~/.netsky/cron.toml (src/crates/netsky-core/src/loops.rs:1-2, src/crates/netsky-core/src/cron.rs:1-27, src/crates/netsky-core/src/paths.rs:317-322).
durability #
The important property is not the syntax. It is the ownership boundary. Loop state survives restart, crash, and agent death because the schedule is not hidden in a live session. The ticker only needs the files and the target name to keep delivering work (src/crates/netsky-core/src/loops.rs:168-191, src/crates/netsky-cli/src/cmd/tick.rs:144-163).
File writes are atomic in the boring UNIX sense. Save writes *.toml.tmp first, then renames it into place. Delete removes the file. Load tolerates the loops directory not existing yet and returns an empty list (src/crates/netsky-core/src/loops.rs:146-152, src/crates/netsky-core/src/loops.rs:180-191, src/crates/netsky-core/src/loops.rs:194-199).
That is enough to make netsky loop durable delayed work, not a fragile reminder.
what this is not #
netsky loop is still narrow. There is no retry policy if inbox delivery fails halfway through a tick. There are no chained dependencies between loop entries. There is no priority system. Due work is just “load every file, dispatch the ones whose next_fire_utc is in the past, then save the updated entries” (src/crates/netsky-cli/src/cmd/loop_cmd.rs:149-170).
That narrowness is a feature for now. The command does one job: persist a delayed prompt, then hand it to the bus when the ticker says it is time.