Installer Deer Workflow sur un VPS avec systemd
Installez Deer Workflow sur Ubuntu avec Bun, verrouillez sa dépendance de juillet 2026 et exécutez un graphe TypeScript headless sous systemd.
Ce que vous allez mettre en place
Deer Workflow est un runtime code-first pour les graphes d’agents : le flux de contrôle se trouve dans un fichier TypeScript que vous pouvez relire, et un coding agent n’effectue que les tâches qui nécessitent un jugement. Ce guide l’installe sur un VPS Ubuntu, exécute un graphe d’exemple sans interface sous systemd et écrit le flux d’événements lisible par machine dans un fichier journal que vous pouvez rechercher lorsqu’une exécution échoue à trois heures du matin.
Les composants sont limités. Bun exécute la CLI. Une CLI de coding agent, Codex ou Claude Code, effectue le traitement avec le modèle. Un package npm verrouillé contient le runtime. Un fichier TypeScript contient votre graphe. Un service et un timer systemd l’exécutent selon un calendrier. La majeure partie de ce guide couvre les éléments qui posent réellement problème : le PATH dans une unité systemd, les identifiants de l’agent dans une session sans login shell et le verrouillage d’une dépendance publiée pour la première fois en juillet 2026.
Créer visuellement, coder ou simplement demander à l’agent
Pour automatiser une tâche avec un modèle, un self-hoster peut choisir l’une de trois approches. Chacune échoue de manière différente.
Un outil de création visuelle fournit un canvas, une bibliothèque de nœuds et une interface qu’un utilisateur qui ne programme pas peut ouvrir. C’est un avantage réel. Le choix est suffisamment large pour disposer d’un guide complet des alternatives self-hosted à n8n. En contrepartie, la logique devient un document JSON généré par l’interface. Le diff de ce document est difficile à lire. Pour examiner une modification, il faut donc ouvrir le canvas plutôt que lire le patch.
Demander directement à un agent constitue la deuxième approche. Vous décrivez toute la tâche dans un paragraphe et laissez le modèle choisir l’ordre, les tentatives et le moment où il doit s’arrêter. Cela fonctionne jusqu’au jour où il prend une autre décision. Il n’y a pas de diff, car il n’y a pas d’artefact : le plan se trouvait dans la conversation, qui a disparu.
L’orchestration en code constitue la troisième approche. L’ordre des étapes, la distribution des tâches, les tentatives et la gestion des erreurs sont du TypeScript classique versionné dans git. Le modèle est appelé aux endroits où un jugement est nécessaire, et nulle part ailleurs. En contrepartie, quelqu’un doit écrire et maintenir ce code, et un collègue qui ne maîtrise pas TypeScript ne peut pas le modifier.
Ce qu’un runtime de graphe apporte et ce qu’il coûte
- Un flux de contrôle révisable. Le graphe est un fichier. Une modification de la stratégie de nouvelle tentative apparaît dans une pull request sous la forme de trois lignes modifiées, et non d’un bloc déplacé.
- Une gestion des échecs dans le contrôle de version. Le comportement en cas d’échec de l’étape quatre est documenté, testé et versionné avec le reste de votre infrastructure.
- Un agent interchangeable. Le runtime fournit des adaptateurs pour Codex, Claude Code et Pi. Changer l’agent qui exécute une étape se résume à modifier un import.
- Une exécution observable. Le runtime produit les phases et les événements sous forme de données structurées. Une exécution headless laisse ainsi une trace interrogeable.
La pratique générale qui consiste à concevoir la boucle dans laquelle le modèle s’exécute, plutôt qu’à perfectionner un prompt unique, s’appelle conception de boucles. Un runtime de graphe constitue une manière concrète de la mettre en œuvre. Son coût réside dans la mise en place : il faut installer un runtime, authentifier un agent CLI, il n’existe aucune interface pour les non-programmeurs et il faut surveiller une dépendance encore jeune.
Le projet est récent : verrouillez donc la version
Deer Workflow est distribué sous licence MIT et reste récent. Au 19 août 2026, le dépôt compte 47 commits sur main. npm contient trois versions publiées : 0.0.1 et 0.1.0 le 26 juillet 2026, puis 0.2.0 le 27 juillet 2026. Un tag git correspond à chacune d’elles, et c’est dans le changelog que vous trouverez les changements entre ces versions. Sa section Unreleased supprime déjà la commande deer-workflow agent. main et la dernière version publiée ne proposent donc plus le même CLI.
Ce n’est pas une raison d’éviter le projet. C’est une raison d’installer une version précise et de savoir laquelle vous avez installée.
- Installez une version précise, jamais une plage de versions.
- Enregistrez cette version dans le même dépôt que vos graphes.
- Après chaque mise à niveau, exécutez manuellement votre propre graphe avant que le timer ne le relance.
Installer Bun et un runtime d’agent
Toutes les commandes ci-dessous s’exécutent avec un utilisateur normal disposant de droits sudo. Ne les exécutez pas en tant que root. Les CLI des agents stockent les identifiants dans le répertoire personnel de l’utilisateur connecté. L’unité systemd devra ensuite s’exécuter avec ce même utilisateur pour pouvoir les trouver.
sudo apt update
sudo apt install -y curl unzip jq git nodejs npm
curl -fsSL https://bun.com/install | bashL’installateur de Bun décompresse une archive zip. unzip doit donc être installé au préalable. L’installateur ajoute les lignes de PATH à votre profil shell. Votre shell actuel a déjà lu ce fichier. Ouvrez donc un nouveau shell, ou ajoutez vous-même ces deux lignes à ~/.bashrc, puis rechargez le profil.
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$HOME/.npm-global/bin:$PATH"bun --versionCette commande affiche un numéro de version. bun: command not found signifie que la ligne de PATH est absente du shell utilisé, et non que l’installation a échoué. Exécutez ls ~/.bun/bin avant de réinstaller quoi que ce soit.
Passons maintenant au runtime de l’agent. Codex CLI est le runtime par défaut et s’installe depuis npm. Définissez un préfixe npm au niveau utilisateur afin que l’installation globale ne nécessite pas les droits root.
npm config set prefix "$HOME/.npm-global"
npm install -g @openai/codex
command -v codex
codexcommand -v codex doit afficher un chemin sous $HOME/.npm-global/bin. Exécutez codex seul pour ouvrir la CLI, puis connectez-vous avec votre compte ChatGPT. Faites-le maintenant, tant que vous pouvez voir l’écran.
Claude Code fonctionne comme runtime alternatif et possède son propre installateur.
curl -fsSL https://claude.ai/install.sh | bash
claude --versionUne installation fonctionnelle affiche une version telle que 2.1.211 (Claude Code). Exécutez claude une fois pour vous connecter. Il s’agit du même type de processus, avec le même accès à vos fichiers, que n’importe quel autre agent que vous hébergez. Les remarques sur le compte et le renforcement de la sécurité dans exécuter un agent de programmation sur un VPS s’appliquent donc ici sans modification.
Installer Deer Workflow et figer la version exacte
bun install --global @deerwork-ai/deer-workflow@0.2.0
command -v deer-workflowcommand -v affiche le chemin absolu, généralement /home/<your user>/.bun/bin/deer-workflow. Copiez-le quelque part. L’unité systemd ne peut pas utiliser le nom seul.
Conservez la version dans la commande d’installation. Supprimer @0.2.0 installe la version la plus récente disponible le jour où vous exécutez la commande. Sur un projet qui compte 47 commits, cela peut modifier l’interface de ligne de commande exécutée par un timer que personne ne surveille.
Placez les graphes dans un dépôt git
mkdir -p ~/workflows/logs
cd ~/workflows
git initCodex vérifie s’il s’exécute dans un dépôt git. C’est pourquoi CodexAgentConfig possède une option skipGitRepositoryCheck pour les cas où vous ne pouvez pas lui en fournir un. Sur votre propre VPS, vous pouvez lui en fournir un, et vous devriez le faire : un graphe est du code, et l’intérêt de gérer l’orchestration sous forme de code disparaît si ce code n’est pas placé sous contrôle de version. Créez maintenant le répertoire logs, car systemd ne le créera pas pour vous.
Écrire un graphe
Un workflow est un module TypeScript ordinaire. Il exporte meta, un objet contenant un nom, une description et la liste ordonnée des phases, ainsi que le handler sous la forme de default ou d’un export nommé run. Dans le handler, vous appelez les helpers du package. phase() indique l’étape en cours, log() écrit une ligne de progression, agent() envoie un prompt à l’agent de codage, parallel() exécute simultanément une liste de tâches et pipeline() fait passer une liste d’éléments par plusieurs étapes.
Enregistrez ce fichier sous ~/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))}`);
}Quatre détails de ce fichier sont importants.
schemadans un appelagent()demande une sortie structurée et l’appel renvoie l’objet analysé.found.patternsest un véritable tableau que le reste du graphe peut parcourir. Sans schéma,agent()renvoie une chaîne et vous devez analyser du texte libre.sandboxdétermine ce que cette étape peut modifier.read-onlybloque les écritures,workspace-writeautorise les écritures contrôlées etdanger-full-accesssupprime cette protection. Ce paramètre est défini pour chaque appel. Un graphe peut donc lire largement et écrire à un seul endroit.parallel()accepte des fonctions, pas des promesses.map((pattern) => () => agent(...))construit une liste de fonctions différées, afin que le runtime décide du moment où chacune démarre. Passer directementagent(...)lancerait chaque appel dès la construction de la liste.- Une tâche en échec dans
parallel()devientnullet l’exécution continue, car la complétion partielle est prévue.notes.filter(Boolean)n’est donc pas décoratif : si vous l’omettez, une branche en échec insère le textenulldans le prompt de l’étape suivante.
Le helper agent() utilise le runtime par défaut, Codex. Pour envoyer une étape à Claude Code, importez la classe de l’agent et appelez-la directement.
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.");Voilà à quoi ressemble un agent interchangeable en pratique : un import et un constructeur, sans modifier le graphe qui l’entoure. Le flag --agent codex|claude|pi de la CLI appartient à deer-workflow create, qui génère un fichier de workflow à partir d’une description. Il ne modifie pas le runtime utilisé par deer-workflow run.
Exécutez-le d’abord manuellement, puis en mode non interactif
cd ~/workflows
deer-workflow run ./log-triage.ts --input '{"service":"nginx","hours":24}'En mode interactif, vous disposez d’une interface de terminal : les phases de meta apparaissent d’un côté et le journal en temps réel de l’autre. Observez une exécution complète de cette manière avant d’automatiser quoi que ce soit. Si l’agent n’est pas connecté ou si votre entrée ne correspond pas à la signature du handler, vous le voyez en quelques secondes au lieu de le découvrir la semaine suivante dans un fichier journal.
Pour l’automatisation, placez l’entrée dans un fichier. Enregistrez ~/workflows/input.json :
{ "service": "nginx", "hours": 24 }deer-workflow run ./log-triage.ts --input-file ./input.json --print >> logs/run.jsonl--print, ou sa forme abrégée -p, désactive l’interface et écrit le flux d’événements sur stdout, avec un objet JSON par ligne. Dans ce mode, rien d’autre n’est écrit sur stdout. Vous pouvez donc rediriger directement la sortie vers un fichier .jsonl pour obtenir un fichier dont chaque ligne peut être analysée.
Le flux d’événements et les éléments à rechercher dans les logs à 3 heures du matin
Chaque ligne contient type, sequence, timestamp, workflowId, depth et scriptPath. Les types sont workflow:start, workflow:meta, workflow:end, workflow:error, workflow:phase:start, workflow:phase:end et log. Les événements de phase contiennent phase, les événements de fin contiennent durationMs, un événement log contient message, et un événement workflow:error contient error avec name, message et généralement stack.
Cette structure suffit pour répondre aux deux questions qui se posent à 3 heures du matin : l’exécution est-elle terminée et où s’est-elle arrêtée ?
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.jsonlPour suivre une exécution en cours, surveillez le fichier : tail -f logs/run.jsonl | jq -c 'select(.type == "log")'. Une exécution écrit un petit nombre de lignes, mais le fichier ne fait que grossir. Ajoutez donc une règle logrotate pour ~/workflows/logs/*.jsonl lorsque le timer fonctionne depuis quelques semaines.
Exécuter le programme avec systemd
Utilisez un service oneshot et un timer plutôt qu’un daemon qui reste actif. Le graphique démarre, s’exécute, puis se termine. Écrivez /etc/systemd/system/log-triage.service en remplaçant deploy par votre utilisateur.
[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=3600Puis /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.timerDémarrez d’abord le service manuellement. Une exécution réussie se termine par la désactivation correcte de l’unité, et logs/run.jsonl reçoit un bloc d’événements qui se termine par workflow:end. Activez ensuite le timer. list-timers affiche la prochaine exécution planifiée, et Persistent=true indique qu’une exécution manquée pendant l’arrêt du serveur aura lieu une seule fois au prochain démarrage. StandardOutput=append: envoie le flux d’événements vers le fichier et laisse le reste dans le journal, afin que journalctl -u log-triage.service reste lisible.
Pourquoi le graphe fonctionne-t-il dans mon shell, mais échoue-t-il avec systemd ?
Vérifiez ces quatre points, dans cet ordre.
L’unité ne trouve pas les binaires. systemd ne lit jamais ~/.bashrc, et son PATH par défaut ne contient ni ~/.bun/bin ni ~/.npm-global/bin. L’unité échoue en moins d’une seconde et journalctl -u log-triage.service indique que l’exécution échoue sur le nom de la commande. C’est pourquoi ExecStart utilise un chemin absolu, et pourquoi Environment=PATH= liste toujours les deux répertoires : le runtime doit lui-même trouver codex ou claude lorsqu’il démarre une étape de l’agent.
L’agent ne trouve pas ses identifiants. La CLI de l’agent lit ses identifiants de connexion dans le répertoire personnel. Définissez donc explicitement User= et Environment=HOME=, puis indiquez-lui le répertoire personnel avec lequel vous vous êtes connecté. Lorsqu’une exécution atteint workflow:start, puis produit une workflow:error dont le message provient de la CLI de l’agent et non de votre propre code, c’est presque toujours la cause.
L’exécution est arrêtée après 90 secondes. Pour Type=oneshot, systemd applique son délai d’expiration de démarrage à l’ensemble de la commande. La valeur par défaut est de 90 secondes. Un graphe d’agent s’exécute pendant plusieurs minutes. Le journal contient Start operation timed out. Terminating., l’unité se termine dans l’état failed et le fichier journal ne contient qu’une partie de l’exécution, sans workflow:end. TimeoutStartSec=3600 lui accorde une heure. Utilisez infinity si vous préférez qu’elle ne soit jamais arrêtée à cause du délai.
Les chemins relatifs sont résolus ailleurs. ./log-triage.ts et ./input.json sont relatifs à WorkingDirectory. Si vous omettez cette ligne, systemd démarre le processus dans /, où aucun des deux fichiers n’existe.
Autorisation d’action de l’orchestrateur
Un orchestrateur qui exécute des étapes d’agent selon un calendrier est un processus qui agit sur votre serveur sans surveillance. Deux contrôles sont nécessaires, ainsi qu’un budget.
Le premier contrôle est le sandbox de chaque appel agent(). read-only est la valeur par défaut adaptée pour toute étape qui effectue uniquement des lectures : journaux, métriques ou dépôt que vous résumez. Passez une étape à workspace-write lorsqu’elle doit réellement écrire, et limitez la zone accessible en écriture avec additionalWritableDirectories au lieu d’utiliser directement danger-full-access.
Le deuxième contrôle est humain. Certaines étapes ne doivent jamais s’exécuter sans surveillance : envoyer des e-mails, transférer de l’argent, supprimer des données ou modifier la configuration de production. Dans un graphe piloté par le code, le point de validation est facile à placer, puisque l’étape correspond à une ligne de code. Arrêtez l’exécution, enregistrez l’action proposée, attendez la réponse d’une personne, puis reprenez l’exécution. Placer un point de validation devant les actions de l’agent décrit ce fonctionnement en détail. Il doit être utilisé dans tout graphe lancé par un timer.
Le budget concerne l’argent. Chaque appel agent() correspond à une session complète d’agent, et parallel() en démarre plusieurs simultanément. Un graphe qui se divise en douze branches exécute donc douze sessions chaque nuit, même si personne ne lit le rapport. Les mesures et les limites présentées dans maîtriser les coûts des agents IA sur un VPS s’appliquent directement à un graphe planifié.
Avant de mettre à niveau le runtime, lisez le changelog, installez la nouvelle version exacte, puis exécutez une fois votre graphe manuellement avec --print. Sur un projet aussi récent, l’interface du CLI évolue encore : la section Unreleased supprime déjà une commande qui existe dans 0.2.0. La fiabilité d’un graphe lancé par un timer dépend de la version que vous avez épinglée et de la dernière exécution que vous avez réellement surveillée.
FAQ
Ai-je besoin de Bun ou Node.js permet-il d’exécuter Deer Workflow ?
Installez Bun. Le package publié pointe son binaire deer-workflow vers src/cli.ts, un fichier source TypeScript, et la documentation indique Bun comme prérequis. Bun exécute directement TypeScript : aucune étape de build n’est nécessaire. Installez-le avec sudo apt install -y unzip, puis exécutez curl -fsSL https://bun.com/install | bash et vérifiez le résultat avec bun --version. Node.js et npm restent nécessaires séparément si vous installez Codex CLI depuis npm.
Pourquoi mon workflow fonctionne-t-il dans le terminal, mais échoue-t-il avec systemd ?
La cause est presque toujours PATH, HOME ou le délai d’attente au démarrage. systemd ne lit pas votre profil shell. ExecStart doit donc contenir le chemin absolu vers deer-workflow, et Environment=PATH= doit contenir le répertoire qui contient codex ou claude. La CLI de l’agent lit ses identifiants d’authentification dans $HOME. Définissez donc User= et Environment=HOME= avec le compte que vous avez utilisé pour vous connecter. Enfin, Type=oneshot utilise par défaut un délai de démarrage de 90 secondes. Ce délai interrompt l’exécution de l’agent et laisse Start operation timed out. Terminating. dans le journal. Définissez donc TimeoutStartSec=3600.
Comment utiliser Claude Code au lieu de Codex pour une étape ?
L’assistant agent() standard utilise l’environnement d’exécution par défaut, Codex. Importez ClaudeAgent depuis le package, instanciez-le, puis appelez .run() pour les étapes que Claude Code doit traiter. L’option --agent codex|claude|pi appartient à deer-workflow create, la commande qui génère un fichier de workflow à partir d’une description. Elle n’a aucun effet sur deer-workflow run. L’agent utilisé doit disposer de sa propre CLI et être connecté avec le même utilisateur que celui utilisé par le service.
Quelle version de Deer Workflow dois-je installer ?
Installez exactement la version que vous avez testée. Au 19 août 2026, la dernière version publiée est la 0.2.0, publiée le 27 juillet 2026, et le dépôt contient 47 commits. Inscrivez @0.2.0, ou la version actuelle au moment de votre lecture, dans la commande d’installation. Conservez ce numéro dans git avec vos graphes. Après chaque mise à niveau, exécutez manuellement un graphe avant de laisser le timer l’exécuter de nouveau.