soma

A declarative language designed for AI agents to build persistent web apps and data pipelines.

zero config · one file · pipe operator · SQLite · HTMX

$ curl -fsSL https://soma-lang.dev/install.sh | sh
// A complete REST API with persistence. One file. One command.
cell App {
    memory {
        users: Map<String, String> [persistent, consistent]
    }

    on create(name: String, email: String) {
        let id = next_id()
        users.set(to_string(id), to_json(map("id", id, "name", name, "email", email)))
        return map("id", id, "name", name, "email", email)
    }

    on get(id: String) {
        let raw = users.get(id)
        if raw == () { return response(404, map("error", "not found")) }
        return from_json(raw)
    }

    on list() {
        let items = list()
        for key in users.keys() { items = list(items, from_json(users.get(key))) }
        return items
    }
}
$ soma serve app.cell
listening on http://localhost:8080   # SQLite auto-resolved from [persistent, consistent]

$ curl -X POST localhost:8080/create -d '{"name":"Alice","email":"[email protected]"}'
{"id": 1, "name": "Alice", "email": "[email protected]"}

Why agents should use Soma

📐

15 keywords

The entire language fits in an agent's context window. Learn it in one pass.

🗄️

Declare storage

[persistent, consistent] → SQLite. [ephemeral] → memory. No config.

🔗

Pipe operator

data |> filter_by() |> sort_by() |> top(10). Spark in 5 lines.

🌐

Web built-in

soma serve = HTTP server + HTMX + JSON API. Zero framework.

Compile-time checks

Property contradictions, signal wiring, promise verification — before runtime.

🌱

Self-growing

Properties, checkers, backends defined as .cell files. The language extends itself.

What agents struggle with today

An agent building a web app in Express needs to coordinate: a JS file, a package.json, npm install, a database connection string, SQL migrations, route configuration, JSON parsing middleware, and error handling. That's 7 files, 3 languages, and 4 tools — any of which can be inconsistent.

In Soma, the agent writes one file. Memory properties declare the database. Signal handlers are the routes. The compiler verifies consistency. soma serve runs it.

Same app — lines of code
LanguageLinesDependenciesFilesSetup commands
Soma74011 (soma serve app.cell)
Express8723+3
Flask11212+2
Go15213+3
What you don't write in Soma

0 SQL statements. 0 imports. 0 database config. 0 route declarations. 0 JSON marshaling.

Install

macOS

curl -fsSL https://soma-lang.dev/install.sh | sh
# or
brew install soma

Linux

curl -fsSL https://soma-lang.dev/install.sh | sh

From source

git clone https://github.com/soma-dev-lang/soma
cd soma/compiler
cargo build --release
cp target/release/soma /usr/local/bin/

Verify

soma --version
soma 0.1.0

Syntax reference

Soma has one structure: the cell. Every cell has a face (contract), memory (state), and signal handlers (behavior).

Cell

cell Counter {
    face {
        signal increment(amount: Int)
        signal get() -> Int
        promise value >= 0
    }

    memory {
        value: Int [persistent, consistent]
    }

    on increment(amount: Int) {
        let current = value.get("count") ?? 0
        value.set("count", current + amount)
        return current + amount
    }

    on get() {
        return value.get("count") ?? 0
    }
}

Types

Int64-bit integer
BigIntArbitrary precision (declare in params to activate)
Float64-bit float
StringUTF-8 text with {interpolation}
Booltrue / false
ListOrdered collection
MapKey-value pairs
()Unit / null

Variables

let x = 42
x = x + 1                      // reassignment
let name = "world"
let greeting = "hello {name}"   // string interpolation
let user = map("name", "Alice", "age", 30)
let info = "{user.name} is {user.age}"  // field interpolation

Control flow

if x > 10 {
    return "big"
} else {
    return "small"
}

while i < n {
    sum += i
    i = i + 1
}

for item in items {
    print(item)
}

Operators

+ - * / %Arithmetic
== != < > <= >=Comparison
&& || !Logic
|>Pipe: a |> f(b) → f(a, b)
??Null coalescing: x ?? default
+=Append: s += "more"

Memory properties

