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]"}
The entire language fits in an agent's context window. Learn it in one pass.
[persistent, consistent] → SQLite. [ephemeral] → memory. No config.
data |> filter_by() |> sort_by() |> top(10). Spark in 5 lines.
soma serve = HTTP server + HTMX + JSON API. Zero framework.
Property contradictions, signal wiring, promise verification — before runtime.
Properties, checkers, backends defined as .cell files. The language extends itself.
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.
| Language | Lines | Dependencies | Files | Setup commands |
|---|---|---|---|---|
| Soma | 74 | 0 | 1 | 1 (soma serve app.cell) |
| Express | 87 | 2 | 3+ | 3 |
| Flask | 112 | 1 | 2+ | 2 |
| Go | 152 | 1 | 3+ | 3 |
0 SQL statements. 0 imports. 0 database config. 0 route declarations. 0 JSON marshaling.
curl -fsSL https://soma-lang.dev/install.sh | sh
# or
brew install soma
curl -fsSL https://soma-lang.dev/install.sh | sh
git clone https://github.com/soma-dev-lang/soma
cd soma/compiler
cargo build --release
cp target/release/soma /usr/local/bin/
soma --version
soma 0.1.0
Soma has one structure: the cell. Every cell has a face (contract), memory (state), and signal handlers (behavior).
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
}
}
| Int | 64-bit integer |
| BigInt | Arbitrary precision (declare in params to activate) |
| Float | 64-bit float |
| String | UTF-8 text with {interpolation} |
| Bool | true / false |
| List | Ordered collection |
| Map | Key-value pairs |
| () | Unit / null |
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
if x > 10 {
return "big"
} else {
return "small"
}
while i < n {
sum += i
i = i + 1
}
for item in items {
print(item)
}
| + - * / % | Arithmetic |
| == != < > <= >= | Comparison |
| && || ! | Logic |
| |> | Pipe: a |> f(b) → f(a, b) |
| ?? | Null coalescing: x ?? default |
| += | Append: s += "more" |
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).
use lib::helpers // local: lib/helpers.cell
use math // package: .soma_env/packages/math/
use std::builtins // stdlib
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
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
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")
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
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)
Soma replaces PySpark for in-memory data processing. Same operations, fraction of the code.
let portfolio = stocks
|> filter_by("market_cap", ">", 20000)
|> filter_by("momentum", ">", -10)
|> sort_by("composite", "desc")
|> top(10)
let by_sector = trades
|> join(prices, "ticker")
|> filter_by("volume", ">", 1000)
|> agg("sector", "volume:sum", "price:avg")
|> sort_by("volume_sum", "desc")
| 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 |
Signal handlers become HTTP routes automatically. soma serve gives you a threaded HTTP server with SQLite, CORS, HTMX, static files, and hot reload.
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()
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(404, map("error", "not found")) // status code
html("<h1>Hello</h1>") // text/html
redirect("/") // 302 redirect
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
| soma run file.cell [args] | Execute a signal handler |
| soma run file.cell --jit | Execute with bytecode VM (2.7x faster) |
| soma serve file.cell | HTTP server |
| soma serve file.cell --watch | Hot reload |
| soma check file.cell | Verify contracts, properties, signals |
| soma test file.cell | Run test cells |
| soma build file.cell | Generate Rust code |
| soma init [name] | Create project + isolated env |
| soma add pkg --git url | Add dependency |
| soma install | Install dependencies |
| soma env | Show environment |
| soma props | List all properties + backends + builtins |
| soma add-provider <name> | Scaffold a storage provider |
| soma test-provider <name> | Run provider conformance tests |
| soma migrate --from a --to b | Migrate data between providers |
| soma repl | Interactive expression evaluator |
| 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) |
// 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"
}
}
Write once, deploy anywhere. Same Soma code on SQLite, DynamoDB, Firestore, D1. The developer writes properties. The provider resolves them to services.
// Your code — doesn't change between providers
memory {
users: Map<String, String> [persistent, consistent]
cache: Map<String, String> [ephemeral, local]
secrets: Map<String, String> [persistent, encrypted]
sessions: Map<String, String> [ephemeral, ttl(30min)]
}
# Default — zero config, uses SQLite
[storage]
provider = "local"
# AWS — resolves to DynamoDB + ElastiCache
[storage]
provider = "aws"
[storage.config]
region = "eu-west-1"
# Cloudflare — resolves to D1 + KV
[storage]
provider = "cloudflare"
| Properties | local | AWS | GCP | Cloudflare |
|---|---|---|---|---|
| [persistent, consistent] | SQLite | DynamoDB | Firestore | D1 |
| [ephemeral] | Memory | ElastiCache | Memorystore | Workers KV |
| [persistent, encrypted] | SQLite | DynamoDB+KMS | Firestore+CMEK | D1 |
| [persistent, immutable] | SQLite | S3+ObjectLock | GCS+Retention | R2 |
| [ephemeral, ttl(30min)] | Memory | ElastiCache+TTL | Memorystore+TTL | KV+TTL |
[provider]
name = "aws"
[[backend]]
name = "dynamodb"
requires = ["persistent", "consistent"]
optional = ["encrypted", "ttl"]
native = "http://dynamo-sidecar:8000" // or native Rust impl
[[backend]]
name = "elasticache"
requires = ["ephemeral"]
optional = ["ttl", "local"]
native = "http://redis-sidecar:6380"
Any language can be a provider. Just expose 5 HTTP endpoints:
// Provider sidecar — implement in any language
POST /get { "table": "T_data", "key": "k" } → { "value": "v" }
POST /set { "table": "T_data", "key": "k", "value": "v" } → { "ok": true }
POST /delete { "table": "T_data", "key": "k" } → { "ok": true }
POST /keys { "table": "T_data" } → { "keys": ["a", "b"] }
POST /len { "table": "T_data" } → { "len": 42 }
soma add-provider aws # scaffold provider manifest
soma test-provider aws # run 5 conformance tests
soma test-provider local # test built-in (5/5 pass)
soma migrate --from local --to aws # migrate data
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/)