Skip to content
Talk to our solutions team

Coordination

Distributed locks and polling primitives. Each function lists its parameters, defaults and return value.

Distributed mutex primitives. Acquire a named lock from a shared store so concurrent workflow runs serialize on a resource.

Two backends:

  • postgres — lease table with token-checked release/refresh. Auto-creates the lease table on first use.
  • dragonfly — Redis-compatible SET NX EX + Lua check-and-act.

Scope: Network

FunctionDescription
lock.acquire()Take a lock; returns a token used for subsequent calls
lock.refresh()Extend the lease on an already-held lock
lock.release()Drop a lock previously held under (key, token)
lock.peek()Inspect the current holder without modifying state

Acquire returns an opaque token (a random UUID). The token must be supplied to lock.refresh and lock.release; without a matching token, neither operation will modify the lease.

The token only proves ownership at release time. It is not a monotonic fencing token — workflows that need fencing-grade verification against a downstream resource (e.g. preventing a stale lock holder from writing to a shared bucket) must build that on top of this primitive.

All four functions accept these parameters for backend selection.

ParameterTypeDefaultDescription
backendstring"postgres""postgres" or "dragonfly"
dsnstringPostgres DSN (postgres://...) or Redis URL (redis://...) — required
tablestring"kis_locks"Postgres lease table name (ignored for dragonfly)
keystringLock key — required for all ops

Take a distributed lock under the given key.

In addition to the common params above:

ParameterTypeDefaultDescription
ttlint or stringLease duration — required ("5m" or integer seconds)
waitint or string0How long to block-and-retry before giving up. Default 0 = fail fast
ownerstringOptional metadata stamped on the lease (visible to peek)
FieldTypeDescription
successboolMirrors acquired
acquiredboolWhether the lock was taken
attemptsintNumber of acquire calls before success or wait expiry
keystringEchoes the requested key
tokenstringOpaque ownership token (present only on success)
expiresAtstringRFC3339 timestamp when the lease will expire
errorstringFailure reason when success is false
// Fail-fast acquire
let r = lock.acquire({
backend: "postgres",
dsn: PG_DSN,
key: "deploy/prod",
ttl: "10m"
})
if (!r.acquired) {
log("someone else is deploying right now")
return
}
// Block up to 30s for the lock
let r = lock.acquire({
backend: "dragonfly",
dsn: "redis://dragonfly.internal:6379/0",
key: "certs/rotation",
ttl: "5m",
wait: "30s",
owner: "rotation-bot@" + Date.now()
})

Extend the lease on an already-held lock.

In addition to the common params:

ParameterTypeDescription
tokenstringToken from the matching lock.acquire() call (required)
ttlint or stringNew lease duration (required)
FieldTypeDescription
successboolMirrors refreshed
refreshedboolWhether the lease was extended. false when the (key, token) pair doesn’t own a live lease
expiresAtstringNew RFC3339 expiry on success
// Hold a lock for an extra 2 minutes during a long migration
lock.refresh({
backend: "postgres",
dsn: PG_DSN,
key: "migration/2026-05-19",
token: heldLock.token,
ttl: "2m"
})

Drop the lock if the supplied token still owns it. Releasing with the wrong token is a no-op — released: false, no error.

In addition to the common params:

ParameterTypeDescription
tokenstringToken from the matching lock.acquire() call (required)
FieldTypeDescription
successboolMirrors released
releasedboolWhether the token still owned a live lease
lock.release({
backend: "postgres",
dsn: PG_DSN,
key: "deploy/prod",
token: heldLock.token
})

Inspect the current holder of a key without modifying state.

FieldTypeDescription
successboolWhether the peek completed (true even when the lock isn’t held)
heldboolWhether a live lease exists
tokenstringCurrent holder’s token (when held: true)
ownerstringCurrent holder’s owner metadata (when set on acquire)
expiresAtstringRFC3339 expiry of the current lease
let peek = lock.peek({
backend: "dragonfly",
dsn: "redis://dragonfly.internal:6379/0",
key: "nightly-report"
})
if (peek.held) {
log("nightly report already running, owner=" + peek.owner)
}
const dsn = "postgres://app:[email protected]:5432/locks?sslmode=disable"
// Acquire — block up to 30 seconds
let held = lock.acquire({
backend: "postgres",
dsn: dsn,
key: "deploy/prod",
ttl: "10m",
wait: "30s",
owner: "deploy-" + Date.now()
})
if (!held.acquired) {
throw "could not get deploy lock"
}
try {
// ... do work ...
// Halfway through, extend the lease
lock.refresh({
backend: "postgres",
dsn: dsn,
key: "deploy/prod",
token: held.token,
ttl: "10m"
})
// ... finish work ...
} finally {
lock.release({
backend: "postgres",
dsn: dsn,
key: "deploy/prod",
token: held.token
})
}
  • Lease table is auto-created (CREATE TABLE IF NOT EXISTS) with an index on expires_at.
  • Expired leases are stealable: a fresh acquire whose WHERE clause sees expires_at < NOW() will steal the slot. There is no separate garbage-collection job.
  • The lock client uses a small connection pool (5 open, 2 idle) shared per-DSN across the process.
  • Table name is validated against [A-Za-z_][A-Za-z0-9_]*.
  • Lock value is encoded as "<token>|<owner>" so peek returns the owner in a single GET.
  • release and refresh use Lua to fold the “is this still my lock?” check and the DEL/PEXPIRE into one atomic step. The same scripts work unchanged against real Dragonfly.
  • The Redis client is shared per-URL across the process.

Polling primitives — block until a runtime condition becomes true.

Today only wait.http is implemented. wait.tcp, wait.dns, wait.exec, wait.file, and wait.db are planned and will land in this namespace without breaking the existing API.

Scope: Network

FunctionDescription
wait.http()Poll an HTTP endpoint until the response satisfies the configured criteria

Poll a URL repeatedly until the response satisfies every configured matcher, or the timeout elapses, or the calling context is cancelled.

ParameterTypeDefaultDescription
urlstringTarget URL (required)
methodstring"GET"HTTP method
headersmapRequest headers — sent on every poll
bodystringRequest body — reused on every poll
statusint or [int]200Acceptable HTTP status code(s)
bodyMatchstringRegex the response body must match
bodyContainsstringSubstring the response body must contain
headerMatchmapPer-header regex constraints (header name → regex)
intervalint or string5sTime between polls ("5s" or integer seconds)
timeoutint or string5mTotal timeout
initialDelayint or string0Delay before the first poll
insecureTLSboolfalseSkip TLS certificate verification
expectFailureboolfalseInvert: succeed when the URL stops responding
FieldTypeDescription
successboolWhether the wait condition was met
attemptsintPolls performed
elapsedMsintTotal elapsed milliseconds
lastStatusintStatus of the final poll (populated on timeout too)
lastBodystringBody of the final poll
lastHeadersmapHeaders of the final poll
errorstringFailure reason when success is false
  • expectFailure: true is the inverse mode. Connection errors (refused, DNS failure, TLS rejection) count as success. A 2xx still counts as “service is up — keep waiting.” Use it to gate on shutdown.
  • bodyMatch is regexp.MatchString (Go’s RE2 dialect). bodyContains is a literal substring test — cheaper and easier to write when you don’t need regex.
  • headerMatch entries combine with AND — every named header must match its regex.
  • interval caps each individual request as well (the HTTP client’s per-request timeout). A response that takes longer than the interval is treated as a non-match for that attempt.
  • Last response is captured even on timeoutlastStatus, lastBody, and lastHeaders reflect the final poll attempt, so you can introspect why the condition never became true.
let r = wait.http({
url: "http://localhost:8080/healthz",
interval: "2s",
timeout: "90s"
})
if (!r.success) {
throw "service never came up: " + r.error
}
let r = wait.http({
url: "https://api.example.com/jobs/" + jobId,
headers: { Authorization: "Bearer " + token },
bodyContains: '"status":"done"',
interval: "5s",
timeout: "30m"
})
log("job took " + r.elapsedMs + "ms (" + r.attempts + " polls)")
wait.http({
url: "https://api.example.com/version",
bodyMatch: "\"version\":\"2\\.\\d+\\.\\d+\"",
interval: "10s",
timeout: "10m"
})
wait.http({
url: "https://api.example.com/health",
status: [200, 201, 202, 204],
interval: "3s",
timeout: "2m"
})
wait.http({
url: "http://localhost:8080/healthz",
expectFailure: true,
interval: "1s",
timeout: "30s"
})
wait.http({
url: "https://api.example.com/health",
headerMatch: {
"X-Version": "^v2\\.",
"X-Mode": "^production$"
},
interval: "5s",
timeout: "5m"
})
Read the captured last response on timeout
Section titled “Read the captured last response on timeout”
let r = wait.http({
url: "https://api.example.com/state",
bodyContains: '"ready":true',
timeout: "2m"
})
if (!r.success) {
log("gave up after " + r.attempts + " polls; last status=" + r.lastStatus)
log("last body: " + r.lastBody)
}
wait.http({
method: "POST",
url: "https://api.example.com/check",
body: '{"region":"us-east-1"}',
headers: { "Content-Type": "application/json" },
bodyContains: '"ok":true',
interval: "10s",
timeout: "5m"
})

Scheduled job management functions. Manages recurring and one-time jobs with YAML-based storage and OS scheduler integration. Cross-platform support for crontab (Linux), launchd (macOS), and Task Scheduler (Windows).

Scope: System

FunctionDescription
cron.add()Add a recurring scheduled job
cron.once()Schedule a one-time job
cron.remove()Remove a job
cron.list()List all managed jobs
cron.show()Show details of a specific job
cron.exec()Execute a job in foreground
cron.run()Run a job in background
cron.logs()View job log output
cron.history()View job execution history
cron.sync()Sync between YAML store and OS scheduler

Add a recurring scheduled job.

ParameterTypeDefaultDescription
namestringJob name (required)
commandstringCommand to execute (required)
schedulestringSchedule expression (required)
dirstringWorking directory
shellstring/bin/shShell to use
envmapEnvironment variables
timeoutstringJob timeout (30m, 1h)
overlapstringConcurrent execution: skip, queue, allow
enabledbooltrueWhether job is active

Schedule accepts human-friendly expressions (daily at 2am, every 5 minutes, weekly on monday at 9am) or raw cron syntax (cron */5 * * * *).

FieldTypeDescription
successboolWhether the job was added
jobmapJob details (name, schedule, cron, command, enabled, etc.)
errorstringError message if failed
// Add a daily backup job
let result = cron.add({
name: "daily-backup",
command: "/opt/scripts/backup.sh",
schedule: "daily at 2am",
dir: "/opt/backup",
timeout: "1h",
env: { BACKUP_DIR: "/mnt/backups" }
})
log("Job created: " + result.job.name)
// Add with raw cron expression
cron.add({
name: "health-check",
command: "curl -sf http://localhost:8080/health",
schedule: "cron */5 * * * *",
timeout: "30s"
})
// Add weekly report
cron.add({
name: "weekly-report",
command: "/opt/scripts/report.sh",
schedule: "weekly on monday at 9am",
env: { REPORT_EMAIL: "[email protected]" }
})

Schedule a one-time job execution.

ParameterTypeDefaultDescription
namestringJob name (required)
commandstringCommand to execute (required)
timestringWhen to run: now, in 5 minutes, today at 3pm (required)
dirstringWorking directory
shellstring/bin/shShell to use
envmapEnvironment variables
timeoutstringJob timeout
FieldTypeDescription
successboolWhether the job was scheduled
jobmapJob details
errorstringError message if failed
// Run immediately
cron.once({
name: "db-migrate",
command: "/opt/scripts/migrate.sh",
time: "now"
})
// Schedule for later
cron.once({
name: "deploy-v2",
command: "/opt/scripts/deploy.sh --version v2.0.0",
time: "today at 11pm",
dir: "/opt/app"
})
// Schedule in relative time
cron.once({
name: "cleanup",
command: "/opt/scripts/cleanup.sh",
time: "in 30 minutes"
})

Remove a scheduled job.

ParameterTypeDefaultDescription
Inputstring or mapJob name string, or map with name key (required)
FieldTypeDescription
successboolWhether the job was removed
errorstringError message if failed
// Remove by name string
cron.remove("health-check")
// Remove with map
cron.remove({ name: "old-backup" })

List all managed jobs.

ParameterTypeDefaultDescription
Inputnil or mapOptional map with data_dir
FieldTypeDescription
successboolWhether the listing succeeded
jobsarrayArray of job objects
errorstringError message if failed

Each job object contains:

FieldTypeDescription
namestringJob name
schedulestringHuman-friendly schedule
cronstringCron expression
commandstringCommand to execute
enabledboolWhether the job is active
createdstringCreation timestamp (ISO 8601)
updatedstringLast update timestamp (ISO 8601)
// List all jobs
let result = cron.list()
for (let job of result.jobs) {
let status = job.enabled ? "enabled" : "disabled"
log(job.name + " [" + status + "] — " + job.schedule)
}
// Count enabled jobs
let result = cron.list()
let enabled = result.jobs.filter(j => j.enabled).length
log("Enabled jobs: " + enabled + "/" + result.jobs.length)

Show details of a specific job.

ParameterTypeDefaultDescription
Inputstring or mapJob name string, or map with name key (required)
FieldTypeDescription
successboolWhether the lookup succeeded
jobmapFull job details
errorstringError message if failed
// Show by name
let result = cron.show("daily-backup")
log("Schedule: " + result.job.schedule)
log("Cron: " + result.job.cron)
log("Command: " + result.job.command)
log("Enabled: " + result.job.enabled)
// Show with map
let result = cron.show({ name: "health-check" })
log("Timeout: " + result.job.timeout)

Execute a job immediately in the foreground. Waits for completion and returns output.

ParameterTypeDefaultDescription
Inputstring or mapJob name string, or map with name key (required)
FieldTypeDescription
successboolWhether execution succeeded
outputstringCommand output
exit_codeintProcess exit code
errorstringError message if failed
// Execute in foreground
let result = cron.exec("daily-backup")
if (result.success) {
log("Output: " + result.output)
log("Exit code: " + result.exit_code)
} else {
log("Failed: " + result.error)
}

Run a job in the background. Returns immediately without waiting for completion.

ParameterTypeDefaultDescription
Inputstring or mapJob name string, or map with name key (required)
FieldTypeDescription
successboolWhether the job was started
outputstringStartup output
errorstringError message if failed
// Run in background
let result = cron.run("daily-backup")
if (result.success) {
log("Job started in background")
}

View job log output.

ParameterTypeDefaultDescription
Inputstring or mapJob name string, or map with options (required)
namestringJob name (required)
linesint50Number of log lines to return
FieldTypeDescription
successboolWhether the logs were retrieved
logsstringLog content
errorstringError message if failed
// View last 50 lines (default)
let result = cron.logs("daily-backup")
log(result.logs)
// View last 100 lines
let result = cron.logs({ name: "daily-backup", lines: 100 })
log(result.logs)

View job execution history with timestamps, duration, and exit codes.

ParameterTypeDefaultDescription
Inputstring or mapJob name string, or map with options (required)
namestringJob name (required)
linesint50Number of history entries
FieldTypeDescription
successboolWhether the history was retrieved
historyarrayArray of history entries
errorstringError message if failed

Each history entry contains:

FieldTypeDescription
jobstringJob name
startedstringStart timestamp (ISO 8601)
finishedstringFinish timestamp (ISO 8601)
durationstringExecution duration
exit_codeintProcess exit code
triggerstringWhat triggered the execution
// View recent history
let result = cron.history("daily-backup")
for (let entry of result.history) {
let status = entry.exit_code === 0 ? "OK" : "FAILED"
log(entry.started + "" + entry.duration + "" + status)
}
// View last 20 entries
let result = cron.history({ name: "health-check", lines: 20 })
let failures = result.history.filter(e => e.exit_code !== 0)
log("Failures: " + failures.length + "/" + result.history.length)

Sync between the YAML job store and the OS scheduler (crontab, launchd, or Task Scheduler).

ParameterTypeDefaultDescription
directionstring"both"Sync direction: to-os, from-os, both
dry_runboolfalsePreview changes without applying
FieldTypeDescription
successboolWhether the sync succeeded
changesarrayArray of change objects
errorstringError message if failed

Each change object contains:

FieldTypeDescription
actionstringChange action (add, remove, update)
namestringJob name
detailstringDescription of the change
// Sync YAML to OS scheduler
let result = cron.sync({ direction: "to-os" })
for (let change of result.changes) {
log(change.action + ": " + change.name + "" + change.detail)
}
// Preview changes (dry run)
let result = cron.sync({ direction: "both", dry_run: true })
log("Pending changes: " + result.changes.length)
for (let change of result.changes) {
log(" [" + change.action + "] " + change.name)
}
// Sync from OS to YAML
cron.sync({ direction: "from-os" })
// Complete job lifecycle: create, verify, execute, check logs, clean up
let jobName = "nightly-backup"
// Create the job
let added = cron.add({
name: jobName,
command: "/opt/scripts/backup.sh --full",
schedule: "daily at 2am",
dir: "/opt/backup",
timeout: "2h",
env: {
BACKUP_TARGET: "/mnt/backups",
RETENTION_DAYS: "30"
}
})
log("Created job: " + added.job.name)
// Verify it was created
let details = cron.show(jobName)
log("Schedule: " + details.job.schedule)
log("Cron expression: " + details.job.cron)
// Execute immediately to test
let execResult = cron.exec(jobName)
log("Test run exit code: " + execResult.exit_code)
// Check logs
let logs = cron.logs({ name: jobName, lines: 20 })
log("Output: " + logs.logs)
// Sync to OS scheduler
let syncResult = cron.sync({ direction: "to-os" })
log("Synced " + syncResult.changes.length + " changes to OS")
// List all jobs
let allJobs = cron.list()
log("Total managed jobs: " + allJobs.jobs.length)

TCP/UDP port management functions.

Scope: System

FunctionDescription
port.check()Check if a port is free
port.find()Find the first free port in a range
port.list()List processes using a port
port.kill()Kill all processes on a port

Check if a TCP/UDP port is free. Uses fuser on Linux.

ParameterTypeDefaultDescription
Inputint or mapPort number, or map with options (required)
portintTarget port number (required)
protocolstring"tcp"Protocol: "tcp" or "udp"
FieldTypeDescription
successboolWhether the check completed
freeboolWhether the port is free
errorstringError message if failed
// Simple check with port number
let result = port.check(8080)
if (result.free) {
log("Port 8080 is available")
} else {
log("Port 8080 is in use")
}
// Check UDP port
let result = port.check({ port: 5353, protocol: "udp" })
log("UDP 5353 free: " + result.free)

Find the first free port in a range. Scans from startport to endport in steps of increment, skipping ports below minport.

ParameterTypeDefaultDescription
startportint3000Range start
endportint65000Range end
incrementint1000Step size
minportint10000Skip ports below this value
protocolstring"tcp"Protocol: "tcp" or "udp"
FieldTypeDescription
successboolWhether a free port was found
portintThe free port number
errorstringError message if failed
// Find free port with defaults
let result = port.find({})
log("Free port: " + result.port)
// Find in a specific range
let result = port.find({
startport: 10000,
endport: 60000,
increment: 100
})
log("Free port: " + result.port)
// Find with minimum port
let result = port.find({
startport: 3000,
endport: 65000,
increment: 1000,
minport: 20000
})
if (result.success) {
log("Found port: " + result.port)
} else {
log("No free port found in range")
}

List processes using a specific port.

ParameterTypeDefaultDescription
Inputint or mapPort number, or map with options (required)
portintTarget port number (required)
protocolstring"tcp"Protocol: "tcp" or "udp"
FieldTypeDescription
successboolWhether the listing succeeded
processesarrayArray of process objects
errorstringError message if failed

Each process object contains:

FieldTypeDescription
pidstringProcess ID
userstringProcess owner
commandstringCommand name
// List processes on port 8080
let result = port.list(8080)
for (let p of result.processes) {
log(p.pid + " " + p.user + " " + p.command)
}
// List with protocol
let result = port.list({ port: 53, protocol: "udp" })
if (result.processes.length === 0) {
log("No processes on UDP port 53")
} else {
log("Found " + result.processes.length + " processes")
}

Kill all processes using a port. Sends the specified signal to all processes.

ParameterTypeDefaultDescription
Inputint or mapPort number, or map with options (required)
portintTarget port number (required)
protocolstring"tcp"Protocol: "tcp" or "udp"
signalstring"KILL"Signal to send (KILL, TERM, HUP, etc.)
FieldTypeDescription
successboolWhether the kill succeeded
freeboolWhether the port is now free
errorstringError message if failed
// Kill with default signal (KILL)
let result = port.kill(8080)
log("Port free: " + result.free)
// Graceful termination
port.kill({ port: 8080, signal: "TERM" })
// Kill processes on UDP port
port.kill({ port: 5353, protocol: "udp", signal: "TERM" })
// Port management workflow: check, list, kill, verify
let targetPort = 8080
// Check if port is in use
let check = port.check(targetPort)
if (!check.free) {
// List what's using it
let procs = port.list(targetPort)
log("Port " + targetPort + " is in use by:")
for (let p of procs.processes) {
log(" PID " + p.pid + ": " + p.command + " (" + p.user + ")")
}
// Graceful kill
port.kill({ port: targetPort, signal: "TERM" })
// Verify it's free
let verify = port.check(targetPort)
if (verify.free) {
log("Port " + targetPort + " is now free")
} else {
// Force kill
port.kill(targetPort)
}
} else {
log("Port " + targetPort + " is available")
}

OS user management functions.

Scope: System

FunctionDescription
user.check()Check if an OS user exists
user.create()Create a new OS user
user.delete()Delete an OS user
user.modify()Modify user properties
user.passwd()Set or change a user password

Check if an OS user exists. Returns user details (UID, GID, home directory) when found.

ParameterTypeDefaultDescription
Inputstring or mapUsername string, or map with username key (required)
FieldTypeDescription
successboolWhether the check completed
existsboolWhether the user exists
uidstringUser’s numeric UID
gidstringUser’s numeric GID
homedirstringUser’s home directory
errorstringError message if failed
// Check with username string
let result = user.check("deploybot")
if (result.exists) {
log("User found — UID: " + result.uid + ", Home: " + result.homedir)
} else {
log("User does not exist")
}
// Check with map
let result = user.check({ username: "appuser" })
log("Exists: " + result.exists)

Create a new OS user. Uses useradd on Linux.

ParameterTypeDefaultDescription
Inputstring or mapUsername string, or map with options (required)
usernamestringTarget username (required)
groupnamestringPrimary group name
shellstring/bin/bashLogin shell
homestringHome directory path
groupsarraySupplementary groups
createhomebooltrueCreate home directory
systemboolfalseCreate as system account
FieldTypeDescription
successboolWhether the user was created
errorstringError message if failed
// Simple create with username string
user.create("deploybot")
// Create with primary group and supplementary groups
user.create({
username: "deploybot",
groupname: "deploy",
groups: ["docker", "sudo"],
shell: "/bin/bash"
})
// Create a system account
user.create({
username: "myservice",
system: true,
createhome: false
})
// Create with custom home directory
user.create({
username: "webapp",
home: "/opt/webapp",
groupname: "www-data"
})

Delete an OS user. Uses userdel on Linux.

ParameterTypeDefaultDescription
Inputstring or mapUsername string, or map with options (required)
usernamestringTarget username (required)
removehomeboolfalseRemove home directory on deletion
FieldTypeDescription
successboolWhether the user was deleted
errorstringError message if failed
// Simple delete
user.delete("olduser")
// Delete and remove home directory
user.delete({ username: "olduser", removehome: true })

Modify OS user properties. Uses usermod on Linux.

ParameterTypeDefaultDescription
usernamestringTarget username (required)
groupnamestringNew primary group
shellstringNew login shell
homestringNew home directory
groupsarrayNew supplementary groups
FieldTypeDescription
successboolWhether the modification succeeded
errorstringError message if failed
// Add user to more groups
user.modify({
username: "deploybot",
groups: ["docker", "sudo", "adm"]
})
// Change shell
user.modify({
username: "deploybot",
shell: "/bin/zsh"
})
// Change primary group and home
user.modify({
username: "webapp",
groupname: "www-data",
home: "/var/www/webapp"
})

Set or change an OS user’s password. Uses chpasswd on Linux.

ParameterTypeDefaultDescription
usernamestringTarget username (required)
passwordstringNew password in plain text (required)
FieldTypeDescription
successboolWhether the password was set
errorstringError message if failed
// Set password
user.passwd({
username: "deploybot",
password: "secure-password-123"
})
// Complete user provisioning workflow
let username = "deploybot"
// Check if user already exists
let result = user.check(username)
if (!result.exists) {
// Create user with groups
let created = user.create({
username: username,
groupname: "deploy",
groups: ["docker", "sudo"],
shell: "/bin/bash"
})
if (created.success) {
// Set initial password
user.passwd({
username: username,
password: "initial-password"
})
log("User " + username + " created successfully")
}
} else {
log("User " + username + " already exists (UID: " + result.uid + ")")
// Ensure correct groups
user.modify({
username: username,
groups: ["docker", "sudo", "adm"]
})
}