memory {
    data:     Map<String, String> [persistent, consistent]     // → SQLite
    cache:    Map<String, String> [ephemeral, local]          // → in-memory HashMap
    secrets:  Map<String, String> [persistent, encrypted]     // → encrypted at rest
    logs:     Log<String>         [persistent, immutable]     // → append-only
    sessions: Map<String, String> [ephemeral, ttl(30min)]    // → auto-expire
}

The compiler verifies: [persistent, ephemeral] → error (contradiction). [immutable] → implies consistent. [retain(7years), ttl(30d)] → error (ttl < retain).

Imports

use lib::helpers            // local: lib/helpers.cell
use math                    // package: .soma_env/packages/math/
use std::builtins           // stdlib

State machines

state status {
    initial: draft

    draft -> submitted
    submitted -> approved
    approved -> shipped
    shipped -> delivered
    * -> cancelled              // from any state
}

// Runtime enforces valid transitions
transition(order_id, "submitted")   // ✓ draft → submitted
transition(order_id, "shipped")     // ✗ error: draft → shipped not allowed

Tests

cell test MathTests {
    rules {
        assert square(5) == 25
        assert factorial(10) == 3628800
    }
}

// $ soma test tests.cell
//   ✓ assert square(5) == 25
//   ✓ assert factorial(10) == 3628800
// 2 tests: 2 passed, 0 failed

Typed records

Named maps with compile-time type tags. No more stringly-typed Map<String, String>.

// Create a typed record
let user = User { name: "Alice", age: 30, email: "[email protected]" }

// Field access
user.name              // → "Alice"
user._type             // → "User"

// Type checking at runtime
is_type(user, "User")  // → true
is_type(user, "Order") // → false

// Records compose with pipes and with()
let updated = user |> with("age", 31)
let clean = user |> without("email")

// Use in collections
let users = list(
    User { name: "Alice", role: "admin" },
    User { name: "Bob", role: "user" }
)
let admins = users |> filter_by("role", "=", "admin")

Error handling

try { } catches runtime errors and returns them as values. No exceptions, no panics — errors are data.

// try wraps errors into a map with value/error fields
let result = try { stock(ticker) }

if result.error != () {
    // Error occurred — handle it
    return response(404, map("error", result.error))
}

// Success — use the value
let data = result.value

// Works with any expression
let safe_divide = try { a / b }
let answer = safe_divide.value ?? 0  // default to 0 on error

HTML templates

Separate HTML from logic. Load external files with variable substitution.

// templates/page.html:
// <h1>{title}</h1>
// <ul>{items}</ul>
// <p>By {author}</p>

// Load and render with variables
let page = load_template("templates/page.html",
    "title", "Dashboard",
    "items", rows,
    "author", "Soma"
)
return html(page)

Data pipelines

Soma replaces PySpark for in-memory data processing. Same operations, fraction of the code.

Filter → Sort → Top

let portfolio = stocks
    |> filter_by("market_cap", ">", 20000)
    |> filter_by("momentum", ">", -10)
    |> sort_by("composite", "desc")
    |> top(10)

Join → Aggregate (GROUP BY)

let by_sector = trades
    |> join(prices, "ticker")
    |> filter_by("volume", ">", 1000)
    |> agg("sector", "volume:sum", "price:avg")
    |> sort_by("volume_sum", "desc")

All pipeline operations

filter_by(list, field, op, val)Filter rows: > >= < <= == !=
sort_by(list, field, "desc")Sort by field
top(list, n)First N elements
join(left, right, key)Inner join on key
left_join(left, right, key)Left outer join
agg(list, group, "col:func"...)GROUP BY + sum/avg/min/max/count
select(list, fields...)Pick columns
pluck(list, field)Extract one field
distinct(list, field)Unique values
group_by(list, field)Group into map
sum_by(list, field)Sum field values
avg_by(list, field)Average
min_by / max_by(list, field)Extremes
count_by(list, field, val)Count matches
with(map, k, v)Update field
without(map, keys...)Remove fields
merge(map1, map2)Combine maps
flatten(list)Flatten nested lists
zip(list1, list2)Pair elements
enumerate(list)Add index

Web applications

