State Machine as a Service

Nami convierte tus flujos de negocio en APIs ejecutables

Define workflows como máquinas de estado en YAML, expónlos como REST APIs y ejecuta lógica compleja: validaciones, scripts Deno, y capacidades de IA (Gemini) en cada transición — sin código de infraestructura.

Máquinas de estado declarativas
DSL YAML compilado a JSON
Capacidades LLM (Gemini)
Scripts Deno sandboxed
Validadores (9 tipos)
REST API + CLI
Conceptos core

Cómo funciona Nami

Nami modela procesos como grafos dirigidos: los nodos son estados y los arcos son transiciones. Cada ítem creado en una máquina avanza de estado en estado ejecutando lógica en cada transición.

ProjectStateMachine

Define el workflow completo: estados, transiciones, validadores, ejecutores y el JSON Schema del body de los ítems. Se almacena como definición JSON o se compila desde el DSL YAML.

ProjectStateMachineItem

Cada ítem es una instancia que recorre el workflow. Tiene un state, un body JSON libre, flags (bitmap) y un audit trail completo de cada transición ejecutada (WorkflowStep).

Transiciones

Cada transición define from (origen/s), to (destino), validadores (condiciones), enter (acción al entrar: capabilities, script o code Deno), else (fallback), flags y properties.

WorkflowStep

Audit trail automático: por cada transición se registra fromState, toState, bodyBefore, bodyAfter, resultados de validadores, outputs de capacidades, duración y timestamp.

Flags

Bitmap de flags por ítem. Las transiciones pueden agregar o quitar flags. Los validadores internal.flag verifican el estado de los flags para controlar flujos condicionales.

Listeners

ProjectStateMachineListeners permiten que al ocurrir una transición en una máquina, se dispare automáticamente la creación de un ítem en otra máquina (cross-machine triggers).

Jerarquía de datos
User → Project → ProjectStateMachine → ProjectStateMachineItem
                                    │                      ├── WorkflowStep (audit trail)
                                    │                      └── ProjectStateMachineItemFile
                                    ├── ProjectScript (Deno)
                                    ├── ProjectCatalog → ProjectCatalogItem
                                    ├── ProjectWebhook
                                    ├── ApiToken
                                    └── ProjectStateMachineListener
DSL YAML

El lenguaje de definición de workflows

Nami ofrece un DSL en YAML que se compila internamente a la definición JSON. Es el formato recomendado para definir máquinas de estado de forma legible y mantenible.

Estructura completa del DSL
name: "Nombre de la máquina"

# itemModel: JSON Schema que valida el body de cada ítem
itemModel:
  type: object
  required: [campo1, campo2]
  properties:
    campo1:
      type: string
      minLength: 5
    campo2:
      type: number
      minimum: 0
    imagen:
      type: string        # UUID de archivo
  metadata:
    flags: [activo, procesado]   # flags disponibles
  additionalProperties: true

dsl: |
  map: nombre-del-mapa    # ID único del mapa visual

  nombre-transicion:
    from: estado-a | estado-b    # pipe = múltiples orígenes
    to: estado-destino           # si se omite, usa el nombre
    else: estado-fallback        # opcional: si fallan validadores

    all:                         # AND lógico (o "some:" para OR)
      internal.not_empty:
        - campo1
      internal.compare:
        operator: ">="
        field: campo2
        value: 100
      internal.script: input.item.body.campo2 < 99999

    enter:
      # Exactamente UNO de los tres modos:
      script: "return { resultado: input.item.body.x * 2 }"
      # code: <uuid-del-script-deno>
      # capabilities: [...]

    properties:
      - stop_after_apply    # no encadenar con la siguiente transición

    flags:
      nombre_flag:
        - add_flag1
        - remove_flag2
Ejemplo: Store Credit
name: "Store Credit"

itemModel:
  type: object
  required: [email]
  properties:
    email:
      type: string
      format: email
    balance:
      type: number
      minimum: 0
      maximum: 99999
    amount:
      type: number

