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.