Signal handlers become HTTP routes automatically. soma serve gives you a threaded HTTP server with SQLite, CORS, HTMX, static files, and hot reload.

JSON API

cell API {
    memory { items: Map<String, String> [persistent, consistent] }

    on create(data: Map<String, String>) {
        let id = next_id()
        items.set(to_string(id), to_json(data))
        return data |> with("id", id)
    }

    on get(id: String) {
        let raw = items.get(id)
        if raw == () { return response(404, map("error", "not found")) }
        return from_json(raw)
    }

    on list() {
        let items = list()
        for key in items.keys() { items = list(items, from_json(items.get(key))) }
        return items
    }
}

// $ soma serve api.cell
// POST /create  →  create({"name":"Alice"})
// GET  /get/1   →  get("1")
// GET  /list    →  list()

HTML + HTMX

on page() {
    return html("<div hx-get='/items' hx-trigger='load'></div>")
}

on items() {
    let rows = ""
    for item in data.keys() {
        let name = data.get(item)
        rows += "<li>{name}</li>"
    }
    return html("<ul>{rows}</ul>")
}

Response control

response(404, map("error", "not found"))    // status code
html("<h1>Hello</h1>")                          // text/html
redirect("/")                                    // 302 redirect

Serve options

soma serve app.cell                # HTTP server on :8080
soma serve app.cell -p 3000       # custom port
soma serve app.cell --watch        # hot reload on file change

CLI reference

soma run file.cell [args]Execute a signal handler
soma run file.cell --jitExecute with bytecode VM (2.7x faster)
soma serve file.cellHTTP server
soma serve file.cell --watchHot reload
soma check file.cellVerify contracts, properties, signals
soma test file.cellRun test cells
soma build file.cellGenerate Rust code
soma init [name]Create project + isolated env
soma add pkg --git urlAdd dependency
soma installInstall dependencies
soma envShow environment
soma propsList all properties + backends + builtins

All builtins

print(args...)Output to stdout
to_string(v) / to_int(v) / to_float(v)Type conversion
concat(a, b)String concatenation
len(s) / split(s, d) / replace(s, a, b)String operations
contains(s, sub) / starts_with(s, p)String search
lowercase(s) / uppercase(s) / trim(s)String transform
index_of(s, sub) / substring(s, start, end)String indexing
to_json(v) / from_json(s)JSON serialization
list(items...) / map(k, v, ...)Collection constructors
next_id()Auto-increment counter
type_of(v)Type introspection
abs(n)Absolute value
response(status, body) / html(body) / redirect(url)HTTP responses
render(template, k, v, ...)Template rendering
transition(id, state) / get_status(id)State machine
is_type(val, "TypeName")Record type check
load_template(path, k1, v1, ...)Load HTML file with variable substitution
try { expr }Catch errors as values (.value / .error)
with(map, k, v) / without(map, k) / merge(m1, m2)Map operations
filter_by / sort_by / top / join / agg / ...Pipeline ops (see above)

Meta-cells (self-growing)

// Define a custom property
cell property geo_replicated {
    face { promise "data replicated across regions" }
    rules {
        implies [persistent, consistent]
        contradicts [ephemeral, local]
    }
}

// Define a custom checker
cell checker auth_required {
    face { promise "every cell with signals must have auth" }
    rules { check { require has_auth else MissingAuth } }
}

// Define a custom backend
cell backend redis {
    rules {
        matches [ephemeral, ttl]
        native "redis"
    }
}

Architecture

Source (.cell)
    │
    ├──→ Lexer → Parser → AST
    │         │
    │         ├──→ Checker (properties, signals, promises)
    │         │
    │         ├──→ Tree-walking Interpreter (soma run)
    │         │
    │         ├──→ Bytecode Compiler → VM (soma run --jit, 2.7x faster)
    │         │
    │         └──→ Rust Codegen (soma build)
    │
    ├──→ Registry (loads stdlib/*.cell — properties, backends, builtins)
    │
    └──→ Runtime
           ├── SQLite backend ([persistent])
           ├── Memory backend ([ephemeral])
           ├── HTTP server (soma serve)
           └── Bytecode cache (.soma_cache/)