# Planificator (/ro/docs/api/gateway/schedules)

Sarcini agent, comenzi terminal, webhook-uri și lanțuri programate, rulate pe oricare PC, telefon, ceas sau VM cloud al tău — cron sau cuvinte, aprobări, reîncercări, failover și stare live.

Planificatorul codai rulează sarcini după un orar pe toate dispozitivele tale. Cloud-ul deține fiecare programare și decide când e scadentă o rulare; gazdele tale (aplicația desktop, `codaid` pe servere și VM-uri, telefonul) preiau rulările, le execută și raportează rezultatul. O singură listă, un singur istoric și aceleași aprobări în hub, pe desktop, telefon și ceas.

## Ce poate rula [#ce-poate-rula]

- **agent** — o sarcină agent codai (`prompt`, opțional `skill`, `permissions` read / edit / pr).
- **shell** — o comandă terminal pe o gazdă (`command`, `cwd`, `env_names`, `shell`). Valorile secrete nu părăsesc gazda: programarea doar numește variabilele.
- **http** — un webhook HTTPS (`method`, `url`, `headers`, `body`, `expect_status`).
- **chain** — rulează alte programări în ordine (`steps`, `stop_on_failure`).
- **phone_action** — notificare, pornește un agent, deschide o aplicație, comută Wi-Fi sau Nu deranja pe un telefon.

## Când [#când]

`trigger` este o expresie cron cu fus orar (5 sau 6 câmpuri, semantica zilelor din vixie-cron), un `at` unic, un `interval`, un `event` de declanșator sau `after` altă programare (la succes, eșec sau mereu). `POST /v1/schedules/preview` transformă cuvinte în română sau engleză („în zilele lucrătoare la 9”, "every day at 3") în cron și întoarce următoarele cinci rulări plus un avertisment când o rulare cade într-o fereastră de schimbare a orei. Rulările ratate (gazdă adormită, întrerupere) urmează `misfire`: `skip`, `once` (implicit) sau `all_within` în `misfire_window_min`.

## Unde [#unde]

`target` alege o singură gazdă, orice gazdă cu toate etichetele `tags`, `any` gazdă online, `cloud`-ul codai sau un `cloud_vm` al unui proiect. O rulare `cloud_vm` (shell sau http) ajunge la daemonul unuia dintre mediile tale cloud codai din acel proiect; dacă niciunul nu e online, planificatorul pornește mediul oprit folosit cel mai recent (e nevoie de soldul pentru o oră în portofel), iar timeout-ul de inactivitate al mediului îl oprește din nou după rulare. `failover` listează seturi de etichete de încercat când ținta e offline. O gazdă preia o rulare cu un lease de 60 de secunde (`POST /v1/scheduler/claim`, `FOR UPDATE SKIP LOCKED`, deci două gazde nu iau niciodată aceeași rulare), îl menține cu heartbeat și raportează rezultatul. Un lease expirat devine o încercare nouă pe care o poate prelua altă gazdă. Cu `native: true` și o singură gazdă țintă, aplicația desktop oglindește programarea și în planificatorul sistemului de operare (Task Scheduler, launchd sau timere systemd), ca să ruleze chiar dacă codai nu e pornit.

## Siguranță [#siguranță]

- **Comenzile shell au nevoie de aprobarea gazdei.** O programare shell păstrează sha256 al `shell, cwd, command, nume variabile`. Fiecare gazdă îți arată o dată comanda exactă și aprobă acel hash (`POST /v1/schedules/{id}/approve-command`); orice modificare schimbă hash-ul și șterge toate aprobările.
- **Aprobare pe rulare.** `policy.approval` = `always` sau `outside_hours` (cu `quiet_hours`) pune rulările în `awaiting_approval`; primești o notificare pe fiecare dispozitiv, inclusiv pe ceas, și aprobi sau sari de pe oricare.
- **Limite.** `policy.max_runs_per_hour`, bugete pe rulare și pe zi (micro-USD), `overlap` (`skip`, `queue`, `kill`), `timeout_s` și `retries` cu `retry_backoff_s`.
- **Alerte.** Eșecurile (`alert_on_fail`) și rulările pe care nu le-a pornit nimeni în `late_after_s` trimit o notificare cu link direct către rulare.

## Pauză, zile sărite și control al zgomotului [#pauză-zile-sărite-și-control-al-zgomotului]

