Deer Workflow su VPS: grafi di agenti con systemd
Installa Deer Workflow su Ubuntu con Bun, blocca la versione npm, esegui un grafo TypeScript headless e risolvi PATH e credenziali in systemd.
Cosa stai creando
Deer Workflow è un runtime code-first per grafi di agenti: il flusso di controllo risiede in un file TypeScript che puoi esaminare, mentre un coding agent esegue soltanto le attività che richiedono valutazioni. Questa guida installa il runtime su un singolo VPS Ubuntu, esegue un grafo di esempio in modalità headless tramite systemd e scrive il flusso di eventi leggibile dalle macchine in un file di log, consultabile quando un'esecuzione non riesce alle tre del mattino.
I componenti sono pochi. Bun esegue la CLI. Una CLI di coding agent, Codex o Claude Code, gestisce il lavoro del modello. Un pacchetto npm bloccato a una versione specifica contiene il runtime. Un file TypeScript contiene il grafo. Un servizio e un timer systemd lo eseguono secondo una pianificazione. La maggior parte del contenuto tratta gli aspetti che causano realmente problemi: il PATH all'interno di un'unità systemd, le credenziali dell'agent in una sessione senza login shell e il blocco della versione di una dipendenza pubblicata per la prima volta a luglio 2026.
Visual builder, codice o semplici prompt per l’agente
Chi gestisce servizi self-hosted e automatizza le attività con un modello può scegliere uno di tre approcci, che presentano problemi diversi.
Un visual builder offre una canvas, una libreria di nodi e un’interfaccia che anche chi non programma può utilizzare. È un vantaggio concreto. Inoltre, l’offerta è abbastanza ampia da poter consultare una panoramica completa delle alternative self-hosted a n8n. Il limite è che la logica finisce in un documento JSON generato da un’interfaccia grafica. Il diff di quel documento è poco leggibile. Per rivedere una modifica è quindi necessario aprire la canvas invece di leggere la patch.
Usare direttamente i prompt con un agente è il secondo approccio. Si descrive l’intero lavoro in un paragrafo e si lascia che sia il modello a decidere l’ordine, i tentativi e il momento in cui fermarsi. Funziona fino al giorno in cui il modello prende una decisione diversa. Non esiste alcun diff, perché non esiste un artefatto: il piano era nella conversazione, che ora non è più disponibile.
L’orchestrazione tramite codice è il terzo approccio. L’ordine dei passaggi, la distribuzione parallela, i tentativi e la gestione degli errori sono normali istruzioni TypeScript versionate in git. Il modello viene chiamato nei punti in cui serve una valutazione e in nessun altro. Il limite è che qualcuno deve scrivere e mantenere quel codice, e un collega che non conosce TypeScript non può modificarlo.
Cosa offre un runtime a grafo e quali costi comporta
- Flusso di controllo verificabile. Il grafo è un file. Una modifica alla politica dei tentativi viene mostrata in una pull request come tre righe modificate, non come uno spostamento di un riquadro.
- Gestione degli errori sotto controllo versione. Il comportamento in caso di errore del quarto passaggio è documentato, testato e associato al resto della tua infrastruttura.
- Un agent sostituibile. Il runtime fornisce adattatori per Codex, Claude Code e Pi. Per cambiare quale viene eseguito in un passaggio basta un import.
- Un'esecuzione monitorabile. Le fasi e gli eventi vengono restituiti dal runtime come dati strutturati, quindi un'esecuzione senza interfaccia lascia un record interrogabile.
La pratica generale consiste nel progettare il ciclo in cui viene eseguito il modello, invece di perfezionare un singolo prompt. Si chiama ingegneria del ciclo ed è un modo concreto per applicarla tramite un runtime a grafo. Il costo è la configurazione iniziale: installare un runtime, autenticare una CLI dell'agent, l'assenza di un'interfaccia per i non programmatori e una dipendenza giovane da monitorare.
Il progetto è nuovo, quindi fissa la versione
Deer Workflow è distribuito con licenza MIT ed è un progetto recente. Al 19 agosto 2026, il repository contiene 47 commit su main. npm contiene tre versioni pubblicate: 0.0.1 e 0.1.0 del 26 luglio 2026, quindi 0.2.0 del 27 luglio 2026. Per ciascuna versione esiste un tag git; il changelog indica le modifiche introdotte tra una versione e l’altra. La sezione Unreleased rimuove già il comando deer-workflow agent, quindi main e la versione pubblicata più recente non offrono più la stessa CLI.
Questo non è un motivo per evitare il progetto. È un motivo per installare una versione esatta e sapere quale versione è stata installata.
- Installa una versione esatta, mai un intervallo.
- Registra la versione nello stesso repository dei grafi.
- Dopo ogni aggiornamento, esegui manualmente una volta il tuo grafo prima che il timer lo esegua di nuovo.
Installare Bun e un runtime per agent
Tutto ciò che segue viene eseguito come utente normale con autorizzazioni sudo. Non eseguire i comandi come root. Le CLI degli agent salvano le credenziali nella home directory dell’utente che ha effettuato l’accesso. L’unità systemd dovrà quindi essere eseguita dallo stesso utente per poterle trovare.
sudo apt update
sudo apt install -y curl unzip jq git nodejs npm
curl -fsSL https://bun.com/install | bashL’installer di Bun estrae un archivio zip, quindi unzip deve essere già presente. L’installer aggiunge le righe relative a PATH al profilo della shell, ma la shell corrente ha già letto quel file. Apri quindi una nuova shell oppure aggiungi manualmente queste due righe a ~/.bashrc e ricarica il profilo.
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$HOME/.npm-global/bin:$PATH"bun --versionIl comando stampa un numero di versione. bun: command not found indica che nella shell corrente manca la riga relativa a PATH, non che l’installazione non sia riuscita. Esegui ls ~/.bun/bin prima di reinstallare qualsiasi componente.
Ora installa il runtime dell’agent. Codex CLI è l’opzione predefinita e viene installato da npm. Imposta un prefisso npm a livello utente, in modo che l’installazione globale non richieda root.
npm config set prefix "$HOME/.npm-global"
npm install -g @openai/codex
command -v codex
codexcommand -v codex dovrebbe stampare un percorso sotto $HOME/.npm-global/bin. L’esecuzione autonoma di codex apre la CLI, dove puoi accedere con il tuo account ChatGPT. Esegui questa operazione ora, una sola volta, mentre puoi vedere lo schermo.
Claude Code funziona come runtime alternativo e dispone di un installer dedicato.
curl -fsSL https://claude.ai/install.sh | bash
claude --versionUn’installazione corretta stampa una versione come 2.1.211 (Claude Code). Esegui claude una volta per accedere. Si tratta dello stesso tipo di processo, con lo stesso accesso ai tuoi file, di qualsiasi altro agent che ospiti. Le indicazioni sull’account e sull’hardening in eseguire un coding agent su un VPS si applicano quindi senza modifiche.
Installare Deer Workflow e fissare la versione esatta
bun install --global @deerwork-ai/deer-workflow@0.2.0
command -v deer-workflowcommand -v stampa il percorso assoluto, normalmente /home/<your user>/.bun/bin/deer-workflow. Copialo in un luogo sicuro. L'unità systemd non può usare il semplice nome.
Mantieni la versione nel comando di installazione. Rimuovere @0.2.0 installa la versione più recente disponibile il giorno dell'esecuzione; in un progetto con 47 commit, questo può modificare la CLI mentre un timer non monitorato è in esecuzione.
Inserire i grafici in un repository git
mkdir -p ~/workflows/logs
cd ~/workflows
git initCodex verifica se è in esecuzione all'interno di un repository git. Per questo CodexAgentConfig include un'opzione skipGitRepositoryCheck per i casi in cui non è possibile indicargliene uno. Sul proprio VPS è possibile indicargliene uno, ed è consigliabile farlo: un grafico è codice e l'approccio all'orchestrazione come codice perde validità se il codice non è sottoposto al controllo versione. Creare ora la directory logs, perché systemd non la creerà automaticamente.
Scrivere un grafo
Un workflow è un modulo TypeScript ordinario. Esporta meta, un oggetto che contiene un nome, una descrizione e l’elenco ordinato delle fasi; inoltre esporta il gestore come default oppure come esportazione denominata run. Nel gestore si chiamano gli helper del pacchetto. phase() indica la fase di esecuzione corrente, log() scrive una riga di avanzamento, agent() invia un prompt al coding agent, parallel() esegue contemporaneamente un elenco di attività e pipeline() fa passare un elenco di elementi attraverso diverse fasi.
Salvare il file con il nome ~/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))}`);
}Quattro dettagli di questo file sono importanti.
schemain una chiamataagent()richiede un output strutturato e la chiamata restituisce l’oggetto analizzato.found.patternsè un array reale che il resto del grafo può scorrere. Senza uno schema,agent()restituisce una stringa e l’utente deve analizzare testo libero.sandboxdetermina quali risorse può modificare quel passaggio.read-onlyblocca le scritture,workspace-writeconsente scritture protette edanger-full-accessrimuove la protezione. Viene impostato per ogni chiamata, quindi un grafo può leggere ampiamente e scrivere in un solo punto.parallel()riceve funzioni, non promise.map((pattern) => () => agent(...))costruisce un elenco di thunk, così il runtime decide quando avviare ciascuno. Passare direttamenteagent(...)avvierebbe ogni chiamata nel momento in cui viene creato l’elenco.- Un’attività non riuscita all’interno di
parallel()diventanulle l’esecuzione continua, perché il completamento parziale è previsto dal progetto. Per questonotes.filter(Boolean)non è un elemento decorativo: se lo si omette, un ramo non riuscito inserisce il testonullnel prompt del passaggio successivo.
L’helper agent() semplice usa il runtime predefinito, Codex. Per inviare un singolo passaggio a Claude Code, importare la classe dell’agent e chiamarla direttamente.
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.");Questo è un agent sostituibile in pratica: basta un import e un costruttore, mentre il grafo circostante resta invariato. Il flag --agent codex|claude|pi della CLI appartiene a deer-workflow create, che genera un file di workflow a partire da una descrizione. Non modifica il runtime usato da deer-workflow run.
Eseguilo una volta manualmente, quindi in modalità non interattiva
cd ~/workflows
deer-workflow run ./log-triage.ts --input '{"service":"nginx","hours":24}'In modalità interattiva viene mostrata un'interfaccia terminale: le fasi da meta sono visualizzate su un lato, mentre il log in tempo reale compare sull'altro. Osserva in questo modo un'esecuzione completa prima di automatizzare qualsiasi operazione. Se l'agent non ha effettuato l'accesso oppure l'input non corrisponde alla firma dell'handler, lo rilevi in pochi secondi invece di trovarlo in un file di log la settimana successiva.
Per l'automazione, sposta l'input in un file. Salva ~/workflows/input.json:
{ "service": "nginx", "hours": 24 }deer-workflow run ./log-triage.ts --input-file ./input.json --print >> logs/run.jsonl--print, nella forma breve -p, disattiva l'interfaccia e scrive il flusso degli eventi su stdout, con un oggetto JSON per riga. In questa modalità su stdout non viene scritto altro, quindi aggiungendo direttamente l'output a un file .jsonl ottieni un file in cui ogni riga può essere analizzata.
Il flusso degli eventi e cosa cercare con grep alle 3 di notte
Ogni riga contiene type, sequence, timestamp, workflowId, depth e scriptPath. I tipi sono workflow:start, workflow:meta, workflow:end, workflow:error, workflow:phase:start, workflow:phase:end e log. Gli eventi di fase contengono phase, gli eventi di completamento contengono durationMs, un evento log contiene message, mentre un evento workflow:error contiene error con name, message e generalmente stack.
Questa struttura è sufficiente per rispondere alle due domande che ci si pone alle 3 di notte: l'esecuzione è terminata e dove si è interrotta.
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.jsonlPer monitorare un'esecuzione in corso, seguite il file: tail -f logs/run.jsonl | jq -c 'select(.type == "log")'. Una singola esecuzione scrive poche righe, ma il file cresce continuamente; quindi aggiungete una regola logrotate per ~/workflows/logs/*.jsonl dopo che il timer è rimasto attivo per alcune settimane.
Eseguilo tramite systemd
Usa un servizio oneshot con un timer, invece di un daemon a esecuzione continua. Il grafico viene avviato, eseguito e terminato. Scrivi /etc/systemd/system/log-triage.service, sostituendo deploy con il tuo utente.
[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=3600Quindi /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.timerAvvia prima il servizio manualmente. Un'esecuzione corretta termina con la disattivazione dell'unità e logs/run.jsonl acquisisce un blocco di eventi che termina con workflow:end. Solo a questo punto abilita il timer. list-timers mostra la prossima esecuzione pianificata e Persistent=true indica che un'esecuzione saltata mentre il server era spento viene eseguita una sola volta al successivo avvio. StandardOutput=append: invia il flusso di eventi al file e lascia tutto il resto nel journal, in modo che journalctl -u log-triage.service resti leggibile.
Perché il grafo funziona nella shell ma non con systemd?
Controllare questi quattro aspetti, in quest'ordine.
L'unità non trova i binari. systemd non legge mai ~/.bashrc e il relativo PATH predefinito non contiene né ~/.bun/bin né ~/.npm-global/bin. L'unità termina in meno di un secondo e journalctl -u log-triage.service mostra che l'esecuzione fallisce sul nome del comando. Per questo ExecStart usa un percorso assoluto e Environment=PATH= elenca comunque entrambe le directory: quando avvia un passaggio dell'agente, anche il runtime deve trovare codex o claude.
L'agente non trova le proprie credenziali. La CLI dell'agente legge le credenziali di accesso dalla home directory. Impostare quindi esplicitamente User= e Environment=HOME= e assegnare all'agente la home con cui è stato eseguito l'accesso. Se un'esecuzione raggiunge workflow:start e poi produce un workflow:error il cui messaggio proviene dalla CLI dell'agente, anziché dal proprio codice, questa è quasi sempre la causa.
L'esecuzione viene terminata dopo 90 secondi. Per Type=oneshot, systemd applica il timeout di avvio all'intero comando e il valore predefinito è 90 secondi. Un grafo dell'agente richiede alcuni minuti. Il journal registra Start operation timed out. Terminating., l'unità termina in stato failed e il file di log contiene un'esecuzione incompleta senza alcun workflow:end. TimeoutStartSec=3600 concede un'ora. Usare infinity se si preferisce impedire del tutto la terminazione per timeout.
I percorsi relativi vengono risolti in un'altra directory. ./log-triage.ts e ./input.json sono relativi a WorkingDirectory. Se si omette quella riga, systemd avvia il processo in /, dove nessuno dei due file esiste.
Che cosa può fare l'orchestrator
Un orchestrator che esegue i passaggi di un agent in base a una pianificazione è un processo che opera sul server senza supervisione. Sono importanti due controlli e un budget.
Il primo controllo è la sandbox di ogni chiamata agent(). read-only è l'impostazione predefinita corretta per qualsiasi passaggio che esegue soltanto letture: log, metriche o un repository da riepilogare. Passa un passaggio a workspace-write solo quando deve davvero scrivere e limita l'area scrivibile con additionalWritableDirectories, invece di usare subito danger-full-access.
Il secondo controllo è una persona. Alcuni passaggi non dovrebbero mai essere eseguiti senza supervisione: inviare email, trasferire denaro, eliminare dati o modificare la configurazione di produzione. In un grafo code-first è semplice inserire il blocco, perché il passaggio è una riga di codice. Arresta l'esecuzione, registra l'azione proposta, attendi la risposta di una persona, quindi riprendi. Inserire un blocco di approvazione prima delle azioni dell'agent descrive completamente questo modello e dovrebbe essere presente in qualsiasi grafo avviato da una pianificazione.
Il budget è economico. Ogni chiamata agent() avvia una sessione completa dell'agent e parallel() ne avvia diverse contemporaneamente. Di conseguenza, un grafo che si dirama in dodici rami esegue dodici sessioni ogni notte, anche se nessuno legge il report. Le misurazioni e i limiti descritti in mantenere sotto controllo i costi degli agent AI su un VPS si applicano direttamente a un grafo pianificato.
Prima di aggiornare il runtime, leggi il changelog, installa la nuova versione esatta ed esegui manualmente il grafo una volta con --print. In un progetto così recente, l'interfaccia CLI è ancora soggetta a modifiche: la sezione Unreleased rimuove già un comando presente nella versione 0.2.0. L'affidabilità di un grafo eseguito in base a una pianificazione dipende dalla versione che hai bloccato e dall'ultima esecuzione che hai controllato personalmente.
FAQ
Mi serve Bun o Deer Workflow funziona con Node.js?
Installa Bun. Il pacchetto pubblicato indirizza il binario deer-workflow a src/cli.ts, un file sorgente TypeScript, e la documentazione indica Bun come prerequisito. Bun esegue direttamente TypeScript, quindi non è necessaria una fase di build. Installalo con sudo apt install -y unzip seguito da curl -fsSL https://bun.com/install | bash, quindi verifica l’installazione con bun --version. Se installi Codex CLI da npm, ti servono comunque Node.js e npm separatamente.
Perché il workflow viene eseguito nel terminale ma non funziona con systemd?
Quasi sempre la causa è PATH, HOME oppure il timeout di avvio. systemd non legge il profilo della shell, quindi ExecStart deve contenere il percorso assoluto di deer-workflow e Environment=PATH= deve contenere la directory che include codex o claude. La CLI dell’agente legge le credenziali da $HOME, quindi imposta User= e Environment=HOME= sull’account con cui hai effettuato l’accesso. Inoltre Type=oneshot usa un timeout di avvio predefinito di 90 secondi. Questo interrompe l’esecuzione dell’agente prima del completamento e lascia Start operation timed out. Terminating. nel journal. Perciò imposta TimeoutStartSec=3600.
Come posso usare Claude Code invece di Codex per un passaggio?
L’helper agent() semplice usa il runtime predefinito, Codex. Importa ClaudeAgent dal pacchetto, costruiscilo e chiama .run() per i passaggi che vuoi affidare a Claude Code. Il flag --agent codex|claude|pi appartiene a deer-workflow create, il comando che genera un file di workflow a partire da una descrizione, e non influisce su deer-workflow run. Qualunque agente utilizzi, devi installare la relativa CLI ed effettuare l’accesso con lo stesso utente con cui viene eseguito il servizio.
Quale versione di Deer Workflow devo installare?
Quella esatta che hai testato. Al 19 agosto 2026, l’ultima versione pubblicata è 0.2.0, rilasciata il 27 luglio 2026, e il repository contiene 47 commit. Scrivi @0.2.0, o la versione corrente al momento della lettura, nel comando di installazione, conserva quel numero in git insieme ai grafici e, dopo ogni aggiornamento, esegui manualmente un grafico prima che il timer lo avvii di nuovo.