The cross-repo scheduling system (sch_* tables, matrx-scheduler). Use when touching features/scheduling/**, /schedules or /administration/scheduling, migrations/sch_*.sql, a scheduled agent, cron, trigger, lease, or claim, or debugging ghost tasks, missed fires, double-runs, or stuck claims.
Scanned 10/3/2026
npx -y skills add armanisadeghi/ai-matrx --skill scheduling --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Scheduling?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/armanisadeghi-scheduling)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: scheduling
description: "The cross-repo scheduling system (sch_* tables, matrx-scheduler). Use when touching features/scheduling/**, /schedules or /administration/scheduling, migrations/sch_*.sql, a scheduled agent, cron, trigger, lease, or claim, or debugging ghost tasks, missed fires, double-runs, or stuck claims."
---
# Scheduling
Cross-repo scheduling spine. Three live systems share four Supabase tables
(`sch_task`, `sch_agent_task`, `sch_trigger`, `sch_run`):
- **matrx-frontend** — control plane (this repo). Users create/observe.
- **aidream + matrx-scheduler** — server-side executor.
- **matrx-extend (Chrome extension)** — browser-context executor for
context-match triggers.
Spec: [`docs/SCHEDULING.md`](../../../docs/SCHEDULING.md). FEATURE doc:
[`features/scheduling/FEATURE.md`](../../../features/scheduling/FEATURE.md).
---
## 30-second mental model
```
sch_task kind='agent' (today). Spine: title, surfaces[], next_due_at.
↳ sch_agent_task 1:1 extension: agent_id, prompt, variables, conversation_id.
sch_trigger when it fires (one-shot, interval, cron, heartbeat, context-match, …). v1 = one trigger per task.
sch_run each execution attempt. Lease via claim_token + claim_expires_at.
```
RLS: owner-or-`is_super_admin()` on all four tables. Service_role is
NEVER used for user-initiated writes; admin pages use the regular browser
client and rely on RLS for cross-user reads.
Atomicity: a **partial unique index** `sch_run_unique_active_per_task`
forces at most one active run per task. Concurrent claimers lose the race
to the index, not the application.
DB invariants: `sch_task.next_due_at = MIN(sch_trigger.next_due_at)` is
maintained by trigger `sch_trigger_cascade_next_due_at`. Don't UPDATE
`sch_task.next_due_at` directly — write to `sch_trigger` and let the
cascade flow.
---
## When to use this skill
- Adding or modifying a `sch_*` migration.
- Changing the trigger taxonomy or adding a new trigger type.
- Building / fixing FE pages under `/schedules` or
`/administration/scheduling`.
- Editing the Python `matrx-scheduler` package or its router on aidream.
- Wiring a new executor surface (e.g. a desktop app scanner).
- Debugging "ghost tasks" (queued runs not getting picked up), missed
fires, double-runs, stuck claims.
## Where each thing lives
| Layer | Files |
|---|---|
| DB migrations | `migrations/sch_*.sql` |
| FE types | `features/scheduling/types.ts` |
| FE Supabase façade (reads) | `features/scheduling/service/queries.ts` — joined task reads + `taskDetailToAgendaTask` reshape + residual `updateAgentTaskFields` write (the one `sch_agent_task` field-patch HTTP doesn't cover yet). ONLY place that `.from('sch_*')`. |
| FE HTTP client (writes + compute) | `features/scheduling/service/schedulerClient.ts` — typed wrapper over aidream's `/scheduler/*` (16 endpoints): tasks/triggers/runs CRUD, cron validate, preview-fires, compute-next-due-at, scanner status. Primary path for all user-facing writes. |
| FE TS twin of next_due | `lib/scheduler-client/next-due.ts` — used by `CronForm` and cron-tester for live-typing previews; also exported to non-HTTP clients (Chrome extension, edge functions). |
| FE Redux slices | `features/scheduling/redux/{tasks,runs}/` |
| FE hooks | `features/scheduling/hooks/` — useScheduledTasks (+ list realtime), useTaskDetail, useTaskRuns, useRunStream |
| FE list / detail / form | `features/scheduling/components/{list,detail,form}/` |
| User routes | `app/(authenticated)/schedules/` |
| Admin routes | `app/(authenticated)/(admin-auth)/administration/scheduling/` |
| Admin service | `lib/services/scheduling-admin-service.ts` (browser client + `is_platform_admin()` RLS) |
| Python package | `aidream/packages/matrx-scheduler/matrx_scheduler/` |
| Python router | `aidream/aidream/api/routers/scheduling.py` |
| Per-request supabase client | `aidream/aidream/services/scheduling/per_request_client.py` |
| matrx-ai bridge | `aidream/aidream/services/scheduling/agent_runner_adapter.py` |
| Capture emitter | `aidream/aidream/services/scheduling/capture_emitter.py` |
## Non-negotiable rules
1. **NEVER call `.from('sch_*')` outside `features/scheduling/service/queries.ts` or `lib/services/scheduling-admin-service.ts`.** The select shape needs to stay consistent; row→AgendaTask flattening lives in one place. New user-facing writes go through `service/schedulerClient.ts` — don't add parallel Supabase writes to `queries.ts`.
2. **NEVER use the aidream service_role supabase singleton in scheduling routes.** Use `make_user_supabase_client(jwt)` so RLS binds to the caller.
3. **NEVER insert into `sch_run` directly from the FE.** Use the `sch_enqueue_manual_run(p_task_id)` RPC — it stamps `user_id` from the task row, sets `status='queued'`, and refuses to spoof other fields.
4. **NEVER UPDATE `sch_task.next_due_at` from application code.** Write to `sch_trigger.next_due_at`; the DB trigger cascades.
5. **NEVER bypass the RLS gate by tightening `is_super_admin()` to `is_platform_admin()` (or vice-versa).** The two tiers exist deliberately — scheduling uses `is_super_admin()` to match the route layout's `requireSuperAdmin` gate.
## Common tasks
### Add a new trigger type (e.g. `event`)
1. Add to the `TriggerType` discriminated union in
`features/scheduling/types.ts` and the Python `models.py`.
2. Add the case to `computeNextDueAt` in `lib/scheduler-client/next-due.ts`
(TS twin used by previews) AND `compute_next_due_at` in the Python
`next_due` module (authoritative for writes).
3. Add a subform component in
`features/scheduling/components/form/triggers/` and wire it in
`ScheduleForm.tsx`.
4. Add validation in `features/scheduling/utils/validation.ts`.
5. Add a Jest + pytest case for the new type.
6. Update the DB CHECK on `sch_trigger.type` if you're adding a brand-new
value — the existing 8 values already cover everything in the spec.
### Add a new executor surface (e.g. `desktop`)
1. Add the value to:
- the FE `SURFACE_VALUES` constant
- the `sch_task_surfaces_chk` CHECK constraint (migration)
- the `SURFACE_META` map for the picker tooltip
2. In the new surface's code (e.g. Tauri desktop app), call
`matrx_scheduler.configure(surface="desktop", ...)` and
`start_scanner()`. Each surface has its own scanner; the partial
unique index ensures no double-runs across surfaces.
### Debug a "ghost task" (queued run that never executes)
1. Confirm the row's `surface` value — manual fires set it to `NULL`
so any surface can claim; if it's hardcoded to a value no executor is
running on, it'll sit forever.
2. Check `sch_task.surfaces` — does it contain `'any'` or a value that
matches an online scanner?
3. Check the scanner status: GET `/scheduler/status` (admin only), or
the FE admin page at `/administration/scheduling/scanner-health`.
4. Confirm `AIDREAM_SCHEDULER=1` is set on the host process you expect to
execute the run.
### Debug a "stuck claimed" run
1. Visit `/administration/scheduling/orphan-leases`.
2. The scanner sweeps any `status IN (claimed,running) AND
claim_expires_at < now()` to `failed` on every tick — if a row sits
in claimed for >60s after expiry, something's broken upstream.
3. Manual remediation: use the page's "Mark failed" button (RPC-less,
direct UPDATE — gated by RLS owner-or-super-admin).
## Verification
After changes, always:
- Python: `uv run pytest packages/matrx-scheduler/tests` (25 tests, must all pass).
- TypeScript: `pnpm exec jest features/scheduling/utils/__tests__/`
(triggerHumanize + validation).
- Type check: `pnpm tsc --noEmit` clean across the feature.
- Smoke: create a schedule via the form, confirm it lands in all 3
tables (use Supabase MCP `execute_sql` to inspect).
- For Python execution changes: enable `AIDREAM_SCHEDULER=1`, create an
interval task on `surfaces=['server']`, watch logs for "scheduler.run
requested" and the resulting `sch_run` row transition.
## Related skills
- `protected-resources` — scheduling is NOT a protected resource (no
audit log, no SECURITY DEFINER mutation gate), but if scheduling ever
grows admin-level operations that warrant audit, follow that pattern.
- `connect-matrx-extend` — relevant when adding `context-match` UX or
cross-repo run-pickup support.
- `window-panels` — for future "Quick Schedule" overlay (v1.5).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!