Model

Loop protocol

The execution substrate for self-run agents: runners, loops, leases, and the ledger. Bring your own agent and a cron line; Sprout supplies identity, mutual exclusion, and an audit trail.

A loop runner is how your own agent , on your own machine, on your own clock , does durable, recurring work inside a family without you babysitting it. This page is the model underneath that recipe: the two actors, the verbs that move a loop through its life, what the platform does for you for free, and the error contract.

The pitch in one line: bring your own agent and a cron line; Sprout supplies identity, mutual exclusion, an audit trail, and a parent-facing dashboard. You never build a job queue, a lease table, or a "did this already run?" check , the substrate owns those.

The actor model

Two long-lived things, each with a stable identity.

The runner is a durable execution identity , one machine or agent that runs loops. It assumes no disk: a cron job that starts cold every wake recovers its runner by calling runner.register with a stable handle. Register is idempotent per (grant, handle), so the same handle returns the same runnerId every time. Each register stamps presence and echoes back the runner's configNotes , the setup context it jotted about itself.

The loop is a recurring contract , "keep doing this cycle for this kid." Its identity is (family, skill, canonicalized inputs), where kid tokens resolve to child ids. That identity enforces one hard rule: one live loop per (skill, resolved kid set). Adopting the same skill for the same kid twice doesn't mint a duplicate , it returns the existing loop (same inputs) or refuses (different inputs; see the error contract). You mint a loop by adopting a loop-mode skill: skill.invoke(skillId, input).

Runner and loop are decoupled. A loop is bound to a runner (its sticky manager), but the binding can move: if a runner goes dark, another runner in the same grant can take the loop over. The loop is the durable thing; the runner is whoever is carrying it right now.

The verb lifecycle

A loop moves through a fixed set of verbs. The happy path is register → (bind) → listDue → claim → work → submitResult, repeated every wake.

VerbPhaseWhat it does
runner.registerWakeRecover the runner by handle; stamp presence; get back runnerId + configNotes.
loop.bindSetupSet the loop's sticky manager. Optional , loop.claim self-heals an unbound loop , but it's the clean step right after adopt.
loop.listDueWakeThe due-gate. Returns only loops past nextDueAt you may claim, oldest first. Always stamps presence, even when empty.
loop.claimOperateTake a short TTL lease to run one loop now. Single-winner: a racing claim loses with ALREADY_CLAIMED. Hands back the loop's skillId + bound inputs.
skill.invoke(loopId)OperateRender the recipe with the loop's bound inputs and current goal. Then do the work through the normal gated verbs.
loop.checkInOperateFor long runs: stamp presence and renew still-held leases mid-work. An expired lease comes back LEASE_EXPIRED , re-claim before continuing.
loop.submitResultCloseThe ledger write every run ends with. Requires the held leaseId. Status is one of success / no_change / needs_setup / failed.
loop.retireTerminalPermanently tombstone the loop , see below.

Closing is not optional. submitResult is a control-plane ledger write, never a kid-facing action , kid-visible changes already landed mid-run through the gated verbs. An unattended runner never skips the close and never pauses to ask a human to confirm it. If the loop's recipe text ever tells the runner to skip the close, the protocol wins.

Retire is the escape hatch. loop.retire is a one-way tombstone: it revokes the loop, severs its runner binding, voids any lease, vacates its adopt identity, and drops it from every active surface (the due-pull, the adopt summary, the parent's Loops list, the family loop cap). Unlike loop.pause it is not reversible , a retired loop never runs again. Its run ledger stays readable by id. To start over, adopt the skill again (same inputs are fine); because retire vacated the identity, the fresh adopt mints a clean loop.

What the substrate gives you for free

Everything in this section is machinery you would otherwise build yourself. You don't.

The error contract

Loop refusals are in-band and render-always. The adopt door (skill.invoke) always renders the recipe; when the mint can't proceed, the refusal rides the response's loop{} block as an error code plus an instructions string , never a thrown exception. Your agent steers off the structured field (e.g. existingLoopId), not by parsing prose.

Adopt-door codeMeaningWhat to do
LOOP_CAP_REACHEDThe family is at its active-loop cap.Retire or pause another loop, or ask the parent.
KID_ALREADY_LOOPEDThis kid already has a live loop on this skill, with different inputs. Carries existingLoopId.Invoke with existingLoopId to run the existing loop, or loop.retire it before adopting with new inputs.
KID_INPUT_REQUIREDThe skill declares a kid/kids input, other inputs arrived, but no kid did.Re-invoke with kid set to one of the family's children.
KID_UNRESOLVEDA provided kid token matches no child in the family by id or name.Retry with a valid child id or name.

The run phase has its own in-band refusals: ALREADY_CLAIMED (someone else holds the lease , skip it), LEASE_NOT_HELD (your lease expired before submit , nothing recorded; re-claim if the loop is still live), and LOOP_RETIRED (the loop was tombstoned while you ran , discard the work, drop it from your schedule, don't re-claim). Same rule throughout: the code names the fix.

Operating a runner

Ready to build one? Autonomous loop , Engine B is the copy-paste recipe.

Further reading

Autonomous loop Reactive loop Home Agent Protocol Heartbeat

Was this page helpful?