Diccionario de Reglas JSON
Las automatizaciones en Axol Systems se configuran mediante objetos JSON almacenados en el campo rules de la tabla de configuraciones.
El sistema soporta dos modalidades de configuración según el motor seleccionado:
- Parte I: Esquema Declarativo JSON (Motor v2): Modelo declarativo modular Evento-Condición-Acción (ECA) con identificador
"version": 2, pipeline de acciones atómicas, operadores delta y resolución polimórfica de campos. - Parte II: Reglas Especializadas (Motor v1): Catálogo de propiedades dedicadas para tipos predefinidos de inventario y edición directa (
kardex,inventory_sync,inventory_transfer, etc.).
Parte I: Esquema Declarativo JSON (Motor v2)
Section titled “Parte I: Esquema Declarativo JSON (Motor v2)”El Motor v2 utiliza un esquema JSON estructurado bajo el paradigma ECA (Event-Condition-Action). Todas las definiciones del Motor v2 deben incluir la llave raíz "version": 2.
{ "version": 2, "options": { "atomic": true, "timeout_ms": 5000, "log_execution": false }, "trigger": { "type": "on_record_create", "source_table": "table20", "description": "Al registrar un traspaso autorizado" }, "conditions": [ { "field": "t20c3", "op": "eq", "value": "APROBADO" }, { "field": "t20c4", "op": "gt", "value": 0 } ], "actions": [ { "id": "kardex_salida", "type": "insert_record", "target_table": "table15", "fields": { "t15c1": { "type": "field", "source": "t20c1" }, "t15c2": { "type": "literal", "value": "SALIDA_TRASPASO" }, "t15c3": { "type": "field", "source": "t20c4" } } } ]}1. Disparadores (trigger)
Section titled “1. Disparadores (trigger)”El bloque trigger define el evento del ciclo de vida y la tabla física que activan la evaluación de la automatización en PostgreSQL.
{ "trigger": { "type": "on_record_create", "source_table": "table20", "description": "Al crear una nueva orden de salida" }}Propiedades de trigger
Section titled “Propiedades de trigger”| Propiedad | Tipo | Requerido | Descripción |
|---|---|---|---|
type | Texto | Sí | Identificador del evento disparador: on_record_create, on_record_update, on_record_delete o on_manual_execution. |
source_table | Texto | Sí | Nombre físico de la tabla en PostgreSQL donde se intercepta la mutación de datos. |
description | Texto | No | Explicación funcional legible para el equipo de desarrollo y administración. |
Tipos de Disparadores Disponibles
Section titled “Tipos de Disparadores Disponibles”on_record_create: Se activa inmediatamente tras la inserción exitosa de una nueva fila ensource_table.on_record_update: Se activa ante la modificación de uno o varios campos de una fila existente ensource_table. Permite evaluar el estado anterior mediante operadores delta.on_record_delete: Se activa durante la eliminación de una fila ensource_table, permitiendo ejecutar acciones de limpieza o actualización de saldos vinculados.on_manual_execution: Se dispara a petición explícita del usuario mediante botones interactivos de automatización configurados en la interfaz.
2. Árbol de Condiciones (conditions) y Operadores Delta
Section titled “2. Árbol de Condiciones (conditions) y Operadores Delta”El bloque conditions define las reglas lógicas que deben cumplirse para autorizar la ejecución del pipeline de acciones. Si la evaluación resulta en false, la automatización finaliza inmediatamente de forma limpia sin generar errores ni alterar datos.
Estructura y Agrupadores Lógicos
Section titled “Estructura y Agrupadores Lógicos”Por defecto, una lista de condiciones en el nivel raíz se evalúa con una conjunción lógica AND (todas deben cumplirse). Para lógicas más complejas, se admiten árboles jerárquicos mediante los agrupadores:
all: Arreglo de condiciones donde todas deben cumplirse obligatoriamente (AND).any: Arreglo de condiciones donde es suficiente que al menos una se cumpla (OR).
{ "conditions": [ { "all": [ { "field": "t20c1", "op": "eq", "value": "PRODUCCION" }, { "any": [ { "field": "t20c4", "op": "gt", "value": 100 }, { "field": "t20c5", "op": "eq", "value": "URGENTE" } ] } ] } ]}Operadores Relacionales Tipados
Section titled “Operadores Relacionales Tipados”| Operador | Significado | Tipos Compatibles | Ejemplo |
|---|---|---|---|
eq | Igual a | Texto, Número, Booleano, Fecha | { "field": "t20c3", "op": "eq", "value": "ACTIVO" } |
neq | Distinto de | Texto, Número, Booleano, Fecha | { "field": "t20c3", "op": "neq", "value": "CANCELADO" } |
gt | Mayor que | Número, Fecha | { "field": "t20c4", "op": "gt", "value": 0 } |
gte | Mayor o igual que | Número, Fecha | { "field": "t20c4", "op": "gte", "value": 10 } |
lt | Menor que | Número, Fecha | { "field": "t20c4", "op": "lt", "value": 50 } |
lte | Menor o igual que | Número, Fecha | { "field": "t20c4", "op": "lte", "value": 100 } |
in | Contenido en lista | Texto, Número | { "field": "t20c2", "op": "in", "value": ["A", "B", "C"] } |
is_null | Es nulo o vacío | Cualquier tipo | { "field": "t20c8", "op": "is_null", "value": true } |
Operadores Delta (Detección de Cambios en on_record_update)
Section titled “Operadores Delta (Detección de Cambios en on_record_update)”Diseñados exclusivamente para disparadores on_record_update, permiten inspeccionar si el valor de un campo varió respecto a su estado previo en la transacción:
changed: Evalúa si el valor actual de la columna difiere del valor que tenía antes de la modificación:
{ "field": "t20c3", "op": "changed" }changed_to: Evalúa si la columna fue modificada y su nuevo valor coincide exactamente con el valor esperado:
{ "field": "t20c3", "op": "changed_to", "value": "AUTORIZADO" }changed_from: Evalúa si la columna fue modificada y su valor previo antes de la mutación coincidía con el valor indicado:
{ "field": "t20c3", "op": "changed_from", "value": "BORRADOR" }3. Resolución Polimórfica de Campos
Section titled “3. Resolución Polimórfica de Campos”En todas las acciones del pipeline, los valores de los campos se definen mediante un objeto de resolución tipado que permite desacoplar los datos:
A. Campo del Registro Origen (field)
Section titled “A. Campo del Registro Origen (field)”Extrae dinámicamente el valor actual de una columna del registro disparador:
{ "type": "field", "source": "t20c1" }B. Valor Constante Literal (literal)
Section titled “B. Valor Constante Literal (literal)”Asigna un valor fijo estático (cadena de texto, número o booleano):
{ "type": "literal", "value": "SALIDA_TRASPASO" }C. Metadatos de Contexto y Sesión (meta)
Section titled “C. Metadatos de Contexto y Sesión (meta)”Resuelve variables de sesión del usuario autenticado o del entorno del sistema:
user.name: Nombre completo o identificador del usuario activo.user.id: Identificador UUID del usuario en sesión.user.rol/user.role: Rol de seguridad del usuario actual.now: Timestamp exacto del servidor (YYYY-MM-DD HH:mm:ss).today: Fecha actual del servidor (YYYY-MM-DD).
{ "type": "meta", "source": "user.name" }4. Catálogo de Acciones Atómicas (actions)
Section titled “4. Catálogo de Acciones Atómicas (actions)”El arreglo actions especifica la secuencia ordenada de operaciones a ejecutar sobre la base de datos. Cada acción requiere un identificador único id y un type.
1. insert_record
Section titled “1. insert_record”Inserta un nuevo registro en una tabla física destino.
{ "id": "crear_registro_kardex", "type": "insert_record", "target_table": "table15_kardex", "fields": { "t15c1": { "type": "field", "source": "t20c1" }, "t15c2": { "type": "literal", "value": "MOV_SALIDA" }, "t15c3": { "type": "field", "source": "t20c4" }, "t15c4": { "type": "meta", "source": "user.name" }, "t15c5": { "type": "meta", "source": "now" } }}target_table: Nombre de la tabla física donde se insertará el nuevo registro.fields: Diccionario donde cada clave es el código de columna detarget_tabley cada valor es un objeto de resolución (field,literalometa).
2. update_record
Section titled “2. update_record”Actualiza columnas en filas existentes de una tabla destino que coincidan con los criterios de búsqueda (match).
{ "id": "actualizar_estado_orden", "type": "update_record", "target_table": "table5_ordenes", "match": [ { "target": "id_orden", "source": "t20c1" } ], "fields": { "t5c8": { "type": "literal", "value": "PROCESADA" }, "t5c9": { "type": "meta", "source": "now" } }}target_table: Nombre de la tabla física a modificar.match: Lista de condiciones de enlace para localizar los registros destino (targetvssourcepara columnas del registro origen, otargetvsvaluepara constantes).fields: Diccionario de columnas destino a actualizar con sus respectivos objetos de resolución.
3. adjust_field
Section titled “3. adjust_field”Efectúa incrementos o decrementos numéricos atómicos de manera segura, aplicando bloqueo de concurrencia y protección contra saldos negativos. Es la acción estándar para existencias, balances y contadores.
{ "id": "descontar_stock_almacen", "type": "adjust_field", "target_table": "table10_stock", "match": [ { "target": "id_producto", "source": "t20c1" }, { "target": "id_almacen", "source": "t20c2" } ], "operation": "decrement", "field": "t10c3", "amount": { "type": "field", "source": "t20c4" }, "allow_negative": false, "auto_create_if_missing": true, "default_values": { "id_producto": { "type": "field", "source": "t20c1" }, "id_almacen": { "type": "field", "source": "t20c2" }, "t10c3": { "type": "literal", "value": 0 } }}target_table: Tabla física donde reside la columna numérica a ajustar.match: Condiciones relacionales para ubicar la fila única a modificar.operation:"increment"(suma) o"decrement"(resta).field: Código de la columna numérica entarget_tablesobre la que se aplica la operación.amount: Objeto de resolución que define la cantidad numérica a sumar o restar (fieldoliteral).allow_negative: Booleano (falsepor defecto). Si la operación resulta en un saldo menor a cero, la acción arroja una excepción y PostgreSQL efectúaROLLBACKde toda la transacción.auto_create_if_missing: Booleano (falsepor defecto). Si la fila destino no existe, la crea automáticamente antes de aplicar el ajuste.default_values: Diccionario de valores iniciales para poblar las columnas del registro si es creado medianteauto_create_if_missing.- Concurrencia Segura: La operación ejecuta internamente un bloqueo a nivel de fila (
SELECT ... FOR UPDATE) para evitar condiciones de carrera entre múltiples transacciones simultáneas.
4. delete_record
Section titled “4. delete_record”Elimina físicamente los registros coincidentes en una tabla destino bajo validación de criterios match.
{ "id": "eliminar_reserva_temporal", "type": "delete_record", "target_table": "table_reservas", "match": [ { "target": "id_orden", "source": "t20c1" }, { "target": "estado", "value": "TEMPORAL" } ]}target_table: Nombre de la tabla física donde se eliminarán registros.match: Criterios de filtrado obligatorios para identificar las filas exactas a suprimir.
5. set_field
Section titled “5. set_field”Modifica en caliente una o más columnas del propio registro origen dentro del ciclo de vida del evento antes de que se confirme el guardado en base de datos.
{ "id": "sellar_folio_y_autor", "type": "set_field", "field": "t20c9", "value": { "type": "meta", "source": "user.name" }}field: Código de la columna en la tabla origen a modificar.value: Objeto de resolución del nuevo valor (field,literalometa).
6. for_each_child
Section titled “6. for_each_child”Itera de forma secuencial sobre colecciones de registros dependientes o listas de objetos (objList) enlazadas al objeto origen, ejecutando un pipeline de acciones anidadas para cada ítem.
{ "id": "procesar_partidas_orden", "type": "for_each_child", "source_list": "items_orden", "actions": [ { "id": "ajustar_stock_partida", "type": "adjust_field", "target_table": "table10_stock", "match": [ { "target": "id_producto", "source": "t2c1" }, { "target": "id_almacen", "source": "t2c2" } ], "operation": "decrement", "field": "t10c3", "amount": { "type": "field", "source": "t2c5" }, "allow_negative": false } ]}source_list: Código técnico o identificador de la propiedad de tipoobjListen el objeto origen.actions: Arreglo de acciones atómicas a ejecutar para cada uno de los registros hijos presentes en la colección.
5. Opciones Globales de Ejecución (options)
Section titled “5. Opciones Globales de Ejecución (options)”El bloque options configura el comportamiento transaccional y de observabilidad de la regla:
{ "options": { "atomic": true, "timeout_ms": 5000, "log_execution": true }}| Opción | Tipo | Por Defecto | Descripción |
|---|---|---|---|
atomic | Booleano | true | Garantiza que todo el pipeline se ejecute dentro de una única transacción atómica en PostgreSQL. Si cualquier acción falla, se ejecuta ROLLBACK total. |
timeout_ms | Entero | 5000 | Límite máximo de tiempo en milisegundos para la ejecución del pipeline. Previene bloqueos por contención en alta concurrencia. |
log_execution | Booleano | false | Habilita el registro detallado de trazas de auditoría de cada acción en las tablas de auditoría del sistema. |
6. Ejemplo Completo: Regla Declarativa v2
Section titled “6. Ejemplo Completo: Regla Declarativa v2”El siguiente ejemplo ilustra una automatización integral que al registrar un movimiento de traspaso aprobado (on_record_create):
- Inserta una partida en la tabla de historial Kardex.
- Descuenta el stock en el almacén de origen validando saldo positivo.
- Incrementa el stock en el almacén de destino, creándolo automáticamente si aún no existía.
{ "version": 2, "options": { "atomic": true, "timeout_ms": 5000, "log_execution": true }, "trigger": { "type": "on_record_create", "source_table": "table20_traspasos", "description": "Al registrar un traspaso autorizado" }, "conditions": [ { "field": "t20c3", "op": "eq", "value": "APROBADO" }, { "field": "t20c4", "op": "gt", "value": 0 } ], "actions": [ { "id": "kardex_salida", "type": "insert_record", "target_table": "table15_kardex", "fields": { "t15c1": { "type": "field", "source": "t20c1" }, "t15c2": { "type": "field", "source": "t20c2" }, "t15c3": { "type": "literal", "value": "SALIDA_TRASPASO" }, "t15c4": { "type": "field", "source": "t20c4" }, "t15c5": { "type": "meta", "source": "user.name" }, "t15c6": { "type": "meta", "source": "now" } } }, { "id": "descontar_origen", "type": "adjust_field", "target_table": "table10_stock", "match": [ { "target": "id_producto", "source": "t20c1" }, { "target": "id_almacen", "source": "t20c2" } ], "operation": "decrement", "field": "existencia", "amount": { "type": "field", "source": "t20c4" }, "allow_negative": false }, { "id": "incrementar_destino", "type": "adjust_field", "target_table": "table10_stock", "match": [ { "target": "id_producto", "source": "t20c1" }, { "target": "id_almacen", "source": "t20c5" } ], "operation": "increment", "field": "existencia", "amount": { "type": "field", "source": "t20c4" }, "auto_create_if_missing": true, "default_values": { "id_producto": { "type": "field", "source": "t20c1" }, "id_almacen": { "type": "field", "source": "t20c5" }, "existencia": { "type": "literal", "value": 0 } } } ]}Parte II: Reglas Especializadas (Motor v1)
Section titled “Parte II: Reglas Especializadas (Motor v1)”Propiedades Generales
Section titled “Propiedades Generales”qty_field
Section titled “qty_field”Especifica la columna de la tabla origen (source_table) que contiene la cantidad numérica a mover.
- Tipo: Texto (identificador de columna)
- Ejemplo:
{ "qty_field": "t0c0"}qty_mappings
Section titled “qty_mappings”Permite definir múltiples cantidades en una sola automatización. Cada mapeo puede aplicar filtros adicionales en la tabla destino mediante la propiedad extra_match. Si se define qty_mappings, la propiedad clásica qty_field pasa a ser opcional.
- Tipo: Arreglo de objetos
- Sub-propiedades por objeto:
qty_field: Columna de cantidad origen.extra_match: Filtro adicional (ej.{ "target": "t1c7", "value": "BUENAS" }).fallback_match(Opcional): Regla de sustitución. Si el origen no cuenta con saldo suficiente de la categoría principal, toma del fallback (ej. tomar deBUENASsi falta saldo deSCRAP), pero el registro resultante sigue etiquetado con la categoría principal (extra_match).
- Ejemplo:
{ "qty_mappings": [ { "qty_field": "t0c2", "extra_match": { "target": "t1c7", "value": "BUENAS" } }, { "qty_field": "t0c9", "extra_match": { "target": "t1c7", "value": "SCRAP" }, "fallback_match": { "target": "t1c7", "value": "BUENAS" } } ]}target_table
Section titled “target_table”Especifica el identificador de la tabla destino donde se aplicarán los cambios (p.ej. tabla de inventario final o de registros consolidados).
- Tipo: Texto
- Ejemplo:
{ "target_table": "table7"}target_field
Section titled “target_field”Especifica la columna numérica en la tabla destino (target_table) que se va a incrementar, decrementar o modificar.
- Tipo: Texto (identificador de columna)
- Ejemplo:
{ "target_field": "t1c0"}match_columns
Section titled “match_columns”Define los criterios de coincidencia para buscar y asociar la fila de origen con la fila destino. Compara columnas entre ambas tablas.
- Tipo: Arreglo de objetos
- Formato por ítem:
- Comparar columnas:
{ "source": "t0c3", "target": "t1c1" } - Comparar con constante:
{ "target": "t1c4", "value": 0 }(ignorasource)
- Comparar columnas:
- Ejemplo:
{ "match_columns": [ { "source": "t0c3", "target": "t1c1" }, { "target": "t1c4", "value": 0 } ]}Validación y Filtrado al Guardar
Section titled “Validación y Filtrado al Guardar”validation_save
Section titled “validation_save”Permite definir reglas para validar cantidades o valores antes de guardar una fila. Si una validación falla, el sistema bloquea el guardado del registro y muestra un mensaje de error personalizado.
- Tipo: Arreglo de objetos
- Sub-propiedades por objeto:
catch: El mensaje de error que se muestra al usuario.logic: Operador lógico de comparación (eq,neq,gt,gte,lt,lte).value: Valor contra el que se compara (puede ser un número constante, texto, o la palabra clave"target_field").formula: Tipo de validación. Usa"if"para comparar directamente la propiedad o"target_qty"para comparar la cantidad contra el stock real en el destino.property: Columna de origen a validar (opcional si se usaqty_mappings).transfer: Indica si la validación aplica sobre el origen ("origin") o el destino ("target").
- Ejemplo:
{ "validation_save": [ { "catch": "El inventario de origen no tiene cantidad suficiente", "logic": "lt", "value": "target_field", "formula": "target_qty", "property": "t8c7", "transfer": "origin" } ]}ignore_save
Section titled “ignore_save”Fórmulas con resultado booleano. Si la condición se evalúa como verdadera (true), la fila de origen se ignora y no ejecuta ninguna lógica en la automatización. Soporta agrupadores lógicos anidados (and y or).
- Tipo: Arreglo de objetos. Los ítems de la lista principal se evalúan con un operador OR implícito (si cualquiera se cumple, se ignora).
- Ejemplo con AND (ignorar si ambas cantidades son menores o iguales a cero):
{ "ignore_save": [ { "type": "and", "param": [ { "type": "if", "param": { "var_a": "<prop:t14c2>", "var_b": 0, "oper": "lte" } }, { "type": "if", "param": { "var_a": "<prop:t14c9>", "var_b": 0, "oper": "lte" } } ] } ]}Reglas de Movimiento e Inventario
Section titled “Reglas de Movimiento e Inventario”transfer_columns
Section titled “transfer_columns”Se utiliza en traspasos (inventory_transfer) y movimientos (inventory_movement) para especificar de qué columnas leer el origen y el destino de la mercancía, y en qué columna de la tabla destino buscar/guardar la ubicación.
- Tipo: Objeto
- Estructura:
origin: Columna origen en la tabla de movimientos.destination: Columna destino en la tabla de movimientos.target: Columna de ubicación en la tabla final de stock.
- Ejemplo:
{ "transfer_columns": { "origin": "t8c5", "destination": "t8c6", "target": "t7c5" }}in_out_property
Section titled “in_out_property”Permite escribir automáticamente un valor literal (como "ENTRADA" o "SALIDA") en una columna del registro de movimientos basándose en la dirección configurada.
- Tipo: Objeto
- Estructura:
column: Columna del registro origen donde se guardará la etiqueta.in(Opcional): Valor a escribir si la dirección es de entrada (in).out(Opcional): Valor a escribir si la dirección es de salida (out).
- Ejemplo:
{ "in_out_property": { "column": "t14c5", "in": "ENTRADA", "out": "SALIDA" }}Control Visual y Grillas (UI)
Section titled “Control Visual y Grillas (UI)”bulk_properties
Section titled “bulk_properties”Especifica una lista de propiedades que aparecerán como campos unificados (cabecera común) en la parte superior del diálogo de captura (InOutView). Escribir o seleccionar un valor en este campo unificado lo replicará instantáneamente a todas las filas de esa columna.
- Tipo: Arreglo de textos o mapas con formato de visualización de referencias.
- Ejemplo:
{ "bulk_properties": [ { "column": "t10c2", "reference_view": "{t0c0} - {t0c1}" }, "t10c3" ]}show_columns / hide_columns
Section titled “show_columns / hide_columns”Actúan como lista blanca (show_columns) y lista negra (hide_columns) para controlar de forma explícita qué columnas se muestran al usuario en las grillas de edición de movimientos (InOutView).
- Precedencia: Primero se aplican filtros internos, luego
show_columnsy finalmentehide_columns. Si una columna está en ambas, prevalecehide_columns. - Ejemplo:
{ "show_columns": ["t8c0", "t8c1", "t8c7"], "hide_columns": ["t8c4"]}Reglas Específicas de Captura Kardex
Section titled “Reglas Específicas de Captura Kardex”stock_selector
Section titled “stock_selector”Habilita el cálculo y filtrado dinámico de existencias en tiempo real dentro del selector de productos de la grilla de captura:
source: Columna del producto que se está seleccionando en la fila (ej."t10c1").match: Columna o lista de columnas de contexto por las que se calcula el saldo (típicamente el almacén en"t10c2").filter_positive_on_out: Si estrue(por defecto), en operaciones de salida el catálogo oculta automáticamente los artículos con stock menor o igual a cero.column_label: Título de la columna virtual de stock calculada en el selector (por defecto:"Stock Actual").allow_negative_stock: Si estrue, permite listar y seleccionar ítems con saldo negativo o nulo en salidas (falsepor defecto).- Ejemplo:
{ "stock_selector": { "source": "t10c1", "match": "t10c2", "filter_positive_on_out": true, "column_label": "Stock Disponible" }}merge_on_match
Section titled “merge_on_match”Controla si las filas capturadas en el diálogo de movimientos que comparten los mismos criterios de match_columns se deben consolidar antes de persistir en base de datos.
- Tipo: Booleano (por defecto
true).true: Suma las cantidades (qty_field) de todas las filas con los mismos criterios de coincidencia, generando un único registro y recalculando elbalance_fieldconsolidado.false: Guarda cada fila de forma individual como eventos independientes.
auto_create_target
Section titled “auto_create_target”Permite que la automatización kardex verifique si ya existe una fila para el producto y almacén en la tabla secundaria de existencias físicas acumuladas, y la cree automáticamente si aún no existe, evitando requerir triggers manuales en PostgreSQL:
target_table(Requerido): Nombre de la tabla destino de existencias donde se asegurarán las filas.match_columns(Requerido): Lista de mapeos entre columnas del movimiento (source) y columnas de la tabla destino (target), o arreglo de nombres simples si coinciden.default_values(Opcional): Mapa con valores por defecto para otras columnas en los registros creados.only_positive(Opcional, booleano,falsepor defecto): Si estrue, solo crea registros cuando el movimiento es de entrada/incremento (+enop_mapping).- Ejemplo:
{ "auto_create_target": { "target_table": "existencias_almacen", "match_columns": [ { "source": "t10c1", "target": "id_producto" }, { "source": "t10c2", "target": "id_almacen" } ], "default_values": { "estado": "ACTIVO" }, "only_positive": true }}Inyecta automáticamente valores de metadatos de contexto en columnas designadas del registro al momento de guardar:
- Secuencia / Folio Correlativo (
"data": "sequence"): Genera números consecutivos automáticos con formato personalizado usandoprefix(ej."MOV-") ypadding(longitud de ceros a la izquierda). - Usuario Actual (
"data": "user.name"): Registra el nombre del usuario autenticado que efectuó la operación. - Ejemplo:
{ "meta": [ { "column": "t10c6", "data": "sequence", "prefix": "MOV-", "padding": 6 }, { "column": "t10c7", "data": "user.name" } ]}