Semántica de ejecución, restricciones y seguridad

Esta página resume cómo se comporta un Script en tiempo de ejecución (orden, transacciones, errores, bloqueo), a qué restricciones estáticas está sujeto al guardarse y su modelo de seguridad. Para la sintaxis, consulte el Catálogo de statements y las Expresiones de valor; para combinaciones prácticas, consulte el Cookbook.

Orden de ejecución

  • Los statements se ejecutan secuencialmente de arriba abajo. Cuando la ejecución alcanza un Return, termina en ese punto.
  • La ejecución ocurre en línea, en la ruta que atiende la petición de la llamada. La respuesta de la llamada es el resultado de la ejecución (para la forma de la respuesta, consulte Petición y respuesta en la descripción general de Script), y el tiempo que recibe una ejecución se trata más abajo en Presupuesto de tiempo.

Semántica de ejecución

Guard (condiciones previas)

No existe un statement guard dedicado. Se expresa con If y then:[Return]. Cuando se incumple la condición, devuelve un resultado y no ejecuta los statements siguientes (por supuesto, también es posible un Script sin guard).

{ "type": "If", "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" }, "statusCode": 402 } ] }

Sin transacciones y compensación best-effort

Un Script no es una transacción. Ante un fallo, el motor intenta compensar el trabajo realizado hasta ese momento y devuelve la causa del error, pero con las siguientes limitaciones (asumidas por diseño).

  • Deshacer un borrado crea un nuevo sys.id, por lo que las referencias que apuntaban a él se rompen.
  • Los efectos externos (Http) son irreversibles (una llamada que ya ha salido, y su cobro, no se pueden deshacer).
  • Si la compensación no se ejecuta en absoluto, puede quedar un estado no compensado.

Si necesita atomicidad real, escriba la compensación en el Script usted mismo o coloque las operaciones irreversibles (como las llamadas externas) al final. El orden más peligroso es el que «encadena hasta el final pero no puede revertirse, y aun así parece seguro».

Bloqueo optimista

La contención de update/patch se acota con version en ResourceUpdate y ResourcePatch. Si se proporciona version (una expresión de valor, Int), la actualización se realiza solo cuando coincide con el sys.version actual del destino; una discrepancia aborta con un error de conflicto de versión (que se puede manejar localmente con Try/catch). Si se omite, es last-write-wins sin comprobación. Normalmente se lee primero con ResourceRead o ResourceFind y se pasa ese sys.version (véase CAS de bloqueo optimista en el Cookbook).

Escrituras en origin

Las escrituras siempre se aplican en origin (draft), y la exposición en delivery (CDA/ACDA) se controla con publish (el publish de ResourceCreate/ResourceUpdate/ResourcePatch, o ResourcePublish/ResourceUnpublish).

Qué se considera un fallo

  • Un fallo real es un error de tiempo de ejecución de un statement: un status final de Http de 400 o superior (4xx·5xx; no es un fallo cuando ignoreStatusCode: true) o un tiempo de espera agotado, un cuerpo de respuesta que supera los 10MiB, o una operación de recurso fallida (destino inexistente, conflicto de versión, operación no admitida, etc.). Ante tales fallos, el motor aborta y compensa, y se pueden manejar localmente con Try/catch/finally.
  • Un Return no es un error, sino una salida anticipada normal. No es un objetivo de catch (no existe el concepto de throw de usuario).
  • Dentro de un catch, se referencia { message } mediante /error. No se incluye en qué statement se produjo el fallo.

En el servidor solo se agrega el recuento

El recuento lo hace ResourceCount en el servidor. Como no trae los elementos, no queda sujeto al máximo de elementos procesados.

Para sum y group-by no hay operaciones de servidor dedicadas. Esas agregaciones hay que calcularlas a mano recorriendo con ResourceForEach y usando SetVar y JsonLogic, por lo que quedan limitadas por el máximo de elementos procesados (no es adecuado para agregar millones de registros). Si solo hace falta el recuento, no se recorre: se usa ResourceCount.

Sin espera ni retardo

