Research loops
Autoresearch’s behavior lives in a small set of registered workflows, each doing one bounded job and each migrated onto triggers rather than hand-cloned per goal. This page walks them in the order a run typically moves through them: ingest first, then the heartbeat that proposes work, then the scheduler that launches it, then the coding lane, then the optional ranking and autonomy layers.
Ingest-run: turning a finished run into a draft
Section titled “Ingest-run: turning a finished run into a draft”The research-ingest outbox consumer subscribes to the same mlops-run-ingested event
MLOps emits from every tracker sweep. For a run whose source page is a
research goal, it first refreshes that goal’s shared profile.
Then, for a terminal run (finished, failed, or killed) against a goal with open
hypotheses (proposed, accepted, or testing, matched by goal id, slug, or title in the
hypothesis’s goals list), it drafts evidence:
-
Observer call
One bounded LLM call (
SUBSPACE_MODEL) returns{observations: [{hypothesis, direction, strength, text}]}. Each observes at most once per run, guarded by anops.plugin_statemarker, so a re-ingest of an already-observed run is a no-op. -
Blind draft
The drafts land as
author: 'agent'observation pages, deliberately not yet linked to anything, alongside one needs-confirm task card (kind: 'research-interpretation',stage: 'blind', scoped to the run). That card’s payload carries no draft text: it renders a bare textarea and a “Commit interpretation” action. -
Reveal and merge
Writing your own reading and confirming persists it as an
author: 'human'observation page, then, in the same transaction, reveals the agent’s drafts and composes a deterministicauthor: 'merged'draft with both as parents. A second card,stage: 'review', shows human, agent, and merged readings side by side. -
Link evidence
Confirming the review card links every observation page id into the hypothesis’s
metadata.research.evidencearray, deduped. Dismissing either card leaves the drafts unlinked rather than deleting them, so the record stays auditable.
An empty interpretation is refused and the blind card stays open, you can’t skip past writing your own read by submitting nothing.
Per-goal heartbeat: gather, reflect, propose, report
Section titled “Per-goal heartbeat: gather, reflect, propose, report”research.heartbeat runs once per goal, invoked with {goal}, and does exactly four steps,
none of which execute anything:
- Gather reads the goal, its pinned profile, open hypotheses and their evidence, new observations since last run, the backlog, and the budget.
- Reflect may park durably on
human.askif it needs a decision it can’t make alone. - Propose writes only pending artifacts: proposed hypotheses, status-change proposals,
draft coding-task pages, and run proposals, then emits
research-proposalevents for each. - Report files a goal-scoped summary card.
Every proposal from this step is inert until a human confirms it. Proposal events reuse the
ordinary event triage severity routing: a silent route appends a
link to the pending page in the goal’s inbox, and a queue route creates a
research-proposal needs-confirmation card in the task queue. Confirming
applies the approved transition and emits a typed research-proposal-confirmed hand-off
that the scheduler and coding lane consume; dismissing preserves the pending page rather
than deleting it.
Confirming a run proposal specifically is what promotes it into the goal’s backlog page.
Research scheduler: draining the backlog within budget
Section titled “Research scheduler: draining the backlog within budget”research.scheduler, seeded as agents/research-scheduler, does one bounded drain pass
per invocation ({goal}, run by hand, on a cron, or from a trigger):
-
Plan
research.scheduler.planreads the goal’s backlog, itsmetadata.research.budget {maxConcurrent?, runsPerWeek?}, and this week’s live-run and submitted-run counters. -
Probe capacity
Calls
mlops.local.capacity(or the bound launcher’s equivalent) against the goal’smetadata.research.launcherbinding for a live free-slot count. See launchers for what feeds that number for each provider. -
Enforce budget
Budget checks run in code, not as an LLM judgment call: a pass never submits past
maxConcurrentor the week’srunsPerWeekallowance. -
Submit
One
mlops.local.submitper drained backlog entry, gated or template-approved exactly like any other MLOps launch, the scheduler never introduces a new execution primitive of its own. -
Record
research.scheduler.recordstampsmetadata.research {goal, hypothesis, proposal}lineage onto the newly launched run page, drains the entry off the backlog bullet, and advances the runs-this-week counter. Every step is claim-guarded, so a replay after a crash is safe.
A denial (an unapproved template, an out-of-budget request) stops the pass; whatever entries didn’t launch simply stay queued for the next drain.
The coding lane
Section titled “The coding lane”Confirming a heartbeat-drafted coding-task proposal appends a dispatch hint carrying the
hypothesis’s [[ref]] onto the resulting task page before it goes to a coding
agent, whichever lane you’ve wired, a bd issue, a prefilled
agent session, or a Devin session inherits that research context directly in its prompt.
When the resulting code lands and gets used in a launched run, the scheduler records the
merged gitRef on that run’s lineage stamp, closing the chain from proposal to shipped
code to the run that exercised it.
Advisory Elo tournaments
Section titled “Advisory Elo tournaments”A goal opts in with metadata.research.elo {judge, k?, maxHypotheses?}. The explicit
research.elo-sweep workflow (not run automatically) compares bounded pairs of open
hypotheses under a pinned judge rubric page and writes research.elo plus eloMeta onto
each hypothesis. A content fingerprint means an unchanged set of hypotheses makes a repeat
sweep a no-op rather than re-scoring for no reason. Ratings are purely advisory: they never
change a hypothesis’s status, approve anything, or gate a launch, Elo is a ranking signal
for you to read, not a decision-maker.
Per-goal autonomy policies
Section titled “Per-goal autonomy policies”research.autonomy on a goal has exactly two independent opt-ins, both narrow by design:
autoAcceptObservationMergeslifts only the second review gate, the merge/review card, after you’ve already committed the blind human interpretation yourself. The blind step itself is never skippable.autoLaunchUnderBudgetlifts only the scheduler’s Docker-lane submit card, and only for that exact goal’s exact launcher binding with an explicit finite budget set. It rides the same template-approval seam MLOps uses.
Everything else keeps its gate regardless of these opt-ins: bash-lane submits, any other workflow, a mismatched launcher, an unbudgeted goal, hypothesis status changes, coding-task dispatch, and plugin installation itself are never auto-approved by a research autonomy policy.
Scheduling: triggers, not cloned definitions
Section titled “Scheduling: triggers, not cloned definitions”Every one of these loops is invoked, not always-on. The recommended way to put the
heartbeat on a schedule is a trigger: copy the shipped
triggers/research-heartbeat-daily example (an inert 0 9 * * * cron trigger) once per
goal, point its action.args.goal at the goal page, choose a cadence, and enable it. Every
goal’s schedule reuses the same one research.heartbeat definition rather than a
per-goal clone of the agent definition, the pattern the trigger system replaced. An older
definition-level metadata.agent.schedule with a static workflowArgs still runs on
existing definitions, but cloning the heartbeat definition per goal is no longer how you
set a new one up.