dsl: |
  map: store-credit-map

  created:
    from: init
    to: created
    all:
      internal.not_empty: [email]

  add:
    from: created | dim      # viene de "created" O "dim"
    to: add
    all:
      internal.script: >
        input.item.body.amount > 0 &&
        input.item.body.amount <= 99999
    enter:
      script: >
        return {
          balance: input.item.body.balance +
                   input.item.body.amount
        }
    properties:
      - stop_after_apply

  dim:
    from: created | add
    all:
      internal.flag:
        "false": [activo]
    enter:
      script: "return { balance: 0 }"
    flags:
      activo:
        - remove_activo
Reglas de compilación DSL
  • from: a | b → múltiples estados origen (pipe-separated)
  • to omitido → usa el nombre de la transición
  • else → estado al que va si los validadores fallan
  • all = AND lógico · some = OR lógico
  • enter acepta exactamente uno: script, code o capabilities
  • stop_after_apply evita encadenamiento automático de transiciones
  • Las transiciones se evalúan en orden; la primera que pasa se aplica
  • Sin stop_after_apply, la ejecución continúa en el nuevo estado
Validadores

9 tipos de validadores

Los validadores determinan si una transición puede ejecutarse. Se evalúan antes de aplicar la transición. Se agrupan con all (AND) o some (OR).

INTERNALValidadores internos (7)
TipoSintaxis DSLDescripción
internal.not_emptyinternal.not_empty: [campo1, campo2]Los campos deben ser no-nulos y no-vacíos en el body del ítem
internal.emptyinternal.empty: [campo1]Los campos deben ser nulos o vacíos
internal.booleaninternal.boolean: "true": [campo_activo] "false": [campo_inactivo]Verifica que campos sean true/false (booleanos)
internal.compareinternal.compare: operator: ">=" field: monto value: 100Comparación numérica/string: >=, >, <=, <, ==, !=
internal.regexinternal.regex: campo: '/^[A-Z]+$/' email: '/^[\w@.]+$/'Valida campos contra patrones regex
internal.flaginternal.flag: "true": [activo] "false": [bloqueado]Verifica el estado del bitmap de flags del ítem
internal.scriptinternal.script: input.item.body.monto > 0Expresión JS inline evaluada en Deno. Retorna true/false
EXTERNALValidadores externos (2)
TipoConfigDescripción
external.geturl: "https://api.com/check/{{id}}"Llama a un endpoint GET externo; la URL soporta interpolación {{campo}} desde el body del ítem
external.posturl: "https://api.com/validate" body: { "id": "{{campo}}" }Llama a un endpoint POST externo con body templado. Soporta interpolación en URL y body
Ejemplo completo de validadores en DSL
  review-validation:
    from: pending
    to: approved
    else: rejected         # va a "rejected" si falla ALGÚN validador

    all:                   # TODOS deben pasar
      internal.not_empty:
        - email
        - product_id
      internal.compare:
        operator: ">="
        field: stars
        value: 4
      internal.regex:
        stars: '/^[4-5]$/'
      internal.flag:
        "true": [verified]       # el flag "verified" debe estar activo
        "false": [spam]          # el flag "spam" debe estar inactivo
      internal.script: >
        input.item.body.amount > 0 &&
        input.item.body.amount <= 99999
      external.get:
        url: "https://my-api.com/users/{{email}}/verify"   # llamada HTTP GET
IA · Gemini

7 Capacidades LLM

Las capacidades son ejecutores LLM que se configuran en el enter de una transición. Usan Google Gemini y soportan interpolación {{campo}} desde el body del ítem.

generator

Genera nuevo contenido desde cero a partir de un prompt. Soporta context y constraints (max_words, language, format).

Output: artifact.value (texto)
improver

Mejora un texto existente referenciado por artifact_field (campo del body). Aplica constraints de calidad.

Output: texto mejorado
summarizer

Resume el campo text con constraints de longitud, formato e idioma (ISO 639-1).

Output: resumen
translator

Traduce text de source_language a target_language. Idiomas en código ISO 639-1 (es, en, fr, pt, etc.).

Output: texto traducido
image_generator

Genera una imagen y la guarda en MinIO. Soporta llm.files para adjuntar imágenes del body como referencia (virtual try-on, edición).

Output: UUID del archivo (MinIO)
video_generator

Genera un script de video para Instagram/TikTok/YouTube Shorts desde un prompt. No genera video binario.

Output: script de video
intention

