Tutorial: Writing Scripts with Sesi
Sesi is a general-purpose programming language with optional reasoning features. This tutorial focuses on writing practical scripts: command-line tools, file processors, API helpers, automation flows, and small orchestration programs.
By the end, you should know how to structure a script, accept inputs, transform data, call the file system and network, run subprocesses, add optional reasoning, debug problems, and split larger scripts into modules.
---
1. What Counts as a Sesi Script?
A Sesi script is a .sesi file that can be run from the command line.
sesi my_script.sesi
Inside this repository, you can also run scripts through npm:
npm run sesi examples/main/01_hello.sesi
Use Sesi scripts when you want to:
- Automate repeated command-line work
- Read, write, convert, or organize files
- Fetch data from APIs
- Build small local tools
- Coordinate multiple scripts or processes
- Add model calls only where they make the script more useful
---
2. Script Structure
Sesi programs are made of statements.
let name = "Sesi"
show "Hello," name
For single-line inline scripts, use semicolons:
sesi -e "let x = 10; let y = 20; show x + y"
Comments use // or /<em> ... </em>/.
// Single-line comment
/*
Multi-line comment
*/
---
3. Your First Useful Script
Create report.sesi:
let started = time()
let files = list_dir(".")
show "Sesi Report"
show "Generated at:" started
show "File count:" len(files)
for file in files {
show "-" file
}
Run it:
sesi report.sesi
This script introduces a common Sesi shape:
- Gather input or state
- Transform it with normal language features
- Print or write the result
---
4. Values and Variables
Use let for bindings.
let count = 3
let title = "Daily Report"
let ready = true
let missing = null
Sesi has numbers, strings, booleans, null, arrays, and objects.
let scores = [10, 20, 30]
let profile = {name: "Ada", role: "developer"}
show scores[0]
show profile.name
You can update variables and object fields:
let total = 0
total = total + 5
let config = {runs: 0}
config.runs = config.runs + 1
Convert values when you need a specific type:
let raw = "42"
let answer = num(raw)
show "Answer:" str(answer)
---
5. Command-Line Arguments
Arguments passed after the script name are available in the global args array.
sesi greet.sesi Alice
greet.sesi:
if len(args) == 0 {
show "Usage: sesi greet.sesi <name>"
} else {
let name = args[0]
show "Hello," name + "!"
}
Validate inputs before using them. This makes scripts easier to run from terminals, cron jobs, package scripts, and other automation.
if len(args) < 2 {
show "Usage: sesi copy_text.sesi <input> <output>"
} else {
let input_path = args[0]
let output_path = args[1]
let text = read_file(input_path)
write_file(output_path, text)
show "Copied" input_path "to" output_path
}
Inline eval can receive arguments too:
sesi -e "show 'First arg:' args[0]" hello
---
6. Control Flow
Use if and else for branching.
let score = 87
if score >= 90 {
show "excellent"
} else if score >= 70 {
show "passing"
} else {
show "needs work"
}
Use while when a loop depends on a condition:
let i = 0
while i < 3 {
show i
i = i + 1
}
Use numeric for loops for ranges:
for i = 0 to 5 {
show "Index:" i
}
Use for ... in loops for arrays:
let names = ["Ada", "Grace", "Linus"]
for name in names {
show "Hello," name
}
break and continue are available for loop control.
for item in [1, 2, 3, 4] {
if item == 2 {
continue
}
if item == 4 {
break
}
show item
}
---
7. Functions
Functions help you keep scripts readable as they grow.
fn format_status(name: string, ok: bool) {
if ok {
return name + ": ok"
}
return name + ": failed"
}
show format_status("database", true)
Document public and nontrivial declarations with a comment immediately above them. Sesi uses leading comments for hover details, retains function documentation across module exports and imports, and can pass handler descriptions to modules such as std/api.
There is no required schema; even one clear descriptive sentence is valuable. For reusable functions, prefer this consistent structure:
/**
Adds two numbers.
Parameters:
- `a`: The first number.
- `b`: The second number.
Returns:
The sum of `a` and `b`.
*/
fn add(a: number, b: number) -> number {
return a + b
}
The same leading comments can document make and let declarations. This is particularly useful when a let contains an array or object. Describe the collection's purpose, array item shape and ordering, and meaningful object keys and defaults.
/** Collects jobs waiting to be processed. */
make JobQueue {
/**
Runtime settings shared by API requests.
Fields:
- `hosts`: Ordered fallback API hosts.
- `retries`: Maximum attempts per request.
*/
let settings = {
"hosts": ["https://primary.example", "https://backup.example"],
"retries": 3
}
/** Pending job objects in insertion order. */
let jobs = []
}
You can use functions as small task units:
fn read_json(path: string) {
let text = read_file(path)
return from_json(text)
}
fn save_json(path: string, data) {
return write_file(path, to_json(data))
}
Default parameters are supported:
fn greet(name: string = "World") {
show "Hello," name
}
greet()
greet("Sesi")
Piping and Function Composition
Sesi supports the piping operator (|) to compose function calls and tool executions cleanly, flowing the output of the left expression into the first argument of the right function call.
fn add(a, b) {
return a + b
}
fn mul(a, b) {
return a * b
}
// Flow 10 as first parameter into add(5), and then flow the result into mul(2)
let result = 10 | add(5) | mul(2)
show result // Prints 30
Rule of Thumb:
Use | for data pipelines: arrays, strings, objects, or multi-step transformations (data | filter | map | show).
Use standard syntax () for math, boolean logic (!, &&, ||), comparisons (==, !=), and inline checks.
---
8. Working with Arrays and Objects
Arrays are useful for batches, queues, and collected results.
let tasks = []
push(tasks, "lint")
push(tasks, "test")
push(tasks, "build")
show join(tasks, ", ")
show pop(tasks)
Array Processing Functions
Sesi provides standard functional primitives to transform, filter, and aggregate arrays:
map(array, callback): Transforms each element of an array using a callback function.filter(array, callback): Selects elements that satisfy a condition.reduce(array, callback, initialValue = null): Accumulates array values into a single result.find(array, callback): Finds the first element matching a condition.slice(array, start, end = null): Extracts a portion of an array.
let numbers = [1, 2, 3, 4, 5, 6]
// 1. map
fn square(x) { return x * x }
let squares = map(numbers, square) // [1, 4, 9, 16, 25, 36]
// 2. filter
fn isEven(x) { return x % 2 == 0 }
let evens = filter(numbers, isEven) // [2, 4, 6]
// 3. reduce
fn sum(acc, x) { return acc + x }
let total = reduce(numbers, sum) // 21
// 4. find
fn greaterThanFour(x) { return x > 4 }
let match = find(numbers, greaterThanFour) // 5
// 5. slice
let subArray = slice(numbers, 1, 4) // [2, 3, 4]
Objects are useful for configuration and structured records.
let app = {
"name": "Sesi",
"version": "1.9.0"
}
for key in keys(app) {
show key ":" app[key]
}
Prefer JSON helpers when storing structured data.
let settings = {theme: "dark", limit: 20}
let encoded = to_json(settings)
let decoded = from_json(encoded)
show decoded.theme
---
9. Working with Strings
Sesi provides native string utilities to format, transform, and slice text:
to_upper(string): Converts a string to uppercase.to_lower(string): Converts a string to lowercase.trim(string): Removes leading and trailing whitespace.swap(string, target, replacement): Replaces all occurrences of a target substring with a replacement substring.slice(string, start, end = null): Extracts a section of a string.contains(string, sub): Returnstrueif the string contains the given substring.locate(string, sub): Returns the zero-based index of the first occurrence of a substring, or-1if not found.
let original = " Hello, Sesi V1.9.0! "
let cleaned = trim(original) // "Hello, Sesi V1.9.0!"
let shouted = to_upper(cleaned) // "HELLO, SESI V1.9.0!"
let part = slice(cleaned, 7, 11) // "Sesi"
let replaced = swap(cleaned, " ", "_") // "Hello,_Sesi_V1.9.0!"
let found = contains(cleaned, "Sesi") // true
let idx = locate(cleaned, "Sesi") // 7
---
10. Reading, Writing, and Listing Files
Sesi file paths are resolved relative to the current working directory unless you provide another allowed path.
let text = read_file("notes.txt")
show text
Write files with write_file:
let body = "Generated by Sesi at " + str(time())
write_file("report.txt", body)
Generate a complete webpage with html() and write it as an .html file:
let page = html("
<main>
<h1>Daily Notes</h1>
<p>Generated by Sesi at " + str(time()) + "</p>
<button onclick='alert(\"Saved from Sesi\")'>Test JavaScript</button>
</main>
", {
"title": "Daily Notes",
"head": "
<style>
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 40px;
}
main {
max-width: 720px;
margin: 0 auto;
}
</style>
"
})
write_file("daily-notes.html", page)
Create folders and list directory entries:
make_dir("reports")
let files = list_dir(".")
for file in files {
show file
}
Wrap file operations in try/catch when missing files, invalid paths, or permission errors are possible.
try {
let content = read_file("input.txt")
write_file("output.txt", content)
} catch (err) {
show "File operation failed:" err
}
---
11. Converting Files and Content
Use convert for supported document, media, or audio conversions.
let html = convert(doc) {file_type: "md", output_type: "html"} {"# Hello"}
show html
When the input is a file path, Sesi writes the converted file next to the original and returns the new path.
let outPath = convert(doc) {output_type: "html"} {"README.md"}
show "Wrote:" outPath
---
12. Error Recovery and retrying
Sesi provides the retry(action, options) built-in function to handle temporary execution failures (e.g. flaky network requests, filesystem contention) automatically with configurable exponential backoff.
let attempts = 0
fn flaky_task() {
attempts = attempts + 1
if attempts < 3 {
raise_error("FlakyError", "Network timeout")
}
return "success"
}
// Retries the flaky task with options
let result = retry(flaky_task, {
"max_retries": 3,
"initial_delay": 500,
"backoff_factor": 2.0
})
show "Result:" result // Prints "success" after 2 retries
---
13. Calling APIs
Use web_get for HTTP GET requests.
let response = web_get("https://jsonplaceholder.typicode.com/posts/1")
let post = from_json(response)
show "Title:" post.title
Use web_send for HTTP POST requests.
let payload = to_json({
"title": "Sesi HTTP Client",
"body": "Posting JSON from a script",
"userId": 42
})
let response = web_send("https://jsonplaceholder.typicode.com/posts", payload)
show response
For scripts that rely on external services, always account for failures.
try {
let response = web_get("https://example.com/data.json")
write_file("data.json", response)
} catch (err) {
show "Request failed:" err
}
---
14. Running Shell Commands and Processes
Use exec when you need to call an existing command and capture its output.
let status = exec("git status --short")
if len(status) > 0 {
show "Uncommitted changes:"
show status
} else {
show "Working tree clean"
}
Use spawn when you want to start another Sesi script in the background.
let pid = spawn("worker.sesi")
show "Worker started with PID:" pid
Sesi runs in safe mode by default. Shell commands and broader filesystem access may be blocked unless you intentionally run locally.
sesi automation.sesi -l
You can also allow specific extra paths:
sesi automation.sesi --a "./data,./reports"
Treat -l as an explicit trust boundary. Use it for your own scripts, not for code you have not inspected.
---
15. Working with Environment Variables
Use env() to retrieve the value of system environment variables. This is particularly useful for loading dynamic settings, credentials, or custom runtime flags.
- Calling
env()with no arguments returns an object containing all process environment variables. - Calling
env(key)returns the value of a specific environment variable, ornullif it is not defined. - Calling
env(key, defaultValue)returns the value of the environment variable if it exists, ordefaultValueif it is not defined.
// Load a configuration or API key with a fallback
let apiKey = env("GEMINI_API_KEY")
let serverPort = env("PORT", "3000")
if apiKey == null {
show "Warning: GEMINI_API_KEY is not set"
}
show "Server starting on port:" serverPort
// Retrieve all environment variables
let allVars = env()
show "USER environment variable:" allVars.USER
---
16. Parallel Work
Use multi_req to run multiple Sesi functions at the same time and collect their results.
fn fetch_post() {
return web_get("https://jsonplaceholder.typicode.com/posts/1")
}
fn fetch_todo() {
return web_get("https://jsonplaceholder.typicode.com/todos/1")
}
let results = multi_req([fetch_post, fetch_todo])
show "Post response:" results[0]
show "Todo response:" results[1]
This pattern is useful for independent API calls, independent checks, or work that does not need shared mutable state.
---
17. Optional Reasoning in Scripts
Reasoning is optional. Use it when a task benefits from classification, summarization, extraction, or generation.
Use model for direct model calls:
let text = read_file("notes.txt")
let summary = model("gemini-3.5-flash-lite") {"Summarize this in 3 bullets: "text}
show summary
Streaming Responses
You can stream model responses chunk-by-chunk in real-time by adding a stream option to the model configuration block.
- Set
stream: trueto output directly to the terminal stdout in real-time. - Set
stream: callbackto pass each chunk as it arrives to a custom function.
// Option 1: Stream directly to stdout
let resp1 = model("gemini-3.5-flash-lite") {stream: true} {"Write a short poem about antigravity."}
// Option 2: Handle chunks using a callback function
fn onChunk(chunk) {
show "Chunk received:" chunk
}
let resp2 = model("gemini-3.5-flash-lite") {stream: onChunk} {"Explain quantum mechanics."}
Use prompt blocks to compose readable prompts from strings and variables.
let file_name = "notes.txt"
let content = read_file(file_name)
prompt request {"Classify this file as TECHNICAL, LEGAL, MARKETING, or OTHER.
Return only the category.
File: "file_name"
Content: "content}
let category = model("gemini-3.5-flash-lite") {request}
show file_name ":" category
Use structured_output when the rest of your script needs reliable fields.
let raw = "{\"title\": \"Buy milk\", \"completed\": false}"
let task = structured_output({title: string, completed: bool})(raw)
show task.title
show task.completed
You can combine model and structured_output:
let review = "The tool is fast, but the setup was confusing."
let result = structured_output({sentiment: string, summary: string})
(model("gemini-3.5-flash-lite") {"Analyze this review and return JSON with sentiment and summary: "review})
show result.sentiment
show result.summary
Use workflow for multi-step reasoning.
let steps = [
{"prompt": "Summarize the input."},
{"prompt": "List the risks in that summary."},
{"prompt": "Turn the risks into an action checklist."}
]
let result = workflow(steps, read_file("proposal.txt"))
show result.final
Reasoning calls require a configured provider key, such as GEMINI_API_KEY, when you use provider-backed models.
---
18. Modules and Reuse
Split larger scripts into modules with export and allow ... in with ....
logger.sesi:
export fn info(message: string) {
show "[INFO]" message
}
export fn warn(message: string) {
show "[WARN]" message
}
main.sesi:
allow "logger" in with {info, warn}
info("Script started")
warn("Using default configuration")
Sesi also includes standard library modules:
allow "std/math" in with {
PI, sqrt
}
allow "std/time" in with {
now, sleep
}
allow "std/base64" in with {
encode, decode
}
allow "std/db" in with {db_open}
show "sqrt(9):" sqrt(9)
show "Current time:" now()
show ({ready: true} | to_json | from_json).ready
show decode(encode("Sesi"))
Scoped Namespace Imports (allow)
You can import standard modules or custom library namespaces into scoped objects using the allow keyword:
// Import entire math module under a single Namespace
allow "std/math" in as Math
show "PI constant:" Math.PI
show "Angle:" Math.atan2(1, 0)
show "Minimum:" Math.min(8, 3, 5)
// floor() is also globally available without importing std/math
show floor(3.9)
// JSON conversion is built in; no module import is needed
let original = {
"project": "Sesi",
"version": "1.9.0"
}
show to_json(original)
Module resolution also supports configured library paths through SESI_PATH and the global ~/.sesi/lib library directory.
---
19. Debugging Scripts
Start with a dry run when you want to check syntax, compilation, undefined symbols,
and unused local symbols without executing the script.
sesi -c my_script.sesi
Inspect the parsed AST:
sesi --ast my_script.sesi
Inspect tokens when parsing fails in a confusing place:
sesi --tokens my_script.sesi
Use the REPL for quick experiments:
sesi
Ask the built-in co-pilot about a file:
sesi my_script.sesi -h "What is this script doing?"
Inside a script, show intermediate values and types.
let value = read_file("data.txt")
show "value type:" type(value)
show "value length:" len(value)
---
20. Encrypting Scripts
For encrypted private scripts, use the CLI encryption commands documented in the CLI reference.
sesi -enc private.sesi -p "my-password"
sesi -dec private.sesi -p "my-password"
You can set SESI_PASSWORD in your environment to avoid passing a password directly on the command line.
---
21. Interactive Input
Sesi provides the input(prompt) built-in function to query the user for data sequentially from standard input. This is useful for interactive command-line utilities.
let name = input("What is your name? ")
let age = input("What is your age? ")
show "Hello," name "! You are" age "years old."
If you are running the script in a non-interactive environment or need to fall back when arguments are not provided:
let queryText = ""
if len(args) > 0 {
queryText = join(args, " ")
} else {
queryText = input("Enter your question: ")
}
show "Processing query:" queryText
---
22. Complete Example: Daily Folder Report
This script accepts a folder path (falling back to interactive input if omitted), lists the files robustly using retries, writes a JSON report, and streams a summary using the AI model.
daily_report.sesi:
let folder = ""
if len(args) > 0 {
folder = args[0]
} else {
folder = input("Enter folder path to analyze: ")
}
let started = time()
// Define a robust folder analysis action
fn run_analysis() {
let files = list_dir(folder)
let report = {
"folder": folder,
"generated_at": started,
"file_count": len(files),
"files": files
}
make_dir("reports")
let output_path = "reports/daily_report.json"
write_file(output_path, to_json(report))
return output_path
}
// Use retry for robust filesystem operations
let report_path = retry(run_analysis, {max_retries: 3, initial_delay: 500})
show "Wrote report to:" report_path
Run it:
sesi daily_report.sesi ./docs
Add reasoning only when needed:
// Read report path and stream the summary directly to terminal stdout
let report_text = read_file(report_path)
let summary = model("gemini-3.5-flash-lite") {stream: true} {"Summarize this folder report:
"report_text}
try {
write_file("reports/daily_report_summary.txt", summary)
show "Summary saved to reports/daily_report_summary.txt"
} catch (err) {
show "Analysis failed:" err
}
---
23. 3D desktop game scripts
Use std/game for imperative Babylon-powered desktop games. Scenes are left-handed with Y up, +X right, and +Z forward. Relative asset paths use the current working directory, matching write_file, Draw.save_png, and Audio.save. Install @misterscan/sesi-game, run scripts with sesi -l, keep per-frame callbacks synchronous, put deterministic motion in on_fixed_update, register assets explicitly, and declare only the packaged capabilities the game needs.
allow "std/game" in as Game
let game = Game.create({title: "Writing Guide Game"})
let scene = game.create_scene("main")
let camera = scene.orbit_camera("camera", {radius: 12})
let light = scene.hemispheric_light("light")
let player = scene.box("player")
fn fixed(dt) { player.position.x = player.position.x + dt }
game.on_fixed_update(fixed)
game.run()
See GAME.md for the complete API and packaging/security model.
24. Practical Checklist
When writing a Sesi script, check these points before you call it done:
- Does it validate
argsor useinput()for interactive prompting? - Does it use
try/catcharound file, network, process, and model calls? - Does it write structured data with
to_jsoninstead of hand-built strings? - Does it use
-lonly when shell commands or broader paths are truly needed? - Does it keep repeated logic in functions or modules?
- Can
-c,--ast, or--tokenshelp diagnose it later? - Are optional model calls using an appropriate model for the job?
---
Next Steps
- Explore the Built-in Functions Reference for every runtime function.
- Read the CLI Reference for execution, sandbox, REPL, encryption, and debugging flags.
- Use the Language Specification when you need exact syntax rules.
- Study the scripts in the examples directory.