- **Mod vacanță.** `POST /v1/schedules/pause` (opțional `until`) oprește crearea rulărilor pentru toate programările; `POST /v1/schedules/resume` o încheie, iar `backfill: true` dă fiecărei programări care a ratat sloturi exact o rulare de recuperare.
- **Zile sărite.** `policy.skip_days` sare peste sărbătorile legale din România (`holidays: 'RO'`, inclusiv Paștele și Rusaliile ortodoxe), zile ale săptămânii alese sau date exacte, în fusul orar al declanșatorului. Sloturile sărite nu se creează deloc.
- **Bugete și pauză automată.** Când rulările de azi ating `policy.budget_micro_usd_per_day`, rulările noi se sar, cu o singură alertă pe zi. După `policy.pause_after_failures` eșecuri consecutive (implicit 3) programarea se dezactivează singură și te anunță.
- **Notificări.** `notify` = `on_failure` (implicit), `on_change` (doar când ieșirea normalizată diferă de ultimul succes: orele și id-urile sunt ignorate), `always` sau `never`.
- **Șabloane.** `GET /v1/schedules/templates` listează programări gata făcute, de exemplu rezumatul săptămânal al transcrierilor (sumar, decizii, acțiuni, trimis pe email în fiecare vineri la 17:00).

## Monitoare heartbeat [#monitoare-heartbeat]

Pentru joburi pe care codai nu le rulează (un crontab pe un VM, un job CI, un job Cloud Scheduler), creează un monitor cu `POST /v1/heartbeats` `{ name, period_s, grace_s }`. Răspunsul conține `ping_url` o singură dată. Jobul îl apelează:

- `GET|POST /ping/<token>` la succes, `/ping/<token>/start` la pornire, `/ping/<token>/fail` la eșec sau `/ping/<token>/<cod de ieșire>`.

Lipsa unui succes în `period_s + grace_s` (sau o pornire fără succes în toleranță) marchează monitorul `down` și alertează o dată toate dispozitivele; următorul succes îl marchează `up` și îți spune că și-a revenit.

```bash
# crontab: backup, apoi raportează codul de ieșire
0 3 * * * /srv/backup.sh; curl -fsS -m 10 https://ai.codai.ro/ping/<token>/$?
```

<OpenAPIPage document="./openapi/ro/gateway.yaml" operations={[{path: '/v1/schedules/pause',method: 'post'},{path: '/v1/schedules/resume',method: 'post'},{path: '/v1/schedules/templates',method: 'get'},{path: '/v1/schedules/templates',method: 'post'},{path: '/v1/heartbeats',method: 'get'},{path: '/v1/heartbeats',method: 'post'},{path: '/v1/heartbeats/{id}',method: 'patch'},{path: '/v1/heartbeats/{id}',method: 'delete'}]} />

## Stare live [#stare-live]

`GET /v1/schedules/stream` trimite un eveniment `schedule` când se schimbă o programare sau o rulare își schimbă starea pe orice dispozitiv, așa că fiecare ecran se actualizează într-o secundă.

```ts
import { Codai } from 'codai-sdk';

const codai = new Codai({ apiKey: process.env.CODAI_API_KEY! });
const preview = await codai.schedules.preview({ text: 'every weekday at 9', tz: 'Europe/Bucharest' });
const backup = await codai.schedules.create({
  name: 'Nightly backup',
  kind: 'shell',
  spec: { command: 'restic backup ~/work' },
  trigger: { type: 'cron', expr: '0 3 * * *', tz: 'Europe/Bucharest' },
  target: { type: 'tags', tags: ['pc'] },
  retries: 2,
});
for await (const e of codai.schedules.stream()) console.log(e.op, e.state);
```

```python
from codai import Codai

codai = Codai(api_key="codai_...", device="7f0e3c1a-...")
job = codai.schedules.executor.claim()  # host side; None when there is nothing to run
if job:
    codai.schedules.executor.result(job["run"]["id"], {"attempt": job["run"]["attempt"], "ok": True, "exit_code": 0})
```

<OpenAPIPage document="./openapi/ro/gateway.yaml" operations={[{path: '/v1/schedules',method: 'get'},{path: '/v1/schedules',method: 'post'},{path: '/v1/schedules/preview',method: 'post'},{path: '/v1/schedules/stream',method: 'get'},{path: '/v1/schedules/{id}',method: 'get'},{path: '/v1/schedules/{id}',method: 'patch'},{path: '/v1/schedules/{id}',method: 'delete'},{path: '/v1/schedules/{id}/run',method: 'post'},{path: '/v1/schedules/{id}/runs',method: 'get'},{path: '/v1/schedules/{id}/approve-command',method: 'post'},{path: '/v1/schedule-runs',method: 'get'},{path: '/v1/schedule-runs/{id}/decision',method: 'post'},{path: '/v1/schedule-runs/{id}/heartbeat',method: 'post'},{path: '/v1/schedule-runs/{id}/result',method: 'post'},{path: '/v1/scheduler/hosts',method: 'get'},{path: '/v1/scheduler/hosts/{id}',method: 'patch'},{path: '/v1/scheduler/claim',method: 'post'}]} />
