Temporal es un motor de workflows duraderos: guarda el historial de cada ejecución en su base de datos, de modo que si un proceso se cae a mitad de un flujo, otro lo retoma exactamente donde se quedó, con reintentos automáticos en los pasos que fallan. Tu código de negocio corre en workers que tú despliegas, y el servidor de Temporal solo coordina y persiste el estado. En este tutorial instalarás la CLI de Temporal en Ubuntu 24.04, ejecutarás su servidor integrado como servicio de systemd con persistencia en disco, crearás un namespace y escribirás y ejecutarás un workflow en Python con reintentos.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • Python 3.10 o superior (Ubuntu 24.04 incluye Python 3.12).

Paso 1: Instalar la CLI de Temporal

La CLI de Temporal es un único binario que sirve tanto para administrar un clúster como para lanzar el servidor integrado. Descarga la última versión para Linux x86_64 desde el servidor de descargas oficial:

cd /tmp
curl -fLo temporal_cli.tar.gz "https://temporal.download/cli/archive/latest?platform=linux&arch=amd64"
tar xzf temporal_cli.tar.gz temporal
sudo install -m 0755 temporal /usr/local/bin/temporal

En servidores ARM64 cambia arch=amd64 por arch=arm64. Comprueba la instalación:

temporal --version
temporal version 1.4.1

Tus números de versión pueden ser distintos.

Paso 2: Ejecutar el servidor de Temporal como servicio

Sin opciones, temporal server start-dev guarda los datos en memoria y los pierde al reiniciar. Para conservarlos, crea un usuario de sistema y un directorio donde guardar la base de datos SQLite:

sudo useradd --system --home-dir /var/lib/temporal --create-home --shell /usr/sbin/nologin temporal

Crea la unidad de systemd:

sudo nano /etc/systemd/system/temporal.service
[Unit]
Description=Temporal development server
Wants=network-online.target
After=network-online.target

[Service]
User=temporal
Group=temporal
WorkingDirectory=/var/lib/temporal
ExecStart=/usr/local/bin/temporal server start-dev --db-filename /var/lib/temporal/temporal.db --ip 127.0.0.1 --port 7233 --ui-port 8233
Restart=on-failure

[Install]
WantedBy=multi-user.target
  • --db-filename activa la persistencia en un archivo SQLite.
  • --ip 127.0.0.1 hace que el servidor (puerto 7233, gRPC) y la interfaz web (puerto 8233) solo escuchen en local. El servidor no tiene autenticación en este modo, así que no lo expongas a Internet.

Recarga systemd y arranca el servicio:

sudo systemctl daemon-reload
sudo systemctl enable --now temporal
sudo systemctl status temporal
● temporal.service - Temporal development server
     Loaded: loaded (/etc/systemd/system/temporal.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 11:02:14 UTC; 5s ago

Comprueba que el servidor responde:

temporal operator cluster health
SERVING

Paso 3: Crear un namespace

Un namespace es la unidad de aislamiento de Temporal: cada uno tiene sus propios workflows, colas y período de retención del historial. El servidor trae un namespace default, pero es buena práctica crear uno por aplicación. Crea el namespace pedidos con una retención de 7 días para el historial de los workflows terminados:

temporal operator namespace create --namespace pedidos --retention 168h
Namespace pedidos successfully registered.

Comprueba su configuración:

temporal operator namespace describe --namespace pedidos

La salida incluye el nombre, el estado Registered y la retención de 168 horas.

Paso 4: Preparar el proyecto Python

Los workflows y las actividades se escriben con el SDK de Temporal. Instala el soporte de entornos virtuales y crea el proyecto:

sudo apt update
sudo apt install python3-venv
mkdir -p ~/pedidos-temporal
cd ~/pedidos-temporal
python3 -m venv venv
./venv/bin/pip install temporalio

Comprueba que el paquete se ha instalado:

./venv/bin/pip show temporalio | head -n 2
Name: temporalio
Version: 1.18.0

Paso 5: Escribir las actividades

Una actividad es una función que hace trabajo con efectos externos: llamar a una API, escribir en una base de datos, enviar un email. Es la parte que puede fallar y que Temporal reintenta. Crea el archivo de actividades:

nano ~/pedidos-temporal/activities.py
import random
from dataclasses import dataclass

from temporalio import activity
from temporalio.exceptions import ApplicationError


@dataclass
class Pedido:
    pedido_id: str
    total: float
    email: str


@activity.defn
async def cobrar_pago(pedido: Pedido) -> str:
    if pedido.total <= 0:
        # Un importe inválido no se arregla reintentando.
        raise ApplicationError("Importe inválido", non_retryable=True)

    # Simula una pasarela de pagos que falla la mitad de las veces.
    if random.random() < 0.5:
        raise RuntimeError("La pasarela de pagos no responde")

    activity.logger.info("Pago cobrado para el pedido %s", pedido.pedido_id)
    return f"pago-{pedido.pedido_id}"


@activity.defn
async def enviar_confirmacion(pedido: Pedido) -> None:
    activity.logger.info("Confirmación enviada a %s", pedido.email)

cobrar_pago falla de forma aleatoria para que puedas ver los reintentos en acción. ApplicationError con non_retryable=True marca los errores que no tiene sentido reintentar.

Paso 6: Escribir el workflow

El workflow orquesta las actividades. Su código debe ser determinista: no puede llamar a redes, leer la hora del sistema ni usar números aleatorios directamente, porque Temporal lo vuelve a ejecutar para reconstruir su estado a partir del historial. Todo lo que no sea determinista va en actividades.

nano ~/pedidos-temporal/workflows.py
from datetime import timedelta

from temporalio import workflow
from temporalio.common import RetryPolicy

with workflow.unsafe.imports_passed_through():
    from activities import Pedido, cobrar_pago, enviar_confirmacion


@workflow.defn
class ProcesarPedido:
    @workflow.run
    async def run(self, pedido: Pedido) -> str:
        pago_id = await workflow.execute_activity(
            cobrar_pago,
            pedido,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=RetryPolicy(
                initial_interval=timedelta(seconds=1),
                backoff_coefficient=2.0,
                maximum_interval=timedelta(seconds=30),
                maximum_attempts=10,
            ),
        )

        await workflow.execute_activity(
            enviar_confirmacion,
            pedido,
            start_to_close_timeout=timedelta(seconds=30),
        )

        return pago_id
  • start_to_close_timeout es el tiempo máximo de cada intento de la actividad. Es obligatorio definir al menos un timeout.
  • La RetryPolicy reintenta el cobro hasta 10 veces, esperando 1, 2, 4, 8... segundos entre intentos, con un máximo de 30. Sin política explícita, Temporal reintenta indefinidamente.
  • imports_passed_through() permite importar el módulo de actividades desde el sandbox en el que se ejecutan los workflows.

Paso 7: Crear el worker y lanzar un workflow

El worker es el proceso que escucha una cola de tareas (task queue) y ejecuta el código de workflows y actividades. Créalo:

nano ~/pedidos-temporal/worker.py
import asyncio
import logging

from temporalio.client import Client
from temporalio.worker import Worker

from activities import cobrar_pago, enviar_confirmacion
from workflows import ProcesarPedido


async def main() -> None:
    logging.basicConfig(level=logging.INFO)
    client = await Client.connect("127.0.0.1:7233", namespace="pedidos")
    worker = Worker(
        client,
        task_queue="pedidos-queue",
        workflows=[ProcesarPedido],
        activities=[cobrar_pago, enviar_confirmacion],
    )
    await worker.run()


if __name__ == "__main__":
    asyncio.run(main())

Crea también un script que inicie un workflow y espere su resultado:

nano ~/pedidos-temporal/starter.py
import asyncio
import sys

from temporalio.client import Client

from activities import Pedido
from workflows import ProcesarPedido


async def main(pedido_id: str) -> None:
    client = await Client.connect("127.0.0.1:7233", namespace="pedidos")
    resultado = await client.execute_workflow(
        ProcesarPedido.run,
        Pedido(pedido_id=pedido_id, total=150.0, email="[email protected]"),
        id=f"pedido-{pedido_id}",
        task_queue="pedidos-queue",
    )
    print(f"Workflow completado: {resultado}")


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1]))

