Everything you can write in a .stim file, with examples.
- Declarations
- Agent Metadata
- Prose
- Variables
- Data Types
- Operators
- Control Flow
- Tasks
- Parallel
- Functions
- Comments
- Naming Rules
- Keywords
- Targets
- Common Errors
Every .stim file contains exactly one declaration: a command, an agent, or a harness.
Interactive workflow invoked by the user (as /name in Claude Code).
command deploy {
ask("What should I deploy?")
wait_for_response()
}
Persona with metadata invoked by the AI tool (as @name in Claude Code).
agent reviewer {
description "Reviews pull requests"
tools [Read, Grep, Bash]
model "sonnet"
"You are a code reviewer."
"Cite file paths and line numbers."
}
Reusable orchestration template, parameterized with param and instantiated with run. Not invoked directly — see Orchestration.
harness review_panel {
param dimensions
fanout over dimensions {
task explore "review" {
ask("Review the diff for this dimension")
}
}
synthesize "merge the findings into one report"
}
Name rules are the same for all three: letters, numbers, underscores; no leading digit.
Only valid inside agent declarations. Must appear before any prose or other statements.
agent x {
description "..." // string, one line
tools [A, B, C] // array of identifiers
model "..." // string
// prose / control flow / etc. below
}
| Field | Type | Notes |
|---|---|---|
description |
string | Used by every target's frontmatter or heading. |
tools |
array | Claude-specific. Other targets warn-and-drop. |
model |
string | Claude-specific. Other targets warn-and-drop. |
Each field may appear at most once. Metadata in a command declaration is a parse error.
Bare string literals on a line become prose statements. Used mostly in agent bodies to build the system prompt.
"You are a technical writer."
"Lead with the shape before mechanics."
Each line compiles to one paragraph in the output. Prose can appear anywhere the parser expects a statement, including inside control flow blocks.
Assign with =. Use anywhere you need the value later.
name = "production"
port = "8080"
ready = true
services = ["api", "web", "worker"]
Reference by name:
ask(name)
ask("Deploying to " + name)
if (ready) { ... }
for service in services { ... }
Variables are scoped to the entire command. Assigning inside an if or for block is visible everywhere.
command example {
result = "pending"
if (confirm("Done?")) {
result = "complete"
}
ask(result) // "complete" if confirmed
}
Double or single quotes. Both work the same.
greeting = "Hello, world!"
greeting = 'Hello, world!'
empty = ""
Escape sequences: \", \', \\, \n, \t
enabled = true
disabled = false
Square brackets, comma-separated. All elements are strings.
items = ["one", "two", "three"]
empty = []
An array may also span multiple lines (a trailing comma is allowed):
questions = [
"What's your primary language?",
"How many years of experience?",
"Favorite tool?",
]
Joins strings together:
full = first_name + " " + last_name
ask("Hello, " + name + "!")
if (status == "done") {
ask("Finished!")
}
if (env != "production") {
ask("Safe to experiment")
}
if (!ready) {
ask("Not ready yet")
}
if (ready && approved) {
ask("Deploying")
}
if (is_admin || is_owner) {
ask("Access granted")
}
Highest to lowest: !, then ==/!=, then &&, then ||, then +.
Use parentheses when needed:
if (!(ready && approved)) {
ask("Blocked")
}
if (confirm("Use TypeScript?")) {
ask("Setting up TypeScript config")
}
if (env == "production") {
ask("Running production deploy")
} else {
ask("Running staging deploy")
}
Iterate over an array:
checks = ["tests", "lint", "types"]
for check in checks {
ask("Running " + check)
wait_for_response()
}
Repeat until a condition changes:
done = false
while (!done) {
ask("Describe the next requirement")
wait_for_response()
if (confirm("All requirements captured?")) {
done = true
}
}
Exit a loop early:
for item in items {
if (item == "stop") {
break
}
ask(item)
}
Tasks spawn Claude Code subagents. The body describes what the agent should do.
task "explore the auth module" {
ask("What authentication patterns exist?")
wait_for_response()
}
Pick the right agent for the job: explore, bash, plan, or general (default).
task explore "find all API endpoints" {
ask("Search the codebase and list every route handler")
}
task bash "run the test suite" {
ask("Execute all tests and report failures")
}
task plan "design the caching layer" {
ask("Propose a caching architecture for this app")
}
| Type | Best for |
|---|---|
general |
General-purpose work (default) |
explore |
Searching files, reading code, answering codebase questions |
bash |
Running commands, git, builds, installs |
plan |
Designing implementation plans, architecture decisions |
A task description is usually a quoted literal, but it can also be an expression — a concatenation or a variable reference — which is passed through to the output. This is what makes per-item task labels work inside for and fanout:
for svc in services {
task bash "deploy " + svc { // concatenation
ask("Deploy " + svc)
}
}
fanout over dimensions {
task explore item { // bare variable (the fanout loop variable)
ask("Review the diff for this dimension")
}
}
A literal is emitted quoted (task: "deploy"); an expression is emitted as written (task: "deploy " + svc). A bare variable that resolves from an import or a harness param is substituted with its value.
Point to another .stim file. It gets parsed and inlined at compile time.
task("helpers/security.stim")
task("helpers/security.stim", explore)
The path is relative to the current file. The output is fully self-contained -- no runtime dependency on the referenced file.
Circular references are detected at compile time and produce an error.
Run multiple tasks at the same time. Only task statements are allowed inside.
parallel {
task explore "check frontend" {
ask("What frontend patterns are used?")
}
task explore "check backend" {
ask("What backend patterns are used?")
}
task bash "run tests" {
ask("Execute the test suite")
}
}
This tells Claude Code to spawn all three subagents concurrently.
You can mix agent types and use file references inside parallel blocks:
parallel {
task("helpers/lint.stim", bash)
task("helpers/typecheck.stim", bash)
task explore "check for TODOs" {
ask("Find all TODO and FIXME comments")
}
}
Higher-level constructs for multi-agent workflows. Like everything in Stim, they compile to natural-language instructions — they describe orchestration to the target AI; they do not execute it.
A bounded, agentic loop. Unlike while, every loop carries a hard iteration cap so it cannot run forever. Three forms:
loop max 5 { // repeat up to 5 times
task explore "scan" {
ask("Find new issues")
}
}
loop until "no new issues surface" max 5 { // repeat until a predicate holds (capped)
task explore "scan" {
ask("Find new issues")
}
}
loop until_dry rounds 2 { // stop after 2 consecutive passes that add nothing new
parallel {
task explore "finder a" {
ask("Find issues from one angle")
}
task explore "finder b" {
ask("Find issues from another angle")
}
}
}
The cap in max <n> may be a number or a harness param name (resolved when the harness is instantiated).
fanout is dynamic fan-out: one concurrent subagent per item in a collection. parallel is the static form (you hand-write each task); fanout iterates a variable, and an optional gather reduces the results.
fanout over dimensions { // default loop variable is `item`
task explore "review" {
ask("Review the diff for this dimension")
}
}
gather "merge findings, dedupe by file and line"
Name the variable explicitly with <var> over:
fanout dim over dimensions {
task explore "review" {
ask("Review for the current dimension")
}
}
If the iterable is an array in scope, the compiled output lists its values.
Dispatch to a branch based on content. Contains case blocks and an optional default.
route by "the file's primary language" {
case "typescript" {
task general "ts review" {
ask("Review the TypeScript")
}
}
case "python" {
task general "py review" {
ask("Review the Python")
}
}
default {
task general "generic review" {
ask("Review the code")
}
}
}
Single-line reduce/filter steps, typically after a fanout/gather:
keep where "a majority of verifiers marked it real"
synthesize "write REVIEW.md from the confirmed findings"
A harness is a reusable, parameterized orchestration template — a third declarator alongside command and agent. It declares inputs with param (before any other statement; a param with no value is required), and its body uses the orchestration constructs above.
harness find_verify {
param finders
param votes 3
fanout over finders {
task explore "investigate" {
ask("Investigate this finding")
}
}
gather "collect candidate findings into state"
fanout over findings {
loop max votes {
task general "refute" {
"Try to refute this finding. Default to refuted unless proven real."
}
}
}
keep where "a majority voted real"
synthesize "write REVIEW.md from confirmed findings"
}
Instantiate a harness from a command (or another harness) with run. The harness body is inlined at compile time with its params bound; bindings override param defaults, and a missing required param is a compile error.
command deep_review {
@memory shared
run "harness/find_verify.stim" {
finders = ["sql-injection", "xss", "authz"]
votes = 3
}
}
run "file.stim" with no block uses every param's default. Circular run references are detected and error at compile time.
ask("What should I work on?") // Prompt the user
wait_for_response() // Pause for user input
confirm("Deploy to production?") // Yes/no confirmation
create_file("README.md", content) // Create a file
ask() accepts a string literal or a variable:
ask("What's your name?") // string literal
ask(my_question) // variable reference
ask("Hello, " + name) // concatenation
Any other function call is passed through as-is:
git_init()
git_commit("initial commit")
deploy(env_name)
run_tests("unit", "integration")
Single-line only. Start with //.
// Setup phase
name = "test"
ask("Hello") // inline comment
// Multiple comment lines
// work fine back to back
Names for commands, variables, and functions must:
- Start with a letter or underscore
- Contain only letters, numbers, and underscores
- Not be a reserved keyword
// Valid
project_name
myVar
_private
user123
// Invalid
123abc // starts with number
my-var // hyphen not allowed
my var // spaces not allowed
These are reserved and cannot be used as variable, command, or agent names:
command agent harness if else for in while
break true false task parallel
bash explore plan general
description tools model import param
loop until until_dry rounds max
fanout over gather route by case default
synthesize keep where run
Built-in function names (ask, confirm, create_file, wait_for_response) are also reserved. Metadata keywords (description, tools, model) are only reserved inside agent declarations; they can be used as variable names elsewhere, but doing so is strongly discouraged.
A declaration can be compiled for multiple AI tools. Pick one with --target <name> on compile or install. The default is claude.
stim compile reviewer.stim --target cursor
stim install reviewer.stim --target chatgpt --local| Target | Output | Install location (global) | Install location (--local) |
|---|---|---|---|
claude |
.md |
~/.claude/{commands,agents}/ |
./.claude/{commands,agents}/ |
cursor |
.mdc |
N/A (always project) | ./.cursor/rules/ |
chatgpt |
.md |
./dist/chatgpt/ |
./prompts/ |
When a target doesn't support a metadata field (Cursor with tools, for instance), it logs a warning and emits without the field. See Targets for details.
Missing declaration:
Error: Expected declaration: command <name> {, agent <name> {, or harness <name> {
Every file must start with command name {, agent name {, or harness name { (after any imports).
Metadata in a command:
Error: Metadata field "description" is only allowed in agent declarations
Move the field into an agent block, or delete it if the declarator should stay as command.
Metadata out of order:
Error: Metadata fields must appear before other statements
Move description/tools/model to the top of the agent body, above any prose or control flow.
Unclosed string:
Error: Invalid ask statement: ask(unclosed string"
Make sure quotes match: ask("text") not ask("text).
Missing value in assignment:
Error: Invalid assignment: name =
The right side of = can't be empty: name = "value".
Non-task inside parallel:
Error: parallel block may only contain task statements
Only task statements go inside parallel { }.
Circular file reference:
Error: Circular task file reference detected: b.stim
Two .stim files can't reference each other (directly or indirectly).
Missing task file:
Error: Task file not found: /path/to/missing.stim
Check that the path in task("path.stim") is correct relative to the current file.
See also:
- API Reference -- detailed documentation for every statement and function
- Tutorial -- learn by building commands step by step
- Examples -- real-world patterns to copy and adapt