Skip to content

Getting started

Platforms: nautilus is one static binary with no runtime dependencies. Every release ships builds for ### macOS (Apple Silicon and Intel), ### Linux (x86-64 and arm64), and ### Windows (x86-64 and arm64); the controller it builds runs anywhere the same binary does, from a laptop to a Raspberry Pi to a Kubernetes pod. VS Code is the editor experience on all three. The release page has the archives and a checksums.txt.

macOS — pick arm64 for Apple Silicon, amd64 for Intel:

Section titled “macOS — pick arm64 for Apple Silicon, amd64 for Intel:”
Terminal window
v=$(curl -fsSL https://api.github.com/repos/joyautomation/nautilus/releases/latest | grep -m1 '"tag_name"' | cut -d'"' -f4)
curl -fsSL "https://github.com/joyautomation/nautilus/releases/download/$v/nautilus_${v#v}_darwin_arm64.tar.gz" | tar xz naut
sudo mv naut /usr/local/bin/

Downloading with curl skips Gatekeeper’s quarantine. If you fetched the archive in a browser instead and macOS refuses to open the binary, clear the flag once: xattr -d com.apple.quarantine /usr/local/bin/naut.

Terminal window
v=$(curl -fsSL https://api.github.com/repos/joyautomation/nautilus/releases/latest | grep -m1 '"tag_name"' | cut -d'"' -f4)
curl -fsSL "https://github.com/joyautomation/nautilus/releases/download/$v/nautilus_${v#v}_linux_amd64.tar.gz" | tar xz naut
sudo install naut /usr/local/bin/naut
Terminal window
$v = (Invoke-RestMethod https://api.github.com/repos/joyautomation/nautilus/releases/latest).tag_name
Invoke-WebRequest "https://github.com/joyautomation/nautilus/releases/download/$v/nautilus_$($v.TrimStart('v'))_windows_amd64.zip" -OutFile nautilus.zip
Expand-Archive nautilus.zip -DestinationPath "$env:LOCALAPPDATA\nautilus" -Force
[Environment]::SetEnvironmentVariable("Path", "$env:LOCALAPPDATA\nautilus;" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")

Open a new terminal afterwards so the Path change is picked up.

Terminal window
go install github.com/joyautomation/nautilus/cmd/naut@latest

This puts the binary in $(go env GOPATH)/bin, which needs to be on your PATH. Whichever route you took, naut version should now answer.

The one binary is the whole toolchain: naut new (scaffold a project), run, test, check (the CI gate: compiles every program and cross-checks it against the manifest), build, pull (bring a controller’s running program back into the repo), lsp (the language server the VS Code extension uses), and the eip, modbus, sparkplug, and historian tools.

Terminal window
naut new my-plant # the tour: 3 tasks, 3 IEC languages, simulated plant
naut new my-plant --template minimal # one task, one program, one test file
naut new my-plant --template sdk # Go project, for a custom field bus
naut new my-plant --template sdk-demo # Go project with plant physics in Go

Run it bare for the interactive form — it asks for the template, the program language, and the features you want.

A nautilus project is your logic and a manifest. Run, test, and ship it with the CLI alone, no toolchain:

Terminal window
cd my-plant
naut run # scan loop + dashboard + tag API on http://localhost:8080
naut test # acceptance tests, in virtual time
naut check # compile (the CI gate)
naut build # emit ./my-plant — a self-contained controller binary

nautilus.yaml declares the tasks (one program file each, any language, own scan rates), the tags by role, the server, and the field driver. naut build emits one deployable binary, with no Go toolchain anywhere.

*_test.yaml holds the acceptance tests, and they run against a virtual clock, so a ten-second on-delay or a loop’s settling time is asserted exactly, deterministically, in milliseconds:

- name: low-temp alarm waits its full 10 s
suspend: [sim] # freeze the plant; drive the value directly
given: { TempC: 45.0 }
steps:
- advance: 9.5s
expect: { TempLowAlm: false } # the TON has not elapsed
- advance: 1s
expect: { TempLowAlm: true } # ... and now it has

See Testing for the whole format.

Go is the SDK. Reach for --template sdk when you need a custom field bus or richer simulation physics. That form is the same runtime with the manifest written as code, and it’s an ordinary Go program:

Terminal window
cd my-plant
go mod tidy # resolves github.com/joyautomation/nautilus from the proxy
go run . # scan loop + tag API on http://localhost:8080
go test ./... # Go tests that scan the program against an in-memory driver

The scaffolded program_test.go calls rt.Scan() one scan at a time against the loopback Memory driver and asserts on the outputs. For timed behaviour, a Go test can run on the same virtual clock as naut test (acceptance.NewClock and acceptance.NewScheduler); the go-sdk example does.

Open http://localhost:8080 for the built-in live dashboard, or GET /api/state for the raw tag snapshot. Setpoints are click-to-set right in the tag table — click a value and type, or flip a BOOL with its toggle — so you can drive the loop before there is an HMI. Inputs and outputs stay read-only: the driver rewrites an input before every scan and the logic rewrites an output after, so an edit there would be discarded within a scan.

Install nautilus IEC 61131-3 from the VS Code Marketplace or Open VSX. (Switch to Pre-Release Version on the extension page gets builds from main.) With your project open and the controller running you get compile diagnostics as you type, go-to-definition, find all references, hover, completion, and live tag values next to identifiers in your program.

On macOS, VS Code launched from the Dock or Spotlight gets the login PATH, not your shell’s, so it may not find naut even though your terminal does. If the extension reports it could not start the language server, run nautilus: Install or Update the naut CLI, set nautilus.cliPath to the full path (which naut), or launch VS Code from a terminal with code ..

  • Write control logic in program.st, or in .ld, .fbd, or .sfc; the graphical languages open in full diagram editors in VS Code.
  • Assert on it in *_test.yaml, and keep asserting as you tune. The fixture comes from nautilus.yaml, so a retuned gain can’t drift away from what the tests verify.
  • Swap the simulated plant for real field I/O when you have hardware: delete the sim task and point driver: at the bus. The control logic doesn’t change — it reads the same tags either way.
  • Add an HMI with the SvelteKit component kit: faceplates, trends, and an SSE realtime client.
  • Ship it as one binary. The scaffolded CI gates on naut check, naut test, and naut build; an SDK project’s CI runs go vet, naut check (through go run), go test, and go build instead. Add --deploy to naut new for a Dockerfile, a redundant-pair Kubernetes manifest, and the workflow that ships a merged commit to the controller.
  • Online edits — change logic on a running controller from VS Code, and pull field edits back into git.
  • Testing — virtual time, and the *_test.yaml format for asserting on timers and loop responses.
  • The tag model — how tags come to exist, which role fits which job, and the one rule that bites.
  • Language reference — evaluation semantics and every built-in operator, function, and function block.