Un Script no tiene el statement Delay. Un Script se ejecuta una sola vez y termina, y no espera ni sondea internamente a que termine un job externo.

Restricciones estáticas (validadas al guardar)

Lo siguiente se comprueba cuando se guarda un Script (al crear/actualizar). Si algo se incumple, se rechaza el guardado (falla en el momento de la autoría, no en tiempo de ejecución). Con qué código se rechaza cada infracción se trata en Errores.

RestricciónValor
Máximo de llamadas externas (Http·EmailSend) por definiciónSegún el plan (véase Planes de precios)
Máximo total de elementos procesados por ResourceForEach (si no hay limit declarado, recorre hasta este valor; si llega a él quedando coincidencias, falla)10.000
Máximo de SetVar por definición (anidados incluidos)10
Máximo de Cache por definición (anidados incluidos, sumando con independencia de la operación)5. Si se supera, guardado rechazado
Un Cache dentro del bloque de un Loop o de un ResourceForEachGuardado rechazado
Cache.keySolo literal, 128 caracteres como máximo. Si es una expresión de valor, guardado rechazado
Cache.ttlEntre 1 y 30 segundos; si se omite, 5 segundos. Fuera de ese rango, guardado rechazado
Máximo total de statements por definición (anidados incluidos)Según el plan (véase Planes de precios)
Límite de Http.retry2
Longitud de Regex.pattern128 caracteres
Statements que modifican un ServiceUserGuardado rechazado. Solo los tres statements de lectura aceptan este recurso
Si anonymousCallEnabled es true, el createdBy: ":self" de whereGuardado rechazado

Los límites fijos de la tabla anterior son valores que fija la plataforma, así que son los mismos con independencia del plan. En cambio, el número total de statements y el número de llamadas externas por definición son límites por plan. Estos dos no son errores de validación sino límites de plan, así que, al superarlos, el guardado o la modificación se rechazan por superación del límite del plan (la misma definición sí se permite en un plan superior) y se desbloquean actualizando el plan. Los valores por plan están en Planes de precios.

La ingesta de archivo de Media, a diferencia de las llamadas externas como Http·EmailSend, no cuenta para el límite de llamadas externas por definición.

ResourceForEach es un statement compuesto que posee hijos, así que él mismo no cuenta en el número de llamadas externas. Son los statements de llamada externa dentro de onEach (Http·EmailSend) los que cuentan (de forma estática cuentan como 1, pero durante el recorrido se ejecutan realmente en cada elemento). En onEach se pueden colocar llamadas externas o ingesta de archivos de Media, y lo mismo vale para el body de Loop. Cuántas veces gira realmente la iteración no entra en este recuento: entra como una multiplicación en el Presupuesto de tiempo de más abajo.

Límites de longitud de los valores (en tiempo de ejecución)

Los statements de firma y de tratamiento de texto, y también Cache, tienen un límite superior en el tamaño de los valores que manejan. No es la longitud de la expresión, sino la longitud del valor resuelto de esa expresión (los dieciséis caracteres de { /rawPayload } pueden apuntar a decenas de KB), y por eso se comprueba durante la ejecución y no en el momento de guardar.

ObjetoLímiteSi se supera
El value de Signature65.536 caracteresEse statement falla (status 422)
El value de Hash128 caracteresEse statement falla (status 422)
El value de Regex10.240 caracteres (10KiB)Ese statement falla (status 400)
El value de Cache10.240 bytes (10KiB)Ese statement falla (status 422)
  • Los cuatro son como cualquier otro fallo en tiempo de ejecución, así que se pueden tratar de forma local con Try/catch.
  • El límite de Signature está ajustado al tamaño de cuerpo que envían los proveedores reales (un evento de pago llega a unos pocos KB, y un webhook de pedido a decenas de KB). El de Hash es mucho más estrecho porque es el lugar donde se concatenan unos cuantos campos.
  • Los 128 caracteres de Regex.pattern son la comprobación al guardar que figura en Restricciones estáticas más arriba. Esa longitud no es un mecanismo para evitar una explosión de cómputo ((a+)+$ es peligroso con solo seis caracteres). Lo que evita la explosión es la regla de escribir el patrón únicamente como literal y el presupuesto de tiempo de más abajo; lo único que promete la longitud es un tamaño que una persona pueda leer y revisar.