Extrae campos estructurados de un mensaje de usuario libre y los inyecta en el body. Preserva campos ya recolectados. Útil para formularios conversacionales.

Output: campos estructurados (JSON)
Estructura de configuración
enter:
  capabilities:
    - type: generator
      input:
        prompt: "Genera un resumen para: {{brief}}"
        context: "Empresa: {{company_name}}"
        constraints:
          max_words: 100
          language: es
          format: paragraph
      llm:
        model: gemini-2.0-flash      # modelo Gemini
        temperature: 0.5             # 0.0-1.0
        files:                       # adjuntar archivos del body
          - campo_imagen_uuid
          - otro_campo_imagen
      response:
        field: campo_output          # dónde guardar el resultado
Ejemplo: Virtual Try-On con imágenes
name: "Virtual Try-On"

itemModel:
  type: object
  required: [user_file, product_file]
  properties:
    user_file: { type: string }     # UUID de foto del usuario
    product_file: { type: string }  # UUID de foto del producto
    model_response: { type: string }

dsl: |
  map: try-on-map

  start:
    from: init
    to: extract
    all:
      internal.not_empty: [user_file, product_file]
    enter:
      capabilities:
        - type: image_generator
          input:
            text: >
              Virtual try-on: coloca el producto de la
              imagen 2 en el modelo de la imagen 1
            style: "realistic photo"
            resolution: "medium resolution"
          llm:
            model: gemini-3-pro-image-preview
            files:
              - user_file        # adjunta imágenes al LLM
              - product_file
          response:
            field: model_response
Encadenamiento de capacidades
# Múltiples capacidades en una transición — se ejecutan en orden
# El output de la primera se puede usar como input de la siguiente
enter:
  capabilities:
    - type: generator
      input:
        prompt: "Crea un artículo sobre: {{tema}}"
      response:
        field: articulo_raw         # ← guardado en body

    - type: summarizer
      input:
        text: "{{articulo_raw}}"    # ← usa el output anterior
        constraints:
          max_words: 50
          language: es
      response:
        field: resumen              # ← guardado en body

    - type: translator
      input:
        text: "{{resumen}}"         # ← usa el output anterior
        source_language: es
        target_language: en
      response:
        field: resumen_en
Deno · TypeScript

Scripts Deno sandboxed

Ejecuta TypeScript/JavaScript en un runtime Deno aislado. Dos modos: scripts almacenados (por UUID) o expresiones inline directamente en el DSL.

Modo 1Script inline (enter.script)
# Expresión JS directamente en el DSL
enter:
  script: >
    return {
      balance: input.item.body.balance
               + input.item.body.amount,
      last_updated: new Date().toISOString()
    }

# También como validador:
all:
  internal.script: >
    input.item.body.amount > 0 &&
    input.item.body.amount <= 99999
Modo 2Script almacenado (enter.code)
# Referencia a un ProjectScript por UUID
enter:
  code: "550e8400-e29b-41d4-a716-446655440000"

# El script es un archivo TypeScript completo:
const rawData = input.raw_data;
const config = input.config;

const result = {
  sku: rawData.sku || rawData.id?.toString(),
  name: rawData.name || rawData.title || '',
  price: parseFloat(rawData.price) || 0,
  external_id: rawData.id?.toString(),
  imported_at: new Date().toISOString()
};

return { success: true, result: result };
Contexto de ejecución
// Variables disponibles en todos los scripts:
input.item.body       // body actual del ítem (JSON libre)
input.item.state      // estado actual del ítem
input.item.code       // UUID del ítem
input.config          // config adicional del request
input.config.additional_info  // datos extra pasados al bulk API

// Solo en bulk import:
input.raw_data        // fila del dataset externo
input.index           // índice del ítem en el batch

// El script debe retornar un objeto:
// → sus campos se mezclan (merge) en el body del ítem
return {
  campo_calculado: input.item.body.x * 2,
  timestamp: new Date().toISOString(),
};
Capacidades del runtime Deno
  • TypeScript nativo (sin compilación manual)
  • fetch() disponible — puede llamar APIs externas
  • async/await soportado
  • Sandboxed — sin acceso a filesystem del servidor
  • Output del script se mezcla en el body del ítem
  • Scripts almacenados versionados como ProjectScript
  • Reutilizables en múltiples máquinas y transiciones
