Skip to main content

Go SDK

The Runix Go SDK (github.com/runixio/runix/sdk) lets you embed a full process supervisor directly into your Go application — no CLI binary or separate daemon required.

Installation​

go get github.com/runixio/runix/sdk

Architecture​

The SDK wraps internal/supervisor directly — the same code path as the CLI. No subprocess spawning, no daemon required.

Quick Start​

package main

import (
"context"
"fmt"
"log"
"time"

"github.com/runixio/runix/sdk"
)

func main() {
mgr, err := sdk.New(sdk.Config{
LogDir: "/tmp/myapp-runix",
})
if err != nil {
log.Fatal(err)
}
defer mgr.Close()

ctx := context.Background()

// Start a Python script.
id, err := mgr.AddProcess(ctx, sdk.ProcessConfig{
Name: "api",
Script: "main.py",
Runtime: "python",
Env: map[string]string{"PORT": "8080"},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Started: %s\n", id)

// Inspect it.
info, _ := mgr.Inspect(id)
fmt.Printf("PID: %d, State: %s\n", info.PID, info.State)

// Stop it gracefully.
mgr.Stop(id, 5*time.Second)
}

Manager API​

Create a manager with sdk.New():

mgr, err := sdk.New(sdk.Config{
LogDir: "/var/lib/myapp/runix",
Defaults: sdk.DefaultsConfig{
RestartPolicy: "on-failure",
MaxRestarts: 5,
StopTimeout: 10 * time.Second,
},
})

Methods​

MethodSignatureDescription
NewNew(cfg Config) (*Manager, error)Create a new manager
AddProcessAddProcess(ctx, cfg ProcessConfig) (string, error)Start a process, returns its ID
StopStop(id string, timeout time.Duration) errorGraceful stop (SIGTERM → SIGKILL)
ForceStopForceStop(id string) errorImmediate kill (SIGKILL)
RestartRestart(ctx context.Context, id string) errorStop and restart (fires restart hooks)
ReloadReload(ctx context.Context, id string) errorGraceful reload (fires reload hooks)
RemoveRemove(id string) errorStop and unregister from manager
ListList() []ProcessInfoList all managed processes
InspectInspect(id string) (*ProcessInfo, error)Get detailed process info
LogsLogs(ctx context.Context, id string, opts LogOptions) (io.ReadCloser, error)Stream process logs
SaveSave() errorPersist process state to disk
ResurrectResurrect() errorRestore saved processes
CloseClose() errorStop all processes and clean up (safe to call multiple times)
LogPathLogPath(id string) stringReturns stdout log file path
LogPathStderrLogPathStderr(id string) stringReturns stderr log file path

ProcessConfig​

sdk.ProcessConfig{
Name: "my-worker", // required

// Use Script for interpreted files, Binary for compiled executables:
Script: "worker.py", // → maps to entrypoint
// -- or --
Binary: "./bin/worker", // → maps to entrypoint

Runtime: "python", // "go", "python", "node", "bun", "deno", "ruby", "php", "auto"
Interpreter: "/usr/bin/python3.12", // explicit interpreter (optional)
UseBundle: false, // wrap with "bundle exec" (Ruby)
Args: []string{"--verbose"},
Cwd: "/app",
Env: map[string]string{"PORT": "3000"},

RestartPolicy: "on-failure", // "always", "on-failure", "never"
MaxRestarts: 10,
RestartWindow: 5 * time.Minute,
StopSignal: "SIGTERM",
StopTimeout: 10 * time.Second,

Autostart: true,
Namespace: "backend",
Labels: map[string]string{"team": "platform"},
Tags: []string{"critical"},
Instances: 3,
DependsOn: []string{"database"},
Priority: 10,

Watch: &sdk.WatchConfig{Enabled: true, Paths: []string{"./src"}},
HealthCheck: &sdk.HealthCheckConfig{Type: "http", URL: "http://localhost:3000/health"},
Hooks: &sdk.HooksConfig{PreStart: &sdk.HookConfig{Command: "echo starting"}},
}

Script vs Binary​

Both Script and Binary map to the same underlying entrypoint. Use whichever reads naturally:

FieldWhen to UseExamples
ScriptInterpreted filesmain.py, server.js, app.rb
BinaryCompiled executables or system commands./bin/server, sleep, nginx

All ProcessConfig Fields​

FieldTypeDescription
NamestringRequired. Unique process identifier
ScriptstringPath to an interpreted script file
BinarystringPath to a compiled executable or command
RuntimestringRuntime: go, python, node, bun, deno, ruby, php, auto
InterpreterstringExplicit interpreter override (e.g., /usr/bin/python3.11)
UseBundleboolWrap command with bundle exec (Ruby)
Args[]stringAdditional arguments
CwdstringWorking directory
Envmap[string]stringEnvironment variable overlay
AutostartboolAuto-start on resurrect
RestartPolicystring"always", "on-failure", "never"
MaxRestartsintMaximum restart attempts
RestartWindowtime.DurationTime window for counting restarts
StopSignalstringSignal for stopping (default: SIGTERM)
StopTimeouttime.DurationGrace period before force kill
Watch*WatchConfigFile watching configuration
HealthCheck*HealthCheckConfigHealth check configuration
Hooks*HooksConfigLifecycle hook commands
InstancesintNumber of copies to run
NamespacestringNamespace for grouping
Labelsmap[string]stringKey-value pairs for filtering
Tags[]stringTags for categorization
DependsOn[]stringProcess names this depends on
PriorityintStartup ordering (lower starts first)

Supported Runtimes​

The SDK supports the same runtimes as the CLI:

RuntimeDetection FilesCommand
gogo.modgo run . or direct binary
pythonrequirements.txt, pyproject.toml, *.pypython3 <script>
nodepackage.jsonnode <script> / npx tsx <script>
bunbun.lockb, bunfig.tomlbun run <script>
denodeno.json, deno.jsoncdeno run <script>
rubyGemfileruby <script> / bundle exec ruby <script>
phpcomposer.json, *.phpphp <script>
auto(detects in order above)Auto-selected

When Runtime is set, the SDK resolves the correct interpreter and arguments automatically. Set Runtime: "auto" or leave empty to auto-detect from project files.

Runtime Resolution​

The SDK uses runtime.NewDetector() to resolve runtimes:

  1. If Runtime is set to a specific value, it looks up that runtime adapter
  2. The adapter's StartCmd() method resolves the interpreter and entrypoint
  3. If Interpreter is set explicitly, it takes precedence
  4. If Runtime is "auto" or empty, auto-detection scans the working directory

ProcessInfo​

type ProcessInfo struct {
ID string // Internal UUID
NumericID int // Sequential number
Name string
Namespace string
InstanceIndex int
Runtime string
State string // "starting", "running", "stopping", "stopped", "crashed", "errored", "waiting"
PID int
ExitCode int
Restarts int
CreatedAt time.Time
StartedAt *time.Time
FinishedAt *time.Time
Uptime time.Duration
Config ProcessConfig
CPUPercent float64
MemBytes int64
MemPercent float64
Threads int
FDs int
Tags []string
}

Lookup​

Inspect(id) accepts multiple formats:

FormatExample
UUID"abc123-def456-..."
Numeric ID"1"
Name"api"
Unique ID prefix"abc"

If a prefix matches multiple processes, an error is returned.

Log Streaming​

logCtx, cancel := context.WithCancel(context.Background())
defer cancel()

reader, err := mgr.Logs(logCtx, processID, sdk.LogOptions{
Tail: 50, // show last 50 lines first
Follow: true, // then stream new output
Stderr: false, // false = stdout, true = stderr
})
if err != nil {
log.Fatal(err)
}
defer reader.Close()

// Read logs like any io.Reader.
buf := make([]byte, 4096)
for {
n, err := reader.Read(buf)
if n > 0 {
os.Stdout.Write(buf[:n])
}
if err != nil {
break
}
}

LogOptions​

FieldTypeDefaultDescription
Tailint0Show last N lines (0 = all)
FollowboolfalseStream new entries as written
StderrboolfalseRead stderr instead of stdout

How It Works​

  1. Opens the log file (~/.runix/apps/<name>/stdout.log or stderr.log)
  2. If Tail > 0, reads the last N lines using a circular buffer
  3. If Follow, seeks to end and polls every 200ms for new content
  4. Returns an io.PipeReader — the caller reads from it, the goroutine writes to it
  5. Context cancellation stops the streaming goroutine

Hooks Configuration​

sdk.ProcessConfig{
Hooks: &sdk.HooksConfig{
PreStart: &sdk.HookConfig{Command: "echo starting", Timeout: 5 * time.Second},
PostStart: &sdk.HookConfig{Command: "curl http://localhost:8080/warm"},
PreStop: &sdk.HookConfig{Command: "curl -X PUT http://localhost:8080/drain"},
PostStop: &sdk.HookConfig{Command: "echo stopped"},
PreRestart: &sdk.HookConfig{Command: "echo restarting"},
PostRestart: &sdk.HookConfig{Command: "echo restarted"},
PreReload: &sdk.HookConfig{Command: "echo reloading"},
PostReload: &sdk.HookConfig{Command: "echo reloaded"},
PreHealthCheck: &sdk.HookConfig{Command: "echo checking"},
PostHealthCheck: &sdk.HookConfig{Command: "echo checked"},
},
}

HookConfig Fields​

FieldTypeDescription
CommandstringShell command (sh -c)
Timeouttime.DurationExecution timeout
IgnoreFailureboolDon't block lifecycle on failure

Health Check Configuration​

sdk.ProcessConfig{
HealthCheck: &sdk.HealthCheckConfig{
Type: "http", // "http", "tcp", "command"
URL: "http://localhost:8080/health",
Interval: "10s",
Timeout: "5s",
Retries: 3,
GracePeriod: "5s",
},
}

Watch Configuration​

sdk.ProcessConfig{
Watch: &sdk.WatchConfig{
Enabled: true,
Paths: []string{"./src", "./config"},
Ignore: []string{".git", "node_modules"},
Debounce: "200ms",
},
}

Multi-Instance​

id, err := mgr.AddProcess(ctx, sdk.ProcessConfig{
Name: "worker",
Binary: "python",
Args: []string{"worker.py"},
Instances: 3, // Starts worker:0, worker:1, worker:2
})

When Instances > 1, processes are named <name>:<index>. The first process ID is returned.

Save and Resurrect​

Persist process state across restarts:

// Session 1: start processes and save.
mgr, _ := sdk.New(sdk.Config{LogDir: "/var/lib/myapp/runix"})

mgr.AddProcess(ctx, sdk.ProcessConfig{
Name: "api",
Binary: "sleep",
Args: []string{"300"},
})
mgr.Save()
mgr.Close()

// Session 2: restore from saved state.
mgr2, _ := sdk.New(sdk.Config{LogDir: "/var/lib/myapp/runix"})
defer mgr2.Close()

mgr2.Resurrect()

for _, p := range mgr2.List() {
fmt.Printf("Restored: %s (PID %d)\n", p.Name, p.PID)
}

Config Reference​

sdk.Config​

FieldTypeDefaultDescription
LogDirstring$TMPDIR/runixDirectory for logs and state
DefaultsDefaultsConfigDefault values for all processes

sdk.DefaultsConfig​

FieldTypeDescription
RestartPolicystring"always", "on-failure", "never"
MaxRestartsintMaximum restart attempts
RestWindowtime.DurationTime window for counting restarts
StopTimeouttime.DurationDefault grace period before force kill

What's Next​