A lightweight, text-first orchestrator for agentic loops: small, human-operated workflows that wire coding harnesses (Claude Code today; Codex, Gemini, OpenCode, Aider planned) to your work platforms (Jira, Linear, Slack, GitHub, mail, calendar) via MCP.
Not n8n. No canvas, no nodes. Loops are plain files you edit, run, grep, and commit — the feel of a human operating an orchestration from the terminal.
loop run feature-intake "users should be able to export their data as CSV"
…and the loop researches your repos and Jira backlog, reports what exists / what's missing / what's possible, asks you clarifying questions in your terminal, and files a fully-contexted ticket. Then next: implement-ticket chains straight into the execution loop: one harness plans, another writes the code, a third reviews it — with approval gates between phases.
claude-code is first-class (Agent SDK: streaming, per-run MCP servers, cost tracking). codex, gemini, opencode, and aider work out of the box as headless CLIs, and any other tool is one config entry away:
harnesses:
claude-code: { model: claude-opus-4-8 }
codex: {} # uses your codex login + config
mytool: { command: mytool, args: [run, "{prompt}"] } # generic adapterPick the harness per loop (harness: in frontmatter) or per step in YAML pipelines — so implement-ticket can plan with Claude, code with codex, and review with gemini.
Requires Bun >= 1.1.
bun install
bun run setup # links `loop` onto your PATH (or: loop setup)
loop --helpOther ways to get loop:
| Method | Command | Notes |
|---|---|---|
| Dev (linked) | loop run ... |
bun run setup once after clone |
| Compiled binary | bun run setup:binary |
Standalone dist/loop → ~/.local/bin/loop, no Bun at runtime |
| Global install | bun install -g loopctl |
When published to npm |
| Ephemeral | bunx loopctl run ... |
Bun's npx equivalent |
If loop isn't found after setup, add to ~/.zshrc:
export PATH="$HOME/.bun/bin:$HOME/.local/bin:$PATH"Run loop doctor anytime to verify Bun, PATH, config, and optional tools (gh, docker).
loop init # scaffold loopctl.config.yaml, loops/, skills/
loop new my-first-loop # scaffold a loop file
loop run my-first-loop "some input"
loop ls # list loops
loop ls --runs # list past runs
loop logs last # event log + artifacts of the latest run- Config —
loopctl.config.yamlis discovered by walking up from your cwd (or~/.loopctl/config.yaml). - Features —
loopctl.features.yamlgates platform capabilities and integrations per environment (loop features). - Runs — every execution is a directory under
~/.loopctl/runs/<id>/(logs, artifacts, status). - Shell loops —
.loop.shscripts get a temporaryloopshim on PATH forloop ask/loop agentprimitives.
Full guide: guides/creating-loops-and-integrations.md. Add any MCP integration: guides/adding-custom-integrations.md or loop run add-integration "describe your integration".
| Command | Purpose |
|---|---|
loop run <name> [input] |
Run a loop (and its next: chain) |
loop serve |
Webhook + schedule daemon |
loop ui |
Local ops dashboard |
loop trigger <source> |
Simulate a webhook locally |
loop logs [run] |
Event log + artifacts |
loop validate |
Static checks before spending tokens |
loop setup / loop doctor |
Install PATH link / verify toolchain |
loop features |
Platform feature flags + integration gates |
loops/ has 14 ready-to-edit examples — PR review on webhook, bug triage, standup digests, inbox triage with memory, dependency updates in worktrees, multi-model consensus, flaky-test hunting, Ralph-style backlog draining, and more.
A loop is a markdown file: frontmatter declares the wiring, the body is plain-language steps a driver agent executes.
---
name: feature-intake
integrations: [jira, github] # MCP servers from loopctl.config.yaml
skills: [jira-conventions] # markdown instruction packs from skills/
harness: claude-code
ask:
enabled: true # set false to run hands-off
channel: terminal # slack coming in M4
outputs:
ticket: The created issue key/URL.
next: implement-ticket
---
1. Research the repos and Jira backlog for the requested use case.
2. Report what exists, what's missing, what's possible.
3. Ask the user clarifying questions.
4. Create a Jira ticket with the full context and emit it as `ticket`.The driver agent gets two loop primitives as tools: ask_user (blocks on your answer — terminal prompt today; auto-proceeds with defaults when ask.enabled: false or --no-ask) and emit_artifact (saves named outputs for chaining). Everything else it does with its own tools plus the MCP integrations the loop declares.
When you want determinism instead of agent interpretation, write the steps explicitly. Outputs of earlier steps feed later ones via {{steps.<id>}}:
name: file-ticket
integrations: [jira]
steps:
- id: research
agent: Search the repos and backlog for {{input}}; summarize what exists.
- id: clarify
ask: "Priority?"
choices: [low, medium, high]
default: medium
# enabled: false # per-step toggle — skip the question, use the default
- gate: "File the ticket?"
- id: create
act: jira.create_issue # direct MCP tool call — no LLM in the path
args: { summary: "{{input}}", description: "{{steps.research}}" }
- emit: ticket
content: "{{steps.create}}"agent runs a harness, ask/gate go through the human channel (auto-proceed when disabled), act calls an MCP tool deterministically, emit saves an artifact.
Maximum unix: the loop is a bash script and the CLI is the API. loopctl injects LOOP_INPUT, LOOP_RUN_ID, and a loop shim on PATH; prompts render on stderr so command substitution stays clean:
#!/usr/bin/env bash
# ---
# name: file-ticket
# integrations: [jira]
# outputs: { ticket: the issue key }
# ---
set -euo pipefail
research=$(loop agent "Search the repos and backlog for $LOOP_INPUT" --integrations jira)
priority=$(loop ask "Priority?" --choices low,medium,high --default medium)
loop gate "File the ticket?"
ticket=$(loop act jira.create_issue summary="$LOOP_INPUT" description="$research")
loop emit ticket "$ticket"loop tools <integration> lists what an MCP server exposes so you know what to act on.
MCP-first: register any MCP server once in loopctl.config.yaml, and loops opt in by name. Secrets stay in env vars ($JIRA_TOKEN), so loops are safely shareable.
mcpServers:
jira:
type: http
url: https://mcp.atlassian.com/v1/mcp
headers: { Authorization: "Bearer $ATLASSIAN_MCP_TOKEN" }
github:
command: docker
args: [run, -i, --rm, -e, GITHUB_PERSONAL_ACCESS_TOKEN, ghcr.io/github/github-mcp-server]
env: { GITHUB_PERSONAL_ACCESS_TOKEN: $GITHUB_TOKEN }Loops don't have to be started by a human. Declare what fires them — a commit, a PR, a Jira/Linear state change, a Slack message, a schedule — and run the daemon:
trigger:
- webhook: github
match: { "workflow_run.conclusion": failure, "workflow_run.head_branch": main }
- webhook: jira
match: { "issue.fields.status.name": Ready }
- every: 30mloop serve --port 8484 --token $LOOPCTL_TOKEN
# point your GitHub/Jira/Linear/Slack webhooks at:
# POST http://your-host:8484/hooks/github (any source name works: /hooks/<source>)match: is dotted-path equality on the event payload — enough to express "this Jira ticket moved to Ready" or "this PR opened against main" without a rules engine. The matched event payload becomes the loop's input. Test trigger wiring locally without a server:
echo '{"issue": {"status": "Ready", "key": "ENG-9"}}' | loop trigger jiraDaemon-triggered runs are hands-off (asks auto-proceed with their defaults) until the Slack ask channel lands.
A loop with a goal: doesn't run once; it iterates. After each pass an evaluator agent judges the result against the goal, sends concrete feedback into the next iteration, and — because plans change once you see reality — can revise the goal itself (e.g. "fix the test" becomes "quarantine the flaky test and file a ticket"):
goal: The test suite on main passes locally and the fix is pushed.
maxIterations: 4
evaluator: gemini # optional: judge with a different model than the workerIteration context flows into every format: markdown drivers get the goal and feedback in their prompt, YAML steps get {{goal}} / {{iteration}} / {{feedback}}, shell loops get LOOP_GOAL / LOOP_ITERATION / LOOP_FEEDBACK. Events (iteration:start, iteration:verdict, goal:updated) land in the run log like everything else.
See loops/fix-failing-ci.loop.md for the two combined: CI fails on main → webhook fires → the loop iterates until the suite is green.
Set isolation: worktree and a run never touches your live working tree: it gets its own git worktree on branch loop/<run-id> (the consensus pattern across coding-agent orchestrators — and the thing that makes parallel and webhook-triggered runs safe). Then:
loop review last # full diff of what the run changed (--stat for the short version)
loop merge last # squash-merge into your current branch, clean up worktree + branch
loop discard last # throw it all awaysteps:
- id: research
agent: ...
retry: { max: 2, backoffSeconds: 10 } # per-step retries with backoff
- parallel: # branches run concurrently; asks stay serialized
- id: code
agent: ...
- id: docs
agent: ...
budget: 5.00 # USD cap — run fails with budget:exceeded when crossed
onFailure: notify # n8n-style error workflow: runs with the failed run's full contextFailed runs are checkpointed: loop resume last continues a YAML pipeline from the failed step (completed step outputs replay from disk), and re-prompts markdown/shell loops with the previous attempt's context. Budgets are enforced natively mid-run for claude-code (Agent SDK maxBudgetUsd) and between steps for everything else; give CLI harnesses an estimated costPerRun in config to include them.
The daemon queues triggered runs FIFO with daemon.maxConcurrent (default 2), dedupes repeat events for an already-queued loop, and daemon.debounceSeconds collapses webhook bursts. Schedules can be intervals (every: 30m) or real cron (cron: "0 9 * * 1-5").
Daemon-triggered runs don't skip their questions anymore — they suspend. A loop whose ask channel is the terminal parks as status: waiting with the question persisted to disk (it survives daemon restarts), until someone answers:
loop ls --runs # ...20260610-bug-triage-x1 waiting ? Severity for this one?
loop logs last --follow # live-tail any running or waiting run
loop answer last "high" # the run wakes up and continuesSlack-channel loops still get asked live in a thread. Crashed daemons recover on restart: the run queue is persisted, and stale running runs get marked interrupted with a loop resume hint.
steps:
- id: tickets
act: jira.search # get the list…
args: { jql: "sprint = active AND status = 'To Do'" }
- id: triage
foreach: "{{steps.tickets}}" # …fan out over it (JSON array or lines)
as: ticket
concurrency: 3
do:
loop: implement-ticket # each item runs a whole sub-loop
input: "{{ticket}}"
- emit: report
content: "{{steps.triage}}" # joined; {{steps.triage.0}} addresses one itemloop: runs another loop inline as a step (own run dir, cost charged to the parent's budget, depth-guarded); foreach: fans any step out over a list with bounded concurrency.
- Webhook signatures — set
webhookSecrets: { github: $GH_WEBHOOK_SECRET }and/hooks/githubverifies GitHub's HMAC (Slack signing and Linear signatures too;X-Loopctl-Signaturefor everything else). Tampered or unsigned payloads are rejected. sandbox: strict | workspace | off— mapped to each harness's native mechanism (claude-code permission modes,codex -s,gemini --sandbox).repos:in config — a registry of repositories agents may research, injected into agent context (andadditionalDirectoriesfor claude-code,LOOP_REPOSfor shell loops). Multi-repo research loops finally know where the code lives.loop validate/loop run <name> --dry-run— catch a typo'd integration, broken{{steps.x}}reference, or missing skill before any agent spends a cent.
Point ask.channel: slack at a Slack app (Socket Mode — no public URL needed) and human-in-the-loop steps post to a channel and block on a threaded reply, with timeouts falling back to defaults. Daemon-triggered loops get real approvals instead of auto-proceeding:
# loopctl.config.yaml
slack:
botToken: $SLACK_BOT_TOKEN # xoxb-, chat:write
appToken: $SLACK_APP_TOKEN # xapp-, connections:write
channel: C0123456789memory: true— the loop keeps notes across runs (~/.loopctl/memory/<loop>.md). Markdown drivers get aremembertool plus past notes in context; YAML steps see{{memory}}; shell loops getLOOP_MEMORY_FILE.loop stats— cost/success rollups per loop and per day;--jsonfor scripts.loop ui— a tiny read-only dashboard (run list, live events, artifacts) over the runs dir. No build step, CLI stays primary.loop mcp— loopctl as an MCP server: any agent (Claude Code, Claude Desktop…) canlist_loops,run_loop,trigger_event,get_run,get_stats. Loops driving loops.loop install <git-url>— loops and skills are files; share them as repos. Copiesloops/*.loop.*andskills/*.mdin, skipping existing files unless--force.
loops/ralph-backlog.loop.sh shows the Ralph-loop pattern done properly: goal iteration with an evaluator instead of while true, in an isolated worktree, with memory across sessions.
Every run is a directory under ~/.loopctl/runs/<id>/: meta.json, an append-only events.jsonl, a plain-text status, and artifacts/ with one file per emitted output.
loop logs last --json | jq 'select(.type == "ask")'
cat ~/.loopctl/runs/$(cat ~/.loopctl/runs/last)/artifacts/ticket- M1 ✅ — markdown loops, Claude Code harness, terminal ask channel, MCP registry, file-based runs.
- M2 ✅ — YAML + shell loop formats, deterministic
actsteps (direct MCP tool calls, no LLM in the path),gate,loop ask/gate/emit/agent/act/toolsCLI primitives. - M3 ✅ — codex/gemini/opencode/aider + generic harness adapters; loop chaining (
next:); the plan → code → review execution loop (loops/implement-ticket.loop.yaml). - M4 ✅ —
loop servetrigger daemon (webhooks + intervals + cron +loop triggersimulation), goal-driven iterative loops with evaluator feedback and revisable goals, Slack ask channel,loop mcpserver mode. - M4.5 ✅ — worktree isolation +
loop review/merge/discard, parallel steps, per-step retries,loop resume, daemon run queue with concurrency control and debounce, cost budgets,onFailureerror loops. - M5 ✅ — per-loop memory,
loop stats,loop ui,loop install <git-url>, mail/calendar/transcript example loops. - M6 ✅ — durable waits (
waitingruns +loop answer), daemon crash recovery + persistent queue, webhook signature verification,loop:/foreach:composition, repos registry,loop validate+--dry-run,loop logs --follow, terminal ask timeouts,sandbox:passthrough. - Open — docs site, community templates, OTel/Langfuse export, container-level isolation.
See guides/dev-quickstart.md for a 2-minute smoke-test workflow and guides/creating-loops-and-integrations.md for loops, MCP integrations, and feature flags.
bun install && bun run setup
loop validate
loop run smoke "hello"
bun test
bun run typecheckMIT.