Background goals that loop.
opencode-loopd is a Codex-inspired goal engine for OpenCode.
Create a goal with /goal, watch an autonomous child session loop in the background,
schedule it to requeue on an interval without manual re-prompt, and monitor everything from a modal TUI dashboard — without blocking the parent chat.
Install in 30s
Add to both configs, restart OpenCode:
// ~/.config/opencode/opencode.jsonc
{ "plugin": ["@bojackduy/opencode-loopd"] }
// ~/.config/opencode/tui.json
{ "plugin": ["@bojackduy/opencode-loopd"] }
Or one-line installer (adds /goal + skill):
npx -y @bojackduy/opencode-loopd@latest
Dashboard: /loop or <leader>o • Shift+B bug report
Modal dashboard (/loop / <leader>o): vivid per-status coloring, running spinner, NORMAL vs INSERT — keys trapped, no chat leak.
⚡ Engine-driven
Idle + lease + 30s polling re-prompts the child with accumulated steering (progress + transcript tail + inbox). Not a parent re-prompt machine.
⏱️ Scheduled
scheduleEveryMs auto-requeues the same goal on an interval (1.7) — 5s poll, skip-if-running, workspaceWrite gate, inbox Scheduled tick N/M.
👁️ Parent ↔ child
Owner tools inspect/steer; inbox injects into next turn. Wake-up on complete/block without polling.
🛡️ Safe
Per-goal artifact isolation .opencode/loopd/goals/<id>/, maxTurns force-finish → semantic summary → parent.
🖥️ Command sessions
Installs, builds, dev servers & REPLs as background processes with a fullscreen terminal — owner exit notifications, filter/until watches, timeouts. Never block the turn in bash.
🧠 Switch models mid-run
Provider hits quota? switch_goal_model moves the goal to another model on the same worker — or configure ordered fallbackModels for automatic 429 failover.
✋ Manual mode
interactive: true stops the engine from starting turns — every turn comes from your :send. For workers that wait for input.
Architecture — how the loop runs
┌─────────────────────────┐
│ Main session (parent) │
│ owner tools │
│ inspect / steer / pause│
└────────────┬────────────┘
│ inbox → child
│ wake-up ← engine
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Goal A │ │ Goal B │ │ Goal C │
│ active │ │ blocked │ │ paused │
└────┬────┘ └────┬─────┘ └────┬────┘
│ │ │
└─────────────────────┼───────────────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
Loopd engine ──────────► Child worker (subagent)
(plugin server) steering get_goal / report / complete
│ lease / retry / polling │ block / question
│ force-finish / notify ▼
│ .opencode/loopd/goals/<id>/
│ progress.md / artifacts
└─────────────────────────────────┘
Each goal is a GoalID — the loop identity. The dashboard is a modal dialog that pushes loopd.dashboard onto the keymap so the chat prompt never sees your keys. Artifacts are namespaced per-goal; if your objective says save to ./reports/, that wins. 1.7 adds a 5s schedule worker that requeues complete scheduled goals on interval with skip-if-running.
Quick start — background goals in OpenCode
/goal fetch the latest AI news and save 10 items to ai-news.md
# agent clarifies → loopd_create_goal → child loops every ~30s
/loop # or <leader>o
j/k move o open child :send hello :force done ? help q close
Shift+B → prefilled GitHub bug report (like lazyjira / lazyconfluence)
# Scheduled — one goal, auto-requeues on interval (1.7)
loopd_create_goal({
name: "hourly-report",
objective: "Append date -u to tick.txt each run",
scheduleEveryMs: 3600000, // 1h
scheduleMaxRuns: 24
})
The dashboard keybinding is <leader>o (also registered in the command palette as "Loop Dashboard" / /loop). Scheduled goals show scheduleRunCount/nextRunAt in inspect.
Why loopd vs. plain loop?
Plain /loop injects prompts into the main session — it pollutes the transcript. Codex goals keep history per-thread and steer via internal context. Loopd does the same for OpenCode: one child session per goal, continuation steering with progress history + transcript tail, and an explicit artifact namespace. The parent stays clean.
opencode-loopd
Child session per goal, no transcript pollution, artifact isolation, scheduled requeue, parent wake-up injection.
Plain /loop
Single session, prompt injected into main chat, manual polling to see progress, no interval.
Codex / Claude Code
Inspiration: Codex goals keep per-thread history and steer via internal context — loopd adds scheduled intervals for repetitive dialogues.
FAQ — SEO for OpenCode loop & background agents
How to run background agents in OpenCode?
Install @bojackduy/opencode-loopd as a plugin (both opencode.jsonc and tui.json), restart, then /goal in any session. The engine spawns a child worker that loops autonomously; you monitor with /loop.
Is this like Claude Code's loop or Codex goals?
Yes — same mental model: engine-driven idle → continuation steering with accumulated context → child calls complete_goal/block_goal → parent wake-up. Like Codex, but native to OpenCode's session model.
Where do loop outputs go?
Per-goal: .opencode/loopd/goals/<id>/ (e.g. progress.md). If your objective names ./reports/, that wins.
How to schedule repetitive tasks?
Use scheduleEveryMs (1.7) — e.g. scheduleEveryMs: 60000 for 1m interval, scheduleMaxRuns: 10 for 10 total. The 5s schedule worker resurrects complete → active with Scheduled tick N/M inbox; `workspaceWrite:true` defers if another writer is active.
How to report a bug?
In the dashboard press Shift+B or :bug — opens github.com/bojackduy/opencode-loopd/issues/new prefilled with environment (version, OS, goal context).
What if my provider hits quota mid-run?
Run loopd_list_models to see what's connected, then switch_goal_model to move the goal to another model — same worker, transcript and progress kept. Or set fallbackModels at creation for automatic ordered failover on 429s. Dashboard: :model / :models.
Can I run dev servers and long installs without blocking?
Yes — use command sessions (loopd_command_start) instead of the blocking shell: installs, builds, dev servers and REPLs run in the background with a fullscreen terminal, owner exit notifications, and until pattern watches that wake you when output appears.