Syntax Reference

Complete language reference for Syntecnia — a readable, agent-aware programming language.

Getting Started

Syntecnia is an indentation-based language designed for building agentic, observable, and human-in-the-loop programs. Programs are stored in .syn files and run via the syntecnia CLI.

Installation

Install from PyPI:

# Install from PyPI
pip install syntecnia

Or install in editable mode directly from the repository:

# Clone, then install from repo root
git clone https://github.com/kitecosmic/Syntecnia.git
cd Syntecnia
pip install -e .
Editable mode (-e .) lets you modify the interpreter source and test changes immediately without reinstalling.

CLI Commands

All commands use the syntecnia executable installed by pip.

Core commands

# Run a program
syntecnia run file.syn

# Start an interactive REPL
syntecnia repl

# Type-check / static analysis without running
syntecnia check file.syn

# Print the token stream
syntecnia tokens file.syn

# Print the AST (Abstract Syntax Tree)
syntecnia ast file.syn

# Auto-generate a test scaffold
syntecnia testgen file.syn

Flags

Flag Description
--secure flag Enable production / hardened security mode. Rejects any capability not explicitly declared with require.
--provider <name> flag Select the LLM backend (e.g. --provider anthropic). Required for reason, decide, analyze, generate statements.
--grant <cap> flag Dynamically grant a capability at launch without editing source (e.g. --grant net:host).
--audit flag Emit a structured JSON audit trail of every agent action, decision, and human interaction.
# Run in production mode with Anthropic LLM backend
syntecnia run app.syn --secure --provider anthropic

# Grant a network capability and record an audit trail
syntecnia run app.syn --grant net:host --audit
Always use --secure in production deployments. Without it, capability checks are advisory rather than enforced.

Basics

Indentation

Syntecnia uses indentation to define blocks — there are no curly braces and no semicolons. Use 4 spaces or 1 tab per level, and be consistent within a file.

task greet(name)
    when name == ""
        give "Hello, stranger!"
    otherwise
        give "Hello, " + name + "!"
Never mix tabs and spaces in the same file. The parser treats them as distinct indentation characters and will raise an indentation error.

Comments

Line comments begin with --. There is no block comment syntax.

-- This is a comment
let x be 42  -- inline comment

File extensions

  • .syn — Standard Syntecnia source file (recommended for all programs).
  • .fsyn — Flat/document style; see the Flat Syntax section.

Types & Values

Primitives

-- number: integers and floats (unified type)
let age     be 42
let pi      be 3.14
let million be 1_000_000   -- underscores for readability

-- text: UTF-8 strings
let greeting be "Hello, world!"

-- bool
let enabled be true
let offline be false

-- nothing (absence of value, like null/None)
let result  be nothing

Collections

-- list: ordered, heterogeneous
let nums  be [1, 2, 3]
let mixed be ["hello", 42, true]

-- map: key-value pairs (string keys)
let config be {"host": "localhost", "port": 8080}

Type coercion

-- Convert between types explicitly
let s be text(42)       -- "42"
let n be number("42")   -- 42.0

-- Division always returns a float
let r be 7 / 2           -- 3.5, not 3

-- Inspect type at runtime
let t be type_of(config)  -- "map"
type_of(value) returns the type as a text string: "number", "text", "bool", "nothing", "list", "map", or the custom type name.

Truthiness

The following values are falsy; everything else is truthy:

  • false
  • nothing
  • 0
  • "" (empty text)
  • [] (empty list)
  • {} (empty map)

Custom types

Define structured data with type. Field types are annotated with : <type>. Instantiate by calling the type name like a task.

type Customer
    name:    text
    email:   text
    balance: number

-- Positional constructor
let alice be Customer("Alice", "alice@example.com", 500)

-- Access fields
let n be name of alice    -- "Alice"

Variables

Variables are declared with let … be and mutated with set … to. These are two distinct operations by design, making mutation explicit and auditable.

-- Declaration: introduces a new binding
let username be "Alice"
let score    be 0

-- Mutation: changes an existing binding
set score to score + 10
set username to "Bob"

-- Computed values
let full_name be "Ada" + " " + "Lovelace"
Using set before let is a runtime error. Always declare a variable with let before attempting to mutate it with set.
Prefer immutable bindings. Only reach for set when you truly need to accumulate state across iterations or conditional branches.

Operators

Arithmetic

let a be 10 + 3    -- 13    addition
let b be 10 - 3    -- 7     subtraction
let c be 10 * 3    -- 30    multiplication
let d be 10 / 3    -- 3.333…  division (always float)
let e be 10 % 3    -- 1     modulo
let f be 2 ** 8    -- 256   exponentiation

Comparison

let eq  be 5 == 5   -- true
let neq be 5 != 4   -- true
let lt  be 3 < 5    -- true
let gt  be 5 > 3    -- true
let lte be 3 <= 3   -- true
let gte be 5 >= 4   -- true

Logical

let both   be true and false   -- false
let either be true or  false   -- true
let inv    be not true          -- false

String & List concatenation

-- + on text: string concatenation (coerces left side to text)
let msg be "Count: " + text(42)   -- "Count: 42"

-- + on lists: concatenates two lists
let all be [1, 2] + [3, 4]        -- [1, 2, 3, 4]

Pipe

The pipe operator |> passes the left-hand value as the sole argument to the right-hand task. See the Pipe Operator section for details.

let result be raw_data |> clean |> validate |> persist

Control Flow

Conditionals — when / otherwise when / otherwise

Equivalent to if / else if / else.

let score be 78

