# Loops

> Loops mean two things in OpenEng. Iteration strategies decide how the engine runs a single turn (observe, act, review, plan, retry), and the engine picks one for you. Global loops repeat a request until a stopping rule is met, such as working every open ticket until none is left; you adopt them on the Console's Loops page and start them from the Web Console. Only two of the six published loops can run as published, and OpenEng AI isn't live yet (as of September 25, 2026).

**Loops** mean two different things in OpenEng, and it helps to keep them apart:

| Aspect | Iteration strategy | Global loop |
| --- | --- | --- |
| What it is | How the engine runs **one** turn: how it observes, acts, reviews, plans and retries | A request repeated **many** times until a stopping rule is met: "work every open ticket until none is left" |
| Who picks it | The engine, for every message; a config can limit and tune the choices | You: adopt one, select it into a config, then start it |
| Where you set it | The config's **Settings** step → **Loop posture** | **Building blocks → Loops** in the Console, then the config's **Loops** step |
| Catalog | Five built-in strategies | [loops.openeng.app](https://loops.openeng.app) — six published loops |

Each iteration of a global loop is an ordinary turn, so it runs on an iteration strategy like any
message you type. The first half of this page covers strategies; [Global loops](#global-loops)
covers the second kind.

> [!WARNING] Status on September 25, 2026: OpenEng AI isn't live yet
> The Console and Web Console interfaces load, but the OpenEng AI API routes and the Broker aren't
> deployed. Nothing can be saved or published, and no AI engine can register or connect. Pro,
> Enterprise and pay-as-you-go launch in Q4 2026 and can't be purchased yet. This page describes
> OpenEng AI as it works once those services are live ([status](ai.md)).

*Figure: Building blocks → Loops — the Mine and Catalog tabs for global loops. Strategies are set per config, in the config's Settings step.*

## Iteration strategies

A **loop strategy** is the iteration strategy the engine runs a turn with: how it observes, acts,
reviews, plans, and retries. You almost never pick one by hand: the engine scores every request and
chooses the right loop for you. Strategies are configured per config (the config builder's
**Settings** step, under **Loop posture**), so you can constrain or tune which strategies a config
may use.

## How the engine decides (no modes)

There is **no user-facing "chat / coding / goal" mode** anymore. For every
message you send, the engine does two things, internally:

1. **Score the complexity.** The engine scores the request from 0 to 1 (with an
   embedded model or a heuristic router) from your message and the previous turn
   for context. At or above the threshold (**0.65** by default) the turn is
   **planned**; below it runs **direct**.
   In the web app you'll see this under the chat box as *planned mode* or
   *non-planned mode* with the score.
2. **Pick the loop + the models.** A direct turn runs the single agent loop
   (`react`); a planned turn runs the planner pipeline (`plan-execute-replan`),
   and each step is classified again to pick its role model from the config's
   [role models](models.md#role-models)
   (the agent, a sub-agent, a researcher, and so on).

So "goal mode" is just *what a complex request looks like* — you never select
it. The threshold is operator-tunable (see **Configuring loops** below).

## The strategy catalog

| Strategy | What it does |
| --- | --- |
| **react** | The baseline chat/coding loop — observe, act, repeat. What a low-complexity turn runs. |
| **reflection** | Actor–critic: a reviewer drives a bounded revise-loop toward a better answer before finishing. |
| **plan-execute-replan** | Plan a DAG of steps, execute it, and replan on failure — the high-complexity (agentic) path. |
| **orchestrator-worker** | Delegators fan a task out to sub-agents and workers, each running its own loop. |
| **durable** | Wrap any base strategy with checkpoint-every-step: crash recovery, time-travel, and human-in-the-loop resume. |

Loops apply at **every altitude** — the top-level turn, a delegator, and a leaf
worker each run a loop. A planned turn's per-node executors run their own
`react` loop on their role's model; a delegator runs `orchestrator-worker`.

These five are the complete set a config can use. loops.openeng.app also publishes a longer
reference of the internal iteration techniques behind them, at
[loops.openeng.app/reference](https://loops.openeng.app/reference); those are documentation, not
strategies you can select or loops you can run.

## Configuring loops

In the Console, open your config and go to its **Settings** step; the **Loop posture** section holds
the config's strategy settings. A config carries three:

- **Loop strategies** (the allowed set) — the strategies this config may use. All five are allowed
  by default; at least one must stay allowed. Restrict it (for example, allow only `react` +
  `reflection`) to keep a config simple and predictable. Allowing **orchestrator-worker** asks you to
  acknowledge it first: it fans out to sub-agents, which costs more and adds failure surface.
- **Per-category defaults** — the strategy each category starts on: Chat, Coding, Goal runs and
  Research. The shipped defaults are `chat → react`, `coding → react`, and goal runs (the
  high-complexity, agentic lane) → `plan-execute-replan`; a category left on *(built-in baseline)*
  uses the engine's own default. Only an allowed strategy can be a default, and removing a
  strategy from the allowed set clears any default that used it. Device Use isn't listed: it runs
  its own dedicated screenshot-and-act loop.
- **Policy tuning** — bounds on a run: max iterations (28 by default), max sub-agent iterations
  (12), concurrency cap (10), the reviewer's revise cap (2), a token ceiling (0 means none), the
  compaction trigger (the share of the context window at which the conversation is compacted,
  0.82 by default), and switches for stuck detection (on by default), speculative workers (on) and
  durable checkpoints (off). **Per-iteration validation** is shown locked on: the reviewer checks
  every iteration, and no config can turn that off.

These ride the published config artifact, so the engine resolves them the same
way it resolves your models and agents — publish the config and they're live.

> [!NOTE] Complexity threshold
> The plan-vs-direct gate is **0.65** by default. It's
> tunable per config through the complexity stage of the config's
> [execution flow](execution-flow.md#the-stages) (a 0–1 threshold), and per engine via the local
> `ai-config.toml` `[complexity] threshold_pct` knob, which writes the same threshold as a
> percentage (65).

## The `durable` wrapper

`durable` is special: it doesn't replace a base strategy, it **wraps** one.
Every step is checkpointed, so a crashed or stopped run resumes from the last
good step instead of restarting, and a human-in-the-loop approval can suspend
and later resume the run. Use it for long, expensive, or approval-gated work.

## Worked example

You ask: *"Build a small FastAPI app with a SQLite store and a health route,
then write tests."*

1. The scorer rates it high (multi-step build) → **planned**.
2. The engine runs `plan-execute-replan`: it plans a DAG (scaffold → db layer →
   routes → tests), executes each node, and replans a node that fails its
   build-verification.
3. Each node is classified — the code nodes run on your **agent** model, a
   research node (if any) on your **research** models (the document and code
   researchers) — and writes real files
   through the worker layer, asking your permission before it touches anything
   outside the workspace.

A follow-up — *"how do I run it?"* — scores low → **direct** (`react`): a single
quick answer, no planning ceremony.

## Global loops

A **global loop** is a loop you define and start: *"iterate all my Jira tickets until every one is
done."* It's a **request**, a **source** of iterations, and a **stopping rule**. The engine runs it
one iteration at a time, and every iteration starts fresh from complexity analysis: each one is a
full turn that independently takes the direct or the planned path, exactly like a message you type.

### Anatomy of a loop

| Field | What it holds |
| --- | --- |
| `request` | The prompt each iteration runs. For a work-items loop, `{{item}}` is replaced with the current item. |
| `source` | `repeat` (run the same request again), or `workItems`: list the items to work through with a connector, cell or MCP server call (`via`, `target`, `tool`, `args`, and `itemsPath` pointing at the array in the response) |
| `termination` | `condition` plus a hard `maxIterations` bound (1–100,000) and an optional `maxTokens` budget |
| `cadence` | `continuous` (the next iteration starts when the last one ends) or `interval` with `secs` between starts |
| `threadPolicy` | `freshPerIteration` (a new thread each time; the default) or `singleThread` (one thread for the whole run; for short loops) |
| `configRef` | The config the iterations run against; empty means the engine's default config |

The stopping condition is one of:

- **`allItemsDone`** — keep listing the work items until the list comes back empty, so items that
  appear or reopen during the run are worked too. It needs a `workItems` source.
- **`conditionMet`** with a `text` — after each iteration, the config's reviewer model judges the
  result against the text and ends the loop when it's satisfied. With no reviewer model bound, the
  loop runs until `maxIterations`.
- **`unbounded`** — no natural end; the loop runs to its `maxIterations` or token bound.

Whatever the condition, `maxIterations` always applies, so a confused loop can't spin forever.

### The loop catalog

As of September 25, 2026, [loops.openeng.app](https://loops.openeng.app) publishes six loops:

| Loop | Source | Stops when | Cadence |
| --- | --- | --- | --- |
| **Alert triage watch** (`loop_def_alert_triage`) | Open incidents from a PagerDuty MCP server | No unacknowledged incidents remain (max 500) | Every 300 seconds |
| **Backlog burn-down** (`loop_def_backlog_burndown`) | Repeat | The backlog is empty (max 50) | Continuous |
| **Failing-tests until green** (`loop_def_failing_tests_green`) | Repeat, one thread | The full test suite passes (max 30, 4,000,000 tokens) | Continuous |
| **Inbox-to-done** (`loop_def_inbox_to_done`) | Unread inbox messages from the Gmail connector | Every item is done (max 200) | Continuous |
| **Nightly PR review sweep** (`loop_def_pr_review_sweep`) | Open, unreviewed pull requests from the GitHub connector | Every item is done (max 100) | Every 86,400 seconds (daily) |
| **Ticket sweep** (`loop_def_ticket_sweep`) | Open Jira issues from a JQL query | Every item is done (max 200) | Continuous |

The two repeat loops, **Backlog burn-down** and **Failing-tests until green**, run as published.

A work-items loop lists its items by calling the connector, cell or MCP server it names (its
`target` id), so that source has to be available in the config it runs on. Its `args` (such as the
Jira project in the ticket sweep's query) are part of the loop as published. As of September 25, 2026,
none of the four work-items loops has a source that works as published:

- **Inbox-to-done** names a Gmail connector (`connector_def_gmail`) that no catalog offers yet.
- **Alert triage watch** expects an MCP server with the id `mcp_pagerduty`, but MCP servers you add
  in the Console get ids of the form `mcp_usr_…`.
- **Nightly PR review sweep** and **Ticket sweep** use the GitHub and Jira integrations, whose
  endpoints return a web page instead of JSON, so their calls fail
  ([Integrations](integrations.md#use-in-a-config)).

### Adopt, select, publish

1. Open **Loops** in the Console (Building blocks → Loops) and switch to **Catalog**. **Adopt**
   makes your own copy of a loop (id `loop_usr_…`) that carries the loop's definition and remembers
   its catalog id; loops you've already adopted show an **Adopted** badge. **Mine** lists your
   adopted loops.
2. Open the config and pick the loops in its **Loops** step. The step lists only loops you've
   adopted.
3. Publish. The published config carries the full definition of every selected loop, and the
   engine stores them under `~/.openeng/loops/` when it starts (definitions in `defs/`, run records
   in `runs/`; `OPENENG_LOOPS_DIR` moves the folder).

The Loops page is **adopt-only**: there's no **New loop** button, because the Console can't author
a loop from scratch yet.

### Run a loop

In the Web Console at [app.openeng.app](https://app.openeng.app), a message that starts with `/loop`
controls global loops instead of starting a chat turn:

| Command | What it does |
| --- | --- |
| `/loop` or `/loop list` | The loops defined on this engine, and its most recent runs |
| `/loop start <loopId>` | Start a new run of a loop |
| `/loop status [runId]` | A run's progress, iteration by iteration (the latest run if you leave out the id) |
| `/loop stop <runId>` | Stop a run; the iteration in progress is canceled too |

A run keeps going in the background while you do other things. If the engine restarts, runs that
were still going resume from their records: a work-items loop skips the items it already finished,
and a repeat loop continues after its last iteration.

When a config selects loops, the engine offers only those: `/loop start` refuses any other loop id.

> **Note:** `/loop <id>` followed by a message (rather than `list`, `start`, `status` or `stop`)
> doesn't start anything. It only adds a line to that message's instructions, as prompt guidance.

## Plan & tier notes

Adopting loops, setting a config's loop posture and publishing are open on every plan. Running
loops needs the `openeng` engine, which starts only on an active Pro plan
([Plans & billing](account.md)).

## Troubleshooting

**"loop '…' is not selected by the served configuration".** The config the engine serves selects
loops, and this one isn't among them. Add it to the config's Loops step and republish.

**`/loop list` says no loops are defined.** Nothing published has reached this engine yet. Adopt a
loop, select it in the config, publish, and restart the engine so it loads the new version.

**Publishing fails with `loop_def_missing`.** The adopted loop has no definition, which happens when
its catalog entry couldn't be validated at adoption. Adopt it again from the catalog.

**A `conditionMet` loop runs to its maximum.** The reviewer never judged the condition satisfied, or
the config has no reviewer model bound. Bind one in the config's Models step, or make the condition
text more concrete.
