Skip to content

Latest commit

 

History

History
685 lines (512 loc) · 15.4 KB

File metadata and controls

685 lines (512 loc) · 15.4 KB

Stim Syntax Reference

Everything you can write in a .stim file, with examples.

Table of Contents

  1. Declarations
  2. Agent Metadata
  3. Prose
  4. Variables
  5. Data Types
  6. Operators
  7. Control Flow
  8. Tasks
  9. Parallel
  10. Functions
  11. Comments
  12. Naming Rules
  13. Keywords
  14. Targets
  15. Common Errors

Declarations

Every .stim file contains exactly one declaration: a command, an agent, or a harness.

Command

Interactive workflow invoked by the user (as /name in Claude Code).

command deploy {
  ask("What should I deploy?")
  wait_for_response()
}

Agent

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."
}

Harness

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.

Agent Metadata

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.

Prose

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.

Variables

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
}

Data Types

Strings

Double or single quotes. Both work the same.

greeting = "Hello, world!"
greeting = 'Hello, world!'
empty = ""

Escape sequences: \", \', \\, \n, \t

Booleans

enabled = true
disabled = false

Arrays

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?",
]

Operators

Concatenation (+)

Joins strings together:

full = first_name + " " + last_name
ask("Hello, " + name + "!")

Comparison (==, !=)

if (status == "done") {
  ask("Finished!")
}

if (env != "production") {
  ask("Safe to experiment")
}

Logical (!, &&, ||)

if (!ready) {
  ask("Not ready yet")
}

if (ready && approved) {
  ask("Deploying")
}

if (is_admin || is_owner) {
  ask("Access granted")
}

Precedence

Highest to lowest: !, then ==/!=, then &&, then ||, then +.

Use parentheses when needed:

if (!(ready && approved)) {
  ask("Blocked")
}

Control Flow

if / else

if (confirm("Use TypeScript?")) {
  ask("Setting up TypeScript config")
}
if (env == "production") {
  ask("Running production deploy")
} else {
  ask("Running staging deploy")
}

for

Iterate over an array:

checks = ["tests", "lint", "types"]

for check in checks {
  ask("Running " + check)
  wait_for_response()
}

while

Repeat until a condition changes:

done = false

while (!done) {
  ask("Describe the next requirement")
  wait_for_response()

  if (confirm("All requirements captured?")) {
    done = true
  }
}

break

Exit a loop early:

for item in items {
  if (item == "stop") {
    break
  }
  ask(item)
}

Tasks

Tasks spawn Claude Code subagents. The body describes what the agent should do.

Inline task

task "explore the auth module" {
  ask("What authentication patterns exist?")
  wait_for_response()
}

With an agent type

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

Dynamic descriptions

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.

File reference

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.

Parallel

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")
  }
}

Orchestration

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.

Loop

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 and gather

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.

Route

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")
    }
  }
}

Synthesize and keep

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"

Harness

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"
}

Run

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.

Functions

Built-in

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

Custom functions

Any other function call is passed through as-is:

git_init()
git_commit("initial commit")
deploy(env_name)
run_tests("unit", "integration")

Comments

Single-line only. Start with //.

// Setup phase
name = "test"

ask("Hello")  // inline comment

// Multiple comment lines
// work fine back to back

Naming Rules

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

Keywords

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.

Targets

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.

Common Errors

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