Skip to Content
We are live but in Staging 🎉
Scriptum LanguageStepsControl Flow & Concurrency

Control Flow & Concurrency

These steps shape the flow of a script: iterate over collections (each), branch on rules or LLM judgment (decide), gate the flow (check), run lanes in parallel (together), stream items through stages (pipe), loop until a condition (repeat), and pause (wait). Each gets a worked example below.

each — iterate over a collection

each runs its body once per item and collects the per-iteration outputs into a list. Bind the result with ->.

each result in raw_results.matches do "Classify relevance" with classify text = result.snippet category = topic -> classified
  • result is a loop-scoped binding — visible only inside the body.
  • classified is a list, one entry per iteration.

Add an index with a comma:

each item, index in chunks do "Process item {index}" with transform data = item -> processed

You can bound the loop’s concurrency with parallel <n> (run up to n items at once) and sleep <ms> (inter-batch delay):

each vec, i in doc_vectors parallel 4 let similarity = cosine_similarity(query_vector, vec) emit "Score" doc_index = i similarity = similarity -> raw_scores

decide — branch the flow

decide routes execution down exactly one branch. There are two strategies: rule-based (with rules) and LLM-based (with llm).

Rule-based: decide ... with rules

Each when tests a condition and names a branch; otherwise is the fallthrough. The named branches follow, each "Name": introducing its body. (Adapted from examples/test_encrypt_pattern.scriptum.)

decide "Encrypt output?" with rules when encrypt_output == true and encryption_key != "" -> "Encrypt" otherwise -> "No Encryption" "Encrypt": do "Encrypt data" with encrypt_data plaintext = clinical_data.text key = encryption_key -> encrypted "No Encryption": do "Passthrough" with echo message = "plaintext" -> encrypted

Rules shine for content routing — dispatch on a MIME type, a status, or a flag:

decide "Route by Type" with rules when contains(content_type, "pdf") -> "PDF" when contains(content_type, "html") -> "HTML" when contains(content_type, "audio") -> "Audio" otherwise -> "Text" "PDF": do "Extract PDF" with extract_pdf { url = resolved_url } -> extracted "HTML": do "Extract HTML" with extract_html { url = resolved_url } -> extracted "Audio": do "Transcribe" with transcribe { model = transcribe_model, audio = resolved_url } -> extracted "Text": do "Read text" with fetch_url { url = resolved_url } -> extracted

LLM-based: decide ... with llm

Here the branch is chosen by the managed LLM. Give it a prompt (a bare string), then list the candidate branches by name; the LLM picks one.

decide "Choose architecture approach" with llm "Given the synthesis:\n{synthesis.text}\n\nWhich architecture best fits this project?" "Microservices": do "Design microservices" with llm prompt = "Design a microservices architecture for '{project_name}'." -> arch_design "Monolith First": do "Design monolith" with llm prompt = "Design a modular monolith for '{project_name}'." -> arch_design "Serverless": do "Design serverless" with llm prompt = "Design a serverless architecture for '{project_name}'." -> arch_design

Branch bindings

Because exactly one branch runs, branches may bind the same name (-> arch_design above). If any step after the decide references that binding, the compiler requires every branch to produce it — otherwise it is a compile error.

check — gate the flow

check evaluates a condition and routes to on pass / on fail. The condition follows the label on the same line. Actions are continue, goto "Label", retry N, or fail "message".

check "Is reachable?" probe.status < 400 on pass: continue on fail: fail "URL returned error status"
check "Found relevant articles?" count(articles) > 0 and articles[0].relevance > 0.7 on pass: continue on fail: goto "Escalate to human"

A check can also defer to the LLM with ask llm "prompt" instead of a boolean expression:

check "Research quality" ask llm "Is this research comprehensive and well-sourced? Research: {research.text}" on pass: continue on fail: goto "Request revision guidance"

together — parallel lanes

together runs several lanes concurrently and waits for all of them. Each lane binds its own result; the outer binding collects them.

together "Research from multiple angles" do "Technical angle" with web_search query = "{topic} technical deep dive" -> technical do "Market angle" with web_search query = "{topic} market analysis 2026" -> market do "Academic angle" with web_search query = "{topic} research papers" -> academic -> all_research

Lanes can hold any steps, including agent calls — fan out independent agents and merge their findings afterward:

together "Diarize and classify" do "Speaker diarization" with infer model = diarize_model input = { "type": "audio_url", "audio_url": resolved_url } -> speakers do "Audio classification" with infer model = audio_class_model input = { "type": "audio_url", "audio_url": resolved_url } -> cls_raw

pipe — stream items through stages

pipe item from source consumes a source (an array or a stream) and runs its body per item. Bind with -> to collect all items, or ->> to forward the items as a stream into the next stage. (Adapted from examples/stream_pipeline.scriptum.)

do "Find Rust files" with file_glob pattern = "crates/scriptum-compiler/src/*.rs" -> glob_result -- array source → stream forward pipe file from glob_result.matches do "Tag file" with echo message = file ->> tagged -- stream source → collect pipe item from tagged do "Echo item" with echo message = item.message -> collected

pipe accepts batch <n> (group items into batches) and parallel <n> (run batches concurrently) — useful for high-throughput embedding:

pipe chunk from chunks.chunks batch 20 parallel 3 do "Contextualize Chunk" with llm prompt = "Document:\n{extracted_text}\n\nChunk:\n{chunk}\n\nEnriched chunk:" max_tokens = contextualize_max_tokens -> enriched_chunks

See Streaming for yield inside a pipe and the ->> operator in depth.

repeat — loop until a condition

repeat runs its body until an until condition becomes true. The until clause goes at the end of the body, and an optional max N caps the iteration count. Combine with let to maintain loop state. (Adapted from examples/repeat_test.scriptum.)

let counter = 0 repeat "Count to five" let counter = counter + 1 do "Log iteration" with echo message = "iteration {counter}" -> log until counter >= 5 -> repeat_result

With a safety cap:

let x = 0 repeat "Capped loop" let x = x + 1 do "Log x" with echo message = "x = {x}" -> x_log until x > 9999 max 3 -> capped_result

wait — pause execution

wait pauses the thread for a duration, or until a condition is met (with an optional timeout).

wait "Brief pause" duration = "100ms"
wait "Until ready" condition = ready == true timeout = "5s"

Durations are written as strings: "100ms", "2s", "24h". A paused thread is durable — it can resume hours later exactly where it stopped.

Next: the agent and human-input primitives in Agents & Human Input.