Skip to content

Idempotency & Safety

A core design goal of SysopKit is that running the same script multiple times produces the same final state — without unnecessary side effects. This is achieved through idempotent operations and a change-tracking system.

An operation is idempotent if applying it multiple times has the same effect as applying it once. For example, creating a file with specific content is idempotent — the second run simply checks that content hasn’t changed.

  • Safe re-runs: Re-run failed scripts without worrying about duplicate changes
  • Predictable outcomes: The same script always converges to the same state
  • Auditable changes: Every modification is captured as a ChangeEntry event

When an operation detects a difference and makes a change, it emits a CHANGE_EVENT via emitChanged(). This event propagates up the context chain, allowing parent scopes to react.

import { emitChanged } from 'sysopkit';
// Inside an operation after detecting a diff
emitChanged({
type: 'file',
resource: '/etc/nginx/nginx.conf',
property: 'content',
to: 'server { listen 80; }',
});

Use onChange() with latch() to conditionally run follow-up actions only when something actually changed:

import { onChange, latch } from 'sysopkit';
const restart = latch();
await onChange(restart, async () => {
await createFile({ path: '/etc/service.conf', content: 'new config' });
});
if (restart()) {
// Only runs if createFile actually changed something
await sh('systemctl restart myservice');
}

Some commands are inherently non-idempotent — sending a notification, appending to a log, or running a one-time migration. For these, use ctx.dryRun to guard execution:

import { context } from 'sysopkit';
if (!ctx.dryRun) {
await sh('notify-admin "Deployment complete"');
}

Set dryRun: true in start() options or SYSOPKIT_DRY_RUN=1 environment variable. Idempotent operations will emit change events (showing what would happen) but skip actual modifications.