lmscript tutorial

In about twenty minutes you write a handful of small scripts and learn the parts of lmscript you will use every day. You need Node.js 24.15 or later and a terminal. Make an empty directory for the files and run every command from it. The outputs below come from real runs of lmscript 0.2.348; where a line shows a file path, yours will show your own directory.

Step 1: Install and check the version

npm install -g @lmctl-ai/lmctl
added 1 package in 366ms
lmscript --version
lmscript 0.2.348

Step 2: Run your first script

Save this as hello.lms. Every statement ends with ;; # starts a comment.

# hello.lms: the smallest useful script
name = "world";
println("Hello, " + name + "!");
lmscript hello.lms
Hello, world!

Step 3: Lists, maps and formatted strings

Lists use [ ], maps use { }. An f"…" string evaluates the expressions in braces. Negative list indexes count from the end. Save as values.lms:

langs = ["lmscript", "C++", "Rust"];
person = {name: "Ada", year: 1815};
println(f"{person.name} was born in {person.year}.");
println(f"{len(langs)} languages, first: {langs[0]}, last: {langs[-1]}");
println(upper("quiet") + " " + str(3 * 4) + " " + json_stringify(person));
lmscript values.lms
Ada was born in 1815.
3 languages, first: lmscript, last: Rust
QUIET 12 {"name":"Ada","year":1815}

Step 4: Loops and conditions

Every if, for and while body needs braces. range(1, 6) is 1 to 5. A map's get(key, default) reads a key that may be missing. Save as loops.lms:

total = 0;
for (n in range(1, 6)) {
  if (n % 2 == 0) {
    continue;
  }
  total = total + n;
}
println(f"sum of odd numbers 1..5 = {total}");
counts = {};
for (word in split("to be or not to be", " ")) {
  counts[word] = counts.get(word, 0) + 1;
}
for (word in keys(counts)) {
  println(f"{word}: {counts[word]}");
}
lmscript loops.lms
sum of odd numbers 1..5 = 9
to: 2
be: 2
or: 1
not: 1

Step 5: Functions

Define functions with fn. They can call themselves and can be used before their definition. Save as functions.lms:

fn greet(name, punctuation) {
  return f"Hello, {name}{punctuation}";
}
fn factorial(n) {
  if (n <= 1) {
    return 1;
  }
  return n * factorial(n - 1);
}
println(greet("Ada", "!"));
println(f"10! = {factorial(10)}");
lmscript functions.lms
Hello, Ada!
10! = 3628800

Step 6: Command-line arguments

Words after the script name arrive in the list ARGV. Save as args.lms:

who = ARGV.get(0, "nobody");
println(f"{len(ARGV)} argument(s); first: {who}");
lmscript args.lms Ada Grace
2 argument(s); first: Ada
lmscript args.lms
0 argument(s); first: nobody

Step 7: Files

The standard library reads and writes text, lines, JSON and CSV. Save as files.lms:

mkdir("out");
write_lines("out/notes.txt", ["first", "second", "third"]);
lines = read_lines("out/notes.txt");
println(f"{len(lines)} lines, last: {last(lines)}");
write_json("out/summary.json", {count: len(lines), items: lines});
data = read_json("out/summary.json");
println(f"summary.json count = {data.count}");
lmscript files.lms
3 lines, last: third
summary.json count = 3

Step 8: Data written inside the script

A data block puts CSV, JSON, TOML or properties text straight into the script. The statement ends with ; on the marker line and the block closes with a line end _csv_ (or end _json_, …). CSV numbers become numbers. Save as data.lms:

scores = _csv_;
name,score
Ada,95
Grace,88
Linus,79
end _csv_
best = scores[0];
for (row in scores) {
  if (row.score > best.score) {
    best = row;
  }
}
println(f"{len(scores)} rows; best: {best.name} ({best.score})");
config = _json_;
{"retries": 3, "hosts": ["a.example", "b.example"]}
end _json_
println(f"retries={config.retries} hosts={join(config.hosts, ",")}");
lmscript data.lms
3 rows; best: Ada (95)
retries=3 hosts=a.example,b.example

