SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-21

Instalar Deer Workflow en un VPS con Bun y systemd

Instala Deer Workflow en Ubuntu con Bun, fija la version de julio de 2026 y ejecuta un grafo TypeScript sin interfaz bajo systemd con logs consultables.

Qué va a crear

Deer Workflow es un runtime basado en código para grafos de agentes: el flujo de control se encuentra en un archivo TypeScript que puede revisar, y un agente de programación sólo realiza las partes que requieren criterio. Esta guía lo instala en un único VPS con Ubuntu, ejecuta un grafo de ejemplo sin interfaz bajo systemd y escribe el flujo de eventos legible por máquina en un archivo de registro que puede buscar cuando una ejecución falla a las tres de la madrugada.

Los componentes son pequeños. Bun ejecuta la CLI. Una CLI de agente de programación, Codex o Claude Code, realiza el trabajo del modelo. Un único paquete npm fijado contiene el runtime. Un archivo TypeScript contiene el grafo. Un servicio y un temporizador de systemd lo ejecutan según un calendario. La mayor parte de esta guía cubre los puntos que suelen fallar: PATH dentro de una unidad de systemd, las credenciales del agente en una sesión sin shell de inicio de sesión y la fijación de una dependencia publicada por primera vez en julio de 2026.

Constructor visual, código o simplemente indicaciones al agente

Quien se autoaloja una solución para automatizar tareas con un modelo puede elegir una de tres opciones, y cada una falla de una forma distinta.

Un constructor visual ofrece un lienzo, una biblioteca de nodos y una interfaz de usuario que puede abrir una persona sin conocimientos de programación. Es una ventaja real, y la oferta es lo bastante amplia como para disponer de una encuesta completa de alternativas autoalojadas a n8n. El coste es que la lógica termina en un documento JSON escrito por una interfaz de usuario. El diff de ese documento es ruidoso, por lo que revisar un cambio implica abrir el lienzo en lugar de leer el parche.

Indicarle directamente las tareas a un agente es la segunda opción. Se describe todo el trabajo en un párrafo y se deja que el modelo decida el orden, los reintentos y cuándo detenerse. Funciona hasta el día en que toma una decisión diferente. No hay diff porque no existe ningún artefacto: el plan estaba en la conversación, y la conversación ha desaparecido.

La orquestación mediante código es la tercera opción. El orden de los pasos, la distribución de tareas, los reintentos y el tratamiento de errores se expresan en TypeScript normal dentro de git. El modelo se invoca en los puntos que requieren criterio, y en ningún otro. El coste es que alguien debe escribir y mantener ese código, y un colega que no escriba TypeScript no puede editarlo.

Qué aporta un runtime de grafos y qué costes tiene

  • Un flujo de control que se puede revisar. El grafo es un archivo. Un cambio en la política de reintentos aparece en un pull request como tres líneas modificadas, no como un cuadro desplazado.
  • Gestión de errores bajo control de versiones. Lo que ocurre cuando falla el paso cuatro queda documentado, probado y etiquetado junto con el resto de la infraestructura.
  • Un agente que se puede sustituir. El runtime incluye adaptadores para Codex, Claude Code y Pi. Cambiar cuál ejecuta un paso requiere un solo import.
  • Una ejecución que se puede supervisar. Las fases y los eventos salen del runtime como datos estructurados, por lo que una ejecución sin interfaz deja un registro que se puede consultar.

La práctica general de diseñar el bucle en el que se ejecuta el modelo, en lugar de perfeccionar un solo prompt, se denomina ingeniería de bucles, y un runtime de grafos es una forma concreta de aplicarla. El coste es la configuración inicial: hay que instalar un runtime, autenticar una CLI de agente, no existe una interfaz para personas que no programan y es necesario vigilar una dependencia todavía reciente.

El proyecto es nuevo, así que fije la versión

Deer Workflow se distribuye con licencia MIT y es un proyecto nuevo. A fecha de 19 August 2026, el repositorio tiene 47 commits en main. npm contiene tres versiones publicadas: 0.0.1 y 0.1.0, ambas del 26 July 2026, y después 0.2.0, del 27 July 2026. Hay una etiqueta de git para cada versión. El changelog indica qué cambió entre ellas. Su sección Unreleased ya elimina el comando deer-workflow agent. Por tanto, main y la versión publicada más reciente ya no ofrecen la misma CLI.

Esto no es un motivo para evitar el proyecto. Es un motivo para instalar una versión exacta y saber cuál instaló.

  • Instale una versión exacta, nunca un rango.
  • Registre esa versión en el mismo repositorio que sus grafos.
  • Después de cualquier actualización, ejecute su propio grafo una vez manualmente antes de que el temporizador vuelva a ejecutarlo.

