These recipes favor typed values and explicit failure handling. Adapt paths/URLs before running, and plan or inspect any mutation in a real environment.
Find the largest files in a directory🔗
(ls ./target)
.where(.type == "file")
.sort_by(.size)
.reverse()
.take(20)
.map({path: .path, size: .size, modified: .modified})size stays a typed byte quantity, so comparisons/rendering are not lexicographic text tricks:
(ls ./downloads)
.where(.type == "file" and .size >= 100mb)
.map(.path)Summarize file types🔗
let entries = (ls .)
{
total: entries.len(),
files: entries.count(.type == "file"),
dirs: entries.count(.type == "dir"),
symlinks: entries.count(.type == "symlink"),
}For grouping:
(ls .).group(.type)Read a file as text, lines, or bytes🔗
let p = path("./notes.txt")
p.read
p.lines
p.read_bytesUse bytes for exact content, text for Unicode/string methods, and lines for line-oriented transforms.
Safely replace a file and retain undo metadata🔗
let target = path("./generated/config.json")
json.stringify({enabled: true, workers: 4}, pretty: true).save(target)In a journal-enabled local session, overwriting an existing file can record its prior bytes and post-fingerprint. Inspect immediately:
journal --head=save --limit=5Then, if no later change made the target stale:
undoThis is not a transaction: creating a previously absent file does not currently record a delete inverse, and large prior content may exceed the journal cap.
Use recoverable removal instead of permanent deletion🔗
let removed = (rm ./scratch.txt)
removedDefault rm moves the target to Shoal’s temporary trash and returns {path, trash} records. Verify the journal, then:
undoAvoid rm --permanent when recovery matters.
Parse and reshape JSON🔗
let doc = json.parse('{"users":[{"name":"Ada","active":true},{"name":"Lin","active":false}]}')
doc.users
.where(.active)
.map(.name)Serialize a compact or pretty result:
json.stringify({names: doc.users.map(.name)})
json.stringify({names: doc.users.map(.name)}, pretty: true)Convert CSV to JSON🔗
let rows = csv.parse("name,count\napi,3\nweb,2\n")
json.stringify(rows, pretty: true)CSV fields are strings. Convert explicitly when numeric behavior matters:
rows.map({name: .name, count: .count.int()})Convert records to CSV🔗
csv.stringify([
{name: "api", count: 3},
{name: "web", count: 2},
])Keep record keys consistent so the table schema is predictable.
Handle an expected external-command failure🔗
let result = (^git rev-parse --verify refs/heads/release)
if result.ok {
{exists: true, commit: result.out}
} else {
{exists: false, status: result.status, stderr: result.stderr}
}Parentheses place the command in value position. Without capture at statement position, a nonzero outcome raises cmd_failed.
Preserve exact argv boundaries🔗
let filename = "report with spaces.csv"
run("printf", "%s\n", filename)Shoal values become arguments; there is no implicit whitespace splitting of filename. Use run when the command name is dynamic:
let tool = "git"
run(tool, "status", "--short")Diagnose an adapter🔗
Compare adapted and forced-native paths:
git status --short
^git status --shortIf the first produces a structured table and the second text/bytes, the adapter is active. If adapted parsing fails after a tool upgrade, capture the native output/version and update/pin the adapter contract.
Make an HTTP JSON request🔗
let response = http.get(
"https://api.example.test/v1/items",
headers: {Accept: "application/json"},
)
if response.ok {
response.json
} else {
{status: response.status, body: response.body}
}HTTP 4xx/5xx returns a response record; it does not raise. Transport/timeout/body-read errors raise net_error.
POST JSON:
let response = http.post(
"https://api.example.test/v1/items",
json.stringify({name: "demo"}),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
)Inject a secret into an HTTP header🔗
First set it outside the language without printing it:
printf %s "$API_TOKEN" | shoal-secret set api-tokenThen:
let token = secret.get("api-token")
http.get(
"https://api.example.test/v1/me",
headers: {Authorization: token},
)The typed secret is redacted in ordinary rendering/wire encoding. The remote server/HTTP stack receives it; downstream output can still leak it. See Security.
Run work in a temporary directory/environment scope🔗
with cwd: path("./crates/shoal"), env: {RUST_LOG: "debug"} {
cargo test
}Prefer scoped state over a global cd/environment mutation inside reusable code.
Build a reusable module🔗
lib/report.shl:
export fn summarize(rows) {
{
total: rows.len(),
failed: rows.where(not .ok).len(),
}
}Caller:
use ./lib/report
report.summarize(results)Use as when the file-stem namespace would collide:
use ./lib/report as reportsBound a timer stream🔗
every(1s).take(5).collect()Live/infinite sources need a bound or a long-running sink. Never call .collect() on an unbounded stream without take/termination.
Watch a directory and rescan after coalescing🔗
watch(path("./src"), recursive: true)
.where(.kind == "modified")
.take(20)
.collect()The watch queue is bounded. If an event reports coalesced: true, treat it as “changes occurred” and rescan authoritative state rather than assuming every filesystem event was preserved.
Tail and filter a log🔗
tail(path("./service.log"))
.where(line => line.contains("ERROR"))
.take(20)
.collect()Use a bound in short scripts. A persistent monitoring task can use .each and cancellation instead.
Connect a stream to a user channel🔗
watch(path("./src"))
.into(channel("user.files"))Consume:
channel("user.files").events().take(10).collect()Only user.* channels bridge to kernel clients; language code cannot spoof kernel-owned journal/approval channels.
Register a channel handler🔗
let handler = on(channel("user.builds"), event => {
echo (event)
})on(...) is a function call in the current grammar; there is no on channel {} keyword form. Remember the current nested-evaluator policy propagation limitation before using handlers for scoped agent work.
Start and observe a language task🔗
let tests = spawn { cargo test }
tests.is_done()
tests.await()Or trailing ampersand:
sleep 30s &
jobsCancellation:
tests.cancel()Cancellation is not rollback; inspect partial effects.
Plan a mutation before running it🔗
plan cp ./report.csv ./archive/report.csvor a block:
plan {
mkdir --parents ./archive
cp ./report.csv ./archive/report.csv
}Review concrete effects and reversibility. In the agent/kernel surface, use shoal_plan and shoal_apply; read the current security limitations around approval and plan refs first.
Create a reproducible Reef scope🔗
Project .reef.toml:
[tools]
rg = "*"
jq = "1"Then interactively:
reef status
reef lock
which rg
which jqCommit the lockfile before expecting script policy to accept constrained tools. Set hermetic scope only when all child tools/interpreters are declared.
Page a large value through MCP🔗
Execute once:
{
"src": "(ls ./large-directory)",
"position": "value",
"elide": {"max_rows":20,"max_bytes":4096}
}If the result is out:17, page without rerunning:
{"ref":"out:17","slice":[0,100]}{"ref":"out:17","slice":[100,200]}or:
shoal://out/17?slice=100..200Run a long MCP action without blocking context🔗
{
"src":"cargo test --workspace",
"position":"value",
"background":true
}Subscribe/read shoal://task/N, then fetch shoal://task/N/out after completion. A timeout similarly returns a task; it does not kill execution.
Drive a small REPL through PTY🔗
Open:
{"cmd":"python3","args":["-q"],"cols":80,"rows":24}Read the screen, then send:
{
"pty_id":"pty:1",
"input":["print(6 * 7)",{"key":"Enter"}]
}Poll with delay until the expected prompt/42 appears, then close. Prefer ordinary shoal_exec when terminal behavior is unnecessary.
Retry a read, not an ambiguous mutation🔗
Safe pattern after a lost response:
1. query journal/transcript event cursor
2. inspect intended artifact/remote idempotency key
3. decide whether operation completed
4. only then retry or compensateBlindly retrying cp, deployment, payment, or API mutation can duplicate effects. Addressable refs and the journal exist to support reconciliation.
For more translation patterns, see Migrating from traditional shells.