Examples & Tutorials

From a two-line hello world to a multi-agent research pipeline — learn Syntecnia through working code.

1. Hello World

Every Syntecnia program starts with require declarations that declare which capabilities the program uses. Here we pull in stdout for terminal output, then call print to write a line.

require stdout
print("Hello, Syntecnia!")
Tip: require is a capability declaration, not an import. Syntecnia uses it to sandbox what each program can access — if you don't declare stdout, your program simply cannot write to the terminal.

2. Variables & Control Flow

Variables are declared with let … be. Conditional branching uses when and otherwise — Python-style indentation, no curly braces required.

let name be "Alice"
let age  be 30

when age >= 18
    print(name + " is an adult")
otherwise
    print(name + " is a minor")
Readable by design: when / otherwise map directly to if / else. You can chain conditions with otherwise when for else-if branches.

3. Working with Lists

Syntecnia treats lists as first-class values. Use where to filter and apply to map — both accept inline task lambdas.

let numbers be [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

task is_even(n)
    give n % 2 == 0

let evens   be where(numbers, is_even)
let doubled be apply(task(n) give n * 2, evens)
print(doubled)
Inline tasks: task(n) give n * 2 is an anonymous function (lambda). You can pass it directly wherever a function is expected — no extra variable needed.

Key list functions

FunctionDescription
where(list, pred)Returns items for which pred is true.
apply(fn, list)Applies fn to every item and returns the new list.
collect(list, key)Extracts a named field from each item.
find_first(list, pred)Returns the first matching item or nothing.
length(list)Returns the number of items.

4. Pattern Matching

The match … is construct cleanly replaces long chains of otherwise when comparisons. Each is arm is checked in order; the first match runs.

let status be "pending"

match status
    is "pending"
        print("Processing...")
    is "done"
        print("Complete!")
    is "error"
        print("Failed!")
Exhaustive matching: Add a bare is _ arm as a catch-all default. Future versions of Syntecnia will warn when a match on a known type is non-exhaustive.

5. Error Handling

Network calls and I/O can fail. Wrap risky operations in a try block and handle failures in recover. The error value is bound to the name after recover.

require net("api.example.com")

try
    let data be fetch("https://api.example.com/data")
    print(data)
recover err
    print("Request failed: " + err)
Capability required: require net("api.example.com") grants the program permission to reach that hostname. Attempting a fetch to an undeclared host raises a capability error at runtime before any network traffic is sent.

6. Pipe Operator

The pipe operator |> passes the result of the left expression as the first argument to the function on the right, enabling readable left-to-right data pipelines without deep nesting.

let result be raw_data |> clean |> validate |> transform |> format

This is equivalent to the nested call:

let result be format(transform(validate(clean(raw_data))))
Best practice: Use pipes whenever you have three or more chained transformations. The left-to-right reading order matches how you think about data flow, making bugs much easier to spot.

7. Custom Types

Declare structured data with type. Fields are annotated with their primitive types (text, number, bool). Access fields using the of keyword.

type Product
    name:  text
    price: number
    stock: number

let p be Product("Widget", 9.99, 100)
print(name of p + " costs $" + text(price of p))
Field access: name of p reads the name field from instance p. This deliberate English phrasing avoids the dot-access ambiguity found in languages that overload . for both method calls and property access.

8. Simple API Server

Syntecnia can serve HTTP routes directly. Declare the port with require serve(port), declare a database path with require db(path), then define routes inside a serve on block. Each route arm handles a method-path pair.

intent: "Product catalog API"

require serve(3000)
require db("./products.db")

db_open("./products.db")
sql_exec("CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT, price REAL)")

serve on 3000
    route "GET /products"
        give sql("SELECT * FROM products")

    route "GET /products/:id"
        let id be params.id
        let results be sql("SELECT * FROM products WHERE id = ?", [id])
        when length(results) == 0
            give not_found("Product not found")
        otherwise
            give results

    route "POST /products"
        expect body {name: text, price: number}
        let b be json of request
        sql_exec("INSERT INTO products (name, price) VALUES (?, ?)", [b.name, b.price])
        give created({"status": "created"})
Body validation: expect body {…} declares the expected shape of the request body. Syntecnia rejects requests that don't match with a 400 response automatically — no manual validation boilerplate needed.
Parameterised queries: Always pass user-supplied values as the second list argument to sql / sql_exec. Syntecnia uses prepared statements under the hood, preventing SQL injection.

9. Blog with Content Negotiation

The content(page(…)) pattern lets a single route serve HTML to browsers, Markdown to AI agents, and JSON to API clients — Syntecnia inspects the Accept header and renders accordingly.

intent: "Agent-friendly blog"

require serve(8080)
require db("./blog.db")
require time

db_open("./blog.db")

describe
    about: "A blog that serves HTML, Markdown, and JSON"
    api:   "GET /blog for all posts, GET /blog/:slug for individual"

serve on 8080
    route "GET /blog"
        let posts be sql("SELECT * FROM posts ORDER BY created DESC")
        give content(page(
            [heading(1, "Blog"), list(collect(posts, "title"))],
            {"title": "Blog"}
        ))

    route "GET /blog/:slug"
        let post be find_first(sql("SELECT * FROM posts"), task(p) give p.slug == params.slug)
        when post == nothing
            give not_found("Post not found")
        give content(page(
            [heading(1, post.title), prose(post.body)],
            {"title": post.title}
        ))
The describe block is read by AI agents to understand what the API does. Together with intent, it makes your server self-documenting to both humans and LLMs — no OpenAPI spec required.

10. Multi-Agent Research System

Syntecnia has first-class support for multi-agent programs. Each agent block declares its own capabilities, communicates via share / observe, and coordinates with signal / wait_for.

intent: "Research and report on a topic"

require net("*.wikipedia.org")
require llm
require stdout

agent Researcher
    require net("*.wikipedia.org")
    task run(topic)
        let data be fetch("https://en.wikipedia.org/api/rest_v1/page/summary/" + topic)
        share data as "research"
        signal "research_done"

agent Analyst
    require llm
    wait_for "research_done"
    observe "research" as data
    let analysis be analyze data for "key insights"
    share analysis as "analysis"
    signal "analysis_done"

agent Reporter
    require llm
    wait_for "analysis_done"
    observe "analysis" as insights
    let report be generate "summary report" given insights
    share report as "final_report"

spawn Researcher with topic = "Artificial_intelligence"
spawn Analyst
spawn Reporter
Capability isolation: Each agent declares only the capabilities it needs. Researcher can access the network but not the LLM; Analyst can use the LLM but has no network access. This principle-of-least-privilege design is enforced at the runtime level.
Shared memory: share … as "key" writes a named value into a shared workspace. observe "key" as name reads it — and blocks until the value is available if it hasn't been written yet.

11. Landing Page with Templates

Use render(template, data) to render HTML templates from the filesystem. Serve static assets (images, CSS, JS) from a directory with static. The data object supports nested lists and HTML fragments.

intent: "Marketing landing page"

require serve(3000)

serve on 3000
    static "/assets" from "./public"

    route "GET /"
        give render("home.html", {
            "title":    "My Startup",
            "tagline":  "Building the future",
            "features": [
                {"name": "Fast",   "desc": "Lightning speed"},
                {"name": "Secure", "desc": "Enterprise security"},
                {"name": "Simple", "desc": "Easy to use"}
            ],
            "highlighted": true,
            "note": "<em>Now in beta!</em>"
        })
Template safety: String values passed to render are HTML-escaped by default. To pass raw HTML (as with "note" above), the template must explicitly opt in using an unescaped interpolation tag. This prevents accidental XSS from user-generated content.

12. Human-in-the-Loop Agent

Some decisions should not be fully automated. Syntecnia provides approve, confirm, and ask to pause execution and request human input before taking irreversible actions.

intent: "Process refunds with human approval"

require db("./orders.db")
require llm
require stdout

let order          be sql("SELECT * FROM orders WHERE id = ?", [order_id])
let recommendation be decide between ["full_refund", "partial_refund", "deny"] given order

when recommendation == "full_refund"
    approve "Issue full refund of $" + text(order.amount) + "?"
    sql_exec("UPDATE orders SET status = 'refunded' WHERE id = ?", [order_id])
otherwise when recommendation == "partial_refund"
    let amount be ask "Partial refund amount?"
    confirm "Refund $" + amount + " to customer?"
    sql_exec("UPDATE orders SET status = 'partial_refund' WHERE id = ?", [order_id])
otherwise
    log "Refund denied for order " + text(order_id)
Irreversibility matters: If the human rejects an approve or confirm prompt, the program halts at that point — subsequent statements (including the database write) do not execute. Design your flows so that dangerous side-effects always come after the checkpoint.
Human-in-the-loop primitives: approve "message" shows a yes/no gate; confirm "message" is a second confirmation for high-stakes actions; ask "prompt" collects free-form input and returns it as text.