W tej lekcji8 sekcji
Lekcja 2412 min czytania

Harmonogramy i cron: zadania cykliczne bez zalewu wiadomości

Które zadania w ogóle nadają się na cykl i jak nie utonąć w powiadomieniach, rozstrzygnęła lekcja piąta. Ta jest o dwóch mechanizmach, które to wykonują — cronie i heartbeacie — i o tym, czym się różnią.

Cron OpenClawa mieszka w procesie Gateway

Cron działa wewnątrz procesu Gateway, a nie wewnątrz modelu i nie w systemie operacyjnym. Definicje zadań, ich stan i historia przebiegów leżą we współdzielonej bazie SQLite, więc przeżywają restart Gateway. Konsekwencja jest jedna i twarda: żeby cokolwiek się odpaliło, Gateway musi działać. Laptop zamknięty na noc to zadanie, które nie wystartuje.

Scheduler jest domyślnie włączony — cron.enabled stoi na true, a wyłączysz go wartością false albo zmienną OPENCLAW_SKIP_CRON=1. Ładunki skryptowe i skrypty warunkowe wymagają dodatkowo cron.triggers.enabled, domyślnie wyłączonego. Miejsce zapisu zadań ustawia cron.store, a cron.sessionRetention domyślnie trzyma sesje 24 godziny; wartość ustawisz ręcznie, gdy chcesz je zachować dłużej.

Każde wykonanie zadania cron zakłada rekord w rejestrze zadań w tle. Podejrzysz je przez openclaw tasks list, a openclaw tasks audit pokazuje problemy. Rekordy zakończone trzymają się siedem dni, a te oznaczone jako „lost” — dobę, po czym znikają automatycznie. To Twoja ścieżka audytu, kiedy ktoś pyta, dlaczego raport przyszedł dwa razy.

Praktyczny skutek: crontab -l tego zadania nie pokaże, a Gateway wyłączony na noc to zadanie, które nie wystartuje wcale. Jeśli potrzebujesz zadania odpornego na wyłączony komputer, potrzebujesz serwera, na którym Gateway stoi całą dobę.

Jawne ustawienie schedulera i wyłączonych wyzwalaczy w openclaw.json. Klucz triggers zostaw na false, dopóki nie potrzebujesz ładunków skryptowych.
{
  cron: {
    enabled: true,
    triggers: { enabled: false }
  }
}

Trzy sposoby zapisania terminu

Termin zadania zapisujesz jedną z trzech flag i różnią się one bardziej, niż wygląda. --at ustawia jednorazowy znacznik czasu — pełny zapis ISO 8601 albo wartość względną w rodzaju 20m. --every ustawia stały odstęp: 10m, 1h, 1d. --cron przyjmuje wyrażenie pięcio- lub sześciopolowe, czyli klasykę, którą znasz z systemu.

Zadanie jednorazowe po udanym wykonaniu kasuje się samo, chyba że dodasz --keep-after-run. Zadania cykliczne zostają. Do tego dochodzą dwa wyzwalacze zdarzeniowe: --on-exit odpala się, gdy obserwowana komenda zakończy pracę, a --stream-command reaguje na paczki linii z nadzorowanego procesu.

Reszta flag opisuje, co i gdzie ma trafić. --name nadaje nazwę czytelną dla człowieka, --session wybiera tryb sesji, --channel i --to wskazują kanał oraz adres, a --announce dostarcza tekst, jeśli agent sam go nie wysłał. Adresy mają format zależny od kanału: Slack i Discord używają zapisu channel:<id> albo user:<id>, a Telegram przyjmuje sam identyfikator z opcjonalnym sufiksem tematu.

Zadania nie musisz kasować, żeby przestało działać. openclaw cron disable <jobId> wyłącza je z zachowaniem definicji i historii, a enable przywraca. Przy sprzątaniu po eksperymencie to różnica między porządkiem a stratą konfiguracji, którą składałeś pół godziny.

Zadanie cykliczne w poniedziałki o 9:00, w świeżej sesji, z dostarczeniem wyniku na kanał Slacka.
openclaw cron create "0 9 * * 1" \
  "Podsumuj zgloszenia z weekendu" \
  --name "Poniedzialkowy brief" \
  --session isolated \
  --announce --channel slack --to "channel:C1234567890"

