Triggers make MagicFrog act without a human turn: a cron trigger fires a scheduled action, and a watcher loops a small script that decides whether something interesting happened. Both kinds share one overview, and both refuse to guess where they run.
Scheduled triggers (cron)
A scheduled trigger carries its identity, a schedule, and an action:
| Field | Meaning |
|---|---|
triggerId |
Stable identity of the trigger |
kind |
cron |
schedule |
Non-empty schedule expression |
intervalMinutes |
Integer; the minimum is 30 minutes, anything below is rejected |
targetComputerId |
The computer the action runs on — required for cron |
ownerTaskId |
Optional: the task the trigger belongs to |
action |
What to run (type plus optional parameters) |
Two contracts make schedules trustworthy:
- Fail-closed targeting. A cron trigger without
targetComputerIdis rejected at registration and re-checked at every execution. It never runs against the local machine and is never silently re-targeted. - No catch-up replay. Schedule slots are grid-aligned. If a slot is missed while the daemon is down or a run overruns, the trigger fires once at the next tick and re-aligns — a missed fire is skipped forward, never replayed.
Every execution outcome is recorded: ok, error, or rejected.
Watchers
A watcher is a temporary bun script authored by you or by an agent, run on a loop against one target. The script is configuration; the daemon is the interpreter.
| Field | Meaning |
|---|---|
watcherId |
Stable identity of the watcher |
scriptPath |
Path to the bun script |
targetKind |
task or agent |
targetId |
The task or agent being watched |
interval |
Loop cadence in seconds (the 30-minute floor is a cron contract, not a watch-loop contract) |
ownerAgentId |
Optional: the agent that owns the watch |
status |
stopped, running, or failed |
The script contract
Each iteration receives its target context on stdin as JSON:
{
"watcherId": "wat_...",
"targetKind": "agent",
"targetId": "...",
"now": 1792000000000,
"artifactsDir": ".../watchers/wat_.../",
"events": [],
"eventCursor": 0
}
The script answers with a single JSON finding line on stdout:
{ "matched": true, "summary": "Build failed twice in a row", "data": { "runId": "run_..." } }
What counts as "matched" is entirely the script's decision — the executor never interprets it. A script exiting non-zero is a recorded failed run, and the watcher stays alive; failed status is reserved for executor-level start failures such as an unreadable script.
Every watcher gets its own artifact directory, used as the child's working directory and exposed as MAGICFROG_WATCHER_ARTIFACT_DIR, so script-produced files can never collide across watchers or leak into the workspace.
Targets
agent— the loop consumes the cloud agent event feed over the devices-ws channel:run.started,tool.called,tool.result,form.pending,form.answered, andtakeover.requestedevents. The feed is cursor-resumable: after a disconnect the watcher resumes from its last cursor instead of starting from scratch.task— the loop receives the injected task status snapshot. If the host has no task probe available, starting the watcher fails explicitly — a fabricated status is never presented.
Watch execution is local first: the daemon hosts the watcher loop and is its runtime owner. Pointing a watcher at a cloud agent's events subscribes to that agent's feed; it does not move the watcher itself.
One overview
GET /v1/triggers/overview is the global aggregation view: one account-scoped read that lists every task's cron triggers and watchers, each with its status and the next due time as the scheduler will settle it. The daemon exposes the same single-view form for the local machine. Both render through the same canonical aggregation contract, so the control plane and the machine-local status cannot drift apart.