Build

Reactive loop

Your home agent reacts to Sprout-pushed events: approve a screen-time request by rule, defer the ambiguous ones to the parent through a skill. Push twin of Autonomous loop.

What you'll build

Something happens in the family , a kid asks for screen time, hits a gem milestone, finishes a task , and your home agent reacts: approve a request that fits the kid's plan, reject one that doesn't, defer to the parent through a skill when the call isn't clear-cut.

This is the reactive twin of Autonomous loop. Same read-decide-act shape; the trigger is an event, not a clock. There are three transports that can carry that event to your agent , the same decision logic behind each, different plumbing.

Three transports, best to simplest

TransportLatencyWhat it costs you
1 , SSE pushReal-timeAn agent session holding an open stream.
2 , activity-triggered heartbeatEvent → next fireNothing to host , Sprout fires it. But sandbox limits apply.
3 , poll on a runner wake~10 minFree if you already run a loop runner , just check on each wake.

Best is the one you can afford to host. Holding a live session? Push. Don't want to host anything? An activity heartbeat is push-without-a-daemon. Already running a loop runner? Folding a check into its wake costs nothing extra.

Pieces you'll combine

Transport 1 , SSE push

Real-time, and the richest: your agent holds an open stream and acts the moment the event lands. Four movements:

1. Subscribe to a channel. Pick the resource URI that matches the event you want to react to.

Shell
resources/subscribe sprout://child/{kidId}/screentime/requests
# Open SSE stream; events arrive whenever the kid requests screen time.
# See /docs/reference/subscriptions for the full channel list.

2. On each event, evaluate. Your agent's rules: kid's earned gems, today's plan, time of day, what's still on the schedule. Make the decision local; don't round-trip a model call for every event if you don't have to.

3a. Act directly if the call is clear.

Shell
// Auto-approve a 15-min request when the kid still has budget.
screentime.review_request({
requestId: "<event.requestId>",
approve: true,
note: "auto-approved: within today's budget"
})

3b. Defer to the parent if it's ambiguous. Wrap the question in a skill so the parent sees it in the family inbox and decides in-app.

Shell
skill.invoke({
skillId: "<parent-decision-skill>",
input: {
question: "Alex is asking for 30 more minutes after dinner. Budget is at 80%. Approve?",
options: ["Approve", "Approve 15min only", "Reject"],
callbackTool: "screentime.review_request",
callbackArgs: { requestId: "<event.requestId>" }
}
})
# The skill posts a card via skill.post_result; the parent taps in-app.
# Sprout's in-app agent calls the callback tool with the parent's choice.

4. Keep the loop open. SSE streams stay open; your agent stays subscribed. Reconnect on drop. Idempotency keys on action calls protect against duplicate events.

Transport 2 , activity-triggered heartbeat

Push without a daemon. Instead of a cron time, a heartbeat fires on a kid activity. Sprout hosts it; you host nothing. Create it with triggerType: 'activity' and the activity types to listen for, and omit cron.

Shell
heartbeat.create({
name: "Screen-time auto-review",
triggerType: "activity",
triggerActivityTypes: ["screen_time.requested"],   // omit cron
runContext: {
runSkillId: "<review-skill>",
notifyRung: "THREAD"
}
})
# Fires when a kid requests screen time; runs your review skill.
# V1 activity heartbeats support screen_time.requested only.

Honest limit: screen_time.requested is the only shipped activity trigger today. Agent- and system-generated activity isn't triggerable yet. If the event you want to react to isn't a screen-time request, this transport can't carry it , use Transport 1 or 3.

warningWhat an activity heartbeat can't do. It runs in Sprout's sandbox, like any heartbeat: no canvas.update, no network, no file access. It shares the heartbeat budget , 4 fires per 24h , so a burst of requests past the cap won't all fire. And latency is event → next fire, not instant. If you need to rewrite a canvas or reach outside on the event, detect here and act on a runner (below).

Transport 3 , poll on your runner's wake

If you already run a loop runner, it already wakes every ~10 minutes. Reacting to an event can be as cheap as reading the relevant state on each wake and acting when it changed , no new infrastructure, no open stream. Reaction latency is your wake interval (~10 min), and it's free because the wake already happens.

Each wake
# Inside your runner's existing wake, after loop.listDue / claims:
screentime.list_requests({ status: "pending" })
# → react to anything new since last wake
screentime.review_request({ requestId: "<id>", approve: true, note: "..." })
# Reaction latency = your wake interval. Free , the wake already runs.

Use this when real-time isn't required and you'd rather not hold a session or depend on the one shipped activity trigger.

Compose them , detect on a heartbeat, act on a runner loop

The transports aren't exclusive. The most robust reactive setup splits detection from action: an activity heartbeat notices the event cheaply (no daemon), and a runner loop does the heavy part the sandbox forbids , the canvas rewrite, the external call, the file write. The heartbeat's job is just to flag that something's due; the runner picks it up on its next wake and does the work with full hands.

That's the same detect-vs-act split the Loop protocol formalizes: the heartbeat is a trigger, the loop is the durable worker with a lease and a ledger.

When to use it

Tools touched

Recommended skills

No skills in the catalog pair with this pattern yet. Browse the catalog.

Roadmap

Seen in walkthroughs

Not yet. A "screentime auto-approver" walkthrough is on the list.

Was this page helpful?