Bulk Import

Importación masiva de ítems

Crea múltiples ítems desde fuentes externas usando un script Deno en dos fases: primero recupera los datos, luego transforma cada fila individualmente.

Request
POST /api/project-state-machine/{code}/bulk

{
  "script_code": "UUID-DEL-SCRIPT-IMPORTER",
  "additional_info": {
    "api_url": "https://external.api.com/products",
    "api_token": "secret-token",
    "category": "electronics"
  }
}

# Response:
{
  "created_count": 47,
  "error_count": 2,
  "created_items": [
    {
      "code": "uuid",
      "state": "init",
      "body": { "sku": "ABC-123", "name": "...", "price": 99.99 }
    }
  ],
  "errors": [
    { "index": 5, "error": "missing required field: sku" }
  ]
}
Script importer (Deno)
// El script se llama DOS veces:
// 1. Para obtener los datos (operation === 'fetch_data')
// 2. Para transformar cada fila individualmente

const additionalInfo = input.config.additional_info;

// FASE 1: Obtener datos de API externa
if (input.config.operation === 'fetch_data') {
  const response = await fetch(additionalInfo.api_url, {
    headers: {
      'Authorization': `Bearer ${additionalInfo.api_token}`,
      'Content-Type': 'application/json'
    }
  });
  const data = await response.json();

  // Retorna el array de filas a procesar
  return { success: true, result: data.products };
}

// FASE 2: Transformar cada fila
const rawData = input.raw_data;   // fila del array anterior
const index = input.index;         // índice (0-based)

return {
  success: true,
  result: {
    sku: rawData.sku || rawData.id?.toString(),
    name: rawData.name || rawData.title || '',
    price: parseFloat(rawData.price) || 0,
    category: additionalInfo.category,
    external_id: rawData.id?.toString(),
    imported_at: new Date().toISOString()
  }
};
File Management

Gestión de archivos

Los ítems pueden tener archivos adjuntos almacenados en MinIO (S3-compatible). Los UUIDs de archivo se guardan en el body del ítem y las capacidades de imagen los usan directamente.

Upload de archivos
POST /api/project-state-machine-items/{code}/files
Content-Type: multipart/form-data
Authorization: Bearer {token}

# Body: multipart con el campo "file"

# Response:
{
  "code": "file-uuid-550e8400...",
  "mimeType": "image/jpeg",
  "size": 204800,
  "createdAt": "2025-01-15T10:30:00Z"
}

# Constraints:
# - Tipos: JPEG, PNG, GIF, WebP, SVG
# - Máx: 100MB por archivo
# - Máx: 10 archivos por ítem
Listar archivos de un ítem
GET /api/project-state-machine-items/{code}/files
Stack de almacenamiento
VichUploader
Manejo de uploads en Symfony con eventos de lifecycle
Flysystem
Abstracción de filesystem con adaptador S3
MinIO
Storage S3-compatible self-hosted (compatible con AWS S3 API)
GeminiFileSaver
Servicio interno que guarda imágenes generadas por IA en MinIO
Archivos en capacidades IA

Cuando usas image_generator con llm.files, los UUIDs almacenados en campos del body se recuperan de MinIO y se envían como contexto visual al modelo Gemini. Esto permite virtual try-on, edición de imágenes, etc.

REST API

Referencia de endpoints

Todos los endpoints siguen las convenciones de API Platform (Hydra / JSON-LD). Base URL: https://<tu-instancia>/api