Strefy czasowe: tu ludzie tracą godzinę

Zasady są trzy i każda ma inny wynik domyślny. Znacznik czasu podany bez strefy traktowany jest jako UTC. Wyrażenie cron bez flagi --tz używa strefy hosta, na którym stoi Gateway. Flaga --tz przyjmuje nazwę ze bazy IANA i działa wyłącznie z --at oraz --cron — z --every nie jest poprawna.

Policzmy skutek na konkretnym przypadku. Gateway stoi na VPS-ie ustawionym na UTC, Ty siedzisz w Warszawie. Wyrażenie „0 8 * * 1-5” bez --tz odpali raport o 8:00 czasu serwera, czyli o 10:00 u Ciebie latem i o 9:00 zimą. Zadanie samo przesunie się o godzinę przy zmianie czasu, a nikt tego nie zauważy przez tydzień.

Druga pułapka siedzi w samym wyrażeniu. Gdy podasz jednocześnie dzień miesiąca i dzień tygodnia, warunki łączą się alternatywą, nie koniunkcją — zadanie odpali się, jeśli spełniony jest którykolwiek. To standardowe zachowanie crona i akurat tutaj dokumentacja mówi wprost, ale wielu ludzi trafia na nie dopiero po pierwszym niespodziewanym uruchomieniu.

Przy wielu zadaniach o tej samej godzinie przydaje się rozłożenie startów. --stagger ustawia okno rozrzutu, a --exact wymusza dokładny czas. Jeśli o 9:00 masz sześć raportów, każdy budzący model, rozrzut na kilka minut oszczędzi Ci kolejki i limitów.

Ta sama godzina niezależnie od strefy hosta i od zmiany czasu. --tz działa z --cron oraz --at, ale nie z --every.
openclaw cron create "0 8 * * 1-5" "Raport poranny" --tz Europe/Warsaw

Cztery typy ładunku i ich cena

Typy ładunku wykluczają się wzajemnie i wybierasz dokładnie jeden. --system-event wstawia zdarzenie do sesji głównej i sam z siebie nie wywołuje modelu. --message uruchamia turę agenta, czyli kosztuje tokeny. --command albo --command-argv uruchamia proces powłoki bez udziału modelu. --script odpala kod bezgłowo i wymaga włączonego cron.triggers.enabled.

Różnica w cenie jest tu ostra i widać ją już przy projektowaniu. Zadanie „sprawdź, czy plik się pojawił” zapisane jako --command nie kosztuje ani jednego tokena. To samo zadanie zapisane jako --message uruchamia model za każdym razem. Do prostych sprawdzeń używaj komend, a model dokładaj dopiero tam, gdzie trzeba coś ocenić albo napisać.

Bezpieczeństwo ładunków skryptowych to poważna sprawa, którą dokumentacja stawia wprost: skrypty i skrypty warunkowe działają bezgłowo z pełną polityką narzędzi agenta będącego właścicielem, łącznie z exec. Włączaj cron.triggers.enabled tylko wtedy, gdy ufasz każdemu, kto może zakładać zadania. To nie jest ustawienie do przełączenia z ciekawości.

Limity czasowe są różne dla każdego typu. Komendy mają domyślny limit dziesięciu minut, sterowany flagą --timeout-seconds. Skrypty mają domyślnie 300 sekund przy suficie 900 i budżet 50 wywołań narzędzi przy suficie 200. Zadania w sesji izolowanej ogranicza dodatkowo własny licznik crona: po sześćdziesięciu minutach przebieg jest ubijany.

Turę agenta możesz dodatkowo przystrzyc. --model podmienia model dla tego jednego zadania, --thinking ustawia poziom rozumowania, --light-context pomija wstrzykiwanie plików startowych workspace, a --tools ogranicza listę narzędzi. Zadanie, które ma tylko czytać i wysyłać, nie musi mieć dostępu do wszystkiego.

Sesje: main, isolated, current i nazwana

Tryb sesji ustawiasz flagą --session. To on decyduje o koszcie i o tym, co zadanie pamięta. main uruchamia zadanie w osobnym pasie sesji głównej — do przypomnień i zdarzeń systemowych. isolated tworzy świeżą sesję o kluczu cron:<jobId>: każdy przebieg startuje bez historii, więc to domyślny wybór do raportów i prac porządkowych.

