A language for AI workflows where cost is visible in the syntax.
Every step that calls a model is marked with ~=. Every step that doesn't is plain code. You can read the price of a workflow by scanning for the squiggles.
type Lead {
domain: text
priority: text
note: obj
}
main(raw: text) -> Lead {
const domain = raw |> after("@") |> before(" ") # free
const details ~= extract(raw, fields: [name, budget]) # costs tokens
if details.budget > 15000 {
const note ~= compress(raw, max: 60)
return @Lead { domain, priority: "hot", note }
} else {
const priority ~= classify(raw, into: [warm, cold])
return @Lead { domain, priority, note: null }
}
}
npm install suede-langA Suede project uses two conventions:
-
config.suede— holds theinitblock (API keys, models, cache, budget). The compiler finds it automatically by walking up from whatever file you run — liketsconfig.json. You never import it. Every file in the project can usewith fastorwith smartwithout importing config. -
main.suede— the entry point. Should contain amain()block. All CLI commands (run,check,analyze) default tomain.suedeif no file is specified.
my-project/
config.suede # init block — models, keys, cache, budget
main.suede # entry point with main() block
helpers.suede # free functions
types.suede # type definitions
init {
api_keys {
gemini = env("GEMINI_API_KEY")
}
model fast = "gemini-3.5-flash" {
provider = "gemini"
temperature = 0.2
max_tokens = 1024
}
model smart = "gemini-3.1-pro-preview" {
provider = "gemini"
temperature = 0.7
max_tokens = 4096
}
cache { enabled = true, ttl = 3600 }
budget { max_tokens = 100000, on_exceed = "stop" }
}
The main block is the default entry point. If a file has one, suede run uses it automatically:
main(raw: text) -> Lead {
const domain = raw |> after("@") |> before(" ")
const details ~= extract(raw, fields: [name, budget])
return @Lead { domain, priority: "new", details }
}
If there's no main, specify which pipeline or agent to run:
suede run app.suede my_pipeline --arg raw="data"# all commands default to main.suede
suede run --arg raw="email text here"
suede check
suede analyze
# or specify a file
suede run app.suede --arg raw="email text here"
# run a specific pipeline or agent instead of main()
suede run app.suede triage_lead --arg raw="email text here"import { run, stubModel } from "suede-lang";
// with a real provider (config.suede is found from basePath)
const { value, stats } = await run(
src,
null,
{ raw: "email text" },
null,
null,
"./",
);
// run a specific pipeline
const { value, stats } = await run(
src,
"triage",
{ raw: "email text" },
null,
null,
"./",
);
// with a stub for testing (no API calls)
const { value, stats } = await run(
src,
"triage",
{ raw: "email text" },
stubModel(),
);
stats.modelCalls; // number of ~= calls
stats.codeSteps; // number of = bindings
stats.inputTokens; // total input tokens
stats.outputTokens; // total output tokensimport { compile } from "suede-lang";
import { analyze } from "suede-lang/analyze";
const prog = compile(src, basePath); // basePath needed to find config.suede
const paths = analyze(prog, { raw: "input" }, () => {});
// paths[0].bestTokens, paths[0].worstTokens, paths[0].modelCalls
// paths[0].byModel — per-model breakdownA pre-built browser bundle is included at dist/suede.browser.js. It exposes window.Suede with run, compileWithFiles, Interpreter, analyze, and check.
<script src="https://unpkg.com/suede-lang/dist/suede.browser.js"></script>
<script>
const { run, compileWithFiles, Interpreter, analyze, check } = window.Suede;
// include config.suede in the file map — it's found automatically
const files = new Map([
["config.suede", configSrc],
["app.suede", appSrc],
]);
// quick — run a program directly
const { value, stats } = await run(src, null, { raw: "text" }, modelFn);
// or compile + run separately
const prog = compileWithFiles(src, files);
const interp = new Interpreter(modelFn, onStep);
const result = await interp.run(prog, null, { raw: "text" });
</script>-
=— free, deterministic, instant. String ops, math, branching. -
~=— costs tokens. Calls a model. Can fail, gets retried.
The interpreter enforces this both ways. You cannot use = on a model verb, and you cannot use ~= on a plain function.
-
const— immutable binding. Stays local to the current block. Use for values that shouldn't change. -
let— mutable binding. Propagates changes back to the outer scope fromforloops andif/elseblocks.
# const for values you compute once
const domain = raw |> after("@") |> before(" ")
const details ~= extract(raw, fields: [name, budget])
# let for accumulators and state that changes
let total = 0
for n in nums {
let total = total + n # updates outer total
}
# let for conditional updates
let status = "pending"
if score > 0.8 {
let status = "approved"
}
Rule of thumb: use const by default, let when you need to accumulate or conditionally update.
Six built-in verbs that require ~=:
| Verb | Purpose | Returns |
|---|---|---|
extract |
Pull structured fields from text | { field: value, ... } |
classify |
Categorize into one of N labels | { label: "category" } |
compress |
Summarize/shorten text | { text: "summary" } |
rewrite |
Transform text style/format | { text: "rewritten" } |
expand |
Elaborate on text | { text: "expanded" } |
generate |
Create new content | { field: value, ... } |
You don't configure anything. The runtime learns your provider's rate limits from 429 response headers and adapts automatically — per model, per provider. Failed requests are never dropped; they're re-queued with exponential backoff or the provider's retry-after value. A 429 on your smart model doesn't slow down fast. Works out of the box with OpenAI, Anthropic, and Gemini.
Define schemas that are enforced at runtime — on record construction, parameter passing, and return values.
type Analysis {
mood: text
score: num
tags: list
}
pipeline analyze(text: text) -> Analysis {
const result ~= extract(text, fields: [mood, score, tags])
return @Analysis { mood: result.mood, score: result.score, tags: result.tags }
}
Field types: text, num, bool, list, obj, any
What gets checked:
-
@Analysis { ... }— missing fields, extra fields, wrong types -
-> Analysison a pipeline/agent/function — return value must match the schema -
(data: Analysis)— parameter must match the schema when passed in - If a return type is a custom name (not
text,num, etc.), a matchingtypeblock must exist
Records without a matching type block (@Foo { ... } with no type Foo) are untyped — no enforcement.
Define typed errors, throw them from anywhere (bypasses return type checks), catch them by type. The static checker warns when you call a pipeline that throws without catching.
error ApiFailed {
status: num
url: text
}
pipeline fetch_data(url: text) -> obj {
const res = fetch(url)
if res.status != 200 {
throw ApiFailed("request failed", status: res.status, url: url)
}
const data ~= extract(res.body, fields: [name, value]) with fast
return data
}
pipeline go(url: text) -> obj {
try {
return fetch_data(url)
} catch ApiFailed as err {
return @Fallback { error: "API ${err.url} returned ${err.status}" }
}
}
Built-in runtime errors are also catchable: TimedOut, BudgetExceeded, AgentMaxIterations, RateLimited.
-
const/let—constis immutable and block-local,letis mutable and propagates out of loops and conditionals - Types — schema declarations with runtime enforcement on records, params, and returns
-
Errors — typed error definitions with
throw/catch, static enforcement of error handling, built-in runtime errors (TimedOut,BudgetExceeded,AgentMaxIterations,RateLimited) - Pipelines — linear processing, top to bottom, returns a value
- Agents — goal-seeking loops with tools (including other agents), memory, and max iteration caps
- Custom prompts — define your own model verbs with typed returns
- Functions — free helper functions, no model calls
-
config.suede— project-wide init block, auto-discovered by walking up the directory tree -
mainblock — default entry point, runs automatically withsuede run - Multi-file imports — selective or namespace imports with cycle detection
-
Control flow —
if/else,for/in/into/emit,match/case,try/catch,throw,recurse -
Parallel —
parallel { }for concurrent model calls,parallel forfor concurrent loop iterations -
Modifiers —
with,retry,cache,timeout,system,expect,guard -
Built-ins — 50+ free functions for strings, lists, math, objects, filtering, JSON parsing, plus
map -
I/O —
fetch(url),read(path),env(key)— free, no tokens, async under the hood -
JavaScript escape —
js { }blocks for anything else - Multi-provider — mix Gemini, OpenAI, and Anthropic models in the same program (or inject your own)
- Adaptive rate limiting — zero config, learns provider limits from 429 responses, per-model queuing
- Cost controls — caching, token budgets
Playground — run Suede in the browser with the full static analyzer.
MIT