scheduled-task · git:20260715.605c0d4 · 2026-07-15 · sha256 e6b8ba32146b6a01
scheduled-task git:20260715.605c0d4A
Immutable. This exact content is served forever at /api/v1/blob/e6b8ba32146b6a01.
--- name: scheduled-task description: Use when the user wants something to run routinely / on a schedule rather than once now -- "tarea programada", "rutinariamente", "cada mañana", "cada N horas", "todas las noches", "schedule", "cron". Covers mounting, structuring, and running an unattended headless task that reports back, plus consuming its reports. NOT for a live in-session agentic loop (that is agentic-loop). --- # Scheduled Task A scheduled task is a Gaia task that runs **unattended on a recurring schedule** via the OS crontab, executes `claude -p` headless, and leaves the user a report in the notifications inbox instead of asking anything mid-run. This skill covers the three flows of its lifecycle: creating one, executing it headless, and consuming what it reports. For a task that iterates in a live session toward a metric, that is `agentic-loop`, not this. The load-bearing constraint that shapes everything below: a headless run has no user to answer a prompt. So a scheduled task must complete everything it can WITHOUT a T3 mutation, and when a T3 is unavoidable it must NOT try to ask -- it accumulates the `approval_id`, finishes the rest, and reports back so the user can resume and grant later. Gaia's T3 layer gates independently of Claude Code's permission dialog: `--dangerously-skip-permissions` removes the TUI prompt, but Gaia still blocks/accumulates T3 mutations exactly the same. ## When to use Trigger when the user asks for routine/scheduled execution: "cada noche corre X", "rutinariamente revisa Y", "cada 6 horas", "prográmame Z". If they want it run once now, or iterated live toward a threshold in this session, this is the wrong skill (one-shot dispatch / `agentic-loop` respectively). ## Flow A -- Creation (mount the task) Build three artifacts, in order. Heavy mechanics and the full wrapper rationale are in `reference.md`; the runnable templates are in `scripts/`. 1. **Write the task as a read-only-first atomic prompt.** State the task's job as a self-contained prompt that opens with the headless preamble (Flow B). Front-load everything read-only; isolate any mutation as an explicit, clearly-labeled step so the headless run can skip-and-accumulate it cleanly. Store the prompt in its own file (one task = one prompt file). 2. **Copy the wrapper** `scripts/run-scheduled-task.sh` to a per-task file (e.g. `~/ws/me/scheduled-tasks/<task>.sh`) and edit its `==CONFIG==` block: `TASK_NAME`, `PROJECT_DIR`, `PROMPT_FILE`. The wrapper exports credentials and PATH **explicitly** (cron has almost no environment), runs the validated headless invocation, persists the session, and parses out the `session_id`. Do not drop `--output-format json` or add `--no-session-persistence` -- the session MUST stay resumable. 3. **Add a staggered crontab entry** from `scripts/crontab.template`. Give the wrapper an ABSOLUTE path, redirect to a per-task log, and offset the minute so no two tasks start in the same minute. ## Flow B -- Headless execution (what the task's prompt instructs) Every scheduled-task prompt begins with this preamble, verbatim in spirit: > Eres una tarea programada headless. Nadie está mirando y no puedes preguntar > nada. Procede así: > > 1. **Intenta completar la tarea SIN ninguna mutación T3.** Haz todo el trabajo > read-only / T0-T2 que puedas. > 2. **Si hay un T3 inevitable, NO llames AskUserQuestion.** El comando se > bloqueará con un `approval_id`. NO reintentes. ACUMULA cada `approval_id` > (con el comando exacto y por qué hace falta) y sigue con TODO lo demás que > sí puedas terminar. > 3. **Captura tu `session_id`** leyendo `$CLAUDE_SESSION_ID` en la sesión > top-level (ver abajo) para poder estamparlo en el reporte. > 4. **Redacta un mensaje final GENÉRICO** (sin nombres propios ni datos > sensibles) con el formato de abajo. > 5. **Guarda ese mensaje** como último paso con `gaia notifications add` (T0). ### Capturing the session_id (`$CLAUDE_SESSION_ID`) The report must carry the run's `session_id` so the user can `claude --resume <session_id>` to grant the accumulated T3s. The headless session reads its OWN id from its shell env var -- it is NOT parsed from `--output-format json`, and NOT asked from the user: ``` echo $CLAUDE_SESSION_ID ``` **Read it at the TOP-LEVEL session, never inside a dispatched subagent.** `$CLAUDE_SESSION_ID` does not propagate to a subagent's shell, so a task that delegates its work to a subagent and reads the var there gets an empty value and the report lands with `session_id` "-". Capture the id in the main session and pass it through. If it is genuinely empty, omit `--session-id` and note in the body that the user recovers pending grants with `gaia approvals pending --all-sessions` (which does not depend on the id). ### Final message format (generic, no PII) ``` He terminado la tarea <nombre>: <qué hizo en una línea>. Aprobaciones pendientes: <lista de approval_id + por qué cada uno>, o "ninguna". session_id: <valor de $CLAUDE_SESSION_ID, o "ninguno — usa gaia approvals pending --all-sessions">. ``` The task's LAST action is to persist that message (substitute the value you read for `$CLAUDE_SESSION_ID`): ``` gaia notifications add \ --task "<nombre>" \ --headline "He terminado la tarea <nombre>: <resumen>" \ --body "<mensaje completo, incluidas las aprobaciones pendientes>" \ --session-id "$CLAUDE_SESSION_ID" ``` `gaia notifications add` is **T0** by design, so a headless run can always record its report without stalling on a gate. The message stays generic because a notification surfaces later out of context -- proper nouns and sensitive data do not belong in an inbox line. ## Flow C -- Consumption (how the user sees and acts on reports) The report surfaces through four escalating touchpoints; the user pulls detail on demand rather than being interrupted: 1. **Per-prompt counter** -- while there are unread reports, each prompt gets a cheap one-line `🔔 N task notifications sin ver` (nothing when N=0). 2. **SessionStart list** -- a compact `## Task Notifications (unread)` block, one line per report (task + headline + time + `session_id`). 3. **Detail on demand** -- `gaia notifications show <id>` prints the full body, including the pending `approval_id`s and the resume line `claude --resume <session_id>`. The user resumes that session to grant the accumulated T3s. Granting happens through the **orchestrated consent flow** (the AskUserQuestion dialog with the nonce) -- it is NOT approved by typing free text into the resumed session (confirmed empirically). If the report has no `session_id` (it came back "-" because the id was read inside a subagent), the pending grants are still reachable session-agnostically via `gaia approvals pending --all-sessions`. 4. **Clear** -- `gaia notifications ack <id>` (or `ack --all`) marks reports seen so the counter and list go quiet. ## Anti-patterns - **Asking the user from a headless run.** There is nobody there. Forbidding `AskUserQuestion` and accumulating `approval_id`s is the only correct move; a run that blocks waiting for an answer hangs until the cron kills it. - **Retrying a blocked T3 in the same run.** The gate did not misfire -- it needs consent the headless run cannot give. Accumulate and report; do not loop. - **Proper nouns / secrets in the report.** A notification is read later, out of context, from an inbox. Keep the message generic. - **Dropping session persistence.** Without a resumable session the user cannot grant the accumulated approvals -- the whole accumulate-and-resume design collapses. Never add `--no-session-persistence`. - **Un-staggered schedules.** Two tasks in the same minute contend for resources and interleave their logs; offset every entry. - **Treating this as a live loop.** Iterating toward a metric in-session is `agentic-loop`; scheduling is OS crontab + headless. Do not conflate them.