Instalar Bun y un runtime de agente

Todo lo siguiente se ejecuta con un usuario normal que tenga permisos de sudo. No lo ejecute como root. Las CLI de los agentes almacenan las credenciales en el directorio personal del usuario que inició sesión. Por tanto, la unidad de systemd deberá ejecutarse más adelante con ese mismo usuario para encontrarlas.

sudo apt update
sudo apt install -y curl unzip jq git nodejs npm
curl -fsSL https://bun.com/install | bash

El instalador de Bun descomprime un archivo ZIP, por lo que unzip debe estar disponible antes. El instalador añade las líneas de PATH al perfil del shell. El shell actual ya ha leído ese archivo. Abra un shell nuevo o añada manualmente estas dos líneas a ~/.bashrc y vuelva a cargarlo.

export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$HOME/.npm-global/bin:$PATH"
bun --version

Esto muestra un número de versión. bun: command not found indica que falta la línea de PATH en el shell que está usando, no que la instalación haya fallado. Ejecute ls ~/.bun/bin antes de reinstalar nada.

Ahora, el runtime del agente. Codex CLI es la opción predeterminada y se instala desde npm. Configure un prefijo de npm para el usuario para que la instalación global no necesite root.

npm config set prefix "$HOME/.npm-global"
npm install -g @openai/codex
command -v codex
codex

command -v codex debería mostrar una ruta bajo $HOME/.npm-global/bin. Ejecutar codex sin argumentos abre la CLI, donde inicia sesión con su cuenta de ChatGPT. Hágalo ahora una vez, mientras pueda ver la pantalla.

Claude Code funciona como runtime alternativo y tiene su propio instalador.

curl -fsSL https://claude.ai/install.sh | bash
claude --version

Una instalación correcta muestra una versión como 2.1.211 (Claude Code). Ejecute claude una vez para iniciar sesión. Es el mismo tipo de proceso, con el mismo acceso a sus archivos, que cualquier otro agente que aloje. Por tanto, las indicaciones sobre la cuenta y el endurecimiento de ejecutar un agente de programación en un VPS se aplican aquí sin cambios.

Instalar Deer Workflow y fijar la versión exacta

bun install --global @deerwork-ai/deer-workflow@0.2.0
command -v deer-workflow

command -v muestra la ruta absoluta, normalmente /home/<your user>/.bun/bin/deer-workflow. Cópiela en algún lugar. La unidad de systemd no puede usar el nombre sin ruta.

Mantenga la versión en el comando de instalación. Si omite @0.2.0, se instala la versión más reciente disponible el día que ejecute el comando. En un proyecto con 47 commits, esto puede cambiar la CLI mientras se ejecuta un temporizador que nadie está supervisando.

Coloque los gráficos en un repositorio git

mkdir -p ~/workflows/logs
cd ~/workflows
git init

Codex comprueba si se ejecuta dentro de un repositorio git. Por eso CodexAgentConfig incluye una opción skipGitRepositoryCheck para los casos en los que no se le puede proporcionar uno. En su propio VPS puede proporcionarle uno, y debería hacerlo: un gráfico es código, y el argumento a favor de escribir la orquestación como código pierde sentido si ese código no está bajo control de versiones. Cree ahora el directorio logs, porque systemd no lo creará por usted.

Escribir un grafo

Un workflow es un módulo TypeScript ordinario. Exporta meta, un objeto que contiene un nombre, una descripción y la lista ordenada de fases. También exporta el controlador como default o como una exportación con nombre run. Dentro del controlador se llaman los helpers del paquete. phase() indica en qué fase se encuentra la ejecución, log() escribe una línea de progreso, agent() envía un prompt al agente de programación, parallel() ejecuta una lista de tareas al mismo tiempo y pipeline() pasa una lista de elementos por varias fases.

