Deer Workflow: jak uruchomić grafy agentów na VPS
Instrukcja samodzielnego hostowania Deer Workflow na serwerze Ubuntu. Konfiguracja środowiska Bun, przypinanie wersji pakietów oraz uruchamianie grafów w trybie headless przez systemd.
Co budujesz
Deer Workflow to środowisko uruchomieniowe typu code-first dla grafów agentów: przepływ sterowania znajduje się w pliku TypeScript, który można poddać przeglądowi, a agent kodujący wykonuje tylko te fragmenty, które wymagają oceny. Niniejszy przewodnik opisuje instalację na jednym serwerze Ubuntu VPS, uruchomienie przykładowego grafu w trybie headless za pomocą systemd oraz zapisywanie maszynowo czytelnego strumienia zdarzeń do pliku dziennika, który można przeszukać, gdy uruchomienie zakończy się niepowodzeniem o trzeciej nad ranem.
Komponenty są niewielkie. Bun obsługuje CLI. Jeden CLI agenta kodującego, Codex lub Claude Code, wykonuje pracę modelu. Jeden przypięty pakiet npm zawiera środowisko uruchomieniowe. Jeden plik TypeScript zawiera graf. Usługa i timer systemd uruchamiają go zgodnie z harmonogramem. Większość treści poświęcono elementom, które faktycznie ulegają awarii: zmiennej PATH wewnątrz jednostki systemd, poświadczeniom agenta w sesji bez powłoki logowania oraz przypięciu zależności opublikowanej po raz pierwszy w lipcu 2026.
Wizualny kreator, kod lub po prostu monitowanie agenta
Osoba hostująca samodzielnie automatyzację pracy z modelem wybiera jedną z trzech form, z których każda wiąże się z innymi ryzykami awarii.
Wizualny kreator udostępnia obszar roboczy, bibliotekę węzłów oraz interfejs użytkownika dostępny dla osób nieprogramujących. Jest to istotna zaleta, a rynek jest na tyle nasycony, że istnieje pełne zestawienie alternatyw dla n8n do samodzielnego hostowania, z których można wybierać. Kosztem tego rozwiązania jest fakt, że logika kończy jako dokument JSON wygenerowany przez interfejs. Różnice w takim dokumencie są nieczytelne, więc weryfikacja zmian wymaga otwarcia obszaru roboczego zamiast analizy pliku patch.
Drugą formą jest bezpośrednie monitowanie agenta. Całe zadanie opisuje się w akapicie, pozwalając modelowi na decydowanie o kolejności działań, ponownych próbach i momencie zakończenia pracy. Rozwiązanie to działa do momentu, w którym model podejmie inną decyzję. Nie istnieje tu pojęcie różnicy (diff), ponieważ nie ma żadnego artefaktu: plan istniał wewnątrz konwersacji, która po zakończeniu znika.
Trzecią formą jest orkiestracja w kodzie. Kolejność kroków, rozgałęzienia, ponowne próby oraz obsługa błędów stanowią zwykły kod TypeScript przechowywany w git. Model jest wywoływany wyłącznie w punktach wymagających oceny. Kosztem tego podejścia jest konieczność napisania i utrzymania kodu, co uniemożliwia edycję przez współpracownika nieznającego języka TypeScript.
Zyski i koszty środowiska uruchomieniowego opartego na grafach
- Przejrzysty przepływ sterowania. Graf jest plikiem. Zmiana polityki ponawiania prób jest widoczna w pull request jako trzy zmienione linie, a nie jako przesunięty element graficzny.
- Obsługa błędów w systemie kontroli wersji. Zachowanie w przypadku niepowodzenia czwartego kroku jest zapisane, przetestowane i otagowane wraz z resztą infrastruktury.
- Wymienny agent. Środowisko uruchomieniowe dostarcza adaptery dla Codex, Claude Code oraz Pi. Zmiana modelu wykonującego dany krok wymaga jedynie zmiany importu.
- Monitorowalne wykonanie. Fazy i zdarzenia są generowane przez środowisko jako dane strukturalne, dzięki czemu uruchomienie bez interfejsu pozostawia rekord, który można przeszukiwać.
Ogólna praktyka projektowania pętli, w której działa model, zamiast dopracowywania pojedynczego promptu, to inżynieria pętli, a środowisko uruchomieniowe oparte na grafach jest konkretnym sposobem jej realizacji. Kosztem jest konfiguracja: konieczność instalacji środowiska, uwierzytelnienia agenta CLI, brak interfejsu dla osób nieprogramujących oraz konieczność monitorowania młodej zależności.
Projekt jest nowy, dlatego należy przypiąć wersję
Deer Workflow jest udostępniany na licencji MIT i jest projektem nowym. Według stanu na 19 sierpnia 2026 r. repozytorium zawiera 47 commitów w main. W npm opublikowano trzy wersje: 0.0.1 oraz 0.1.0 w dniu 26 lipca 2026 r., a następnie 0.2.0 w dniu 27 lipca 2026 r. Każda z nich posiada tag git, a zmiany między nimi opisano w pliku changelog. Sekcja Unreleased usuwa już polecenie deer-workflow agent, co oznacza, że main oraz najnowsza opublikowana wersja nie oferują już tego samego CLI.
Nie jest to powód, aby unikać tego projektu. Jest to natomiast powód, aby zainstalować konkretną wersję i mieć świadomość, która z nich została wdrożona.
- Należy instalować dokładną wersję, nigdy zakres.
- Wersję tę należy zapisać w tym samym repozytorium, w którym znajdują się wykresy.
- Po każdej aktualizacji należy ręcznie uruchomić własny wykres, zanim zrobi to timer.
Instalacja Bun i środowiska uruchomieniowego agenta
Wszystkie poniższe operacje należy wykonywać jako zwykły użytkownik z uprawnieniami sudo. Nie należy uruchamiać ich jako root. Interfejsy CLI agentów przechowują dane uwierzytelniające w katalogu domowym użytkownika, który się zalogował, a jednostka systemd musi później działać jako ten sam użytkownik, aby uzyskać do nich dostęp.
sudo apt update
sudo apt install -y curl unzip jq git nodejs npm
curl -fsSL https://bun.com/install | bashInstalator Bun rozpakowuje archiwum zip, dlatego w systemie musi być najpierw obecny unzip. Instalator dopisuje linie PATH do profilu powłoki, a bieżąca powłoka już odczytała ten plik, więc należy otworzyć nową powłokę lub samodzielnie dodać te dwie linie do ~/.bashrc i przeładować konfigurację.
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$HOME/.npm-global/bin:$PATH"bun --versionPolecenie to wyświetla numer wersji. Błąd bun: command not found oznacza, że w bieżącej powłoce brakuje wpisu w zmiennej PATH, a nie że instalacja zakończyła się niepowodzeniem. Przed ponowną instalacją czegokolwiek należy wykonać ls ~/.bun/bin.
Teraz należy zainstalować środowisko uruchomieniowe agenta. Domyślnym rozwiązaniem jest Codex CLI, który instaluje się z poziomu npm. Należy ustawić prefiks npm na poziomie użytkownika, aby instalacja globalna nie wymagała uprawnień root.
npm config set prefix "$HOME/.npm-global"
npm install -g @openai/codex
command -v codex
codexPolecenie command -v codex powinno wyświetlić ścieżkę wewnątrz $HOME/.npm-global/bin. Uruchomienie samego codex otwiera interfejs CLI, w którym należy zalogować się za pomocą konta ChatGPT. Należy wykonać to teraz, dopóki dostępny jest ekran.
Claude Code stanowi alternatywne środowisko uruchomieniowe i posiada własny instalator.
curl -fsSL https://claude.ai/install.sh | bash
claude --versionPoprawna instalacja wyświetla wersję, taką jak 2.1.211 (Claude Code). Należy raz uruchomić claude, aby się zalogować. Jest to ten sam typ procesu, z takim samym dostępem do plików, jak w przypadku każdego innego hostowanego agenta, dlatego uwagi dotyczące konta oraz zabezpieczeń zawarte w uruchamianie agenta programistycznego na VPS mają tutaj pełne zastosowanie.
Instalacja Deer Workflow i przypięcie konkretnej wersji
bun install --global @deerwork-ai/deer-workflow@0.2.0
command -v deer-workflowcommand -v wyświetla ścieżkę bezwzględną, zazwyczaj /home/<your user>/.bun/bin/deer-workflow. Należy ją zapisać w bezpiecznym miejscu. Jednostka systemd nie może korzystać z samej nazwy.
Wersję należy zachować w poleceniu instalacyjnym. Pominięcie @0.2.0 powoduje instalację najnowszej dostępnej wersji w dniu uruchomienia polecenia, co w przypadku projektu z 47 commitami może doprowadzić do zmiany CLI w niekontrolowany sposób.
Umieszczenie wykresów w repozytorium git
mkdir -p ~/workflows/logs
cd ~/workflows
git initCodex sprawdza, czy jest uruchomiony wewnątrz repozytorium git, dlatego CodexAgentConfig posiada opcję skipGitRepositoryCheck dla przypadków, w których nie można go w nim umieścić. Na własnym serwerze VPS można to zrobić i należy to wykonać: wykres jest kodem, a argumentacja za pisaniem orkiestracji jako kodu traci sens, jeśli kod nie jest objęty kontrolą wersji. Należy teraz utworzyć katalog logs, ponieważ systemd nie utworzy go automatycznie.
Przepływ pracy jako graf
Przepływ pracy jest zwykłym modułem TypeScript. Eksportuje on meta, obiekt zawierający nazwę, opis oraz uporządkowaną listę faz, a także eksportuje procedurę obsługi jako default lub jako nazwany eksport run. Wewnątrz procedury obsługi wywołuje się funkcje pomocnicze z pakietu. phase() oznacza etap, w którym znajduje się uruchomienie, log() zapisuje wiersz postępu, agent() wysyła pojedyncze zapytanie do agenta programistycznego, parallel() uruchamia listę zadań jednocześnie, a pipeline() przesyła listę elementów przez kilka etapów.
Zapisz to jako ~/workflows/log-triage.ts.
import { agent, log, parallel, phase } from "@deerwork-ai/deer-workflow";
export const meta = {
name: "log-triage",
description: "Groups recent service errors and writes one short report.",
phases: [{ title: "Collect" }, { title: "Classify" }, { title: "Report" }],
exampleArgs: { service: "nginx", hours: 24 },
};
export default async function workflow(args: { service: string; hours: number }) {
if (!args?.service) throw new Error("input needs a service name");
phase("Collect");
log(`Reading ${args.hours}h of logs for ${args.service}`);
const found = await agent<{ patterns: string[] }>(
`Read the last ${args.hours} hours of journalctl -u ${args.service} and list the distinct error patterns.`,
{
sandbox: "read-only",
schema: {
type: "object",
properties: { patterns: { type: "array", items: { type: "string" } } },
required: ["patterns"],
additionalProperties: false,
},
},
);
phase("Classify");
log(`Classifying ${found.patterns.length} patterns`);
const notes = await parallel(
found.patterns.map((pattern) => () =>
agent(`Explain this error and its most likely cause: ${pattern}`, { sandbox: "read-only" }),
),
);
phase("Report");
return agent(`Write a short operations report from these notes: ${JSON.stringify(notes.filter(Boolean))}`);
}Cztery szczegóły w tym pliku mają istotne znaczenie.
schemaw wywołaniuagent()wymusza ustrukturyzowane wyjście, a wywołanie zwraca sparsowany obiekt.found.patternsjest rzeczywistą tablicą, po której reszta grafu może iterować. Bez schematuagent()zwraca ciąg znaków, co zmusza do parsowania tekstu.sandboxokreśla zakres uprawnień danego kroku.read-onlyblokuje zapis,workspace-writezezwala na zapis chroniony, adanger-full-accessusuwa ochronę. Ustawienie to jest przypisane do wywołania, dzięki czemu graf może odczytywać dane z wielu źródeł, a zapisywać tylko w jednym miejscu.parallel()przyjmuje funkcje, a nie obietnice (promises).map((pattern) => () => agent(...))buduje listę funkcji typu thunk, więc środowisko uruchomieniowe decyduje, kiedy każda z nich się rozpocznie. Przekazanieagent(...)bezpośrednio spowodowałoby uruchomienie każdego wywołania w momencie tworzenia listy.- Nieudane zadanie wewnątrz
parallel()staje sięnull, a proces jest kontynuowany, ponieważ częściowe ukończenie jest dozwolone z założenia. Dlategonotes.filter(Boolean)nie jest tylko dekoracją: pominięcie tego elementu spowoduje, że nieudana gałąź wstawi tekstnulldo zapytania kolejnego kroku.
Standardowy pomocnik agent() korzysta z domyślnego środowiska uruchomieniowego, Codex. Aby wysłać krok do Claude Code, należy zaimportować klasę agenta i wywołać ją bezpośrednio.
import { ClaudeAgent } from "@deerwork-ai/deer-workflow";
const claude = new ClaudeAgent({ sandbox: "read-only" });
const summary = await claude.run<string>("Summarise ./report.md in five lines.");Tak w praktyce wygląda wymienny agent: jeden import i jeden konstruktor, przy niezmienionym grafie. Flaga --agent codex|claude|pi w interfejsie CLI należy do deer-workflow create, który generuje plik przepływu pracy na podstawie opisu. Nie zmienia ona środowiska uruchomieniowego używanego przez deer-workflow run.
Uruchomienie ręczne, a następnie w trybie bezobsługowym
cd ~/workflows
deer-workflow run ./log-triage.ts --input '{"service":"nginx","hours":24}'W trybie interaktywnym dostępny jest interfejs terminalowy: fazy z meta po jednej stronie oraz bieżący dziennik po drugiej. Przed automatyzacją należy przeprowadzić jeden pełny cykl w ten sposób. Jeśli agent nie jest zalogowany lub dane wejściowe nie pasują do sygnatury modułu obsługi, błąd zostanie zauważony w kilka sekund, zamiast szukać go w pliku dziennika w przyszłym tygodniu.
W celu automatyzacji należy przenieść dane wejściowe do pliku. Zapisz ~/workflows/input.json:
{ "service": "nginx", "hours": 24 }deer-workflow run ./log-triage.ts --input-file ./input.json --print >> logs/run.jsonl--print, w formie skróconej -p, wyłącza interfejs i zapisuje strumień zdarzeń do stdout, po jednym obiekcie JSON w linii. W tym trybie nic innego nie jest wysyłane do stdout, więc bezpośrednie dopisywanie do pliku .jsonl pozwala uzyskać plik, w którym każda linia jest poprawnie parsowana.
Strumień zdarzeń i co filtrować poleceniem grep o trzeciej nad ranem
Każda linia zawiera type, sequence, timestamp, workflowId, depth oraz scriptPath. Typy zdarzeń to workflow:start, workflow:meta, workflow:end, workflow:error, workflow:phase:start, workflow:phase:end oraz log. Zdarzenia fazy zawierają phase, zdarzenia końcowe zawierają durationMs, zdarzenie log zawiera message, a zdarzenie workflow:error zawiera error wraz z name, message oraz zazwyczaj stack.
Taka struktura wystarcza, aby odpowiedzieć na dwa pytania, które pojawiają się o trzeciej nad ranem: czy proces się zakończył oraz w którym miejscu został przerwany.
grep workflow:error logs/run.jsonl
jq -r 'select(.type == "workflow:error") | .error.message' logs/run.jsonl
jq -r 'select(.type == "workflow:phase:end") | [.phase, .durationMs] | @tsv' logs/run.jsonl
jq -r 'select(.type == "log") | .message' logs/run.jsonlAby monitorować trwające uruchomienie, należy śledzić plik: tail -f logs/run.jsonl | jq -c 'select(.type == "log")'. Pojedyncze uruchomienie zapisuje niewielką liczbę linii, jednak plik stale rośnie, dlatego po kilku tygodniach działania timera należy dodać regułę logrotate dla ~/workflows/logs/*.jsonl.
Uruchamianie w systemd
Zamiast długo działającego demona należy użyć usługi oneshot wraz z timerem. Wykres uruchamia się, wykonuje zadanie i kończy działanie. Utwórz /etc/systemd/system/log-triage.service, zastępując deploy nazwą swojego użytkownika.
[Unit]
Description=Log triage workflow
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/home/deploy/workflows
Environment=HOME=/home/deploy
Environment=PATH=/home/deploy/.bun/bin:/home/deploy/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/deploy/.bun/bin/deer-workflow run ./log-triage.ts --input-file ./input.json --print
StandardOutput=append:/home/deploy/workflows/logs/run.jsonl
StandardError=journal
TimeoutStartSec=3600Następnie wykonaj /etc/systemd/system/log-triage.timer:
[Unit]
Description=Run the log triage workflow every night
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl start log-triage.service
systemctl status log-triage.service
sudo systemctl enable --now log-triage.timer
systemctl list-timers log-triage.timerNajpierw uruchom usługę ręcznie. Poprawne wykonanie kończy się pomyślnym wyłączeniem jednostki, a logs/run.jsonl zyskuje blok zdarzeń kończący się wpisem workflow:end. Dopiero wtedy włącz timer. list-timers wyświetla czas następnego zaplanowanego uruchomienia, a Persistent=true oznacza, że uruchomienie pominięte podczas wyłączenia serwera nastąpi jednorazowo przy następnym starcie systemu. StandardOutput=append: przesyła strumień zdarzeń do pliku i pozostawia dziennik dla pozostałych komunikatów, dzięki czemu journalctl -u log-triage.service pozostaje czytelny.
Dlaczego wykres działa w powłoce, ale nie działa pod systemd?
Sprawdź te cztery elementy w podanej kolejności.
Jednostka nie może odnaleźć plików binarnych. systemd nigdy nie odczytuje ~/.bashrc, a jego domyślna zmienna PATH nie zawiera ani ~/.bun/bin, ani ~/.npm-global/bin. Jednostka kończy działanie w czasie krótszym niż sekunda, a journalctl -u log-triage.service wskazuje na błąd wykonania nazwy polecenia. Dlatego ExecStart używa ścieżki bezwzględnej, a Environment=PATH= nadal wymienia oba katalogi: samo środowisko uruchomieniowe musi odnaleźć codex lub claude podczas uruchamiania kroku agenta.
Agent nie może odnaleźć swoich danych uwierzytelniających. Interfejs CLI agenta odczytuje dane logowania z katalogu domowego, dlatego należy jawnie ustawić User= oraz Environment=HOME= i wskazać katalog domowy użytkownika, na którego dokonano logowania. Uruchomienie, które dociera do workflow:start, a następnie generuje workflow:error, którego komunikat pochodzi z CLI agenta, a nie z własnego kodu, niemal zawsze wynika z tej przyczyny.
Proces jest przerywany po 90 sekundach. W przypadku Type=oneshot, systemd stosuje limit czasu uruchomienia dla całego polecenia, a wartość domyślna wynosi 90 sekund. Wykres agenta wymaga minut pracy. Dziennik rejestruje Start operation timed out. Terminating., jednostka kończy pracę w stanie błędu, a plik dziennika zawiera połowiczny wynik bez workflow:end. TimeoutStartSec=3600 pozwala na wydłużenie czasu do godziny. Użyj infinity, jeśli proces nigdy nie powinien być przerywany ze względu na czas trwania.
Ścieżki względne wskazują na inne lokalizacje. ./log-triage.ts oraz ./input.json są relatywne względem WorkingDirectory. Pominięcie tej linii spowoduje, że systemd uruchomi proces w /, gdzie żaden z tych plików nie istnieje.
Uprawnienia orkiestratora
Orkiestrator wykonujący kroki agenta według harmonogramu to proces działający na serwerze bez nadzoru użytkownika. Istotne są dwa mechanizmy kontroli oraz jeden budżet.
Pierwszym mechanizmem kontroli jest piaskownica (sandbox) dla każdego wywołania agent(). read-only stanowi właściwe ustawienie domyślne dla każdego kroku wykonującego wyłącznie operacje odczytu: dzienników, metryk lub repozytorium poddawanego analizie. Przejdź do workspace-write tylko wtedy, gdy krok faktycznie wymaga zapisu, ograniczając obszar zapisu za pomocą additionalWritableDirectories zamiast stosowania danger-full-access.
Drugim mechanizmem kontroli jest człowiek. Niektóre kroki nigdy nie powinny być wykonywane bez nadzoru: wysyłanie wiadomości e-mail, transfery środków, usuwanie danych czy zmiana konfiguracji produkcyjnej. W grafie definiowanym kodem łatwo umieścić punkt kontrolny, ponieważ krok jest linią kodu. Wstrzymaj wykonanie, zarejestruj proponowaną akcję, zaczekaj na decyzję operatora, a następnie kontynuuj. Umieszczanie bramki akceptacji przed akcjami agenta szczegółowo opisuje ten wzorzec; powinien on znaleźć się w każdym grafie uruchamianym przez harmonogram.
Budżet to koszty. Każde wywołanie agent() stanowi pełną sesję agenta, a parallel() uruchamia kilka takich sesji jednocześnie. Graf rozgałęziający się na dwanaście ścieżek uruchamia dwanaście sesji każdej nocy, niezależnie od tego, czy ktokolwiek zapozna się z raportem. Pomiary i limity opisane w kontrola kosztów agentów AI na serwerze VPS mają bezpośrednie zastosowanie do grafów uruchamianych według harmonogramu.
Przed aktualizacją środowiska uruchomieniowego przeczytaj dziennik zmian, zainstaluj nową, konkretną wersję i uruchom graf ręcznie za pomocą --print. W tak wczesnym projekcie interfejs CLI wciąż ulega zmianom: sekcja Unreleased zawiera już polecenie, które istnieje w wersji 0.2.0. Graf działający według harmonogramu jest tak niezawodny, jak przypięta wersja oprogramowania oraz ostatnie uruchomienie, które faktycznie nadzorowałeś.
FAQ
Czy potrzebuję Bun, czy Node.js uruchomi Deer Workflow?
Zainstaluj Bun. Opublikowany pakiet wskazuje swój plik binarny deer-workflow na src/cli.ts, plik źródłowy TypeScript, a dokumentacja wymienia Bun jako wymaganie wstępne. Bun wykonuje kod TypeScript bezpośrednio, więc nie jest wymagany etap budowania. Zainstaluj go za pomocą sudo apt install -y unzip, a następnie curl -fsSL https://bun.com/install | bash, po czym potwierdź instalację poleceniem bun --version. Nadal potrzebujesz osobno Node.js i npm, jeśli instalujesz Codex CLI z npm.
Dlaczego mój workflow działa w terminalu, ale zawodzi pod systemd?
Przyczyną jest niemal zawsze zmienna PATH, HOME lub limit czasu startu. systemd nie odczytuje profilu powłoki, więc ExecStart wymaga pełnej ścieżki do deer-workflow, a Environment=PATH= wymaga katalogu zawierającego codex lub claude. Agent CLI odczytuje poświadczenia z $HOME, dlatego ustaw User= oraz Environment=HOME= na konto, na którym przeprowadzono logowanie. Ponadto Type=oneshot dziedziczy 90-sekundowy limit czasu startu, co przerywa działanie agenta i pozostawia Start operation timed out. Terminating. w dzienniku, więc ustaw TimeoutStartSec=3600.
Jak użyć Claude Code zamiast Codex w danym kroku?
Standardowy pomocnik agent() używa domyślnego środowiska uruchomieniowego, czyli Codex. Zaimportuj ClaudeAgent z pakietu, skonstruuj obiekt i wywołaj .run() dla kroków, które ma obsłużyć Claude Code. Flaga --agent codex|claude|pi należy do deer-workflow create, polecenia generującego plik workflow na podstawie opisu, i nie wpływa na deer-workflow run. Każdy używany agent wymaga zainstalowanego własnego CLI oraz zalogowania jako użytkownik, na którym działa usługa.
Którą wersję Deer Workflow należy zainstalować?
Dokładnie tę, która została przetestowana. Na dzień 19 sierpnia 2026 najnowszą opublikowaną wersją jest 0.2.0 z 27 lipca 2026, a repozytorium zawiera 47 commitów. Wpisz @0.2.0 lub wersję aktualną w momencie czytania tego tekstu w poleceniu instalacji, zachowaj ten numer w git obok swoich grafów i uruchom jeden graf ręcznie po każdej aktualizacji, zanim timer ponownie go wyzwoli.