| name | loop-sdk |
| description | Author, validate, and run .loop automation files (loop-sdk). Use when writing or editing a .loop file, composing automation loops with claudeCli/verify/browser steps, or using the loop-sdk JavaScript API. |
Authoring .loop files
A .loop file is YAML front-matter followed by ## step sections. Steps run
in order; each step's output is referenceable downstream as {{step-name}}.
---
name: research-topic
model: claude-sonnet-5
vars:
topic: browser automation
---
## gather
action: claudeCli
prompt: List 5 recent developments in {{topic}}. One line each.
## check
action: verify
assert: "{{gather}} contains exactly 5 items, each about {{topic}}"
## report
action: log
message: "{{gather}}"
Front-matter reference
| Field | Meaning |
|---|
name | Required. |
vars: | Declared inputs with defaults — applied by the engine; run-time vars override. |
model | Default model for claudeCli/verify steps. |
mode | explore (default) | strict. Explore runs workers frictionless (permissions skipped) unless a step declares tools:. Strict scopes every worker step to an allowlist and denies unlisted tools. Auto-escalates to strict when unset and the loop ships hard-to-reverse changes (worktree, onSuccess: merge|pr); set explicitly to override. |
tools | Default tool allowlist for claudeCli/verify steps (Claude Code names, e.g. [Read, Grep, Bash]). A step's own tools: overrides. Declaring any allowlist enforces it regardless of mode. |
session: browser | This loop drives a browser. Omit for pure AI/data loops. |
browserMode | isolated (fresh Chrome, default) | chrome (CDP attach) | extension (the user's running Chrome via the LoopDeLoop extension). |
browserProfile | Which Chrome profile/identity acts. May be a {{var}} so one loop serves several accounts. |
workdir | cwd for claudeCli/verify — set to a repo path to make them build workers. ~ and {{refs}} ok. |
worktree: true | Each run's file changes go to an isolated git worktree + branch loop/<name>-<id>. |
onSuccess | Runner action after a successful worktree run: keep | merge | pr. |
mcp: | List of MCP server names (from ~/.loopdeloop/mcp.json) or inline defs — extra tools for Claude workers. Step-level mcp: overrides. |
reflexion: false | Disable the failed-verify → retry-prior-prompt-step-with-critique behavior (on by default). |
notify: | {onStart, onComplete, onError, onStepError, sound} — macOS notifications. |
Actions
| Action | Key fields | Notes |
|---|
claudeCli | prompt, model, maxSteps, screenshot, workdir, mcp, tools, expect | Spawns claude -p. Output = the step's {{ref}}. tools: scopes it to an allowlist; expect: gates its output. |
codexCli | prompt, model, expect, mcp | OpenAI Codex CLI. mcp: servers are wired via Codex -c mcp_servers.* overrides (stdio + HTTP). |
agent | prompt, model, system, maxSteps, screenshot, expect, mcp | Any AI SDK model via agent() (AI SDK 6). model is a registry string — claude-code:sonnet (alias claude:), codex:gpt-5.2-codex, anthropic:claude-opus-4-8, openai:gpt-5; a bare id → claude-code; falls back to meta.model. The backing provider is an optional peer dep (ai-sdk-provider-claude-code, etc.) — installed on demand. mcp: servers (registry names or inline, incl. remote HTTP) are handed to CLI-backed providers as mcpServers; permissions follow the loop mode (explore = frictionless, strict = allowlist). CLI providers ignore browser MCP tools — use claudeCli for browser work. |
verify | assert | AI judge; failure fails the step. Put one after any step whose output matters. |
expect: — a deterministic output contract enforced in code after ANY step (not just claudeCli) runs. json (parseable) | non-empty, or an object { json, nonEmpty, contains, matches } (all declared checks must hold). Fails the step if it doesn't. Prefer this over a verify AI-judge whenever the check is mechanical — it's a true guarantee, not a probabilistic one.
| send | message, channel: imessage\|ntfy, to/topic | Push to the user's phone. |
| navigate | url | Browser. Also click (selector/text/x+y), type (text), key, scroll (deltaY), screenshot, wait (ms). |
| log | message | Emit to the run log. |
| set-variable | key, value | Referenceable as {{key}} and {{step-name}}. |
| subloop | loop: ./other.loop or inline steps:, vars, output | Nested loop sharing context. Use loop: for a separate file or steps: to inline the body (self-contained, no dependent file) — same either/or as each. Circular loop: refs and runaway depth (> 50) fail fast, not hang. (Alias: sub, deprecated.) |
| each | items, as, steps or loop, concurrency: 1-8, continueOnError, output | Iterate. items may be a YAML array, a {{ref}} (array/JSON/lines), or literal lines. Inside: {{item}} (or as name), {{_index}}, {{_total}}. Concurrency > 1 gives each item isolated state. |
| parallel | steps | Run inline steps concurrently. |
| per-step error handling | retries, retryDelay, retryBackoff: flat\|linear\|exponential, onError: skip | |
Runners may register custom actions (e.g. LoopDeLoop adds approve — human
gate with a message, and card — move: a kanban card). Only use those when
the target runner is known to support them.
Validation (fail fast). loadLoop() checks the whole file before running any
step and throws listing every problem: unknown actions (with a "did you mean…?"),
duplicate or non-referenceable step names, and missing required fields. Keep step
names kebab-case (## fetch-names) — a name with spaces parses but can never
be reached via {{…}}, and is now a validation error. Register custom actions via
the actions map so they aren't flagged as unknown.
Interpolation rules (the part people get wrong)
{{name}} resolves prior step outputs first, then vars. Step names must be
kebab-case to be referenceable — ## fetch-names → {{fetch-names}}. A name
with spaces is a validation error (it would be unreachable).
- Every
{{ref}} needs a source: a prior step's name, a set-variable key, a
declared input under vars:, or a var the runner supplies at run time
(webhook payload fields, kanban card vars like {{card-title}},
{{card-notes}}, {{previous-output}}, trigger vars).
- A ref with no source is left as literal braces at run time (with a warning) —
runners using pre-flight validation will refuse the run instead. When writing
a loop that DEPENDS on runner-supplied context (e.g. a kanban stage loop),
that's correct — just don't expect it to run standalone.
- Multiline YAML values: use
| block scalars for prompts. Never put a block
scalar inside vars: — quote with escaped \n instead.
Validating and running
npx loop-sdk run path/to/file.loop
npx loop-sdk run path/to/file.loop --var topic=AI --json
node examples/run-loopfile.js path/to/file.loop
import { describeLoop, loadLoop, runFile } from 'loop-sdk'
const d = describeLoop(content, depsMap)
d.needsBrowser
d.mode
d.referencedVars
d.reachableDeps
await runFile('./file.loop', session, { vars: { topic: 'X' } })
const loop = loadLoop('./file.loop', { approve: async (ctx, step) => {} },
{ maxTurnsCap: 10, skipNotify: true })
Authoring guidance
- Prefer one claudeCli step per intent with a clear output contract
("Output ONLY the reply text — no preamble") over mega-prompts; downstream
steps consume
{{refs}}.
- Add a
verify step after any step whose failure should stop the pipeline —
reflexion then gives the prior step one self-correction attempt for free.
- Gate mechanical checks with
expect: (deterministic, a true guarantee) and
reserve verify for judgment calls that genuinely need an AI judge.
- Leave
mode unset for exploration; the engine auto-escalates to strict for
loops that merge/PR or use a worktree. For an unattended factory loop, set
mode: strict and give each worker step a tight tools: allowlist.
- Put human gates (runner
approve action) before irreversible side effects
(posting, sending, merging).
- Capabilities belong in tools, not shell commands: if a prompt needs
curl,
the runner is missing an MCP tool — prefer mcp: servers.
- Loops that act as a specific identity should take
browserProfile: "{{profile}}"
and receive profile from the runner (board/trigger vars) rather than
hardcoding it.