Skip to Content
We are live but in Staging πŸŽ‰
Scriptum LanguageScriptum Language

Scriptum Language

Scriptum is a purpose-built domain-specific language for orchestrating AI workflows. A Scriptum program lives in a .scriptum file and reads top to bottom like a recipe: every step starts with a verb (do, each, decide, together, ask, agent, emit), and indentation β€” not braces β€” defines structure.

The design goal is that a non-developer can read a script and understand the flow, while the compiler still has the full type graph of the program before a single step runs. Inference (LLM chat, embeddings, reranking, transcription, classification) is handled by Ignite, and structured data lands in K3 through native tools. You write the recipe; Scriptum runs it in the cloud as a durable, resumable thread.

If you are new to the platform, read Core Concepts first for the script / draft / version / thread vocabulary, and the API Reference for the gRPC surface that creates and watches threads. This section is the deep dive into the language itself.

The pipeline: .scriptum β†’ compile β†’ IR β†’ thread

A script never executes as raw text. It goes through a fixed pipeline:

1. Parse .scriptum source β†’ AST 2. Resolve tools section β†’ ToolCatalog (native + Ignite) 3. Validate types, bindings, β†’ typed, checked program field access, contract 4. Codegen each construct β†’ YAML IR (one node per step) 5. Execute IR runs in the cloud β†’ Thread (durable, resumable)

Steps 1–4 are the compiler (scriptum-compiler). Because every tool declares its input and output schema, the compiler knows the type of every binding before anything runs β€” typos, missing inputs, and bad field access are caught at compile time, not in production. Step 5 is the runtime: the compiled IR is scheduled as a thread that persists its state after every step, so it can pause for human input and resume exactly where it left off.

Each language construct maps to a single IR primitive. You will see these names in the YAML IR and in step records:

You writeIR primitive
do "X" with toolexecute
each x in listiterate
decide "Q"decide (or branch for rules)
check "C"evaluate
togetherparallel
pipe x from srcpipe
repeatrepeat
plan "X" with llmplan
run scriptrun
waitwait
ask "X"ask
agent "X" with llmagent
let name = exprlet
emitemit
yieldyield
send "X" via "ch"send

A complete annotated example

Here is a small, end-to-end script β€” input block, tools, two action steps, and an output β€” walked through line by line. It probes a URL, checks that it is reachable, and emits the result. (Adapted from examples/url_probe.scriptum.)

script "URL Probe" version "0.1.0" input url: text -- URL to probe output status: number content_type: text result: object import tools get_url_content_type from native fetch_url from native do "Probe URL" with get_url_content_type url = url -> probe check "Is reachable?" probe.status < 400 on pass: continue on fail: fail "URL returned error status" do "Fetch content" with fetch_url url = url -> fetched emit "Result" status = probe.status content_type = probe.content_type ?? "unknown" result = fetched

Line by line:

  • script "URL Probe" β€” every file starts with the script keyword and a human-readable name. The name is the primary identifier (it shows up in the thread UI), not a comment.
  • version "0.1.0" β€” an optional semantic version for the script.
  • input block β€” declares the script’s parameters. url: text says this script takes one required text input named url. The -- URL to probe trailing comment becomes the field’s documentation. Inputs, outputs, and tool declarations together form the ScriptContract β€” the typed interface the runtime validates calls against.
  • output block β€” declares what the script returns: a number, a text, and an object. The compiler checks that your emit actually produces these.
  • import tools β€” lists the tools this script may call. get_url_content_type and fetch_url are resolved from the catalog (here, both from native). A tool not in this block is a compile error if you try to do ... with it.
  • do "Probe URL" with get_url_content_type β€” the core action step. It calls the tool with one named input (url = url, mapping the script input into the tool’s url argument) and binds the whole result object to probe via -> probe.
  • check "Is reachable?" probe.status < 400 β€” a gate. The condition uses a field accessed off the previous step’s binding. on pass: continue proceeds; on fail: fail "..." stops the thread with a message.
  • do "Fetch content" with fetch_url β€” a second action; probe is in scope because bindings are visible to every step that comes after them.
  • emit "Result" β€” declares the script’s output. Note probe.content_type ?? "unknown": the ?? operator supplies a default if the field is null. This is what fulfils the output contract.

Where to go next