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.
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.
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.
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).
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.
Audit trail automático: por cada transición se registra fromState, toState, bodyBefore, bodyAfter, resultados de validadores, outputs de capacidades, duración y timestamp.
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.
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).
User → Project → ProjectStateMachine → ProjectStateMachineItem
│ ├── WorkflowStep (audit trail)
│ └── ProjectStateMachineItemFile
├── ProjectScript (Deno)
├── ProjectCatalog → ProjectCatalogItem
├── ProjectWebhook
├── ApiToken
└── ProjectStateMachineListenerNami 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.
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_flag2name: "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_activofrom: a | b → múltiples estados origen (pipe-separated)to omitido → usa el nombre de la transiciónelse → estado al que va si los validadores fallanall = AND lógico · some = OR lógicoenter acepta exactamente uno: script, code o capabilitiesstop_after_apply evita encadenamiento automático de transicionesstop_after_apply, la ejecución continúa en el nuevo estadoLos 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).
| Tipo | Sintaxis DSL | Descripción |
|---|---|---|
internal.not_empty | internal.not_empty: [campo1, campo2] | Los campos deben ser no-nulos y no-vacíos en el body del ítem |
internal.empty | internal.empty: [campo1] | Los campos deben ser nulos o vacíos |
internal.boolean | internal.boolean:
"true": [campo_activo]
"false": [campo_inactivo] | Verifica que campos sean true/false (booleanos) |
internal.compare | internal.compare:
operator: ">="
field: monto
value: 100 | Comparación numérica/string: >=, >, <=, <, ==, != |
internal.regex | internal.regex:
campo: '/^[A-Z]+$/'
email: '/^[\w@.]+$/' | Valida campos contra patrones regex |
internal.flag | internal.flag:
"true": [activo]
"false": [bloqueado] | Verifica el estado del bitmap de flags del ítem |
internal.script | internal.script: input.item.body.monto > 0 | Expresión JS inline evaluada en Deno. Retorna true/false |
| Tipo | Config | Descripción |
|---|---|---|
external.get | url: "https://api.com/check/{{id}}" | Llama a un endpoint GET externo; la URL soporta interpolación {{campo}} desde el body del ítem |
external.post | url: "https://api.com/validate"
body: { "id": "{{campo}}" } | Llama a un endpoint POST externo con body templado. Soporta interpolación en URL y body |
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 GETLas 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.
generatorGenera nuevo contenido desde cero a partir de un prompt. Soporta context y constraints (max_words, language, format).
artifact.value (texto)improverMejora un texto existente referenciado por artifact_field (campo del body). Aplica constraints de calidad.
texto mejoradosummarizerResume el campo text con constraints de longitud, formato e idioma (ISO 639-1).
resumentranslatorTraduce text de source_language a target_language. Idiomas en código ISO 639-1 (es, en, fr, pt, etc.).
texto traducidoimage_generatorGenera una imagen y la guarda en MinIO. Soporta llm.files para adjuntar imágenes del body como referencia (virtual try-on, edición).
UUID del archivo (MinIO)video_generatorGenera un script de video para Instagram/TikTok/YouTube Shorts desde un prompt. No genera video binario.
script de videointentionExtrae campos estructurados de un mensaje de usuario libre y los inyecta en el body. Preserva campos ya recolectados. Útil para formularios conversacionales.
campos estructurados (JSON)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 resultadoname: "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# 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_enEjecuta TypeScript/JavaScript en un runtime Deno aislado. Dos modos: scripts almacenados (por UUID) o expresiones inline directamente en el DSL.
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 <= 99999enter.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 };// 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(),
};fetch() disponible — puede llamar APIs externasasync/await soportadoProjectScriptCrea múltiples ítems desde fuentes externas usando un script Deno en dos fases: primero recupera los datos, luego transforma cada fila individualmente.
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" }
]
}// 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()
}
};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.
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 ítemGET /api/project-state-machine-items/{code}/filesCuando 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.
Todos los endpoints siguen las convenciones de API Platform (Hydra / JSON-LD). Base URL: https://<tu-instancia>/api
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /api/project-state-machines | Listar máquinas de un proyecto |
| POST | /api/project-state-machines | Crear 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 |
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /api/project-state-machine-items | Listar ítems (filtrable por stateMachine, state, flags) |
| POST | /api/project-state-machine-items | Crear í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-steps | Historial de transiciones (audit trail) |
| POST | /api/project-state-machine-items/{code}/files | Subir archivo al ítem |
| POST | /api/project-state-machine/{code}/bulk | Importar ítems masivamente vía Deno script |
| Método | Endpoint | Descripción |
|---|---|---|
| GETPOST | /api/project-scripts | Listar/crear scripts Deno |
| PATCHDELETE | /api/project-scripts/{code} | Actualizar/eliminar script |
| GETPOST | /api/project-catalogs | Listar/crear catálogos |
| GETPOST | /api/project-catalog-items | Listar/crear ítems de catálogo |
| GETPOST | /api/project-webhooks | Listar/crear webhooks |
| GETPOST | /api/api-tokens | Listar/crear API tokens (requiere JWT) |
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /oauth/authorize | Authorization endpoint |
| POST | /oauth/token | Token endpoint (authorization_code, password, refresh_token) |
| GET | /oauth/userinfo | User info endpoint |
| POST | /oauth/revoke | Token revocation |
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 ejecutadasPATCH /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
}Nami soporta tres modos de autenticación según el caso de uso.
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.
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.
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.
SUPER_ADMIN > ORGANIZATION_ADMIN > USERNode.js CLI para autenticar, desplegar workflows y operar Nami desde terminal. Soporta tanto el formato DSL YAML como el JSON de definición legacy.
| Comando | Descripción |
|---|---|
nami login | Autenticar y guardar JWT localmente (email/password) |
nami logout | Eliminar JWT local |
nami whoami | Mostrar usuario autenticado actual |
nami api-key | Generar API token (opcionalmente scoped a máquina) |
nami deploy <file> | Desplegar máquina desde YAML DSL o JSON definition |
nami projects | Listar proyectos del usuario autenticado |
nami workflows | Listar/eliminar máquinas de estado en un proyecto |
nami run <item-code> | Loop interactivo PATCH para testear un ítem en ejecución |
nami files | Listar/subir archivos para un ítem |
# 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)Resumen estructurado de todas las features de Nami para generar herramientas, integraciones o código que interactúe con la plataforma.
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.