Guarde esto como ~/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))}`);
}

Cuatro detalles de ese archivo son importantes.

  • schema en una llamada agent() solicita una salida estructurada y la llamada devuelve el objeto analizado. found.patterns es una matriz real que el resto del grafo puede recorrer. Sin un esquema, agent() devuelve una cadena y debe analizar el texto.
  • sandbox determina qué puede modificar ese paso. read-only bloquea las escrituras, workspace-write permite escrituras protegidas y danger-full-access elimina esa protección. Se establece en cada llamada, por lo que un grafo puede leer ampliamente y escribir en un único lugar.
  • parallel() recibe funciones, no promises. map((pattern) => () => agent(...)) crea una lista de thunks para que el runtime decida cuándo inicia cada uno. Pasar agent(...) directamente iniciaría todas las llamadas en cuanto se cree la lista.
  • Una tarea fallida dentro de parallel() se convierte en null y la ejecución continúa, porque el diseño permite una finalización parcial. Por eso notes.filter(Boolean) no es decorativo: si se omite, una rama fallida introduce el texto null en el prompt del paso siguiente.

El helper agent() sencillo usa el runtime predeterminado, Codex. Para enviar un paso a Claude Code, importe la clase del agente y llámela directamente.

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.");

Así funciona en la práctica un agente intercambiable: una importación y un constructor, sin modificar el grafo que lo rodea. La opción --agent codex|claude|pi de la CLI pertenece a deer-workflow create, que genera un archivo de workflow a partir de una descripción. No cambia el runtime que usa deer-workflow run.

Ejecutarlo una vez manualmente y después sin interfaz

cd ~/workflows
deer-workflow run ./log-triage.ts --input '{"service":"nginx","hours":24}'

En modo interactivo se muestra una interfaz de terminal: las fases de meta aparecen a un lado y el registro en tiempo real, al otro. Observe una ejecución completa de esta forma antes de automatizar nada. Si el agente no ha iniciado sesión o la entrada no coincide con la firma del controlador, lo verá en segundos en lugar de descubrirlo la semana siguiente en un archivo de registro.

Para automatizarlo, mueva la entrada a un archivo. Guarde ~/workflows/input.json:

{ "service": "nginx", "hours": 24 }
deer-workflow run ./log-triage.ts --input-file ./input.json --print >> logs/run.jsonl

--print, en su forma abreviada -p, desactiva la interfaz y escribe el flujo de eventos en stdout, con un objeto JSON por línea. En este modo no se escribe nada más en stdout, por lo que añadir directamente la salida a un archivo .jsonl produce un archivo en el que se puede analizar cada línea.

El flujo de eventos y qué buscar con grep a las 3 de la madrugada

Cada línea contiene type, sequence, timestamp, workflowId, depth y scriptPath. Los tipos son workflow:start, workflow:meta, workflow:end, workflow:error, workflow:phase:start, workflow:phase:end y log. Los eventos de fase contienen phase, los eventos de finalización contienen durationMs, un evento log contiene message y un evento workflow:error contiene error con name, message y normalmente stack.

Esta estructura basta para responder a las dos preguntas que surgen a las 3 de la madrugada: si terminó y dónde se detuvo.

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.jsonl

Para supervisar una ejecución que está ocurriendo ahora, siga el archivo: tail -f logs/run.jsonl | jq -c 'select(.type == "log")'. Una ejecución escribe pocas líneas, pero el archivo sólo crece. Por eso, añada una regla de logrotate para ~/workflows/logs/*.jsonl cuando el temporizador lleve varias semanas en funcionamiento.

Ejecutarlo con systemd

Use un servicio oneshot y un temporizador, en lugar de un daemon de larga duración. El grafo se inicia, se ejecuta y termina. Escriba /etc/systemd/system/log-triage.service y sustituya deploy por su usuario.

[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=3600

A continuación, /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.target
sudo 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.timer

Inicie primero el servicio manualmente. Una ejecución correcta termina con la unidad desactivándose correctamente, y logs/run.jsonl recibe un bloque de eventos que finaliza con workflow:end. Sólo después habilite el temporizador. list-timers muestra la próxima ejecución programada, y Persistent=true indica que una ejecución omitida mientras el servidor estaba apagado se realizará una vez durante el siguiente arranque. StandardOutput=append: envía el flujo de eventos al archivo y deja el journal para todo lo demás, de modo que journalctl -u log-triage.service siga siendo legible.

¿Por qué el grafo funciona en mi shell, pero falla con systemd?

Compruebe estos cuatro puntos, en este orden.

La unidad no encuentra los binarios. systemd nunca lee ~/.bashrc y su PATH predeterminado no contiene ~/.bun/bin ni ~/.npm-global/bin. La unidad falla en menos de un segundo y journalctl -u log-triage.service muestra que la ejecución falla al resolver el nombre del comando. Por eso ExecStart usa una ruta absoluta y Environment=PATH= sigue incluyendo ambos directorios: el propio runtime tiene que encontrar codex o claude cuando inicia un paso del agente.

El agente no encuentra sus credenciales. La CLI del agente lee sus credenciales de inicio de sesión desde el directorio personal. Por tanto, establezca User= y Environment=HOME= explícitamente y asígnele el directorio personal con el que inició sesión. Si una ejecución llega a workflow:start y después produce un workflow:error cuyo mensaje procede de la CLI del agente y no de su propio código, casi siempre se debe a esto.

La ejecución termina después de 90 segundos. Para Type=oneshot, systemd aplica su tiempo de espera de inicio al comando completo y el valor predeterminado es 90 segundos. Un grafo de agentes tarda varios minutos. El journal registra Start operation timed out. Terminating., la unidad termina en estado fallido y el archivo de registro contiene una ejecución incompleta sin workflow:end. TimeoutStartSec=3600 le concede una hora. Use infinity si prefiere que no termine por tiempo de espera.

Las rutas relativas se resuelven en otro lugar. ./log-triage.ts y ./input.json son relativas a WorkingDirectory. Si omite esa línea, systemd inicia el proceso en /, donde no existe ninguno de los dos archivos.

Qué puede hacer el orquestador

Un orquestador que ejecuta pasos de agentes con un temporizador es un proceso que actúa en el servidor sin supervisión. Hay dos controles importantes y un presupuesto.

El primer control es el sandbox de cada llamada a agent(). read-only es la opción predeterminada adecuada para cualquier paso que sólo lea: registros, métricas o un repositorio que esté resumiendo. Cambie un paso a workspace-write cuando realmente necesite escribir y mantenga pequeña el área con permisos de escritura usando additionalWritableDirectories, en lugar de recurrir a danger-full-access.

El segundo control es una persona. Algunos pasos nunca deben ejecutarse sin supervisión: enviar correo, mover dinero, eliminar datos o cambiar la configuración de producción. En un grafo basado en código, es fácil colocar la aprobación porque el paso es una línea de código. Detenga la ejecución, registre la acción propuesta, espere la respuesta de una persona y continúe después. Cómo colocar una aprobación delante de las acciones de los agentes explica este patrón en detalle y debe formar parte de cualquier grafo que se ejecute con un temporizador.

El presupuesto es el dinero. Cada llamada a agent() es una sesión completa del agente y parallel() inicia varias a la vez. Por tanto, un grafo que se divide en doce ramas ejecuta doce sesiones cada noche, aunque nadie lea el informe. Las mediciones y los límites de cómo mantener bajo control los costes de los agentes de IA en un VPS se aplican directamente a un grafo programado.

Antes de actualizar el runtime, lea el registro de cambios, instale la nueva versión exacta y ejecute el grafo una vez manualmente con --print. En un proyecto tan reciente, la interfaz de línea de comandos todavía cambia: la sección Unreleased ya elimina un comando que existe en 0.2.0. Un grafo ejecutado con un temporizador sólo es tan fiable como la versión que fijó y la última ejecución que supervisó realmente.

FAQ

¿Necesito Bun o Deer Workflow funciona con Node.js?

Instale Bun. El paquete publicado apunta su binario deer-workflow a src/cli.ts, un archivo fuente de TypeScript, y la documentación indica que Bun es un requisito previo. Bun ejecuta TypeScript directamente, por lo que no se necesita una compilación. Instálelo con sudo apt install -y unzip seguido de curl -fsSL https://bun.com/install | bash y, después, confirme la instalación con bun --version. Si instala Codex CLI desde npm, también necesita Node.js y npm por separado.

¿Por qué mi flujo de trabajo funciona en el terminal, pero falla con systemd?

Casi siempre se debe a PATH, HOME o al tiempo de espera de inicio. systemd no lee el perfil del shell, por lo que ExecStart necesita la ruta absoluta a deer-workflow y Environment=PATH= necesita el directorio que contiene codex o claude. La CLI del agente lee sus credenciales de $HOME, así que establezca User= y Environment=HOME= con la cuenta con la que inició sesión. Además, Type=oneshot hereda un tiempo de espera de inicio de 90 segundos. Esto interrumpe la ejecución del agente y deja Start operation timed out. Terminating. en el journal, por lo que debe establecer TimeoutStartSec=3600.

¿Cómo uso Claude Code en lugar de Codex en un paso?

El helper agent() simple usa el runtime predeterminado, Codex. Importe ClaudeAgent desde el paquete, constrúyalo y llame a .run() para los pasos que quiera que gestione Claude Code. El indicador --agent codex|claude|pi pertenece a deer-workflow create, el comando que genera un archivo de flujo de trabajo a partir de una descripción, y no afecta a deer-workflow run. El agente que utilice necesita su propia CLI instalada y una sesión iniciada con el mismo usuario con el que se ejecuta el servicio.

¿Qué versión de Deer Workflow debo instalar?

La versión exacta que haya probado. A fecha de 19 August 2026, la versión publicada más reciente es 0.2.0, del 27 July 2026, y el repositorio contiene 47 commits. Escriba @0.2.0, o la versión vigente cuando lea esto, en el comando de instalación, conserve ese número en git junto a sus grafos y ejecute un grafo manualmente después de cada actualización, antes de que el temporizador vuelva a ejecutarlo.