Messy text in. Validated JSON out.
One static binary that turns messy, human-written text into schema-validated NDJSON in a single pipe stage. The script is the filter: it reads stdin, asks an LLM for typed fields, and prints one validated row per line. Every row matches your schema or the run fails naming the field that broke, so the next pipe stage never sees garbage.
$ cat tickets.txt | loqi extract.lq > tickets.ndjson
# one record per input line, type-checked against your schema
let schema = { type: "object", required: ["id", "severity"],
fields: { id: { type: "int" }, severity: { type: "string", enum: ["low", "high"] } } }
for line in lines(stdin()) {
print(json.stringify(ai_json("Extract id, severity from:\n{line}", schema)))
}
The extraction step shouldn't need a virtualenv.
You have prose on stdin and you want typed JSON on stdout. Loqi makes that one pipe stage: a short script, a schema, one static binary.
Validated, or it fails
ai_json(prompt, schema) returns typed fields, retried against the model, or the run exits naming the field that broke. The next stage in your pipe never gets garbage.
One binary in your pipe
Reads stdin, writes one JSON object per line. Composes with jq, grep, sort and xargs. Nothing to pip install, no virtualenv, no SDK to vendor.
Parallel by default
ai_all fires prompts concurrently, so a 10k-line file isn't 10k sequential round-trips. The slow part is the network, and Loqi overlaps it.
Everything the extraction job needs, in the binary.
The model call, the JSON codec, the HTTP client, vector search and file/env I/O all live in the runtime. No installs, no SDK, no virtualenv.
A first-class LLM call to the Anthropic Messages API. Reads ANTHROPIC_API_KEY; pass a model as a second arg or via LOQI_AI_MODEL. Rate limits and transient errors are retried with backoff automatically.
let poem = ai("Write a haiku about clean code")
print(poem)
json.parse(str) and json.stringify(value), a built-in codec between JSON text and native Loqi values.
let cfg = json.parse(`{"name": "Ada"}`)
print(cfg.name)
http.get(url) and http.post(url, body, content_type?), a built-in HTTP client returning the response body as a string.
let body = http.get("https://api.github.com")
print(len(body))
Cosine similarity over numeric vectors, the primitive behind semantic search, ranking and clustering.
let s = similarity(a, b)
print(s) # a float in [-1, 1]
read(path) returns a whole file as a string; write(path, content) writes one back. Simple, whole-file I/O.
write("out.txt", "hi")
print(read("out.txt"))
Read environment variables straight from the runtime, keys, config and feature flags, no wrapper library.
let key = env("ANTHROPIC_API_KEY")
print(type(key))
Instant startup, Python-class compute.
For a text-extraction pipe the wall-clock is dominated by model calls, which Loqi overlaps with ai_all. The interpreter is a stack bytecode VM: measured on Apple Silicon with process_time, on par with CPython 3.13 on fib(30) and about 1.4× faster on a tight loop to 50M. No virtualenv to spin up, no JIT to warm: the binary starts and the pipe runs.
fib(30) faster (0.017s) because it JITs. Loqi does not JIT yet. We do not claim to beat Go, Rust, C or Node, just a single compiled binary with Python-class speed that gets faster each iteration.
Real Loqi, top to bottom.
Recursion, closures, AI-powered extraction, and verbatim raw strings for JSON, every snippet below runs as-is.
fn fib(n) {
if n < 2 { return n }
return fib(n - 1) + fib(n - 2)
}
print("fib(10) = {fib(10)}")
fn counter() {
let n = 0
return fn() { n = n + 1; return n }
}
let next = counter()
print(next(), next(), next()) # 1 2 3
# prose in, typed + validated JSON out
let schema = { type: "object",
fields: { name: { type: "string" }, year: { type: "int" } } }
let row = ai_json("Extract name, year from: {text}", schema)
print(json.stringify(row))
# backticks = verbatim: no escapes, no interpolation
let cfg = json.parse(`{"name": "Ada", "tags": ["x", "y"]}`)
print(cfg.name)
Up and running in three commands.
Built with nothing but clang, portable by default (LOQI_NATIVE=1 for -mcpu=apple-m1 -flto). No package manager, no toolchain zoo. Clone, build, run.
Clone the repo
Grab the source, reference implementation lives in src/loqi.c.
Build the binary
One script compiles a single native binary into build/loqi.
Run an example
Point Loqi at any .lq file in examples/ and go.
# 1. clone
$ git clone <repo-url> loqi
$ cd loqi
# 2. build, single native binary
$ ./scripts/build.sh
# 3. run your first program
$ ./build/loqi examples/01_hello.lq
Hello, world!
Frequently asked questions
What is Loqi?
A command-line tool that turns messy, unstructured text into schema-validated NDJSON in a single pipe stage, the way jq transforms structured JSON. A short .lq script reads stdin, calls ai_json(prompt, schema) to extract typed fields with an LLM, and prints one validated row per line: cat tickets.txt | loqi extract.lq > tickets.ndjson. If a row can't be made to validate, the run fails naming the field that broke, so the next stage never gets garbage. One static binary, macOS and Linux.
How is it different from jq, grep/awk, or Python + an SDK?
jq needs structured input; Loqi produces the structure from prose. grep and awk match and slice text but can't reason about it. Python plus an SDK drags a virtualenv, pip install and import boilerplate into what should be one pipe stage, then you hand-roll the client, JSON parse, schema check and retries. Loqi bakes typed, validated-or-fails, parallel extraction into one binary where the script is the filter.
How fast is it?
A single static binary with instant startup (no virtualenv or import cost). The interpreter is a stack bytecode VM in C, roughly CPython-class on compute, but for a text-extraction pipe the time is dominated by the model calls, which Loqi runs in parallel with ai_all.
How do I install it?
Clone github.com/ferdinandobons/loqi and run ./scripts/build.sh to get ./build/loqi. It builds on macOS and Linux. At runtime it needs curl and, for model calls, an ANTHROPIC_API_KEY.