when score >= 90
    print("Excellent!")
otherwise when score >= 70
    print("Good job!")
otherwise
    print("Keep trying!")

For loop — each … in

Iterates over any collection (list, map, or range).

let fruits be ["apple", "banana", "cherry"]

each fruit in fruits
    print("I like " + fruit)

-- Accumulate with set
let total be 0
each n in [1, 2, 3, 4, 5]
    set total to total + n

While loop — while

let count be 0

while count < 5
    print("tick: " + text(count))
    set count to count + 1

Break — stop

Use stop to exit a loop early.

each item in items
    when item == "done"
        stop
    print(item)

Pattern matching — match … is

match evaluates a value and branches on the first is pattern that holds. Use is true as a catch-all (like default or _).

task categorize(price)
    match true
        is price > 2000
            give "premium"
        is price > 500
            give "mid-range"
        is true
            give "budget"

-- Match on a value directly
let status be "active"
match status
    is "active"   then print("Running")
    is "paused"   then print("Paused")
    is true       then print("Unknown state")
The then keyword lets you write a single-expression branch on the same line as is. For multi-statement branches, use indentation on the next line instead.

Tasks (Functions)

Functions in Syntecnia are called tasks. Define them with task, return values with give, and call them like any other language.

Basic task

task add(a, b)
    give a + b

let result be add(3, 4)   -- 7

Default return and void tasks

-- A task without give returns nothing implicitly
task log_event(msg)
    print("[event] " + msg)

Recursion

task fib(n)
    when n <= 1
        give n
    otherwise
        give fib(n - 1) + fib(n - 2)

print(fib(10))   -- 55

Nested tasks & closures

task make_counter(start)
    let count be start
    task increment()
        set count to count + 1
        give count
    give increment

let counter be make_counter(0)
print(counter())   -- 1
print(counter())   -- 2

Named parameters with as

task send_email(to as address, subject as title)
    -- address and title are local aliases
    print("Sending to: " + address)
give propagates out of try/recover blocks. If a give is reached inside a try, the value is returned from the enclosing task — the recover branch is bypassed.

Error Handling

Use try / recover to handle runtime errors. The recover block receives the error message as text in the named variable.

try
    let data be fetch("https://api.example.com/data")
    print(data)
recover err
    print("Request failed: " + err)

Nested try/recover

task safe_parse(raw)
    try
        let n be number(raw)
        give n
    recover err
        log "Parse error: " + err
        give 0   -- fallback value
The err variable inside recover is always of type text. If you need structured error data, parse the message string or attach metadata before raising.

Security invariants

Use invariant to assert preconditions. A failed invariant is an unrecoverable error — it does not trigger recover.

invariant balance >= 0   -- crashes if violated

Pipe Operator

The pipe operator |> passes the value on the left as the first (and only) argument to the task on the right. It chains transformations in a readable left-to-right flow.

-- These two lines are equivalent:
let result be persist(validate(clean(raw_data)))

let result be raw_data |> clean |> validate |> persist

Practical pipeline

task double(x)
    give x * 2

task increment(x)
    give x + 1

task square(x)
    give x ** 2

-- 3 → double → 6 → increment → 7 → square → 49
let answer be 3 |> double |> increment |> square
Pipe works with any single-argument task. For tasks with multiple parameters, wrap them in a helper that closes over the extra arguments.

Property Access

Syntecnia provides three equivalent forms for accessing fields on custom types and maps:

type Person
    name:  text
    email: text

let person be Person("Ada", "ada@example.com")

-- Form 1: English-style  (most readable)
let n1 be name of person

-- Form 2: Dot notation  (familiar from other languages)
let n2 be person.name

-- Form 3: Bracket notation  (dynamic / computed keys)
let key be "name"
let n3 be person[key]
All three forms produce the same result. Use name of x for Syntecnia-idiomatic code, dot notation when interoperating with mapped data, and bracket notation when the key is computed at runtime.

Reserved Keywords

Hard keywords are always reserved and cannot be used as identifiers. Soft keywords are only special inside a serve block and are valid identifiers elsewhere.

Flow

when otherwise each in while match is then stop

Definitions

task give let be set to type as of with

Agent coordination

agent spawn share observe signal wait_for

Security & capabilities

require sandbox invariant intent

Human interaction

approve confirm ask show

LLM integration

reason decide analyze generate

Error handling

try recover

Observability

trace log measure checkpoint

Logic

and or not

Literals

true false nothing

Soft keywords (serve context only)

These identifiers are only treated as keywords inside a serve block. Outside of it, they can be used freely as variable or task names.

serve on route auth requires expect max_body max_streams stream send rate_limit per static from cors describe private
Hard keywords appear in violet chips; soft keywords appear in blue. Neither set can begin an identifier — start names with a letter or underscore.

Flat Syntax (.fsyn)

The .fsyn (flat Syntecnia) format is a document-oriented dialect where top-level statements execute sequentially without any mandatory indented block structure. It is designed for configuration files, data pipelines, and scripting scenarios where the program reads more like a structured document than a traditional code file.

-- config.fsyn  — flat/document style

let app_name    be "MyService"
let version     be "2.4.1"
let max_retries be 3

require stdout
require net:outbound

print(app_name + " v" + version)
Use .syn for all production application code. Reserve .fsyn for lightweight scripts, configuration declarations, and one-off document-processing pipelines where the flat structure aids readability.

The Syntecnia runtime automatically selects the appropriate parser based on the file extension. All language features — tasks, types, control flow, error handling — remain fully available in .fsyn files.