Skip to content
MagicFrog

Triggers

Scheduled runs and event watchers — both tied to an explicit target and visible in one overview.

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 targetComputerId is 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, and takeover.requested events. 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.