El id del workflow identifica la ejecución de forma única: Temporal no permite dos ejecuciones abiertas con el mismo ID, lo que evita procesar dos veces el mismo pedido.

Arranca el worker en una terminal:

cd ~/pedidos-temporal
./venv/bin/python worker.py

En una segunda terminal, lanza un workflow:

cd ~/pedidos-temporal
./venv/bin/python starter.py 1001
Workflow completado: pago-1001

En la terminal del worker verás advertencias con el error La pasarela de pagos no responde cuando la actividad falla, seguidas del cobro correcto en un reintento posterior. Detén el worker con Ctrl+C cuando termines.

Paso 8: Inspeccionar los workflows

La CLI permite consultar el estado y el historial completo de cualquier ejecución. Lista los workflows del namespace:

temporal workflow list --namespace pedidos
  Status     WorkflowId    Type            StartTime
  Completed  pedido-1001   ProcesarPedido  1 minute ago

Muestra el historial de eventos. Cada intento fallido de la actividad queda registrado, y el campo attempt del evento ActivityTaskStarted indica cuántos intentos hicieron falta:

temporal workflow show --workflow-id pedido-1001 --namespace pedidos

La interfaz web ofrece la misma información de forma visual. Como solo escucha en local, ábrela a través de un túnel SSH desde tu ordenador:

ssh -L 8233:127.0.0.1:8233 your_user@your_server_ip

Con el túnel abierto, visita http://localhost:8233 en tu navegador, selecciona el namespace pedidos y abre pedido-1001 para ver la línea de tiempo de las actividades y sus reintentos.

Paso 9: Ejecutar el worker como servicio

En un entorno real el worker debe arrancar solo y reiniciarse si falla. Crea una unidad de systemd que lo ejecute con tu usuario:

sudo nano /etc/systemd/system/pedidos-worker.service
[Unit]
Description=Temporal worker for pedidos
After=temporal.service
Wants=temporal.service

[Service]
User=your_user
WorkingDirectory=/home/your_user/pedidos-temporal
ExecStart=/home/your_user/pedidos-temporal/venv/bin/python worker.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Sustituye your_user por tu usuario. Activa el servicio y comprueba sus logs:

sudo systemctl daemon-reload
sudo systemctl enable --now pedidos-worker
journalctl -u pedidos-worker -n 20 --no-pager

Lanza otro workflow para confirmar que el worker del servicio lo procesa:

cd ~/pedidos-temporal
./venv/bin/python starter.py 1002
Workflow completado: pago-1002

Para comprobar la durabilidad, lanza un pedido nuevo y reinicia el worker mientras la actividad está reintentando (sudo systemctl restart pedidos-worker). El workflow continuará tras el reinicio sin repetir los pasos que ya habían terminado.

Solución de problemas

Failed client connect o connection refused en el puerto 7233. El servidor no está en marcha. Revisa sudo systemctl status temporal y journalctl -u temporal -n 50.

Namespace pedidos is not found. El namespace no existe en este servidor. Créalo con temporal operator namespace create --namespace pedidos. Si acabas de crearlo, espera unos segundos a que se propague.

El workflow se queda en Running y no avanza. No hay ningún worker escuchando la cola pedidos-queue en ese namespace. Comprueba los pollers con temporal task-queue describe --task-queue pedidos-queue --namespace pedidos; si la lista está vacía, arranca el worker.

Error de no determinismo (Nondeterminism error) tras cambiar el código. Has modificado la secuencia de actividades de un workflow que ya tenía ejecuciones abiertas. Termina esas ejecuciones con temporal workflow terminate --workflow-id <id> --namespace pedidos o usa el versionado de workflows del SDK para cambios en producción.

Conclusión

Tienes un servidor de Temporal persistente como servicio de systemd, un namespace propio y un workflow en Python que sobrevive a fallos de sus actividades y a reinicios del worker. Como siguientes pasos, puedes usar señales y queries para interactuar con workflows en ejecución, programar ejecuciones periódicas con temporal schedule create y, cuando pases a producción, desplegar el servidor con el chart de Helm oficial sobre PostgreSQL manteniendo el mismo código de workers.