State Machines
MétodoEndpointDescripción
GET/api/project-state-machinesListar máquinas de un proyecto
POST/api/project-state-machinesCrear máquina (JSON definition o DSL)
GET/api/project-state-machines/{code}Ver una máquina por código
PATCH/api/project-state-machines/{code}Actualizar definición/DSL
DELETE/api/project-state-machines/{code}Eliminar máquina
Ítems
MétodoEndpointDescripción
GET/api/project-state-machine-itemsListar ítems (filtrable por stateMachine, state, flags)
POST/api/project-state-machine-itemsCrear ítem → dispara transición inicial
GET/api/project-state-machine-items/{code}Ver ítem con estado y body actual
PATCH/api/project-state-machine-items/{code}Actualizar body y/o ejecutar transición
DELETE/api/project-state-machine-items/{code}Eliminar ítem
GET/api/project-state-machine-items/{code}/workflow-stepsHistorial de transiciones (audit trail)
POST/api/project-state-machine-items/{code}/filesSubir archivo al ítem
POST/api/project-state-machine/{code}/bulkImportar ítems masivamente vía Deno script
Scripts · Catalogs · Webhooks · Tokens
MétodoEndpointDescripción
GETPOST/api/project-scriptsListar/crear scripts Deno
PATCHDELETE/api/project-scripts/{code}Actualizar/eliminar script
GETPOST/api/project-catalogsListar/crear catálogos
GETPOST/api/project-catalog-itemsListar/crear ítems de catálogo
GETPOST/api/project-webhooksListar/crear webhooks
GETPOST/api/api-tokensListar/crear API tokens (requiere JWT)
OAuth 2.0
MétodoEndpointDescripción
GET/oauth/authorizeAuthorization endpoint
POST/oauth/tokenToken endpoint (authorization_code, password, refresh_token)
GET/oauth/userinfoUser info endpoint
POST/oauth/revokeToken revocation
Crear ítem
POST /api/project-state-machine-items
Authorization: Bearer {jwt_or_api_token}
Content-Type: application/json

{
  "stateMachine": "/api/project-state-machines/{code}",
  "body": {
    "email": "user@example.com",
    "amount": 500,
    "product_id": "prod-123"
  }
}

# Response: ítem creado con estado inicial
# y todas las transiciones automáticas ejecutadas
Ejecutar transición
PATCH /api/project-state-machine-items/{code}
Authorization: Bearer {jwt_or_api_token}
Content-Type: application/merge-patch+json

{
  "body": {
    "amount": 150,
    "note": "crédito adicional"
  },
  "transition": "add"    # opcional: forzar transición específica
}

# Response:
{
  "code": "item-uuid",
  "state": "add",
  "body": {
    "email": "user@example.com",
    "balance": 650,
    "amount": 150
  },
  "active": true,
  "flags": 1
}
Autenticación

JWT · API Tokens · OAuth 2.0

Nami soporta tres modos de autenticación según el caso de uso.

JWT (Dashboard)

Autenticación principal para el dashboard y pruebas manuales. Login con email/password → JWT en cookie httpOnly (nami_token). Acceso completo al audit trail y traza de ejecución.

Desarrollo, testing, dashboard visual
API Token (Producción)

Tokens estáticos por máquina creados vía POST /api/api-tokens (requiere JWT). Se almacenan hasheados (SHA-256); el token en texto plano se devuelve UNA sola vez. No exponen traza en responses.

Integraciones externas, producción, clientes API
OAuth 2.0

Client credentials: nami-client / nami-secret. Soporta authorization_code, password y refresh_token. Códigos: 10 min · Access tokens: 1 hora · Refresh tokens: 30 días.

Integraciones OAuth, SSO, apps de terceros
Multi-tenancy y roles
  • Todas las queries se filtran automáticamente por proyecto del usuario
  • Jerarquía: SUPER_ADMIN > ORGANIZATION_ADMIN > USER
  • Voters personalizados para cada operación y recurso
  • API Tokens son por defecto globales al usuario pero pueden ser scoped por máquina
  • La traza de ejecución solo es visible con autenticación JWT
  • Doctrine extensions auto-aplican filtros de tenant
nami-cli

CLI para gestión local

Node.js CLI para autenticar, desplegar workflows y operar Nami desde terminal. Soporta tanto el formato DSL YAML como el JSON de definición legacy.

Comandos disponibles
ComandoDescripción
nami loginAutenticar y guardar JWT localmente (email/password)
nami logoutEliminar JWT local
nami whoamiMostrar usuario autenticado actual
nami api-keyGenerar API token (opcionalmente scoped a máquina)
nami deploy <file>Desplegar máquina desde YAML DSL o JSON definition
nami projectsListar proyectos del usuario autenticado
nami workflowsListar/eliminar máquinas de estado en un proyecto
nami run <item-code>Loop interactivo PATCH para testear un ítem en ejecución
nami filesListar/subir archivos para un ítem
Uso típico
# Autenticarse
nami login

# Listar proyectos
nami projects --json

