This guide walks you through installing Winn, creating your first project, and building a small web service.
curl -fsSL https://winn.ws/install.sh | bashRequires Erlang/OTP 25+. The script detects your version, downloads the pre-built binary, installs to ~/.local/bin, and verifies the install.
If you don't have Erlang installed:
# macOS
brew install erlang
# Ubuntu / Debian
sudo apt install erlang
# Fedora
sudo dnf install erlangbrew tap gregwinn/winn
brew install winncurl -fsSL https://winn.ws/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/winn.gpg
echo "deb [signed-by=/usr/share/keyrings/winn.gpg] https://gregwinn.github.io/apt.winn.ws stable main" \
| sudo tee /etc/apt/sources.list.d/winn.list
sudo apt update && sudo apt install winnsudo dnf config-manager --add-repo https://gregwinn.github.io/rpm.winn.ws/winn.repo
sudo dnf install winngit clone https://github.com/gregwinn/winn-lang.git
cd winn-lang
rebar3 escriptize
cp _build/default/bin/winn /usr/local/bin/Requires Erlang/OTP 25+ and rebar3.
winn version
# => winn 0.9.2
winn helpYou should see:
Winn 0.9.2 - a compiled language on the BEAM
Usage:
winn new <name> Create a new Winn project
winn compile Compile all .winn files (src/ or current dir)
winn compile <file> Compile a single .winn file
winn run <file> Compile and run a single .winn file
winn start Compile project and start (keeps VM alive)
winn start <module> Start with a specific module
winn version Show version
winn help Show this help text
Install the Winn VS Code extension for syntax highlighting and diagnostics.
winn new my_app
cd my_appThis creates:
my_app/
├── rebar.config # Erlang build config
├── .gitignore
├── package.json
├── src/
│ ├── my_app.winn # Your first Winn file
│ ├── models/
│ ├── controllers/
│ └── tasks/
├── test/
├── db/
│ ├── migrations/
│ └── seeds.winn
└── config/
The generated src/my_app.winn:
module MyApp
def main()
IO.puts("Hello from MyApp!")
end
end
winn run src/my_app.winnOutput:
Hello from MyApp!
Compile your .winn files to .beam bytecode:
# Compile a single file
winn compile src/my_app.winn
# Compile all .winn files in src/ (or current dir)
winn compile
# => Compiled 1 file(s) to ebin/Compiled .beam files are written to ebin/.
For multi-file projects (especially servers), use winn start:
winn startThis compiles all src/*.winn files, loads dependencies (Cowboy, hackney, etc.), calls main(), and keeps the VM alive — essential for HTTP servers and GenServers.
You can also specify which module to start:
winn start my_appA typical Winn project looks like this:
my_app/
├── rebar.config # Dependencies and build config
├── .gitignore
├── package.json
├── src/
│ ├── my_app.winn # Application entry point
│ ├── models/
│ │ └── user.winn # User schema
│ ├── controllers/
│ │ └── api_controller.winn # HTTP routes
│ └── tasks/
│ └── db_seed.winn # Background tasks
├── test/
├── db/
│ ├── migrations/ # Database migrations
│ └── seeds.winn
├── config/ # Configuration files
└── ebin/ # Compiled .beam files (generated)
Winn programs are organized into modules. Each module lives in its own .winn file.
Create src/greeter.winn:
module Greeter
def hello(name)
"Hello, #{name}!"
end
def hello(:world)
"Hello, World!"
end
end
Compile it:
winn compile src/greeter.winnUse it from another module:
module MyApp
def main()
msg = Greeter.hello("Alice")
IO.puts(msg)
end
end
module DataExample
def main()
# Lists
numbers = [1, 2, 3, 4, 5]
doubled = for n in numbers do n * 2 end
IO.inspect(doubled)
# Maps
user = %{name: "Alice", age: 30}
IO.puts(user.name)
# Tuples
result = {:ok, "success"}
IO.inspect(result)
end
end
module Patterns
def describe({:ok, value})
"Success: #{value}"
end
def describe({:error, reason})
"Error: #{reason}"
end
def main()
IO.puts(describe({:ok, "done"}))
IO.puts(describe({:error, "not found"}))
end
end
module PipeExample
def main()
1..10
|> Enum.filter() do |x| x > 5 end
|> Enum.map() do |x| x * 100 end
|> Enum.join(", ")
|> IO.puts()
end
end
module SafeMath
def divide(a, b) when b != 0
{:ok, a / b}
end
def divide(_, 0)
{:error, "division by zero"}
end
def main()
match divide(10, 3)
ok result => IO.puts("Result: #{to_string(result)}")
err msg => IO.puts("Error: #{msg}")
end
# Or use try/rescue for exceptions
result = try
risky_operation()
rescue
_ => :fallback
end
end
end
Create src/api.winn:
module Api
use Winn.Router
def routes()
[
{:get, "/", :index},
{:get, "/health", :health},
{:get, "/users/:id", :get_user},
{:post, "/users", :create_user}
]
end
def middleware()
[:log_request]
end
def log_request(conn, next)
Logger.info("#{Server.method(conn)} #{Server.path(conn)}")
next(conn)
end
def index(conn)
Server.json(conn, %{message: "Welcome to MyApp"})
end
def health(conn)
Server.json(conn, %{status: "ok"})
end
def get_user(conn)
id = Server.path_param(conn, "id")
Server.json(conn, %{id: id, name: "User #{id}"})
end
def create_user(conn)
params = Server.body_params(conn)
Logger.info("Creating user", params)
Server.json(conn, params, 201)
end
end
Create src/my_app.winn to start the server:
module MyApp
def main()
IO.puts("Starting server on port 4000...")
Server.start(Api, 4000)
end
end
Start the server:
winn startOutput:
Compiled 2 file(s) to ebin/
Starting myapp...
Starting server on port 4000...
Test it:
curl http://localhost:4000/
# => {"message":"Welcome to MyApp"}
curl http://localhost:4000/users/42
# => {"id":"42","name":"User 42"}
curl -X POST http://localhost:4000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice"}'
# => {"name":"Alice"}Create src/user.winn:
module User
use Winn.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
end
end
Use it with changesets and the Repo:
module UserService
def create(params)
data = User.new(%{})
changeset = Changeset.new(data, params)
changeset = Changeset.validate_required(changeset, [:name, :email])
if Changeset.valid(changeset)
Repo.insert(User, Changeset.apply_changes(changeset))
else
{:error, Changeset.errors(changeset)}
end
end
def find(id)
Repo.get(User, id)
end
def all()
Repo.all(User)
end
end
module Config
def setup()
# Read environment variables
port = System.get_env("PORT", "4000")
secret = System.get_env("JWT_SECRET", "dev_secret")
# Load config
Config.load(%{
http: %{port: to_integer(port)},
auth: %{secret: secret}
})
end
end
Winn gives you clear error messages when something goes wrong:
-- Syntax Error ----------------------------- src/app.winn --
3 | x +
4 | end
| ^^^
5 | end
Unexpected 'end'.
Hint: Did you close a block too early, or forget an expression?
If you're using the VS Code extension, errors show as red squigglies inline.
Write tests in Winn using use Winn.Test:
module MathTest
use Winn.Test
def test_addition()
assert(1 + 1 == 2)
end
def test_greeting()
result = "Hello, " <> "World"
assert_equal("Hello, World", result)
end
end
Save as test/math_test.winn and run:
winn testOutput:
✓ mathtest:test_addition
✓ mathtest:test_greeting
2 tests, 0 failures (0ms)
Test functions must be named test_* with zero arguments. Use assert(expr) for boolean checks and assert_equal(expected, actual) for value comparisons.
Generate API docs from your source with winn docs:
winn docs
# => Generated docs for 3 module(s) in doc/api/This creates Markdown files in doc/api/ with function signatures extracted from # doc comments, plus a Mermaid dependency graph in index.md that renders on GitHub.
Use winn watch for automatic recompilation and hot code reloading during development:
winn watch --startThis starts your app and shows a live terminal dashboard. When you edit a .winn file, the module is recompiled and hot-reloaded without restarting the VM.
Scaffold models, migrations, tasks, and routers:
winn create model User name:string email:string
# => src/models/user.winn
winn create migration CreateUsers name:string email:string
# => db/migrations/TIMESTAMP_create_users.winn
winn create task db:seed
# => src/tasks/db_seed.winn
winn create router Api
# => src/controllers/api_controller.winn (module ApiController)
winn create scaffold Post title:string body:text
# => src/models/post.winn, src/controllers/post_controller.winn, test/post_test.winn
winn c model User # shorthandFormat your code for consistent style with winn fmt:
winn fmt # format all .winn files
winn fmt src/app.winn # format a specific file
winn fmt --check # check formatting without modifying (CI)Run static analysis to catch common issues:
winn lint # lint all .winn files
winn lint src/app.winn # lint a specific fileThe linter checks for unused variables, unused imports/aliases, naming conventions, redundant boolean comparisons, empty function bodies, and more. See winn lint for the full list of rules.
Manage your database schema with migrations:
winn migrate # Run all pending migrations
winn rollback # Rollback last migration
winn migrate --status # Show migration statusBuild production releases:
winn release # Build a production release
winn release --docker # Generate a DockerfileMonitor your app with built-in metrics:
Metrics.enable()
Metrics.increment(:api_calls)
Metrics.set(:active_users, 42)
Metrics.observe(:latency, 12.5)
View a live dashboard:
winn metricsRun load tests:
winn bench bench/api_bench.winnInstall optional add-on packages with winn add:
winn add redis # install Redis client
winn add mongodb # install MongoDB client
winn add amqp # install RabbitMQ client
winn packages # list installedUse them in your code:
Redis.configure(%{url: "redis://localhost:6379"})
Redis.set("key", "value")
{:ok, val} = Redis.get("key")
Packages are written in Winn and declared in package.json. Anyone can create and share packages.
- Language Guide — full syntax reference
- Standard Library — IO, String, Enum, List, Map, System, UUID, DateTime, Logger, Crypto
- OTP Integration — GenServer, Supervisor, Application
- ORM — Schema, Repo, Changeset
- Modules — HTTP server/client, JWT, WebSockets, Tasks, Config
- CLI Reference — all CLI commands