current wiąże zadanie z sesją aktywną w momencie tworzenia, a session:<id> podpina je do sesji nazwanej, trwałej między przebiegami. Ta ostatnia opcja ma sens tam, gdzie zadanie buduje coś przez wiele tur — na przykład prowadzi tygodniowy dziennik. Wszędzie indziej jest to niepotrzebny bagaż kontekstu.

Wybór trybu przekłada się bezpośrednio na rachunek. Świeża sesja startuje bez historii rozmowy, więc każdy przebieg zaczyna od zera. Sesja główna wciąga cały dotychczasowy kontekst, co przy zadaniu odpalanym co godzinę robi się kosztowne szybciej, niż podpowiada intuicja.

Sesje utworzone przez cron sprzątane są zgodnie z cron.sessionRetention, domyślnie po 24 godzinach. Jeśli chcesz je zachować dłużej, podnieś tę wartość ręcznie — z pełną świadomością, że baza urośnie.

Heartbeat to nie cron i nie zastępuje crona

Heartbeat uruchamia okresową turę agenta w sesji głównej, żeby model mógł zauważyć rzeczy wymagające uwagi bez zasypywania Cię wiadomościami. Konfigurację trzyma agents.defaults.heartbeat albo agents.entries.*.heartbeat. Domyślny odstęp to 30 minut, a przy logowaniu tokenem albo OAuth — godzina. Wartość 0m wyłącza mechanizm.

Umowa co do odpowiedzi jest prosta. Gdy nic nie wymaga uwagi, agent odpowiada HEARTBEAT_OK, a ten token jest wycinany z początku lub końca odpowiedzi w ramach budżetu 300 znaków. Odpowiedź inna niż OK uruchamia dostarczenie alertu. Zamiast tekstu agent może wywołać heartbeat_respond z polem notify, tekstem powiadomienia i opcjonalnym polem scratch, które podmienia notatnik monitora na następny przebieg.

Domyślny cel dostarczenia to „none”, czyli przebieg wykonuje się wewnętrznie i nic nie wychodzi na zewnątrz. Widoczność sterują trzy flagi ustawiane per kanał albo per konto: showOk pokazuje potwierdzenia HEARTBEAT_OK i domyślnie jest wyłączona, showAlerts wypuszcza odpowiedzi inne niż OK, a useIndicator emituje zdarzenia stanu w interfejsie. Gdy wszystkie trzy są wyłączone, przebieg jest pomijany — w logu zobaczysz „heartbeat skipped (alerts-disabled)”.

Najważniejsza różnica wobec crona: heartbeat nie zakłada rekordów w rejestrze zadań w tle. Praca cykliczna, którą chcesz później rozliczyć albo skontrolować, ma mieszkać w zadaniach cron, a nie w notatniku heartbeatu. Notatnik ma zresztą limit 256 KiB, a pusty albo złożony z samych komentarzy powoduje pominięcie z komunikatem „heartbeat skipped (empty-heartbeat-file)”. Sekretów tam nie trzymaj, bo trafia do kontekstu promptu.

Okno aktywności ustawia activeHours z polami start i end w formacie HH:MM oraz opcjonalnym timezone. Start jest włączający, koniec wyłączający, dozwolona jest wartość 24:00, a równych wartości start i end nie ustawiaj — to okno zerowej szerokości. Poza oknem zobaczysz „heartbeat skipped (quiet-hours)”.

Heartbeat w świeżej sesji, bez plików startowych, tylko w godzinach pracy. Trzy ustawienia, które najbardziej ścinają rachunek.
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m",
        isolatedSession: true,
        lightContext: true,
        activeHours: { start: "08:00", end: "18:00", timezone: "Europe/Warsaw" }
      }
    }
  }
}

Czym różni się zadanie cykliczne od pętli agenta

Agent nie czeka i nie działa w tle między turami. Tura kończy się, model przestaje istnieć, a kontekst zostaje zapisany. Zadanie cykliczne nie jest więc pętlą wewnątrz agenta — jest wywołaniem nowej tury z zewnątrz, przez scheduler siedzący w Gateway. Ta różnica tłumaczy większość nieporozumień z pierwszego tygodnia.