Presupuesto de tiempo (en tiempo de ejecución)

El tiempo que recibe una ejecución se decide con una sola fórmula: min(30 segundos + la suma de los tiempos que declaran los statements, 180 segundos).

  • El presupuesto se calcula a partir de ese Script. Al presupuesto base se le suma únicamente el tiempo que la definición ha declarado. El único tiempo declarado es el timeoutMs de Http y de EmailSend. Http vuelve a emplear su propio timeoutMs en cada reintento, así que se cuenta como timeoutMs × (1 + retry); EmailSend no reintenta, así que se cuenta una sola vez. Si no se escribe timeoutMs, se cuenta con el valor predeterminado (30 segundos en Http, 10 segundos en EmailSend).
  • Las operaciones sin tiempo declarado salen del presupuesto base de 30 segundos. Aquí entran la lectura y la escritura de recursos, la ingesta de archivos de Media y lo que una iteración hace en su interior. Por eso el presupuesto base no es una cifra formal, sino una parte real.
  • La manera de sumarlos sigue la estructura de los statements. Los statements colocados en secuencia se suman; de un If se toma la mayor de las dos ramas y de un Parallel, la mayor de todas sus ramas. En un Loop, el body se multiplica por el número de iteraciones (maxIterations; 10.000 si no hay declaración), y en un ResourceForEach, el onEach se multiplica por el número de elementos procesados (limit; 10.000 si no hay declaración).
  • Una iteración sin llamadas externas tiene un tiempo declarado de 0. Por eso el presupuesto base de 30 segundos pasa a ser el límite real, y ese es también el punto donde se atasca de verdad un Script que contiene una iteración.
  • El límite superior de 180 segundos no impide el guardado: recorta. Aunque el resultado del cálculo supere el límite, ese Script se guarda y se ejecuta, y al alcanzar los 180 segundos se interrumpe ahí.

Límites de cantidad por plan

El número de Script por Organization está limitado por plan.

PlanNúmero de Script
Free10
Basic30
Pro100
EnterpriseIlimitado

Con independencia de esto, el número de statements y el número de llamadas externas (Http·EmailSend) que una definición de Script puede contener también están limitados por plan. Al guardar o modificar una definición, si se supera el límite de ese plan, se rechaza; para los valores concretos, consulte Planes de precios.

Cuando se alcanza el límite, se rechaza la creación de un nuevo Script.

Modelo de seguridad

Cabeceras secret

Un elemento de Http.headers con secret:true es exclusivo de CMA (administrador): no se expone al usuario final (ServiceUser) y solo se descifra justo antes de la transmisión. Coloque aquí secretos como una clave de API de LLM (incluso al empaquetarse en un App Bundle, el valor secret se enmascara y nunca sale del Space de origen).

Dentro del Space, el secret de Signature no recibe este trato. No se cifra y se almacena tal cual se escribió en la definición, así que los roles que pueden leer ese Script ven el valor. Un miembro (ServiceUser) no puede leer la definición de un Script (la consulta y la autoría son exclusivas de CMA, y en ACMA no hay API de Script). En un Script que aloja una clave de verificación, es más seguro mantener acotados los roles que pueden leerlo.

Al salir del Space la cosa cambia. Cuando ese Script se empaqueta en un App Bundle, el secret de Signature se enmascara y no sale del Space de origen. En Http.headers se ocultan los elementos con el flag secret y la cabecera Authorization, mientras que el secret de Signature se oculta sin condiciones porque el campo en sí es la clave de firma. Un Signature anidado dentro de un If, un Loop o un Try también se oculta.

