Skip to content

Loop API

A loop runs cycles until a termination condition is met or a seatbelt trips. This page is the reference for the builder, its types, and the distribution surface. For the conceptual walkthrough, see the Loops guide. Signatures are transcribed from src/loop/ and verified by bun run docs:symbols.

loop<S>(name) returns a LoopBuilder<S> (defined in src/loop/builder/types.ts). Every method returns the builder for chaining; .run() executes.

interface LoopBuilder<S = unknown> {
reason<B extends BackendTag, Output = unknown>(
backend: LlmBackend<B>,
request: AutonomousRequest<Output, B>,
): LoopBuilder<S>;
step(name: string, body: (state: S) => Promise<S> | S): LoopBuilder<S>;
measure(fn: (state: S) => Promise<number> | number): LoopBuilder<S>;
until(termination: TerminationPreset<S> | LoopVariant<S>): LoopBuilder<S>;
guard(opts: LoopGuards): LoopBuilder<S>;
run(initial?: S, options?: LoopRunOptions): Promise<Result<LoopOutcome<S>, LoopRunError>>;
}
MethodPurpose
.reason(backend, request)Adds an autonomous backend turn to each cycle. See Backend Matrix and Errors and Results.
.step(name, body)A deterministic state transition; body receives and returns S.
.measure(fn)Overrides the preset’s measure with a custom number.
.until(preset)Sets the termination condition (a preset or custom LoopVariant<S>).
.guard(opts)Adds seatbelts: maxIterations, wallClockMs, tokenBudget.
.run(initial?, options?)Executes; resolves to Result<LoopOutcome<S>, LoopRunError>. Never rejects — failures arrive as Err(LoopRunError).
interface LoopRunOptions {
readonly args?: readonly string[];
readonly overrides?: FlowOverrides;
readonly context?: LoopExecutionContextOptions;
readonly onCycle?: (cycle: LoopCycleReport) => void;
}
interface LoopCycleReport {
readonly iteration: number;
readonly measure: number;
readonly usage?: Usage;
readonly contextPressure?: LoopContextPressure;
}
type LoopRunError = RuntimeError | TerminationContractError;

onCycle fires after each cycle with the iteration index, the current measure, optional token usage, and context-pressure evidence when offload or compaction ran. Providing context enables managed loop context and tunes the aggressive default compaction/offload thresholds. Without context, loop execution does not capture raw step observations. Loop execution owns recurrence, guards, stop evaluation, progress, and context pressure; direct executeLoop is internal and not exported from the package root. LoopRunError is either a RuntimeError (see Runtime Errors) or a TerminationContractError (a violated termination contract).

interface LoopOutcome<S = unknown> {
readonly state: S;
readonly stopReason: LoopStopReason;
readonly iterations: number;
}
type LoopStopReason =
| "converged"
| "unfixable"
| "stuck"
| "timeout"
| "ceiling"
| "budget-exhausted"
| "cancelled";

orcats run and orcats serve map each stop reason to a process exit code via exitCodeForStop(reason), exported from the loop surface. A build/runtime error exits 70.

Stop reasonExit codeMeaning
converged0Termination condition met.
unfixable1The loop concluded the issue cannot be fixed.
stuck2No progress across cycles.
timeout3Wall-clock guard expired.
ceiling4Iteration ceiling reached without convergence.
budget-exhausted5Token budget guard exhausted.
cancelled6Cancelled via signal or cancel().
(build/runtime error)70The loop failed to run at all.
PresetStops when
untilGatesGreen()failing gates reach 0
untilManifestComplete()pending tasks reach 0
untilNoIssues()open issues reach 0
untilConfident(threshold)confidence reaches the threshold
times(n)n cycles have run

.measure(fn) overrides a preset measure. .guard() adds or overrides seatbelts such as maxIterations, wallClockMs, and tokenBudget.

defineLoop({ name, source, sink, onTrigger }) packages a loop module. Put it under .orca/loops/, export it, then use orcats loops, orcats run, or orcats serve. orcats run and served children share one firing contract for event decoding, definition.run(event), sink emission, diagnostics, and exit-code mapping. During orcats run, loop-cycle progress from the active RunReporter is rendered on stderr while sink output remains on stdout. See the Served Loops guide for the supervisor isolation contract and ORCA_LOOP_EVENT payload.

Built-in source kinds: manual, cron, watch, webhook, queue, linear-issue, linear-agent.

Built-in sink kinds: pr, file, slack, queue, stdout, linear-issue, linear-agent.

The linear-issue and linear-agent kinds are provided by linearIssueSource / linearAgentSource / linearIssueSink / linearAgentSink in src/loop/io/linear.ts. See the Linear guide for env vars, webhook verification, and Slack composition.

Loops checkpoint state through a StateStore<S> port — see State Stores. Store-backed fan-out uses StateStore.branch() plus non-history BranchWritableStateStore.saveBranch() for each branch and StateStore.merge() at fan-in; pure fanOut/fanIn remains available for summary-only in-memory work. Every store method is Result-typed over RuntimeError; createSqliteStore returns Result and can fail at construction.