Praktyczny wniosek: instrukcja „sprawdzaj skrzynkę co pięć minut przez najbliższe dwie godziny” wpisana w rozmowie nie zadziała tak, jak myślisz. Model spróbuje zrobić to w jednej turze, spali kontekst i limit narzędzi, a po zakończeniu tury i tak nic nie zostanie. To samo zadanie założone jako cron z --every 5m działa poprawnie i można je wyłączyć jedną komendą.

Dla zadań, których tempo ma zależeć od sytuacji, istnieje sterowanie odstępem. --pacing-min i --pacing-max ustawiają dolną i górną granicę odstępu, a samo zadanie może w trakcie przebiegu zaproponować termin następnego sprawdzenia przez narzędzie cron. Skrypt warunkowy podany przez --trigger-script decyduje, czy ładunek w ogóle ma się odpalić — zwraca pole fire i opcjonalnie wiadomość oraz stan. Wyzwalacze mają wbudowany minimalny odstęp 30 sekund, a każda ocena mieści się w 30 sekundach zegara i najwyżej pięciu wywołaniach narzędzi.

Awarie mają własną mechanikę i dobrze o niej wiedzieć, zanim zaczniesz podejrzewać, że coś umarło. Zadania jednorazowe przy błędach przejściowych korzystają z wbudowanego harmonogramu ponowień, a błąd trwały wyłącza zadanie od razu. Zadania cykliczne wchodzą przy kolejnych błędach na wydłużony odstęp: 30 sekund, 60 sekund, 5 minut, 15 minut, 60 minut. Po udanym przebiegu licznik wraca do zera. Zaległe zadania w sesji izolowanej po starcie Gateway są przeplanowane, a nie odtwarzane wszystkie naraz.

Jak nie zrobić sobie 300 wiadomości na dobę

Zacznij od arytmetyki, bo ona rozstrzyga wszystko. Zadanie z --every 5m odpala się 288 razy na dobę. Heartbeat co 30 minut daje 48 tur. Razem 336 przebiegów, a jeśli pierwsze ma --announce, a drugie włączone showOk, tyle właśnie dostaniesz wiadomości. Nikt nie planuje takiego zalewu — on powstaje z dwóch niewinnych ustawień.

Rachunek tokenowy wygląda podobnie. Heartbeat w pełnej sesji głównej to według dokumentacji rząd stu tysięcy tokenów na przebieg, a przy 48 przebiegach robi się z tego 4,8 miliona tokenów dziennie za samo sprawdzanie, czy coś się dzieje. Ustawienie isolatedSession razem z lightContext ścina pojedynczy przebieg do 2–5 tysięcy tokenów. To jedna zmiana w konfiguracji i dwa rzędy wielkości różnicy.

Dźwignie, które ograniczają hałas, są cztery i każda działa w innym miejscu. --no-deliver zdejmuje awaryjne dostarczanie wyniku, zostawiając agentowi możliwość wysłania wiadomości narzędziem message, gdy naprawdę ma co powiedzieć. showOk wyłączone kasuje potwierdzenia „wszystko w porządku”. activeHours obcina heartbeat do godzin pracy — okno od 8:00 do 18:00 zamienia 48 tur na 20. A --failure-alert-after wraz z --failure-alert-cooldown sprawiają, że o awarii dowiadujesz się raz, a nie przy każdym nieudanym przebiegu.

Diagnostyka mieści się w czterech komendach. openclaw cron list pokazuje zadania włączone, z flagą --all także wyłączone. openclaw cron status daje stan schedulera. openclaw cron runs --id <jobId> --limit 20 wyświetla historię przebiegów. openclaw cron run <jobId> --wait odpala zadanie ręcznie, co oszczędza czekania do rana na sprawdzenie, czy działa.

Komunikaty mają swój słownik i po jego poznaniu przestają być zagadką. „scheduler disabled” to wyłączony scheduler. „heartbeat skipped (quiet-hours)” to pora poza oknem aktywności. „heartbeat skipped (empty-heartbeat-file)” oznacza notatnik złożony z samego szkieletu. „heartbeat skipped (alerts-disabled)” wymaga włączenia przynajmniej jednej z flag showOk, showAlerts albo useIndicator. „unknown accountId” to nieistniejące konto docelowe.

