Relay / Getting started / 0.2.0-rc.1

From first script
to first service.

Learn Relay’s indentation-based syntax, work with asynchronous results, and build a small JSON API. You’ll need a terminal, a text editor, and Rust 1.93 or newer. No database is needed for this tutorial.

01 / Build Relay

From a local checkout of the Relay repository, build the interpreter and confirm its version:

cargo build --release --locked
./target/release/relay --version
mkdir -p tutorial-work

Keep your terminal at the repository root for all commands below. Put the files you create in tutorial-work/. The examples invoke the built binary directly, so you don’t need to install it on your PATH.

This tutorial targets the 0.2 release candidate. If you have older Relay code, read the migration notes for changes to scopes, async behavior, and HTTP responses.

02 / Your first script

Create tutorial-work/hello.ry:

// A function with a checked argument and a default value.
fn greet(name: str, prefix = "Hello")
    return prefix + ", " + name

print(greet("Ada"))
print(greet("Grace", prefix="Welcome"))

Check it, then run it:

./target/release/relay check tutorial-work/hello.ry
./target/release/relay run tutorial-work/hello.ry

You should see Hello, Ada and Welcome, Grace. Use four spaces for indentation. Functions begin with fn; block headers have no trailing colon. Comments use // or /* ... */.

The str hint checks the argument at runtime. Calling greet(42) raises a type error; use greet(str(42)) when you want conversion. The check command validates syntax, signatures, control flow, and imports without running the program; it is not a static type checker.

03 / Values and loops

Create tutorial-work/values.ry. Lists hold ordered values; dictionaries associate string keys with values.

scores = [12, 7, 19]
scores.append(15)
passing = [score for score in scores if score >= 12]

for (score in passing)
    print("Score: " + str(score))

student = {"name": "Ada", "active": True, "scores": passing}
print(format("Hello {{ name }}", {"name": student["name"]}))
assert_equal(len(passing), 3)
assert(19 in passing)
./target/release/relay run tutorial-work/values.ry

The output includes scores 12, 19, and 15 and Hello Ada. Printing is asynchronous, so output order may vary. Successful assertions produce no output. Boolean values are True and False; an absent value is None.

Convert numbers with str before concatenating them with text. String literals don’t interpolate automatically: call format explicitly.

Assignments inside functions are local. Captured values are read-only, including captured collections. For state shared across web requests, use explicit sessions or database operations.

04 / Handle errors

Create tutorial-work/errors.ry. Use raise for a failure, except(error) to handle it, and finally for cleanup that must run either way.

name = "   "
try
    if (name.strip() == "")
        raise "Name cannot be empty"
    print(name.strip())
except(error)
    print(error.code, error.message)
finally
    print("Validation finished")
./target/release/relay run tutorial-work/errors.ry

You’ll see the error’s code and message and Validation finished; asynchronous print output may arrive in either order. Try changing name to " Ada " to follow the successful path.

05 / Work concurrently

Relay has no await keyword. I/O and timers start when called; consuming their results waits for completion. Create tutorial-work/concurrent.ry:

first = sleep(100, "first")
second = sleep(20, "second")
results = all([first, second])
assert_equal(results, ["first", "second"])
print(results[0], results[1])
./target/release/relay run tutorial-work/concurrent.ry

Both timers run concurrently. Although the second finishes earlier, all preserves input order, so the final print produces first second. In this release, sleep also prints its supplied value when the timer completes, so you’ll see separate second and first lines. Timer durations are in milliseconds.

race returns the first completion, including failures. timeout(expression, milliseconds) limits expression execution, and cancel(task) requests cancellation. Async results cache both success and failure for repeated consumption.

Scripts and requests join their child work before finishing. Unobserved task failures propagate. Starting a task and ignoring its result does not make it independent background work.

06 / Build a JSON API

Create tutorial-work/app.ry. This service greets a visitor and adds two incoming integers:

app = WebApp()

@app.get("/hello/<name>")
fn hello(name: str)
    return {"hello": name, "ok": True}

@app.post("/sum")
fn sum_values(a: int, b: int)
    return {"total": a + b}

server = WebServer()
server.run(app)
./target/release/relay check tutorial-work/app.ry
./target/release/relay run tutorial-work/app.ry

Leave the server running. In another terminal:

curl http://127.0.0.1:8080/hello/Ada
curl -X POST http://127.0.0.1:8080/sum \
  -H 'Content-Type: application/json' \
  -d '{"a": 12, "b": 7}'

The responses contain {"hello":"Ada","ok":true} and {"total":19} (JSON key order may differ). Dictionaries and lists become JSON automatically. HTTP handler arguments bind from path, body, then query, in that order of precedence. Unlike ordinary function calls, HTTP input is coerced to the hinted types.

Strings produce plain text. To return HTML, use Response(html, content_type="text/html; charset=utf-8") or app.render_template(path, data), which escapes interpolated HTML values.

Stop the server with Ctrl+C. If port 8080 is busy, restart on another port and update your curl URLs:

RELAY_BIND=127.0.0.1:8081 ./target/release/relay run tutorial-work/app.ry

07 / Extract a module

Create tutorial-work/helpers.ry so the addition logic can be reused and tested:

fn add(a: int, b: int)
    return a + b

At the top of app.ry, add import helpers as helpers. Replace the sum handler’s return statement with:

    return {"total": helpers.add(a, b)}

Imports resolve relative to the source file, so this finds helpers.ry beside app.ry. Each module has its own namespace. Names beginning with _ are private, and import cycles are rejected. Restart the server to load your changes and repeat the POST request.

08 / Test your code

Create tutorial-work/test_helpers.ry:

import helpers as helpers

fn test_add_positive()
    assert_equal(helpers.add(12, 7), 19)

fn test_add_negative()
    assert_equal(helpers.add(-2, 2), 0)
./target/release/relay test tutorial-work
./target/release/relay fmt --check tutorial-work

The test command discovers test_*.ry files and runs their test_* functions in isolated runtimes. Expect two passing tests. If the formatting check reports changes, run ./target/release/relay fmt tutorial-work to apply conservative whitespace formatting.

09 / Keep building

You now have working scripts, concurrent tasks, a JSON API, a reusable module, and tests. A useful next exercise: add a /double/<value> route that calls helpers.add(value, value), then test it with curl.