# Desplegar workflow desde archivo DSL
nami deploy workflow.yaml \
  --project /api/projects/{uuid} \
  --name "Mi Workflow v2"

# Generar API token de producción
nami api-key \
  --machine /api/project-state-machines/{code}

# Testear un ítem interactivamente
nami run {item-code}

# Opciones globales:
#   --json          solo output JSON
#   -v              verbose (log cada request/response)
#   --debug-http    debug HTTP completo
#   -k, --insecure  deshabilitar TLS (solo dev)
Referencia para LLMs

Todas las capacidades en un vistazo

Resumen estructurado de todas las features de Nami para generar herramientas, integraciones o código que interactúe con la plataforma.

DSL YAML — features soportadas
  • Multi-origen en transiciones: from: estado1 | estado2
  • to implícito: si se omite, usa el nombre de la transición
  • Fallback: else: estado-fallback cuando validadores fallan
  • Agrupación AND/OR: all (todos deben pasar) vs some (uno basta)
  • Interpolación: {{campo}} en prompts de capabilities
  • Encadenamiento: sin stop_after_apply las transiciones se encadenan automáticamente
  • Flags: add/remove en cada transición, verificables con internal.flag
  • itemModel: JSON Schema completo con required, format, minimum, maximum, enum
Validadores (9)
  • internal.not_empty — campos no nulos/vacíos
  • internal.empty — campos nulos/vacíos
  • internal.boolean — campos booleanos true/false
  • internal.compare — comparación numérica/string (>=, >, <=, <, ==, !=)
  • internal.regex — validación con regex
  • internal.flag — estado de flags (bitmap)
  • internal.script — expresión JS en Deno (retorna true/false)
  • external.get — HTTP GET con URL interpolada
  • external.post — HTTP POST con body interpolado
Capacidades LLM (7)
  • generator — genera texto nuevo desde prompt + context + constraints
  • improver — mejora texto existente (artifact_field apunta al campo)
  • summarizer — resume texto con constraints de longitud/idioma/formato
  • translator — traduce texto entre idiomas ISO 639-1
  • image_generator — genera imagen → guarda en MinIO → retorna UUID
  • video_generator — genera script de video para redes sociales
  • intention — extrae campos estructurados de mensaje libre → inyecta en body
  • llm.files: [campo] adjunta imágenes al contexto del LLM (multimodal)
Ejecución de scripts
  • enter.script — expresión JS inline (acceso a input.item.body, input.item.state)
  • enter.code — UUID de ProjectScript (Deno TypeScript completo)
  • Disponible también como validador: internal.script
  • fetch() disponible en scripts: puede llamar APIs externas
  • async/await soportado en scripts almacenados
  • Output del script se mezcla en el body del ítem
  • Bulk import: dos fases (fetch_data + transform per row)
  • input.raw_data disponible en bulk, input.config.additional_info para parámetros
API REST
  • POST /api/project-state-machine-items — crear ítem (dispara transición inicial)
  • PATCH /api/project-state-machine-items/{code} — actualizar body y/o transicionar
  • GET /api/project-state-machine-items/{code}/workflow-steps — audit trail
  • POST /api/project-state-machine-items/{code}/files — upload archivo
  • POST /api/project-state-machine/{code}/bulk — importar masivamente
  • POST /api/project-state-machines — crear máquina (dsl o definition)
  • GET/POST /api/project-scripts — gestionar scripts Deno
  • GET/POST /api/api-tokens — gestionar tokens de producción
Auth · Observabilidad · Integraciones
  • JWT auth: email/password → cookie httpOnly nami_token
  • API Tokens: SHA-256 hasheado, scoped por máquina, no expira, no expone traza
  • OAuth 2.0: authorization_code, password, refresh_token grants
  • WorkflowStep: audit trail automático (fromState, toState, bodyBefore, bodyAfter, duración)
  • Traza: solo visible con JWT auth, no con API tokens
  • Webhooks: inbound + outbound para integraciones
  • Listeners: cross-machine triggers al ejecutar transición
  • Multi-tenancy: queries auto-filtradas por proyecto del usuario autenticado

¿Listo para modelar tus workflows con Nami?

Contáctanos para conocer más sobre cómo Nami puede automatizar tus procesos de negocio con máquinas de estado, IA y scripts Deno.