Step 9: Run other programs

system runs a program directly (no shell) and returns its output and exit code as data; cmd only builds the argument list. ${…} inserts a value as one argument. Save as run.lms:

r = system node --version;
println(f"exit code {r.code}; node says {trim(r.stdout)}");
argv = cmd git log --oneline -n ${3};
println(json_stringify(argv));
lmscript run.lms
exit code 0; node says v24.22.0
["git","log","--oneline","-n","3"]

Step 10: Let the checker find mistakes

--check parses and checks a script without running it. This file forgets a ;:

total = 1 + 2
println(total);
lmscript --check broken.lms
error:/home/me/lmscript-tutorial/broken.lms:1:14: Expected ';' after the assignment; every statement ends with ';'

The exit code is 2 for check errors. A clean file prints nothing:

lmscript --check hello.lms

Step 11: Missing keys are errors

Reading a key that a map does not have stops the script with a message that names the fix. Save as strict.lms:

user = {name: "Ada"};
println(user.email);
lmscript strict.lms
error:/home/me/lmscript-tutorial/strict.lms:2:14: map user has no key "email"; test with user.has("email") or read user.get("email", default)

Step 12: Preview effects with a dry run

--dry-run executes the script in a simulation and prints the plan of effects instead of performing them. Save as cleanup.lms:

write_file("out/report.txt", "done\n");
delete_file("out/notes.txt");
println("cleaned up");
lmscript --dry-run cleanup.lms
PLAN complete
1. /home/me/lmscript-tutorial/cleanup.lms:1:1 dummy write_file ["out/report.txt","done\n"]
2. /home/me/lmscript-tutorial/cleanup.lms:2:1 dummy delete_file ["out/notes.txt"]
3. /home/me/lmscript-tutorial/cleanup.lms:3:1 handled println ["cleaned up"]
ls out
notes.txt
summary.json

Nothing was written or deleted. A normal run does this dry pass silently first and only then runs for real.

Step 13: Put functions in a module

A file that starts with package NAME; is a module; import includes it and its functions are called as NAME.fn(). Save as lib/textutil.lms:

package textutil;

fn shout(s) {
  return upper(s) + "!";
}

fn initials(full_name) {
  parts = split(full_name, " ");
  result = "";
  for (p in parts) {
    result = result + char_at(p, 0);
  }
  return result;
}

and use_module.lms next to the lib directory:

import "lib/textutil.lms";
println(textutil.shout("hello"));
println(textutil.initials("Ada King Lovelace"));
lmscript use_module.lms
HELLO!
AKL

Step 14: Exit codes and quick help

exit(n) ends the script with that exit code. Save as errors.lms:

path = ARGV.get(0, "missing.txt");
if (!file_exists(path)) {
  println(f"no such file: {path}");
  exit(3);
}
println(read_file(path));
lmscript errors.lms
no such file: missing.txt
echo $?
3

Every built-in function documents itself, and -e runs a one-line program:

lmscript help trim
Usage: lmscript [--check|--dry-run|--lint] [--no-preflight] [--trace FILE]
  [--dry-loop-limit N] [--dryrun-timeout MS] [--plan-format text|json]
  [--resumepoint PATH] [--pref FILE] [--module SPEC=FILE]... [--check-files]
  [--base-dir dir] [--source-name label] <file>|-| -e <code> [args...]
Function: trim(s: string): string
Built-in: core
Category:    Strings
Description: Trims whitespace from both ends of a string.
Example: clean = trim("  hello  ");
lmscript -e 'println(join(reverse(split("a,b,c", ",")), "-"));'
c-b-a

What you learned

Next: the user guide for the full reference, or the skill page if an agent will write your scripts.