Agent Primer
This page is the source of the generated section of AGENTS.md that skein new
and skein agents write into Skein projects. It is a compact primer for coding
agents working inside a Skein project, not a guide to the Skein compiler
codebase itself.
What Skein Is
Section titled “What Skein Is”Skein is a programming language for cloud services where AI agents are
first-class constructs. It compiles to BEAM bytecode and runs on the Erlang VM.
Source files use the .skein extension. Comments start with --.
Project Layout and Commands
Section titled “Project Layout and Commands”skein.toml -- project configsrc/*.skein -- source modules (one module or agent per file)test/*.skein -- test modules| Command | Purpose |
|---|---|
skein build | Compile all .skein files under src/ |
skein test | Run all test/scenario declarations in src/ and test/ |
skein run | Start the service (HTTP handlers; --port <n>, default 4000) |
skein compile <file> | Compile a single file |
skein trace | View recent trace spans (--last <n>, --kind <kind>) |
skein agents | Regenerate the generated block of this file |
skein mcp | Start the Skein MCP server (stdio) for spec lookup, docs search, and compile checks |
Compiler errors are structured: every error has a code, message,
location, and usually a fix_hint and fix_code telling you exactly what to
change. Read them — they are written for agents.
Syntax Cheatsheet
Section titled “Syntax Cheatsheet”-- A module: capabilities, types, functions, handlers, tools, testsmodule UserService { capability http.in capability store.table("users", User)
type User { id: Uuid @primary email: Email @unique name: String }
fn greet(name: String) -> String { "Hello, ${name}!" -- interpolation: ${ident} / ${record.field} }
handler http GET "/users/:id" (req) -> { let id = Uuid.parse(req.params.id)! match store.users.get(id) { Ok(u) -> respond.json(200, u) Err(_) -> respond.json(404, { error: "not found" }) } }
test "greet works" { assert greet("World") == "Hello, World!" }}Core forms:
let x = expr— single-assignment binding; type is inferred.match expr { Pattern -> expr ... }— arms must cover all enum variants and all return the same type.expr |> fn_name(args)— pipe.expr!— unwrap aResult, crashing onErr.expr?— propagate theErrto the caller (enclosing function must returnResult).- Function params and return types are always annotated:
fn add(a: Int, b: Int) -> Int { a + b }. - Calls may pass arguments by name after any positional ones:
add(b: 2, a: 1),llm.chat(model: "...", system: "...", input: t). Named args work for same-module fns and documented effect signatures; stdlib calls are positional only. Never put a positional arg after a named one. - Built-in types:
Int,Float,String,Bool,Uuid,Instant,Duration,Email,Url,Option[T],Result[T, E],List[T],Map[K, V],Set[T]. Field constraints:@min(n),@max(n),@one_of([...]),@default(v),@description(s),@primary,@unique. - Stdlib modules (
String,Int,Float,List,Map,Set,Option,Result,Uuid,Instant,Duration) are ambient — no capability or declaration needed:String.upcase(s),List.map(items, &double).
Capabilities and Effects
Section titled “Capabilities and Effects”Every effect call requires a matching capability declaration in the same
module or agent. There are no exceptions.
| Capability | Grants |
|---|---|
http.in | Declaring handler http ... |
http.out("host") | http.get/post/put/delete to that host |
store.table("name", RecordType) | store.<name>.get/put/delete/query, typed against RecordType (a declared type with exactly one @primary field) |
memory.kv("namespace") | memory.put/get/delete/list |
model("provider", "model") | llm.chat/json/stream/embed |
tool.use(Mod.ToolName) | tool.call(Mod.ToolName, { ... }) |
queue.publish("name") / queue.consume("name") | queue.publish / handler queue |
topic.publish("name") / topic.consume("name") | topic.publish / handler topic |
schedule.trigger("name") | handler schedule |
event.log("stream") | event.log(name, data) |
process.spawn / timer | process.spawn / timer.after/interval/cancel |
uuid / instant | uuid.new() / instant.now() — all nondeterminism is capability-gated (deterministic under skein test) |
emit EventName { field: value } and trace.annotate(key, value) need no
capability. idempotent(key) (handlers only) skips re-processing a key.
Modules Are Sealed: Tools Are the Only Cross-Module Seam
Section titled “Modules Are Sealed: Tools Are the Only Cross-Module Seam”Functions, types, and handlers are private to their module. There is no
import and no OtherModule.fn_name(...) — that is a compile error (E0016).
To share behavior across modules, declare a tool in one module and call it
from another:
-- callee moduletool Billing.CreateRefund { description: "Issue a refund" input { customer_id: String, amount: Int @min(1) } output { id: String, status: String } implement { ... }}
-- caller module (or agent)capability tool.use(Billing.CreateRefund)let result = tool.call(Billing.CreateRefund, { customer_id: cid, amount: 500 })! -- ! unwraps the ResultAgents, Phases, and Transitions
Section titled “Agents, Phases, and Transitions”Agents declare state, a Phase enum with legal transitions, and on
handlers. They can be top-level (one per file) or nested inside a module —
a nested agent compiles to its own BEAM module and sees the enclosing
module’s types, capabilities, and functions in addition to its own (a
phase handler may call the module’s fns directly). Transitions are
checked at compile time — transition() to a phase not listed in the current
phase’s -> [...] list is an error (E0030), and every phase needs an
on phase handler (E0032).
agent RefundAgent { capability memory.kv("refund_sessions")
state { ticket_id: String }
enum Phase { Analyze -> [Refund, Done] Refund -> [Done] Done -> [] }
on start(ticket_id: String) -> { -- typed params, NOT `on start(ctx)` memory.put("ticket_id", ticket_id) transition(Phase.Analyze) }
on phase(Phase.Analyze) -> { -- decide, then: transition(Phase.Refund) }
on phase(Phase.Refund) -> { transition(Phase.Done) } on phase(Phase.Done) -> { stop() } -- stop() needs the parens}Agent lifecycle calls (inside agents only): transition(Phase.X), stop(),
suspend(reason). There is no in-agent resume — a suspended agent is
resumed from outside by the host-side Skein.Runtime.Agent.resume(pid, phase).
Memory keys are automatically scoped per agent instance.
Known Gotchas
Section titled “Known Gotchas”- Effects return a
Result— unwrap it.llm.chat,llm.json[T],req.json[T],store.<t>.get/put/delete/query,http.get/post/...,queue.publish,topic.publish, andtool.callall returnResult[T, E]at runtime. Useexpr!(crash onErr) orexpr?(propagate) before touching the value — otherwise you get an opaque BEAM crash. This is the single most common trap. - No
if/else. Conditionals arematchon aBool:match cond { true -> ... false -> ... }. store.<t>.getnot-found is a named variant: match it asErr(StoreError.NotFound) -> ...(or the bareErr(NotFound));Err(_)still works as a catch-all.- Tool
implementbodies see their input fields directly in scope (amount, notinput.amount) and must return the output shape wrapped inOk({ ... });input/outputare contextual keywords only inside tool declarations. stop()must be called with parentheses.on starttakes typed parameters (on start(order_id: String)), not a bare context argument.- Match arms must all return the same type, and enum matches must cover every
variant (or use
_). - One top-level module (or agent) per file; agents may be nested inside a module’s body.
handler queue/handler topictake the queue/topic name as a string route;handler schedulehandlers take no payload parameter.- String interpolation takes identifiers and field access only —
${name},${user.name}; bind any other expression withletfirst. Comments are--(not//or#).
Going Deeper
Section titled “Going Deeper”-
Full language spec, examples, and stdlib reference (agent-friendly):
- https://kormie.github.io/Skein/llms.txt (entry point)
- https://kormie.github.io/Skein/llms-small.txt (compact)
- https://kormie.github.io/Skein/llms-full.txt (full corpus)
-
On-demand spec lookup, docs search, and structured compile checks without leaving your editor: register the Skein MCP server. It speaks MCP over stdio:
Terminal window # Claude Codeclaude mcp add skein -- skein mcpTools:
skein_spec_lookup(fetch a spec section),skein_docs_search(search the spec corpus),skein_compile_check(compile a file or project and get structured JSON errors with fix hints).