Cztery komendy do codziennej pracy: co jest założone, czy scheduler żyje, co się działo i jak wyciszyć zadanie bez kasowania go.
openclaw cron list --all
openclaw cron status
openclaw cron runs --id <jobId> --limit 20
openclaw cron disable <jobId>

Co warto zapamiętać

  • Cron działa w procesie Gateway, a zadania i historia leżą w SQLite. Gateway wyłączony to zadanie, które nie wystartuje — crontab systemowy o nim nie wie.
  • Znacznik czasu bez strefy jest traktowany jako UTC, a wyrażenie cron bez --tz używa strefy hosta Gateway. Flaga --tz działa z --at i --cron, ale nie z --every.
  • Typy ładunku wykluczają się: --system-event nie wywołuje modelu, --message kosztuje tokeny, --command uruchamia proces bez modelu, a --script wymaga cron.triggers.enabled i działa z pełną polityką narzędzi agenta.
  • Heartbeat domyślnie chodzi co 30 minut (godzinę przy OAuth lub tokenie), odpowiada HEARTBEAT_OK i — w odróżnieniu od crona — nie zakłada rekordów w rejestrze zadań w tle.
  • isolatedSession razem z lightContext ścinają pojedynczy przebieg heartbeatu z rzędu 100 tysięcy tokenów do 2–5 tysięcy. Notatnik monitora ma limit 256 KiB.
  • Zadanie z --every 5m to 288 przebiegów na dobę, heartbeat co 30 minut to kolejne 48. Bez --no-deliver, wyłączonego showOk i activeHours dostaniesz tyle samo wiadomości.

Częste pytania

Co dzieje się z zadaniami, gdy Gateway był wyłączony przez noc?+

Zaległe zadania w sesji izolowanej, które uruchamiają turę agenta, są po starcie Gateway przeplanowane, a nie odtwarzane natychmiast jedno po drugim. Dzięki temu nie dostajesz o poranku dwunastu raportów naraz. Zadania jednorazowe, które minęły, obsługiwane są według własnych reguł ponawiania — sprawdzisz to komendą openclaw cron runs.

Do monitorowania skrzynki lepszy cron czy heartbeat?+

Zależy, czy potrzebujesz śladu. Heartbeat nadaje się do okresowej świadomości w kontekście sesji głównej i nie tworzy rekordów zadań. Cron nadaje się do pracy o dokładnym terminie albo wykonywanej w izolacji i przy każdym przebiegu zakłada rekord, który potem obejrzysz przez openclaw tasks list. Jeśli ktoś kiedyś zapyta „dlaczego to się odpaliło o trzeciej w nocy”, chcesz mieć cron.

Zadanie nie wystartowało. Od czego zacząć diagnozę?+

Od openclaw cron status, żeby wykluczyć wyłączony scheduler, a potem openclaw cron runs --id <jobId> --limit 20. Komunikat „scheduler disabled” oznacza wyłączony cron, komunikaty z rodziny „heartbeat skipped” mają dopisany powód w nawiasie, a „unknown accountId” wskazuje nieistniejące konto docelowe. Jeśli chcesz sprawdzić samo działanie, odpal zadanie ręcznie przez openclaw cron run <jobId> --wait.

Czy mogę mieć jedno zadanie odpalane co godzinę w polskiej strefie czasu?+

Tak, ale nie flagą --every, bo ta nie przyjmuje --tz. Użyj wyrażenia cron z odpowiednim polem godzin i dodaj --tz Europe/Warsaw. Różnica jest widoczna przy zmianie czasu: --every liczy stały odstęp niezależnie od kalendarza, a wyrażenie cron ze strefą trzyma się godziny zegarowej.

Źródła danych z tej lekcji

Krok 2Ćwiczenie do wykonaniaTeoria bez ćwiczenia zostaje wiedzą, której nie używasz.

Chcesz to przećwiczyć na żywo?

Uruchom własnego agenta i wypróbuj to, o czym przed chwilą przeczytałeś. Pierwsza subskrypcja pakietu Premium ma 5 dni za darmo, a kurs czytasz dalej niezależnie od tego.

Zobacz plany