Identidad de ejecución y autorización

  • Identidad de ejecución: durante la ejecución, toda operación de recurso se realiza bajo la identidad del usuario que llamó a /execute. El createdBy/updatedBy de cualquier recurso que se cree o actualice es el llamante, y un ámbito createdBy: ":self" también se resuelve respecto al llamante. La excepción es la llamada anónima. Una ejecución que entra por /execute/anonymous no tiene llamante, así que ambas cosas se resuelven respecto al autor (Llamada anónima).
  • Hay dos límites de autorización y en tiempo de ejecución el motor no vuelve a comprobar los permisos de recurso en cada statement.
    1. En el momento de la autoría (guardado): al guardar un Script, se comprueba si el autor posee realmente los permisos de recurso y de acción que usan sus statements. Si falta aunque sea uno, se rechaza el guardado. Es decir, un Script que contiene una operación no autorizada no se guarda en primer lugar. Los statements que eligen un recurso pasan todos por esta comprobación, tanto los hoja como el ResourceForEach, que posee un bloque. Una definición ya guardada se vuelve a comprobar al modificarla, así que, una vez retirados los permisos, esa definición no se puede corregir y guardar.
      • El directorio de miembros (ServiceUser) no se comprueba con el mapa de permisos, sino por el eje de la configuración. Para usar resource: "ServiceUser" en los tres statements de lectura, el settings del SpaceRole del autor debe tener SETTING_SERVICE_LOGIN (o SETTING_ALL) (el settings de SpaceRole). Es porque el directorio de miembros, en todas las demás vías, es un recurso que gobierna la configuración del Space.
      • Los statements que modifican miembros no se guardan con ningún rol. Como en un Script no existe ninguna vía para crear, modificar ni eliminar miembros, el rechazo no es por falta de permisos (403), sino por un statement mal escrito (400). Es decir, no es un hueco que se pueda cerrar añadiendo permisos.
    2. En el momento de la llamada (/execute): solo se comprueba el permiso Execute de Script del llamante. Sin él, el resultado es 403. Una vez que pasa, los permisos de recurso por statement no se vuelven a comprobar en tiempo de ejecución; la ejecución continúa. Funciona como el permiso de ejecución de una función en programación. Si se tiene permiso para ejecutar la función, no se vuelve a preguntar por el permiso de cada operación individual dentro de ella. La ruta de llamada anónima no tiene esta comprobación. Es porque no hay ningún llamante que comprobar, y por eso abrir esa ruta equivale a publicar un Script sin autenticación.
  • Bloqueo de la invocación directa (directCallEnabled): si el directCallEnabled del Script es false, la propia invocación directa de /execute se rechaza. Esta puerta se aplica después de pasar la comprobación del permiso Execute, por lo que bloquea incluso si se tiene el permiso Execute. Un llamante que no tenga permiso recibe un 403 antes de llegar a esta puerta. Como esta puerta existe solo en ese endpoint, la acción vinculada (script) de un Webhook y un Scheduler lo ejecutan igualmente. El valor predeterminado es true (invocación directa permitida).
  • Llamada anónima (anonymousCallEnabled): el valor predeterminado es false. Si se pone en true, solo ese Script se ejecuta también por una ruta dedicada sin autenticación (/execute/anonymous), y entonces la identidad de ejecución no es el llamante, sino el autor. De los dos límites anteriores, la comprobación en el momento de la llamada (el permiso Execute) no existe en esa ruta, así que la autenticación efectiva la hace el propio Script (verificando la firma de la petición recibida). Las condiciones para activarlo y las reglas de guardado se tratan en Llamada anónima.
  • Ámbito de propiedad: createdBy: ":self" en un filtro where significa «solo lo que ha creado el llamante actual» (por ejemplo, consultar solo la propia cartera). En un Script que permite la llamada anónima no se puede usar este filtro. Como no hay llamante y se resuelve como el autor, el sentido original de un ámbito de propiedad no se sostiene.
  • Permisos delegados (atención al autor): al combinar los dos límites anteriores, ejecutar un Script equivale a actuar con los permisos del autor delegados. El llamante solo necesita Execute, y los statements dentro del Script se ejecutan exactamente dentro del ámbito para el que se autorizó al autor al guardar. En consecuencia, una operación de recurso que el llamante no podría realizar por sí mismo puede ocurrir igualmente a través del Script. Dado que los permisos concedidos al autor son el alcance efectivo de ese Script, decida con cuidado qué operaciones incluye en un Script.

