- name
- schedule
- description
- This skill should be used when creating, listing, or deleting scheduled agent tasks via GitHub Actions cron workflows. It generates workflow YAML files that invoke Soleur skills on a recurring schedule using claude-code-action.
<!-- soleur-cloud-mode:start -->
**Cloud Mode (Devin):** before pipeline work run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/cloud-detect.sh"`. If `CLAUDE_PLUGIN_ROOT` is unset (cloud exec shells do not export it), resolve the root by IDENTITY, never by script basename: for `d` in `"$HOME/.local/share/devin/cli/plugins/cache"` and `/opt/.devin/plugins`, skip unless `[ -d "$d" ]`, then `MANIFEST="$(find "$d" -path '*/.claude-plugin/plugin.json' -exec grep -l '"name"[[:space:]]*:[[:space:]]*"soleur"' {} + 2>/dev/null | head -1)"`, `ROOT="${MANIFEST%/.claude-plugin/plugin.json}"` — one resolution, two consumers: `$ROOT/scripts/cloud-detect.sh` and `$ROOT/scripts/precommit-guard.sh`. (Shape check, not authentication: a planted `{"name":"soleur"}` dir passes — ADR-179 A11.) `local` or `not-local:no-devin-env` proceeds normally; any other `not-local:<reason>` applies `<plugin-root>/devin/INSTRUCTIONS.md` §Cloud Mode: emit the `--banner`, fan out sequentially with `Reviewed-Coverage: sequential-fallback` disclosure (never claim an independent review ran), `message_user` ack before any secrets read or production mutation, and run `precommit-guard.sh` before any `git commit` — hooks do not fire in cloud.
<!-- soleur-cloud-mode:end -->
<!-- grok-harness-invoke:start -->
**Grok Build (`plugins/soleur/lib/harness.ts` `invokeSkill()`):** Read this SKILL.md in this process and run it to completion. A one-segment `soleur:<name>` in this document names a SKILL — on Grok Build, Read `plugins/soleur/skills/<name>/SKILL.md` in this process; it is not a nested tool_use. A multi-segment id such as `soleur:<domain>:<name>` names an AGENT: spawn it, never Read it, and on Grok Build spawn_subagent takes the id with its colons replaced by hyphens (`agentIdToGrokSubagentType`). **Claude Code:** Skill tool for a skill (`soleur:<name>`), Task tool with `subagent_type` for an agent. Forbidden is executing a subset, not the Read.
<!-- grok-harness-invoke:end -->
# Schedule Manager
Generate GitHub Actions workflow files that run Soleur skills on a schedule. Two modes:
- **Recurring** (`--cron`): Standard cron-triggered workflow that fires on every matching tick.
- **One-time** (`--once --at <YYYY-MM-DD>`): Single-fire workflow that self-disables after running. Useful for "remind me to do X in 2 weeks" style tasks.
Each schedule becomes a standalone `.github/workflows/scheduled-<name>.yml`.
## When to use this skill vs harness `schedule`
Two skills exist with the name `schedule`. They serve different jobs:
| Use this (`soleur:schedule`) when | Use harness `schedule` when |
|---|---|
| Push commits, open PRs, modify the user's repo | Analyze, summarize, report — no repo writes |
| Use repo secrets (Doppler, Vercel, Cloudflare) | No secrets needed |
| Invoke a Soleur skill (`soleur:<skill>`) | Generic Claude API task |
| Run Terraform / migrations / deploys | Read-only research, posting somewhere |
Examples for `soleur:schedule`:
- "Open a cleanup PR removing feature flag X in 2 weeks" → `--once`
- "Run a weekly Terraform drift check" → recurring
Examples for `harness schedule`:
- "Summarize recent issues every Monday and post to Slack"
- "Check if a vendor's API changed and email the diff"
If the agent doesn't need access to your repo, prefer harness `schedule`.
**Both `schedule` skills generate GitHub-Actions cron only.** For a **one-time, autonomous, server-side action** that needs fire-time prd secrets or an installation-token repo write (no operator, no GH-Actions runner), use the Inngest path instead: a self-armed **oneshot** (ADR-046, bespoke logic) or the generic **reminder primitive** (`POST /api/internal/schedule-reminder`, no deploy for a comment / registered check — incl. the parametric `sentry-issue-rate` check). See [`inngest-oneshot-and-reminder-patterns.md`](../../../../knowledge-base/engineering/operations/runbooks/inngest-oneshot-and-reminder-patterns.md). **`create`'s Step 0 routing gate (below) enforces this automatically** — Inngest is the structural default for secret/server-side work; GHA is the pure-GH-ops exception.
## Commands
### `create`
Generate a new scheduled workflow.
**Step 0: Execution-substrate routing gate (run FIRST — before generating any GHA YAML).**
This skill generates GitHub-Actions cron. But a GHA runner has **no Doppler/prd
access**, so a scheduled task that needs fire-time prd secrets (Sentry/Supabase/
Doppler tokens) will *fire but fail to authenticate* — and the
`new-scheduled-cron-prefer-inngest` PreToolUse hook blocks the YAML write anyway.
Route by what the task actually needs BEFORE generating anything; Inngest is the
**structural default** for secret/server-side work, GHA the exception.
Classify `$ARGUMENTS` (the task description):
1. **Needs fire-time prd secrets OR autonomous server-side logic.** Grep the task
for `doppler`, `SUPABASE_SERVICE_ROLE_KEY`, `SENTRY_*` tokens, an installation-
token repo write, or "server-side"/"runtime" execution. **Route to Inngest —
STOP, do not generate GHA:**
- **Verification-shaped** ("did Sentry issue X drop below N/day?", "is metric Y
under threshold?", "comment/close an issue after a check") → the reminder
primitive's **`named-check`** (zero-deploy to arm; `POST /api/internal/schedule-reminder`).
If a suitable check exists in `CHECK_REGISTRY` (e.g. **`sentry-issue-rate`**,
the worked example — events/day of a tagged Sentry issue over a window, with
optional `close_on_pass`), arming is a single zero-deploy POST. If not, add a
small reviewed check to the registry (one deploy), then arm. Note: a check
that sets `close_on_pass` performs a **scoped issue mutation** — it closes
the action's own `report_to_issue` (never an arbitrary issue) — so treat
arming a closing check as a privileged action, not a read-only probe.
- **Bespoke server-side logic** (multi-step, custom data shape) → a self-armed
**oneshot** (ADR-046; one `.ts` + boot-arm + deploy).
- Full decision matrix + the exact arm HTTP shape: [`inngest-oneshot-and-reminder-patterns.md`](../../../../knowledge-base/engineering/operations/runbooks/inngest-oneshot-and-reminder-patterns.md).
2. **Periodic verification needing a secret already wired to the follow-through
sweeper** (`scheduled-followthrough-sweeper.yml` passes an allowlist; e.g. the
job exposes the repo secret `SENTRY_ACTIONS_RO_TOKEN`, the org-level read-only
`actions-read-prd` Sentry integration, under that same name)
→ a vetted follow-through script (under the repo's
`followthroughs` dir) + a follow-through directive. No new workflow, no
Inngest deploy.
3. **Pure-GH ops** (no prd secrets — a skill reading public state, a Dependabot-
like job). Only this class falls through to Step 0a and GHA-cron generation.
Announce the routing decision. If routing to Inngest/sweeper, do NOT proceed to
Step 0a — point the operator at the substrate and stop. The
`new-scheduled-cron-prefer-inngest` hook is now the BACKSTOP (it should rarely
fire because this gate routes upstream); its `gate-override` is reserved for a
genuinely pure-GH op and the override comment must assert "no prd secrets".
**Step 0a: Mode detection (recurring vs one-time)**
Inspect `$ARGUMENTS`:
- If both `--cron` and `--once` are present → error: `Cannot specify both --once and --cron`. Stop.
- If neither `--cron` nor `--once` is present → error: `Specify either --once <YYYY-MM-DD> or --cron <expression>`. Stop. (No silent default — the operator must declare intent.)
- If `--once` is present → **one-time mode**. Validate per Step 0c below, then skip to Step 2 → Step 3b.
- If `--cron` is present → **recurring mode**. Continue with Step 0b below.
**Step 0b: Check arguments (recurring mode)**
If `$ARGUMENTS` contains `--name`, `--skill`, `--cron`, and `--model` flags, extract values directly and skip to Step 2. Validate each value using the same rules below. If any required flag is missing, proceed to Step 1 for the missing parameters only. Optional flags: `--timeout` (minutes, default 30) and `--max-turns` (default 30).
**Step 0c: Check arguments (one-time mode)**
For `--once`, all five flags are MANDATORY. There is no AskUserQuestion fallback (Step 1 is recurring-only). Operator-supplied values are required so the workflow file is self-documenting at `delete`/cleanup time. Each value MUST match its regex EXACTLY before substitution into the YAML — these regexes are the load-bearing defense against shell/YAML injection at fire time.
If ANY regex below fails, emit `ERROR: --<flag> value '<value>' does not match required pattern <regex>` to stderr and stop.
- `--once` (mode flag, no value)
- `--at <YYYY-MM-DD>` — strict ISO date pattern `^\d{4}-\d{2}-\d{2}$`. Default time = 09:00 UTC. Natural-language dates (`"2 weeks from now"`, `tomorrow`) and ISO datetimes (`2026-05-17T03:00`) are intentionally rejected. Validate with:
```bash
python3 -c "
import re, sys
from datetime import datetime, timezone, timedelta
if not re.fullmatch(r'\d{4}-\d{2}-\d{2}', sys.argv[1]):
sys.exit(f\"ERROR: --at value '{sys.argv[1]}' does not match required pattern ^\\\d{{4}}-\\\d{{2}}-\\\d{{2}}\$\")
d = datetime.fromisoformat(sys.argv[1]).replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
if d.date() < now.date():
sys.exit('ERROR: --at date is in the past')
if (d - now) > timedelta(days=50):
sys.exit('ERROR: --at date is more than 50 days out (GHA auto-disables workflows after 60d inactivity; 10d margin)')
" '<AT_VALUE>'
```
- `--issue <N>` — GitHub issue number, pattern `^[1-9][0-9]{0,8}$` (positive integer, no leading zeros, ≤9 digits).
- `--comment <id>` — GitHub comment ID, pattern `^[1-9][0-9]{0,18}$` (positive int64, no leading zeros). Look it up via `gh api repos/<OWNER>/<REPO>/issues/<N>/comments --jq '.[] | "\(.id) \(.user.login) \(.created_at): \(.body | .[0:80])"'` if the operator needs to find it.
- `--name <kebab-case>` — schedule name, pattern `^[a-z][a-z0-9-]{0,49}$` (lowercase, leading letter, ≤50 chars). Reject if `.github/workflows/scheduled-<name>.yml` already exists with the exact error:
```text
ERROR: .github/workflows/scheduled-<name>.yml already exists. Pick a different --name (no --force flag in v1).
```
**Comment integrity capture (D5 — comment-author-pin defense):**
The fire-time agent fetches the task spec from the referenced comment body. If the comment is editable between authoring and fire, an attacker with comment-edit access can rewrite the task — single-user incident vector. Pin the comment at create time:
```bash
COMMENT_META=$(gh api "repos/${REPO_OWNER_AND_NAME}/issues/comments/${COMMENT_ID}" \
--jq '"\(.user.login)\t\(.created_at)\t\(.updated_at)"' 2>/dev/null) || {
echo "ERROR: --comment <id> not found or not accessible" >&2; exit 1; }
EXPECTED_AUTHOR=$(echo "$COMMENT_META" | cut -f1)
COMMENT_CREATED_AT=$(echo "$COMMENT_META" | cut -f2)
COMMENT_UPDATED_AT=$(echo "$COMMENT_META" | cut -f3)
if [[ "$COMMENT_CREATED_AT" != "$COMMENT_UPDATED_AT" ]]; then
echo "ERROR: comment $COMMENT_ID has been edited (created_at != updated_at)." >&2
echo "Re-post the task spec as a fresh comment and pass the new --comment <id>." >&2
exit 1
fi
```
Both `$EXPECTED_AUTHOR` and `$COMMENT_CREATED_AT` are embedded into the workflow's `env:` block (Step 3b) and re-checked by the fire-time pre-flight (D5) — see Step 3b below.
**Default-branch warning:**
If the current branch is not the default branch (`gh repo view --json defaultBranchRef --jq .defaultBranchRef.name`), print a **WARNING** (not an error):
```text
WARNING: GHA cron triggers fire only from workflows on the default branch.
Merge this workflow before <FIRE_DATE> or it will not fire.
```
If any one-time validation fails, stop. Do not write the file.
**Step 1: Collect inputs (recurring mode only)**
One-time mode does not pass through Step 1 — all flags are mandatory at the command line so the resulting workflow is self-documenting. For recurring mode, use the **AskUserQuestion tool** to gather missing inputs one at a time:
1. **Schedule name** — A short kebab-case name (e.g., `weekly-security-audit`). Validate: lowercase letters, numbers, and hyphens only. Check that `.github/workflows/scheduled-<name>.yml` does not already exist.
2. **Skill to run** — Which Soleur skill to invoke. Show available skills:
```bash
ls plugins/soleur/skills/*/SKILL.md | sed 's|plugins/soleur/skills/||;s|/SKILL.md||' | sort
```
Present the list and let the user choose. Prefix with `soleur:` in the generated prompt.
3. **Cron expression** — A 5-field POSIX cron expression (minute hour day-of-month month day-of-week).
- Reject named values (`MON`, `JAN`) — GitHub Actions does not support them
- Reject schedules more frequent than every 5 minutes
- Note: GitHub Actions cron has ~15-minute variance in trigger timing
4. **Model** — Which Claude model to use. Default: `claude-sonnet-5-5` (good balance of cost and capability). Accept any valid Anthropic model identifier.
5. **Timeout (minutes)** — Job-level timeout to prevent runaway billing. Default: 30. Validate: positive integer, minimum 5 minutes.
6. **Max turns** — Maximum number of agent turns before stopping. Default: 30. Validate: positive integer, minimum 5 turns. Budget formula: plugin overhead (~10 turns) + task tool calls + error buffer (~5). Multi-platform data collection or PR-based persist workflows typically need 40-50 turns.
**Step 2: Resolve action SHAs**
View on GitHub