Lista de comprobación resumida

Antes de guardar, compruebe lo siguiente.

  • Si ha activado la llamada anónima (anonymousCallEnabled), en where no hay ningún createdBy: ":self" y ha colocado al principio el statement que verifica la petición recibida (Signature, etc.).
  • Las llamadas externas (Http·EmailSend) y el número total de statements están dentro del límite del plan, SetVar es 10 o menos y Cache, 5 o menos.
  • Si ha usado Cache, ha escrito la key como literal y no lo ha colocado dentro de un Loop ni de un ResourceForEach.
  • Si recorre un conjunto grande con ResourceForEach, ha declarado un limit o ha comprobado que el tamaño se puede recorrer por completo.
  • Si ha incluido una iteración (Loop·ResourceForEach), ha comprobado que esa iteración entra como una multiplicación en el Presupuesto de tiempo (si no hay llamadas externas, el límite es el presupuesto base de 30 segundos).
  • Los valores secret se han colocado únicamente mediante secret:true en Http.headers (como Signature.secret no se almacena cifrado, ha revisado qué roles pueden leer ese Script).
  • El mensaje que se usa para verificar la firma se ha tomado con { /rawPayload }, no con /payload.
  • Si hay statements que leen miembros (ServiceUser), el autor tiene SETTING_SERVICE_LOGIN y no ha incluido statements que modifiquen ese recurso.
  • Las operaciones irreversibles (llamadas externas) se han colocado lo más tarde posible.
  • Si preocupa la contención de update/patch, se usa version en ResourceUpdate o ResourcePatch.
  • Para devolver un resultado, se ha especificado Return.value.

Errores

Códigos que aparecen cuando la forma de la definición infringe una restricción estática y el guardado se rechaza. Los códigos por infringir las reglas de las expresiones de valor están en los errores de Expresiones de valor, y los códigos que aparecen al invocar o eliminar, en los errores de los endpoints. Para los códigos comunes a todos los recursos, consulte Errores comunes.

CódigoCondición
WGL400066La definición contiene más de 5 statements Cache.
WGL400068La definición coloca un statement Cache dentro del bloque de un Loop o de un ResourceForEach.
WGL400067El key de un statement Cache lleva una referencia { /pointer } en lugar de un literal.
WGL400065El ttl de un statement Cache queda fuera del rango permitido.
WGL400063Un statement Cache lleva un campo que no corresponde a su action (ttl en Get, defaultValue en Set).
WGL400060El resource de un statement de escritura (ResourceCreate·ResourceUpdate·ResourcePatch·ResourceDelete y los statements de publicación y archivado) lleva "ServiceUser".
WGL400061En un Script que permite la llamada anónima (anonymousCallEnabled), el where de un statement de lectura lleva createdBy: ":self".
WGL400023La definición contiene más de 10 statements SetVar.
WGL400026El retry de un statement Http supera el límite máximo de 2.
WGL400036Un ResourceForEach supera el límite máximo de elementos que puede procesar.
WGL429005El número total de statements de la definición supera el límite del plan de precios.
WGL429006El número de llamadas externas (Http·EmailSend) de la definición supera el límite del plan de precios.
WGL403015El autor no tiene los permisos de recurso y de acción que usan los statements de la definición. Aunque tenga el permiso, se rechaza si esa regla Allow lleva un filtro contentType, createdBy o tag. Tiene que ser una regla Allow sin condiciones. La única excepción es el Create de Content: en ese caso también se acepta una regla Allow acotada por contentType, que se contrasta con el contentType escrito en el statement (el Create de Media no tiene esta excepción). Este código cubre también el caso en que el resource de un statement de lectura lleva "ServiceUser" y el autor no tiene SETTING_SERVICE_LOGIN.